diff --git a/gmc/file_widgets/one_source_one_destination.py b/gmc/file_widgets/one_source_one_destination.py index e2d875c..f286d07 100644 --- a/gmc/file_widgets/one_source_one_destination.py +++ b/gmc/file_widgets/one_source_one_destination.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List, Sequence +from typing import Any, Callable, Sequence from PyQt5 import QtCore, QtWidgets, QtGui from ..utils import separator, new_action from ..views.filesystem_widget import SingleFilesystemWidget, FilesystemTitle @@ -14,7 +14,7 @@ class OneSourceOneDestination: @classmethod def create_data_widget( - cls, mdi_area: QtWidgets.QMdiArea, extra_args: Dict[str, Any] + cls, mdi_area: QtWidgets.QMdiArea, extra_args: dict[str, Any] ) -> QtWidgets.QSplitter: """ :param cls: like `gmc.schemas.tagged_objects.TaggedObjects` @@ -174,7 +174,7 @@ def __init__( dst_dir: QtCore.QDir, src_dir: QtCore.QDir, file_path: str, - all_files: List[str], + all_files: list[str], schema, ): """ diff --git a/gmc/i18n/gmc_ru.ts b/gmc/i18n/gmc_ru.ts index aa9c1af..4e25c29 100644 --- a/gmc/i18n/gmc_ru.ts +++ b/gmc/i18n/gmc_ru.ts @@ -4,22 +4,22 @@ @default - + Warning Предупреждение - + Destination directory must be specified Целевой каталог должен быть указан - + Error Ошибка - + Start by selecting a schema Начните с выбора схемы @@ -64,608 +64,538 @@ Номера автомобилей на видео - + Information Информация - + Click the target the arrow is pointing at Щелкните кнопку, на которую указывает стрелка - + &Schema &Схема - + &About Qt &О Qt - + E&xit &Выход - + Previous File Предыдущий файл - + Next File Следующий файл - + Save Сохранить - + Close Tab Закрыть вкладку - + Save changes in tab "{title}" before closing? Сохранить изменения во вкладке -"{title}" +"{title}" перед закрытием? - + &Source Directory &Исходный каталог - + Select Source Directory Выберите исходный каталог - + Now select source directory И выберите исходный каталог - + &Destination Directory Каталог &назначения - + Select Destination Directory Выберите каталог назначения - + Then select destination directory Потом выберите каталог назначения - + Default &Action: Действие по &умолчанию: - + Select and Transform Objects Выбор и преобразование объектов - + Add Quadrangle Добавить четырёхугольник - + Add Line Добавить линию - + Add Segment Добавить отрезок - + Add Point Добавить точку - + Add Rectangle Добавить прямоугольник - + Add Broken Line Добавить ломанную линию - + Add Region Добавить регион - + Default &Tags: &Метки по умолчанию: - + &Unique Tag Уникальная &Метка - + Mad Cat Mode Режим Кошки - + Can't interpolate: Не получилось интерпретировать: - + Delete Selection Удалить выделенное - + Select All Выделить всё - + Zoom In Приблизить - + Zoom Out Отдалить - + Zoom 1:1 Масштаб 1:1 - + Auto Zoom Автомасштаб - + Movement передвижения - + Tag &Red Пометить &красным - + Tag &Green Пометить &зелёным - + Tag &Blue Пометить &синим - + Tag &Yellow Пометить &желтым - + Tag &Text Изменить &метку - + Show Mad Cat Toolbar Показать бар кошки - + &Interpolate &Интерполировать - + Interpolate &Tags Интерполировать использу&я метки - + Toggle Tags &Visibility Переключить &видимость меток - + Toggle Selected &Items Visibility Переключить видимость выделенных &элементов - + Point Creation создание точки - + Point Move движение точки - + Debug Отладка - + Undo Отменить - + Redo Повторить - + Open Image Открыть изображение - + Open Image in New Tab Открыть изображение в новой вкладке - + Open Image Corresponding to Markup Открыть изображение от разметки - + View File Просмотр содержимого - + Copy Path Копировать путь - + Default OS Action Действие ОС по умолчанию - + Polygon Creation создание многоугольника - + Polygon Point Addition добавление точки в многоугольник - + Property Свойство - + Value Значение - - Select action or object - Выбирите действие или объект - - - - Add Number Plate - Добавить автомобильный номер - - - - Add Car Logo - Добавить автомобильный логотип - - - + GMC {} - General Markup Creator GMC {} - Универсальный разметчик - - &High - &Отл. - - - - &Normal - &Норм - - - - &Low - &Плохо - - - - N&one - Н&е видно - - - - &Number - &Номер - - - - &Region - &Регион - - - - &Country - &Страна - - - - None selected - Не выбрано - - - - &Quality - &Качество - - - - Set quality for all fields - Установить качество для всех полей - - - - &Usual High, High, None - &Обычное Выс., Выс., Не видно - - - + &Language / Язык &Language / Язык - + Delete Удалить - + Move selected files to trash? Переместить выбранные файлы в корзину? - + Tab Name Имя вкладки - + Close &All Закрыть &всё - + Close &Others Закрыть &другие - + &Fullscreen Полный &экран - + &Tile &Замостить - + &Tile Vertical Замостить &вертикально - + &Tile Horizontal Замостить &горизонтально - + &Tile Vertical Reverse Замостить &вертикально (обратный порядок) - + &Tile Horizontal Reverse Замостить &горизонтально (обратный порядок) - + &Cascade &Каскад - + Ne&xt &Следующее - + Pre&vious &Предыдущее - + Objects with every tag Объекты со всеми метками - + Objects with any tag Объекты с любой из меток - + Interpolate objects with tags (comma separated): Интерполировать объекты с метками (разделитель запятая): - - GMC Interpolcation - GMC Интерполяция - - - + Are you sure want to interpolate all objects in {} files? Вы уверен, что хотите проинтерполировать {} файлов? - + Copy Копировать - + Paste Вставить - + Se&ttings &Настройки - + Default По умолчанию - + Settings Настройки - + Checker Шахматка - + Label &font &Шрифт метки - + &Help &Документация - + Help not bundled. {} does not exist. Файл помощи не предоставлен. {} не существует. - + Polygon Points Deletion удаление точек многоугольника - + Polygon Edition редактирование многоугольника - + Rectangle Creation создание прямоугольника - + Rectangle Modification изменение прямоугольника - + &Add Tag: &Добавить метку: - + A&ppend Д&обавить - + Tag Modification изменение метки - + Deletion удаление - + Edit Tags Метки - + &Tags: &Метки: - - Save Plate Image - Сохранить изображение номерного знака - - - + &Paste Objects &Вставить объекты - + Paste {} objects into {} files? Вставить {} объектов в {} файл(ов)? - + Time between press and release with possible mouse movements that will be registered as single mouse click Время между нажатием и отпусканием с возможными движениями мыши, которые будут зарегистрированы как одиночный щелчок мыши - + Click Reaction &time (ms) Время реакции &щелчка (мс) + + + Drag side bar to make it visible + Потяниче мышкой для показа боковой панели + + + + GMC Interpolation + GMC Интерполяция + diff --git a/gmc/i18n/update.sh b/gmc/i18n/update.sh index b1dfa83..7d784d2 100755 --- a/gmc/i18n/update.sh +++ b/gmc/i18n/update.sh @@ -14,8 +14,6 @@ pylupdate5 \ gmc/markup_objects/rect.py \ gmc/markup_objects/tags.py \ gmc/mdi_area.py \ - gmc/schemas/number_plates/__init__.py \ - gmc/schemas/number_plates/plate_frame.py \ gmc/schemas/tagged_objects/__init__.py \ gmc/schemas/tagged_objects/markup_interpolation.py \ gmc/settings/dialog.py \ diff --git a/gmc/main_window.py b/gmc/main_window.py index 6dc6e23..8b75b54 100644 --- a/gmc/main_window.py +++ b/gmc/main_window.py @@ -1,5 +1,4 @@ # encoding: utf-8 -from typing import Any, Dict, Optional, Type from PyQt5 import QtCore, QtWidgets, QtGui from .mdi_area import MdiArea from .schemas import MarkupSchema, load_schema_cls, iter_schemas @@ -14,7 +13,7 @@ class MainWindow(QtWidgets.QMainWindow): - _schema_cls: Optional[Type[MarkupSchema]] = None + _schema_cls: type[MarkupSchema] | None = None _extra_args: GMCArguments def __init__( @@ -74,7 +73,7 @@ def _setup_ui(self): self.setCentralWidget(self._main_splitter) - def _sub_window_changed(self, window): + def _sub_window_changed(self, window: QtWidgets.QMdiSubWindow | None): if window is not None: try: on_activate = window.widget().on_activate diff --git a/gmc/markup_objects/__init__.py b/gmc/markup_objects/__init__.py index 62759eb..40223c9 100644 --- a/gmc/markup_objects/__init__.py +++ b/gmc/markup_objects/__init__.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Any, ClassVar, Dict, Tuple +from typing import Any, ClassVar from PyQt5 import QtGui, QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QGraphicsView, QGraphicsSceneMouseEvent @@ -13,7 +13,7 @@ class MarkupObjectMeta: PEN_SELECTED_DASHED = QtGui.QPen( Qt.GlobalColor.blue, 0, Qt.PenStyle.DashLine ) - ACTION_KEYS: ClassVar[Dict[Qt.Key, Tuple[int, ...]]] = {} + ACTION_KEYS: ClassVar[dict[Qt.Key, tuple[int, ...]]] = {} def __init__(self, *args: Any, **kwargs: Any) -> None: assert not hasattr(self, "_edit_mode") diff --git a/gmc/markup_objects/point.py b/gmc/markup_objects/point.py index 3dbefaf..7bd3d42 100644 --- a/gmc/markup_objects/point.py +++ b/gmc/markup_objects/point.py @@ -2,7 +2,7 @@ from ..views.image_view import ImageView from . import MarkupObjectMeta -from typing import Callable, Optional +from typing import Callable from PyQt5.QtCore import Qt, QRectF, QPointF, QCoreApplication tr: Callable[[str], str] = lambda text: QCoreApplication.translate( @@ -16,7 +16,7 @@ class MarkupPoint(QtWidgets.QGraphicsItem, MarkupObjectMeta): PEN_SELECTED = QtGui.QPen(Qt.GlobalColor.yellow, 4) CURSOR = QtGui.QCursor(QtGui.QPixmap("gmc:cursors/add_point.svg"), 6, 6) - def __init__(self, pos: Optional[QPointF] = None): + def __init__(self, pos: QPointF | None = None): super(MarkupPoint, self).__init__() if pos is None: pos = QPointF() diff --git a/gmc/markup_objects/polygon.py b/gmc/markup_objects/polygon.py index a818144..1496ec2 100644 --- a/gmc/markup_objects/polygon.py +++ b/gmc/markup_objects/polygon.py @@ -1,10 +1,11 @@ +from __future__ import annotations from PyQt5 import QtGui, QtWidgets from ..views.image_view import ImageView from . import MarkupObjectMeta from .moveable_diamond import MoveableDiamond from math import hypot -from typing import Any, Optional, List, Callable +from typing import Any, Callable from PyQt5.QtCore import Qt, QPointF, QCoreApplication, QRectF tr: Callable[[str], str] = lambda text: QCoreApplication.translate( @@ -22,7 +23,7 @@ class MarkupPolygon(QtWidgets.QGraphicsItem, MarkupObjectMeta): CURSOR = QtGui.QCursor(QtGui.QPixmap("gmc:cursors/add_line.svg"), 6, 6) UNDO = True # Allow subclasses to disable undoing - def __init__(self, polygon: Optional[QtGui.QPolygonF]) -> None: + def __init__(self, polygon: QtGui.QPolygonF | None) -> None: super().__init__() if polygon is None: polygon = QtGui.QPolygonF() @@ -170,7 +171,7 @@ def mouseDoubleClickEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): MarkupObjectMeta.mouseDoubleClickEvent(self, event) def notify_delete(self) -> None: - indices: List[int] = [] + indices: list[int] = [] for item in self.childItems(): if isinstance(item, MoveableDiamond) and item.isSelected(): indices.append(item.idx) @@ -296,7 +297,7 @@ def undo(self) -> None: class UndoPolygonDelPoints(QtWidgets.QUndoCommand): def __init__( - self, markup_polygon: MarkupPolygon, indices: List[int] + self, markup_polygon: MarkupPolygon, indices: list[int] ) -> None: indices.sort(reverse=True) polygon = markup_polygon._polygon diff --git a/gmc/markup_objects/quadrangle.py b/gmc/markup_objects/quadrangle.py index cdf136d..d7a9667 100644 --- a/gmc/markup_objects/quadrangle.py +++ b/gmc/markup_objects/quadrangle.py @@ -105,7 +105,7 @@ def notify_delete(self) -> None: for item in self.childItems() if isinstance(item, MoveableDiamond) ): - self.stop_edit_nodes() + self.ensure_edition_canceled() self.scene().removeItem(self) def _finish(self, view: ImageView): diff --git a/gmc/markup_objects/rect.py b/gmc/markup_objects/rect.py index 1839d30..dd58442 100644 --- a/gmc/markup_objects/rect.py +++ b/gmc/markup_objects/rect.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Tuple +from typing import Any from PyQt5 import QtGui, QtWidgets from PyQt5.QtCore import Qt, QRectF, QPointF, QSizeF, QElapsedTimer @@ -23,7 +23,7 @@ class MarkupRect(QtWidgets.QGraphicsItem, MarkupObjectMeta): } CURSOR = QtGui.QCursor(QtGui.QPixmap("gmc:cursors/add_rect.svg"), 6, 6) - def __init__(self, rect: Optional[QRectF] = None): + def __init__(self, rect: QRectF | None = None): super().__init__() if rect is None: rect = QRectF() @@ -83,7 +83,7 @@ def notify_delete(self) -> None: for item in self.childItems() if isinstance(item, MoveableDiamond) ): - self.stop_edit_nodes() + self.ensure_edition_canceled() self.scene().removeItem(self) def itemChange( @@ -96,7 +96,7 @@ def itemChange( self.on_deselect() return value - def _four_points(self) -> Tuple[QPointF, QPointF, QPointF, QPointF]: + def _four_points(self) -> tuple[QPointF, QPointF, QPointF, QPointF]: p = self._rect.topLeft() return ( p + QPointF(0.0, 0.0), @@ -211,7 +211,7 @@ def redo(self) -> None: def undo(self) -> None: mr = self._markup_rect - mr.stop_edit_nodes() + mr.ensure_edition_canceled() self._scene.removeItem(mr) @@ -226,12 +226,12 @@ def __init__( def redo(self) -> None: mr = self._markup_rect - mr.stop_edit_nodes() + mr.ensure_edition_canceled() mr._rect = QRectF(self._new_rect) mr.update() def undo(self) -> None: mr = self._markup_rect - mr.stop_edit_nodes() + mr.ensure_edition_canceled() mr._rect = QRectF(self._old_rect) mr.update() diff --git a/gmc/markup_objects/tags.py b/gmc/markup_objects/tags.py index 8d265d0..f8d71c2 100644 --- a/gmc/markup_objects/tags.py +++ b/gmc/markup_objects/tags.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, DefaultDict, Dict, List, Sequence, Set +from typing import Any, Callable, Sequence from PyQt5 import QtCore, QtGui, QtWidgets from collections import defaultdict from gmc.views.image_widget import ImageWidget @@ -97,8 +97,8 @@ class HasTags: del grad def __init__(self, *args: Any, tags: Sequence[str] = (), **kwargs: Any): - self._tags: Set[str] = set(tags) - self._draws: List[Callable[[QtGui.QPainter], None]] = [] + self._tags: set[str] = set(tags) + self._draws: list[Callable[[QtGui.QPainter], None]] = [] self._tag_polygon = QtGui.QPolygonF() self._last_fm = None super().__init__(*args, **kwargs) @@ -127,7 +127,7 @@ def _on_tags_changed(self): self.update() pass # for overriding - def data(self) -> Dict[str, Any]: + def data(self) -> dict[str, Any]: tags = sorted(self._tags) ret = {"data": super().data()} if tags: @@ -217,7 +217,7 @@ def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: def edit_tags( parent: ImageWidget, - items: List[MarkupObjectMeta], + items: list[MarkupObjectMeta], extra_tags: Sequence[str] = (), ) -> None: """ @@ -227,7 +227,7 @@ def edit_tags( """ if not items: return - tags: DefaultDict[str, int] = defaultdict( + tags: defaultdict[str, int] = defaultdict( int, {tag: 0 for tag in extra_tags} ) for item in items: @@ -323,7 +323,7 @@ def append_tag() -> None: class UndoTagModification(QtWidgets.QUndoCommand): def __init__( - self, items: List[HasTags], add: List[str], remove: List[str] + self, items: list[HasTags], add: list[str], remove: list[str] ): self._items = items self._add = add diff --git a/gmc/mdi_area.py b/gmc/mdi_area.py index 6e1f5ce..f139e77 100644 --- a/gmc/mdi_area.py +++ b/gmc/mdi_area.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations from PyQt5 import QtGui, QtCore, QtWidgets from .utils import get_icon, separator, new_action, tr @@ -26,16 +26,16 @@ def eventFilter( class MdiArea(QtWidgets.QMdiArea): - def __init__(self, parent: QtCore.QObject) -> None: + def __init__(self, parent: QtWidgets.QSplitter) -> None: + self._splitter = parent super().__init__( parent, objectName="mdi_area", - viewMode=QtWidgets.QMdiArea.TabbedView, + viewMode=QtWidgets.QMdiArea.ViewMode.TabbedView, ) - # Setting elide mode tab_bar: QtWidgets.QTabBar = self.findChild(QtWidgets.QTabBar) - tab_bar.setElideMode(Qt.ElideMiddle) + tab_bar.setElideMode(Qt.TextElideMode.ElideMiddle) tab_bar.setMovable(True) tab_bar.setTabsClosable(True) self._renamer = TabRenamer() @@ -111,12 +111,12 @@ def __init__(self, parent: QtCore.QObject) -> None: triggered=self.cascadeSubWindows, ) - KS = QtGui.QKeySequence + SK = QtGui.QKeySequence.StandardKey next_act = new_action( self, "next", tr("Ne&xt"), - (KS.NextChild,), + (SK.NextChild,), triggered=self.activateNextSubWindow, ) @@ -124,7 +124,7 @@ def __init__(self, parent: QtCore.QObject) -> None: self, "prev", tr("Pre&vious"), - (KS.PreviousChild,), + (SK.PreviousChild,), triggered=self.activatePreviousSubWindow, ) @@ -153,22 +153,20 @@ def close_others(self) -> None: def fullscreen(self, state: bool) -> None: if state: # go fullscreen - self.splitter: QtWidgets.QSplitter = self.parent() - self._splitter_state = self.splitter.saveState() - self.setParent(self.splitter.parent()) + self._splitter_state = self._splitter.saveState() + self.setParent(self._splitter.parent()) self.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint) self.showFullScreen() else: - self.setParent(self.splitter) - self.splitter.restoreState(self._splitter_state) - self.splitter = None + self.setParent(self._splitter) + self._splitter.restoreState(self._splitter_state) self.showNormal() def add( self, window: QtWidgets.QWidget, new: bool = False, - icon: Optional[str] = None, + icon: str | None = None, ) -> None: if ( not new @@ -232,3 +230,10 @@ def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: self.fullscreen_act.setChecked(False) else: return QtWidgets.QMdiArea.keyPressEvent(self, event) + + def paintEvent(self, event: QtGui.QPaintEvent) -> None: + super().paintEvent(event) + if not self._splitter.sizes()[0]: + with QtGui.QPainter(self.viewport()) as p: + p.setPen(Qt.GlobalColor.darkRed) + p.drawText(3, 20, tr("Drag side bar to make it visible")) diff --git a/gmc/schemas/__init__.py b/gmc/schemas/__init__.py index 9c0fdca..d9f2935 100644 --- a/gmc/schemas/__init__.py +++ b/gmc/schemas/__init__.py @@ -2,7 +2,7 @@ import sys import abc import importlib -from typing import Type, Iterable, Sequence +from typing import Iterable, Sequence from PyQt5 import QtWidgets, QtCore from ..application import GMCArguments @@ -49,7 +49,7 @@ def create_data_widget( raise NotImplementedError(cls) -def load_schema_cls(mod_name: str, path: str | None) -> Type[MarkupSchema]: +def load_schema_cls(mod_name: str, path: str | None) -> type[MarkupSchema]: if path is None: mod_path = "gmc.schemas." + mod_name else: diff --git a/gmc/schemas/tagged_objects/__init__.py b/gmc/schemas/tagged_objects/__init__.py index 808bef9..bca1b25 100644 --- a/gmc/schemas/tagged_objects/__init__.py +++ b/gmc/schemas/tagged_objects/__init__.py @@ -1,8 +1,9 @@ +from __future__ import annotations from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets from collections import defaultdict from math import hypot -from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type +from typing import Any, ClassVar from .. import MarkupSchema from ...markup_objects.polygon import EditableMarkupPolygon, MarkupObjectMeta @@ -37,7 +38,7 @@ class with_brush: "concrete": QtGui.QColor(255, 0, 255, 128), } - def __new__(cls, markup_object: Type[HasTags]): + def __new__(cls, markup_object: type[HasTags]): assert not hasattr(markup_object, "_current_color") assert issubclass(markup_object, HasTags), markup_object markup_object._current_color = Qt.NoBrush @@ -47,7 +48,7 @@ def __new__(cls, markup_object: Type[HasTags]): @staticmethod def on_tags_changed( - self: HasTags, brushes: Dict[str, QtGui.QColor] = _colors + self: HasTags, brushes: dict[str, QtGui.QColor] = _colors ) -> None: for tag in self._tags: color = brushes.get(tag) @@ -66,18 +67,18 @@ def on_tags_changed( HasTags._on_tags_changed(self) -def from_json_polygon(cls, schema: "TaggedObjects", data: Dict[str, Any]): +def from_json_polygon(cls, schema: TaggedObjects, data: dict[str, Any]): polygon = QtGui.QPolygonF([QtCore.QPointF(x, y) for x, y in data["data"]]) return cls(schema, polygon, tags=data.get("tags", ())) -def from_json_point(cls, schema: "TaggedObjects", data: Dict[str, Any]): +def from_json_point(cls, schema: "TaggedObjects", data: dict[str, Any]): point = QtCore.QPointF(*data["data"]) return cls(schema, point, tags=data.get("tags", ())) def from_json_rect( - cls: type["CustomRectangle"], schema: "TaggedObjects", data: Dict[str, Any] + cls: type["CustomRectangle"], schema: "TaggedObjects", data: dict[str, Any] ): rect = QtCore.QRectF(*data["data"]) return cls(schema, rect, tags=data.get("tags", ())) @@ -93,7 +94,12 @@ def __init__( super().__init__(frame, **kwargs) self._schema = schema - def paint(self, painter: QtGui.QPainter, option, widget) -> None: + def paint( + self, + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionGraphicsItem, + widget: QtWidgets.QWidget | None, + ) -> None: painter.setBrush(self._current_color) super().paint(painter, option, widget) @@ -173,7 +179,7 @@ class CustomRectangle(HasTags, MarkupRect): PEN_DASHED = QtGui.QPen(Qt.GlobalColor.green, 0, Qt.PenStyle.DashLine) from_json = classmethod(from_json_rect) - def __init__(self, schema, rect: Optional[QtCore.QRectF] = None, **kwargs): + def __init__(self, schema, rect: QtCore.QRectF | None = None, **kwargs): super().__init__(rect, **kwargs) self._schema = schema @@ -198,9 +204,9 @@ class CustomPath(HasTags, EditableMarkupPolygon): def __init__( self, schema: "TaggedObjects", - polygon: Optional[QtGui.QPolygonF] = None, + polygon: QtGui.QPolygonF | None = None, **kwargs: Any, - ): + ) -> None: if polygon is None: polygon = QtGui.QPolygonF() elif not isinstance(polygon, QtGui.QPolygonF): @@ -208,13 +214,13 @@ def __init__( super().__init__(polygon, **kwargs) self._schema = schema - def paint(self, painter: QtGui.QPainter, option, widget): + def paint(self, painter: QtGui.QPainter, option, widget) -> None: EditableMarkupPolygon.paint( self, painter, option, widget, f=QtGui.QPainter.drawPolyline ) self.draw_tags(painter) - def shape(self): + def shape(self) -> QtGui.QPainterPath: return EditableMarkupPolygon.shape(self, close=False) def tag_pos(self) -> QtCore.QPointF: @@ -241,7 +247,7 @@ def shape(self): class TaggedObjects(OneSourceOneDestination, MarkupSchema): - _cls_to_type: ClassVar[Dict[str, str]] = { + _cls_to_type: ClassVar[dict[str, str]] = { "CustomQuadrangle": "quad", "CustomLine": "line", "CustomSegment": "seg", @@ -350,11 +356,11 @@ def __init__(self, markup_window, default_actions): # so that user can go to the next file self._next_action = markup_window._next_action - self._unique_cache: Dict[str, Tuple[QtCore.QDateTime, Set[str]]] = {} + self._unique_cache: dict[str, tuple[QtCore.QDateTime, set[str]]] = {} @classmethod def create_data_widget( - cls, mdi_area: QtWidgets.QMdiArea, extra_args: Dict[str, Any] + cls, mdi_area: QtWidgets.QMdiArea, extra_args: dict[str, Any] ): splitter = super().create_data_widget(mdi_area, extra_args) iterpolate_act = new_action( @@ -472,7 +478,7 @@ def _default_action_cb_updated(self, text: str): last_used_default_action = text @classmethod - def _list_paths(cls) -> Tuple[List[str], List[str]]: + def _list_paths(cls) -> tuple[list[str], list[str]]: dst_dir = cls._destination_widget.get_root_qdir() relative_path = cls._source_widget.get_root_qdir().relativeFilePath source_view = cls._source_widget.view() @@ -500,7 +506,7 @@ def _on_paste_into_files(cls) -> None: image_paths, markup_paths = cls._list_paths() new_objects = clipboard.get_objects() - def convert_to_markup(obj: Dict[str, Any]): + def convert_to_markup(obj: dict[str, Any]): obj["type"] = cls._cls_to_type[obj.pop("_class")] return obj @@ -522,7 +528,7 @@ def convert_to_markup(obj: Dict[str, Any]): else: size = load_pixmap(image_path).size() data = {"objects": [], "size": [size.width(), size.height()]} - existing_objects: List[Any] = data["objects"] + existing_objects: list[Any] = data["objects"] filtered_new_objects = [ obj for obj in new_objects if obj not in existing_objects ] @@ -553,7 +559,7 @@ def _on_unique_tag(self): qdir = QtCore.QFileInfo(self._dst_markup_path).dir() dir_filter = qdir.Files | qdir.NoDotAndDotDot - all_sets: List[Set[str]] = [] + all_sets: list[set[str]] = [] cache = self._unique_cache for fi in qdir.entryInfoList(dir_filter, qdir.Name): if fi.suffix() != "json": @@ -571,13 +577,13 @@ def _on_unique_tag(self): except Exception: print("invalid json", absolute_path, "(ignored)") continue - tags: Set[str] = set() + tags: set[str] = set() for obj in data.get("objects", ()): tags |= set(obj.get("tags", ())) all_sets.append(tags) cache[absolute_path] = (last_modified, tags) - all_tags: Set[str] = set() + all_tags: set[str] = set() for item in self._image_widget.scene().items(): if isinstance(item, HasTags): all_tags |= item.get_tags() @@ -624,7 +630,7 @@ def _trigger_tag_edit(self) -> None: self._image_widget, self._get_selected_items(), self._user_tags ) - def _get_selected_items(self) -> List[HasTags]: + def _get_selected_items(self) -> list[HasTags]: try: all_items = self._image_widget.scene().selectedItems() except RuntimeError: @@ -732,7 +738,7 @@ def save_markup(self, force: bool = True) -> None: dump_json(self._dst_markup_path, markup) self._original_markup = markup - def _get_markup(self) -> Dict[str, Any]: + def _get_markup(self) -> dict[str, Any]: markup = defaultdict(list, self._original_markup) markup["objects"] = [] markup["size"] = self._size diff --git a/gmc/schemas/tagged_objects/markup_interpolation.py b/gmc/schemas/tagged_objects/markup_interpolation.py index 26cff23..f3294bd 100644 --- a/gmc/schemas/tagged_objects/markup_interpolation.py +++ b/gmc/schemas/tagged_objects/markup_interpolation.py @@ -4,12 +4,8 @@ from typing import ( Any, Callable, - Dict, Iterator, - List, Literal, - Optional, - Tuple, TypedDict, ) from ...utils.json import dump as dump_json @@ -28,7 +24,7 @@ QtGui.QImage.Format.Format_Indexed8: 1, } -IMAGES_CACHE: Dict[str, Any] = OrderedDict() +IMAGES_CACHE: dict[str, Any] = OrderedDict() class NumpyHolder: @@ -38,16 +34,16 @@ def __init__(self, interface): class GMCItem_(TypedDict): type: Literal["quad", "rect", "point"] - data: List[Any] + data: list[Any] class GMCItem(GMCItem_, total=False): - tags: List[str] + tags: list[str] class GMCMarkup(TypedDict): - size: Tuple[int, int] - objects: List[GMCItem] + size: tuple[int, int] + objects: list[GMCItem] def load_image(path: str) -> Any: @@ -74,7 +70,7 @@ def load_image(path: str) -> Any: return nparray -def read_markup(file_paths: List[str]) -> Iterator[Any]: +def read_markup(file_paths: list[str]) -> Iterator[Any]: for path in file_paths: try: with open(path, "r", encoding="utf-8") as inp: @@ -109,8 +105,8 @@ def get_filter_input(): class iter_interpolatable_objects: def __new__( - cls, markup_list: List[Any], use_filter: bool - ) -> Iterator[Tuple[GMCItem, GMCItem, int, int]]: + cls, markup_list: list[Any], use_filter: bool + ) -> Iterator[tuple[GMCItem, GMCItem, int, int]]: objects = cls._iter_all_objects(markup_list) if use_filter: result, tags = get_filter_input() @@ -132,8 +128,8 @@ def __new__( @staticmethod def _iter_all_objects( - markup_list: List[GMCMarkup], - ) -> Iterator[Tuple[int, GMCItem]]: + markup_list: list[GMCMarkup], + ) -> Iterator[tuple[int, GMCItem]]: for idx, markup in enumerate(markup_list): for obj in markup.get("objects", ()): assert isinstance(obj, dict), obj @@ -141,13 +137,13 @@ def _iter_all_objects( yield idx, obj @staticmethod - def _key(obj: GMCItem) -> Tuple[str, Tuple[str, ...]]: + def _key(obj: GMCItem) -> tuple[str, tuple[str, ...]]: return obj["type"], tuple(obj.get("tags", ())) @classmethod def _find_next_object( - cls, obj: GMCItem, markup_list: List[GMCItem] - ) -> Tuple[int, Optional[GMCItem]]: + cls, obj: GMCItem, markup_list: list[GMCItem] + ) -> tuple[int, GMCItem | None]: """ Find first `obj` in `markup_list` using `cls.key` """ @@ -162,10 +158,10 @@ def _find_next_object( @staticmethod def filter_by_tags( - objects: Iterator[Tuple[int, GMCItem]], - tags: List[str], + objects: Iterator[tuple[int, GMCItem]], + tags: list[str], f: Callable[[Iterator[bool]], bool], - ) -> Iterator[Tuple[int, GMCItem]]: + ) -> Iterator[tuple[int, GMCItem]]: for idx, obj in objects: obj_tags = obj.get("tags") if not obj_tags: # because f(empty list) is always true @@ -175,7 +171,7 @@ def filter_by_tags( def interpolate_many( - image_paths: List[str], markup_paths: List[str], use_filter: bool + image_paths: list[str], markup_paths: list[str], use_filter: bool ) -> None: MB = QtWidgets.QMessageBox if ( @@ -192,8 +188,8 @@ def interpolate_many( ): return - markup_list: List[Any] = list(read_markup(markup_paths)) - save: Dict[str, Dict[str, Any]] = {} + markup_list: list[Any] = list(read_markup(markup_paths)) + save: dict[str, dict[str, Any]] = {} objects = iter_interpolatable_objects(markup_list, use_filter) for obj, next_obj, idx, shift in objects: if shift == 0: @@ -219,7 +215,7 @@ def interpolate_many( def prepare_obj( obj: GMCItem, -) -> Tuple[Tuple[float, float], Tuple[float, float]]: +) -> tuple[tuple[float, float], tuple[float, float]]: if obj["type"] == "point": size = (20, 20) # TODO: size as parameter center = tuple(obj["data"]) @@ -234,7 +230,7 @@ def prepare_obj( return center, size -def move_obj(pt: Tuple[float, float], obj: GMCItem) -> GMCItem: +def move_obj(pt: tuple[float, float], obj: GMCItem) -> GMCItem: obj_moved = copy.deepcopy(obj) if obj["type"] == "point": @@ -251,11 +247,11 @@ def move_obj(pt: Tuple[float, float], obj: GMCItem) -> GMCItem: def predict_cv( - objects: List[Any], frame1_path: str, frame2_path: str -) -> List[GMCItem]: + objects: list[Any], frame1_path: str, frame2_path: str +) -> list[GMCItem]: frame1 = load_image(frame1_path) frame2 = load_image(frame2_path) - prediction: List[GMCItem] = [] + prediction: list[GMCItem] = [] for obj in objects: center, size = prepare_obj(obj) center = np.atleast_2d(np.array(center)).astype(np.float32) @@ -283,7 +279,7 @@ def merge_two_obj(obj1: GMCItem, obj2: GMCItem, k: float) -> GMCItem: return merged -def normalize_rects(objects: List[GMCItem]) -> List[GMCItem]: +def normalize_rects(objects: list[GMCItem]) -> list[GMCItem]: for obj in objects: if obj["type"] == "rect": d = obj["data"] @@ -297,10 +293,10 @@ def normalize_rects(objects: List[GMCItem]) -> List[GMCItem]: def interpolate_core( - first_objects: List[GMCItem], - last_objects: List[GMCItem], - file_paths: List[str], -) -> List[List[GMCItem]]: + first_objects: list[GMCItem], + last_objects: list[GMCItem], + file_paths: list[str], +) -> list[list[GMCItem]]: assert file_paths, file_paths # normalize rects @@ -308,14 +304,14 @@ def interpolate_core( last_objects = normalize_rects(last_objects) # forward loop - forw: List[List[GMCItem]] = [] + forw: list[list[GMCItem]] = [] forw.append(first_objects) for file1, file2 in zip(file_paths[:-1], file_paths[1:]): new_obj = predict_cv(forw[-1], file1, file2) forw.append(new_obj) # back loop - back: List[List[GMCItem]] = [] + back: list[list[GMCItem]] = [] back.append(last_objects) for file1, file2 in reversed(list(zip(file_paths[:-1], file_paths[1:]))): new_obj = predict_cv(back[-1], file2, file1) @@ -323,14 +319,14 @@ def interpolate_core( back = back[::-1] # merge - ret: List[List[GMCItem]] = [] + ret: list[list[GMCItem]] = [] ret.append(first_objects) n_frames = len(forw) - 1 for frame_idx, (frame1_obj, frame2_obj) in enumerate( list(zip(forw, back))[1:-1] ): k = float(n_frames - frame_idx - 1) / n_frames - merged_frame: List[GMCItem] = [] + merged_frame: list[GMCItem] = [] for obj1, obj2 in zip(frame1_obj, frame2_obj): m_obj = merge_two_obj(obj1, obj2, k) merged_frame.append(m_obj) @@ -341,8 +337,8 @@ def interpolate_core( def intersect_objects( - a: List[GMCItem], b: List[GMCItem] -) -> Tuple[List[GMCItem], List[GMCItem]]: + a: list[GMCItem], b: list[GMCItem] +) -> tuple[list[GMCItem], list[GMCItem]]: # unused function a_objects = { ((obj["type"],), tuple(obj["tags"])): idx for idx, obj in enumerate(a) @@ -362,7 +358,7 @@ def intersect_objects( def main_interpolate( first_frame_markup_path: str, last_frame_markup_path: str, - file_paths: List[str], + file_paths: list[str], ) -> None: # unused function with open(first_frame_markup_path) as f: diff --git a/gmc/settings/__init__.py b/gmc/settings/__init__.py index 628df0d..16e9a1b 100644 --- a/gmc/settings/__init__.py +++ b/gmc/settings/__init__.py @@ -1,5 +1,5 @@ -# encoding: utf-8 -from typing import Any, ClassVar, Dict, Type, Union +from __future__ import annotations +from typing import Any, ClassVar from PyQt5.QtCore import QSettings, QByteArray from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QMainWindow @@ -13,7 +13,7 @@ class Settings: # `settings` as a public attribute, so custom properties are possible settings = QSettings("Visillect", "GMC") - _defaults: ClassVar[Dict[str, Union[QColor, QFont, int]]] = { + _defaults: ClassVar[dict[str, QColor | QFont | int]] = { "bg_1": QColor(0), "bg_2": QColor(0xF, 0xF, 0xF), "font_label": default_font_label, @@ -33,7 +33,7 @@ def get_default(self, key: str): @classmethod def value( - cls, key: str, default: Any = None, type_: Type[Any] = str + cls, key: str, default: Any = None, type_: type[Any] = str ) -> Any: """for values that are rarely needed""" if key in cls._defaults: @@ -45,7 +45,7 @@ def sync(self) -> None: self.set_value(attr, getattr(self, attr)) self.settings.sync() - def set_value(self, key: str, value: Any, type_: Type[Any] = str) -> None: + def set_value(self, key: str, value: Any, type_: type[Any] = str) -> None: if value is not None: if key in self._defaults: type_ = type(self._defaults[key]) diff --git a/gmc/settings/dialog.py b/gmc/settings/dialog.py index f9be7fd..e030d02 100644 --- a/gmc/settings/dialog.py +++ b/gmc/settings/dialog.py @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Any from PyQt5 import QtCore, QtGui, QtWidgets from gmc.settings import settings from ..utils import tr @@ -62,7 +62,7 @@ def _on_click(self) -> None: def add_default( form: QtWidgets.QFormLayout, label: str, - widget: Union[QtWidgets.QSpinBox, FontWidget, ColorWidget], + widget: QtWidgets.QSpinBox | FontWidget | ColorWidget, attr: str, ) -> None: """ """ diff --git a/gmc/utils/__init__.py b/gmc/utils/__init__.py index 082e454..d93f292 100644 --- a/gmc/utils/__init__.py +++ b/gmc/utils/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, Tuple, Union +from typing import Any from PyQt5.QtWidgets import QAction from PyQt5.QtGui import QIcon, QKeySequence from PyQt5.QtCore import QObject, Qt, QCoreApplication @@ -27,9 +27,9 @@ def get_icon(name: str) -> QIcon: def new_action( parent: QObject, - icon: Union[str, QIcon], + icon: str | QIcon, text: str, - shortcuts: Tuple[Union[str, QKeySequence.StandardKey], ...] = (), + shortcuts: tuple[str | QKeySequence.StandardKey | Qt.Key, ...] = (), **kwargs: Any, ) -> QAction: sequences = [QKeySequence(s) for s in shortcuts] @@ -45,7 +45,7 @@ def new_action( parent, toolTip=f"{text.replace('&', '')} ({shrtctext})", **kwargs, # type: ignore[call-overload] - ) # type: QAction + ) action.setShortcuts(sequences) return action diff --git a/gmc/utils/clipboard.py b/gmc/utils/clipboard.py index e8cf31c..2d726d5 100644 --- a/gmc/utils/clipboard.py +++ b/gmc/utils/clipboard.py @@ -4,13 +4,13 @@ from one scheme to another. """ -from typing import Optional, List, Any +from typing import Any from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QMimeData from json import dumps, loads -def set_objects(data_list: List[Any]) -> None: +def set_objects(data_list: list[Any]) -> None: json_data = dumps( data_list, allow_nan=False, ensure_ascii=False, separators=",:" ) @@ -19,7 +19,7 @@ def set_objects(data_list: List[Any]) -> None: QApplication.clipboard().setMimeData(mime_data) -def get_objects() -> Optional[List[Any]]: +def get_objects() -> list[Any] | None: mime_data = QApplication.clipboard().mimeData() json_bytes = mime_data.data("application/gmc-json") if not json_bytes: diff --git a/gmc/utils/dicts.py b/gmc/utils/dicts.py index aff62aa..bb7c994 100644 --- a/gmc/utils/dicts.py +++ b/gmc/utils/dicts.py @@ -1,5 +1,5 @@ from collections.abc import Mapping, Sequence -from typing import Any, Dict, Tuple, Type +from typing import Any from .json import ZeroDict @@ -7,7 +7,7 @@ def dicts_are_equal( a: Any, b: Any, eps: float = 1e-7, - _basic_types: Tuple[Type[Any], ...] = (int, str, type(None)), + _basic_types: tuple[type[Any], ...] = (int, str, type(None)), ) -> bool: if isinstance(a, _basic_types) or isinstance(b, _basic_types): return a == b @@ -35,16 +35,16 @@ def dicts_are_equal( raise Exception("Invalid dict objects ({} vs {})".format(type(a), type(b))) -assert dicts_are_equal([1, 2, 3], (1, 2, 3)) +# assert dicts_are_equal([1, 2, 3], (1, 2, 3)) -def dicts_merge(d: Dict[str, Any], u: Dict[str, Any]): +def dicts_merge(d: dict[str, Any], u: dict[str, Any]): for k, v in u.items(): - if isinstance(v, Dict): + if isinstance(v, dict): d[k] = dicts_merge(d[k], v) else: d[k] = u[k] return d -assert dicts_merge({"a": 1}, {"a": 2, "c": 3}) == {"a": 2, "c": 3} +# assert dicts_merge({"a": 1}, {"a": 2, "c": 3}) == {"a": 2, "c": 3} diff --git a/gmc/utils/json.py b/gmc/utils/json.py index 862c587..239b1be 100644 --- a/gmc/utils/json.py +++ b/gmc/utils/json.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict +from typing import Any from PyQt5.QtWidgets import QMessageBox, QWidget from PyQt5.QtCore import QFileInfo from . import CantOpenMarkup @@ -26,7 +26,7 @@ def load(json_filename: str, widget: QWidget): return ZeroDict() # special class so `dicts_are_equal` returns False -def dump(json_filename: str, data: Dict[str, Any]) -> None: +def dump(json_filename: str, data: dict[str, Any]) -> None: the_qdir = QFileInfo(json_filename).absoluteDir() the_qdir.mkpath(the_qdir.absolutePath()) diff --git a/gmc/utils/read_properties.py b/gmc/utils/read_properties.py index bb7fe8b..04b1e06 100644 --- a/gmc/utils/read_properties.py +++ b/gmc/utils/read_properties.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Iterator, Sequence +from typing import Any, Iterator, Sequence from PyQt5.QtCore import QFileInfo, QDir from PyQt5.QtWidgets import QWidget from .json import load @@ -17,8 +17,8 @@ def read_properties( widget: QWidget, filename: str = ".gmc_properties.json", depth: int = 6, -) -> Dict[str, Any]: - ret: Dict[str, Any] = {} +) -> dict[str, Any]: + ret: dict[str, Any] = {} for path in paths: the_dir = QFileInfo(path).dir() properties_paths = list(_paths(the_dir, filename, depth)) diff --git a/gmc/views/filesystem_view.py b/gmc/views/filesystem_view.py index eb5408b..b5a94f1 100644 --- a/gmc/views/filesystem_view.py +++ b/gmc/views/filesystem_view.py @@ -1,4 +1,4 @@ -from typing import Any, Iterable, List, Optional, Tuple +from typing import Any, Iterable from PyQt5 import QtCore, QtGui, QtWidgets from ..utils import separator, new_action, tr @@ -8,7 +8,7 @@ class FilesystemView(QtWidgets.QTreeView): _valid_root: bool = False - def __init__(self, actions: Optional[List[QtWidgets.QAction]] = None): + def __init__(self, actions: Iterable[QtWidgets.QAction] | None = None): super().__init__( headerHidden=True, selectionMode=self.ExtendedSelection, @@ -108,7 +108,7 @@ def _update_actions(self) -> None: for action in self._actions: action.setEnabled(all_file) - def selected_files(self, *_args: Any) -> List[str]: + def selected_files(self, *_args: Any) -> list[str]: return [ info.filePath() for info in self._selected_info_map @@ -120,7 +120,7 @@ def _selected_info_map(self) -> Iterable[QtCore.QFileInfo]: return map(self.model().fileInfo, self.selectedIndexes()) @property - def selected_files_relative(self) -> List[str]: + def selected_files_relative(self) -> list[str]: root_dir: QtCore.QDir = self.model().root_dir_qdir return [ root_dir.relativeFilePath(info.filePath()) @@ -130,7 +130,7 @@ def selected_files_relative(self) -> List[str]: def all_files_in( self, path: QtCore.QDir, src_path: QtCore.QDir - ) -> List[str]: + ) -> list[str]: """ :param path: `QDir` to list files in :param src_path: base `QDir` instance (== self.get_root_qdir()) @@ -166,8 +166,9 @@ def set_path(self, path: str) -> None: index = model.setRootPath(path) self.setRootIndex(index) - def set_name_filters(self, name_filters: Tuple[str, ...]): - self.model().setNameFilters(name_filters) + def set_name_filters(self, name_filters: Iterable[str]): + model: MinimalFileSystemModel = self.model() + model.setNameFilters(name_filters) def user_select_path( self, title: str, callback=lambda view, path: view.set_path(path) @@ -184,7 +185,8 @@ def user_select_path( class MinimalFileSystemModel(QtWidgets.QFileSystemModel): - _sel_path = _root_dir = None + _sel_path: QtCore.QDir | None = None + _root_dir: QtCore.QDir | None = None HIGHLIGHT_BRUSH = QtGui.QBrush(QtGui.QColor(68, 170, 0, 90)) def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -226,7 +228,7 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any: return QtWidgets.QFileSystemModel.data(self, index, role) @property - def root_dir_qdir(self) -> Optional[QtCore.QDir]: + def root_dir_qdir(self) -> QtCore.QDir | None: # to check that directory is open, allow '_root_dir' to be None return self._root_dir # or QtCore.QDir() diff --git a/gmc/views/filesystem_widget.py b/gmc/views/filesystem_widget.py index 67f9320..d93878e 100644 --- a/gmc/views/filesystem_widget.py +++ b/gmc/views/filesystem_widget.py @@ -1,4 +1,4 @@ -from typing import List, NamedTuple, Sequence +from typing import NamedTuple, Sequence from PyQt5 import QtCore, QtWidgets from .filesystem_view import FilesystemView from ..help_label import HelpLabel @@ -22,7 +22,7 @@ def __init__( self, parent: QtWidgets.QWidget, title: FilesystemTitle, - actions: List[QtWidgets.QAction], + actions: list[QtWidgets.QAction], ): assert isinstance(title, FilesystemTitle), title super().__init__( @@ -65,8 +65,8 @@ def __init__( self, parent: QtWidgets.QWidget, title: FilesystemTitle, - captions: List[str], - actions_list: Sequence[List[QtWidgets.QAction]], + captions: list[str], + actions_list: Sequence[list[QtWidgets.QAction]], N: int, ) -> None: super().__init__( @@ -114,7 +114,7 @@ def _root_changed(self) -> None: self._layout.addWidget(QtWidgets.QLabel(caption)) self._layout.addWidget(view) - def views(self) -> List[FilesystemView]: + def views(self) -> list[FilesystemView]: return self._views def get_root_qdir(self) -> QtCore.QDir: diff --git a/gmc/views/image_view.py b/gmc/views/image_view.py index d25f3c8..5b9c542 100644 --- a/gmc/views/image_view.py +++ b/gmc/views/image_view.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from __future__ import annotations +from typing import Any, Callable from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QPointF @@ -8,16 +9,16 @@ Qt = QtCore.Qt -def no_action(event: QtGui.QMouseEvent, self: "ImageView") -> bool: +def no_action(event: QtGui.QMouseEvent, self: ImageView) -> bool: return False -def no_cancel(view: "ImageView") -> None: +def no_cancel(view: ImageView) -> None: pass class MarkupScene(QtWidgets.QGraphicsScene): - MOVEMENTS: Dict[int, QPointF] = { + MOVEMENTS: dict[int, QPointF] = { Qt.Key.Key_Up: QPointF(0, -1), # Qt.Key.Key_8: QPointF(0, -1), Qt.Key.Key_Right: QPointF(1, 0), @@ -27,13 +28,13 @@ class MarkupScene(QtWidgets.QGraphicsScene): Qt.Key.Key_Left: QPointF(-1, 0), # Qt.Key_4: QPointF(-1, 0), } - _current_object: Optional["MarkupObjectMeta"] = None + _current_object: MarkupObjectMeta | None = None # False: don't try to show cursor # None: cursor not on window # QPointF: draw cursor - _cross_pos: Optional[Union[QPointF, bool]] = False + _cross_pos: QPointF | bool | None = False _cross_pen = QtGui.QPen( QtGui.QColor(255, 32, 32, 224), 0.0, Qt.PenStyle.CustomDashLine ) @@ -97,7 +98,7 @@ def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: vec = vec * 0.25 elif modif & Qt.ShiftModifier: vec = vec * 4.0 - moved: List[MarkupObjectMeta] = [] + moved: list[MarkupObjectMeta] = [] for item in selected: if isinstance(item, MarkupObjectMeta): moved.append(item) @@ -116,7 +117,7 @@ def _on_selection_changed(self) -> None: self.parent().copy_action.setEnabled(bool(items)) def set_current_markup_object( - self, markup_object: Optional["MarkupObjectMeta"] + self, markup_object: MarkupObjectMeta | None ) -> None: """ :purpose: unknown @@ -125,14 +126,11 @@ def set_current_markup_object( """ if self._current_object is markup_object: return - if ( - self._current_object is not None - and self._current_object.in_edit_mode() - ): - self._current_object.stop_edit_nodes() + if self._current_object is not None: + self._current_object.ensure_edition_canceled() self._current_object = markup_object - def show_cross_cursor(self, pos: Optional[QPointF]) -> None: + def show_cross_cursor(self, pos: QPointF | None) -> None: self._cross_pos = pos self.invalidate(QtCore.QRectF(), self.SceneLayer.ForegroundLayer) @@ -233,7 +231,7 @@ def unset_all_events(self) -> None: self.set_mouse_doubleclick(None) self.set_cancel(None) - def set_markup_object(self, cls: Type["MarkupObjectMeta"]) -> None: + def set_markup_object(self, cls: type[MarkupObjectMeta]) -> None: """ :param cls: callable, that returns markup object. """ @@ -245,30 +243,30 @@ def set_markup_object(self, cls: Type["MarkupObjectMeta"]) -> None: obj = cls() # SHOULD keep itself alive obj.attach(self) - def set_mouse_press(self, func: Optional[MouseCallback]) -> None: + def set_mouse_press(self, func: MouseCallback | None) -> None: self._current_mouse_press = func or no_action - def set_mouse_release(self, func: Optional[MouseCallback]) -> None: + def set_mouse_release(self, func: MouseCallback | None) -> None: self._current_mouse_release = func or no_action - def set_mouse_doubleclick(self, func: Optional[MouseCallback]) -> None: + def set_mouse_doubleclick(self, func: MouseCallback | None) -> None: self._current_mouse_doubleclick = func or no_action - def set_mouse_move(self, func: Optional[MouseCallback]) -> None: + def set_mouse_move(self, func: MouseCallback | None) -> None: self._current_mouse_move = func or self._dummy_mouse_move - def set_cancel(self, func: Optional[CancelCallback]) -> None: + def set_cancel(self, func: CancelCallback | None) -> None: self._current_cancel = func or no_cancel def _delete(self) -> None: - deleted_items: List[MarkupObjectMeta] = [] + deleted_items: list[MarkupObjectMeta] = [] for item in self._scene.selectedItems(): # we delete even `MoveableDiamond` because how otherwise delete it if hasattr(item, "delete"): # we check that item's scene exists, because `delete` method # can remove other selected items. if item.delete() and item.scene(): - item.stop_edit_nodes() + item.ensure_edition_canceled() self._scene.removeItem(item) if isinstance(item, MarkupObjectMeta): deleted_items.append(item) @@ -278,11 +276,11 @@ def _delete(self) -> None: ) def _copy(self) -> None: - data_list: List[Any] = [] + data_list: list[dict[Any, Any]] = [] for item in self._scene.selectedItems(): if hasattr(item, "data"): try: - data: Dict = item.data() + data: dict[Any, Any] = item.data() except TypeError: continue # data(self, int): not enough arguments data["_class"] = item.__class__.__name__ @@ -321,7 +319,7 @@ def set_pixmap( def get_zoom_actions( self, - ) -> Tuple[ + ) -> tuple[ QtWidgets.QAction, QtWidgets.QAction, QtWidgets.QAction, @@ -331,28 +329,28 @@ def get_zoom_actions( self, "zoom_in", tr("Zoom In"), - [Qt.Key_Plus, Qt.Key_Equal], + (Qt.Key.Key_Plus, Qt.Key.Key_Equal), triggered=lambda: self._scale_view(1.2), ) zoom_out = new_action( self, "zoom_out", tr("Zoom Out"), - (Qt.Key_Minus, Qt.Key_Underscore), + (Qt.Key.Key_Minus, Qt.Key.Key_Underscore), triggered=lambda: self._scale_view(1 / 1.2), ) zoom_1_1 = new_action( self, "zoom_1_1", tr("Zoom 1:1"), - (Qt.Key_0, Qt.Key_Insert), + (Qt.Key.Key_0, Qt.Key.Key_Insert), triggered=lambda: self._set_scale(1.0), ) self._auto_zoom_act = new_action( self, "zoom_auto", tr("Auto Zoom"), - (Qt.Key_ParenRight,), + (Qt.Key.Key_ParenRight,), checkable=True, triggered=self._auto_zoom, ) @@ -497,7 +495,7 @@ def hide_cross_cursor(self): class UndoObjectsMovement(QtWidgets.QUndoCommand): def __init__( - self, items: List[QtWidgets.QGraphicsItem], vec: QPointF + self, items: list[QtWidgets.QGraphicsItem], vec: QPointF ) -> None: self._items = items self._prev_poses = [item.pos() for item in items] @@ -517,7 +515,7 @@ class UndoObjectsDelete(QtWidgets.QUndoCommand): def __init__( self, scene: QtWidgets.QGraphicsScene, - items: List[QtWidgets.QGraphicsItem], + items: list[QtWidgets.QGraphicsItem], ) -> None: self._scene = scene self._items = items diff --git a/gmc/views/image_widget.py b/gmc/views/image_widget.py index b2b09f8..47154ba 100644 --- a/gmc/views/image_widget.py +++ b/gmc/views/image_widget.py @@ -1,4 +1,4 @@ -from typing import Any, List, Tuple, Union +from typing import Any from PyQt5 import QtCore, QtGui, QtWidgets from ..utils import separator, new_action from .image_view import ImageView @@ -13,7 +13,7 @@ class ImageWidget(QtWidgets.QWidget): on_paste = QtCore.pyqtSignal(list) def __init__( - self, default_actions: List[QtWidgets.QAction], view_cls=ImageView + self, default_actions: list[QtWidgets.QAction], view_cls=ImageView ): super().__init__() self._default_actions = default_actions @@ -28,7 +28,7 @@ def add_user_action( self, name: str, shortcut: str, - icon: Union[str, QtGui.QIcon], + icon: str | QtGui.QIcon, **kwargs: Any, ) -> QtWidgets.QAction: action = new_action(self, icon, name, (shortcut,), **kwargs) @@ -43,7 +43,7 @@ def add_action(self, action: QtWidgets.QAction) -> None: def add_markup_action( self, name: str, - shortcut: Union[str, Tuple[str, ...]], + shortcut: str | tuple[str, ...], icon: str, markup_object, **kwargs: Any, diff --git a/gmc/views/properties_view.py b/gmc/views/properties_view.py index f9efc33..8fdf2ee 100644 --- a/gmc/views/properties_view.py +++ b/gmc/views/properties_view.py @@ -1,12 +1,13 @@ -from PyQt5 import QtCore, QtWidgets +from __future__ import annotations +from PyQt5 import QtCore, QtWidgets, QtGui from ..utils import get_icon, tr -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, TypeVar Qt = QtCore.Qt def _check_state( - value: Optional[bool], + value: bool | None, checked: Qt.CheckState = Qt.Checked, unchecked: Qt.CheckState = Qt.Unchecked, partially: Qt.CheckState = Qt.PartiallyChecked, @@ -15,8 +16,8 @@ def _check_state( class BaseItem: - parent: "BaseItem" - children: List["BaseItem"] + parent: BaseItem + children: list[BaseItem] def row_count(self) -> int: return len(self.children) @@ -43,7 +44,7 @@ def delete(self) -> None: model.removeRows(position, 1, self.parent.index(model, 0)) def insert( - self, model: "PropertiesModel", items: list["BaseItem"], idx: int = -1 + self, model: PropertiesModel, items: list[BaseItem], idx: int = -1 ): total = len(items) first = len(self.children) if idx == -1 else idx @@ -57,7 +58,7 @@ def insert( self.children[idx:idx] = items model.endInsertRows() - def get_model(self) -> "PropertiesModel": + def get_model(self) -> PropertiesModel: item = self while not hasattr(item, "model"): item = item.parent @@ -78,7 +79,7 @@ class PropertyItemBase(BaseItem): has_children = False flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable - def __init__(self, parent: BaseItem, kwargs: Dict[str, str]) -> None: + def __init__(self, parent: BaseItem, kwargs: dict[str, str]) -> None: self.parent = parent self.name = kwargs.pop("name") self._display_name = kwargs.pop("display", self.name) @@ -96,24 +97,26 @@ def display_role1(self) -> str: def set_editor_value( self, - widget: Union[ - QtWidgets.QDoubleSpinBox, QtWidgets.QCheckBox, QtWidgets.QLineEdit - ], + widget: ( + QtWidgets.QDoubleSpinBox + | QtWidgets.QCheckBox + | QtWidgets.QLineEdit + ), index: QtCore.QModelIndex, ) -> None: """common method""" - widget.setValue(index.data(Qt.EditRole)) + widget.setValue(index.data(Qt.ItemDataRole.EditRole)) - def edit_role(self) -> Union[str, int, bool]: + def edit_role(self) -> str | int | bool: """common method""" return self._value @property - def value(self) -> Union[str, int, bool, List[str]]: + def value(self) -> str | int | bool | list[str]: """common method; value to return as property""" return self._value - def set_edit(self, value: Union[str, int, bool]) -> None: + def set_edit(self, value: str | int | bool) -> None: """common method""" self._value = value @@ -121,7 +124,7 @@ def set_edit(self, value: Union[str, int, bool]) -> None: TWidget = TypeVar("TWidget", bound=QtWidgets.QWidget) -def _apply_kwargs(widget: TWidget, kwargs: Dict[str, Any]) -> TWidget: +def _apply_kwargs(widget: TWidget, kwargs: dict[str, Any]) -> TWidget: for key, val in kwargs.items(): attr = "set" + key.capitalize() getattr(widget, attr)(val) @@ -129,7 +132,7 @@ def _apply_kwargs(widget: TWidget, kwargs: Dict[str, Any]) -> TWidget: class IntegerItem(PropertyItemBase): - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]) -> None: self._value = kwargs.pop("value", 0) super().__init__(parent, kwargs) @@ -138,7 +141,7 @@ def create_widget(self, parent: QtWidgets.QWidget) -> QtWidgets.QSpinBox: class FloatItem(PropertyItemBase): - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]) -> None: self._value = kwargs.pop("value", 0.0) super().__init__(parent, kwargs) @@ -161,7 +164,7 @@ class BoolItem(PropertyItemBase): | Qt.ItemIsUserCheckable ) - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]): + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]): self._value = kwargs.pop("value", None) super().__init__(parent, kwargs) @@ -171,12 +174,12 @@ def create_widget(self, parent: QtWidgets.QWidget) -> QtWidgets.QCheckBox: def set_editor_value( self, widget: QtWidgets.QCheckBox, index: QtCore.QModelIndex ) -> None: - edit_data = index.data(Qt.EditRole) + edit_data = index.data(Qt.ItemDataRole.EditRole) widget.setCheckState(_check_state(edit_data)) class StringItem(PropertyItemBase): - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]) -> None: self._value = kwargs.pop("value", "") super().__init__(parent, kwargs) @@ -186,7 +189,7 @@ def create_widget(self, parent: QtWidgets.QWidget): def set_editor_value( self, widget: QtWidgets.QLineEdit, index: QtCore.QModelIndex ): - widget.setText(index.data(Qt.EditRole)) + widget.setText(index.data(Qt.ItemDataRole.EditRole)) class RadioItem(PropertyItemBase): @@ -198,11 +201,13 @@ class RadioItem(PropertyItemBase): | Qt.ItemIsUserCheckable ) - def __init__(self, parent, kwargs, checked): + def __init__( + self, parent: BaseItem, kwargs: dict[str, str], checked: bool + ): super().__init__(parent, kwargs) self._value = checked - def create_widget(self, parent): + def create_widget(self, parent: QtWidgets.QWidget): return _apply_kwargs(QtWidgets.QCheckBox(parent), self._kwargs) def set_edit(self, value: bool): @@ -220,8 +225,10 @@ def set_edit(self, value: bool): if not any(item._value for item in self.parent.children): self._value = True - def set_editor_value(self, widget, index): - edit_data = index.data(Qt.EditRole) + def set_editor_value( + self, widget: QtWidgets.QCheckBox, index: QtCore.QModelIndex + ): + edit_data = index.data(Qt.ItemDataRole.EditRole) widget.setCheckState(_check_state(edit_data)) @@ -230,8 +237,8 @@ class SingleItem(PropertyItemBase): has_children = True flags = Qt.ItemIsSelectable - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: - self.children: List[RadioItem] = [] + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]) -> None: + self.children: list[RadioItem] = [] items = kwargs["items"] assert items self._value = kwargs.pop("value", None) # None to indicate lazy user @@ -245,7 +252,7 @@ def set_edit(self, value: str) -> None: item.set_edit(item.name == value) @property - def value(self) -> Optional[str]: + def value(self) -> str | None: for item in self.children: if item._value: return item.name @@ -256,8 +263,8 @@ class SetItem(PropertyItemBase): has_children = True flags = Qt.ItemIsSelectable - def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: - self.children: List[BoolItem] = [] + def __init__(self, parent: BaseItem, kwargs: dict[str, Any]) -> None: + self.children: list[BoolItem] = [] items = kwargs["items"] assert items self._value = set(kwargs.pop("value", set())) @@ -266,12 +273,12 @@ def __init__(self, parent: BaseItem, kwargs: Dict[str, Any]) -> None: item["value"] = item["name"] in self._value BoolItem(self, item) - def set_edit(self, value: List[str]) -> None: + def set_edit(self, value: list[str]) -> None: for item in self.children: item.set_edit(item.name in value) @property - def value(self) -> List[str]: + def value(self) -> list[str]: return [item.name for item in self.children if item._value] @@ -378,7 +385,7 @@ def _get_item(self, index: QtCore.QModelIndex): return index.internalPointer() return self.root - def _create_item(self, prop: Dict[str, Any]): + def _create_item(self, prop: dict[str, Any]) -> None: root = self.root type_name = prop.pop("type") if type_name == "separator": @@ -400,7 +407,7 @@ def _create_item(self, prop: Dict[str, Any]): else: raise TypeError("unsupported type `{}`".format(type_name)) - def set_schema(self, schema: Dict[str, Any]): + def set_schema(self, schema: dict[str, Any]): self.beginResetModel() del self.root.children[:] self.endResetModel() @@ -410,7 +417,7 @@ def set_schema(self, schema: Dict[str, Any]): if span_row is not None: yield span_row - def set_properties(self, properties: Dict[str, Any]): + def set_properties(self, properties: dict[str, Any]): current_items = { prop.name: prop for prop in self.root.children @@ -424,7 +431,7 @@ def set_properties(self, properties: Dict[str, Any]): else: print("skipped `{}` = `{}`".format(name, value)) - def get_properties(self) -> Dict[str, Any]: + def get_properties(self) -> dict[str, Any]: properties = {} for child in self.root.children: if isinstance(child, SeparatorItem): @@ -464,11 +471,15 @@ def sizeHint(self, option, index: QtCore.QModelIndex) -> QtCore.QSize: return self.size return super().sizeHint(option, index) - def paint(self, painter, option, index: QtCore.QModelIndex): + def paint( + self, painter: QtGui.QPainter, option, index: QtCore.QModelIndex + ): self._is_radio = isinstance(index.internalPointer(), RadioItem) return super().paint(painter, option, index) - def drawCheck(self, painter, option, rect, state): + def drawCheck( + self, painter: QtGui.QPainter, option, rect: QtCore.QRect, state + ): if not rect.isValid(): return widget = option.widget @@ -590,7 +601,7 @@ def edit( return False return QtWidgets.QTreeView.edit(self, index, trigger, event) - def set_schema(self, schema: Dict[str, Any]): + def set_schema(self, schema: dict[str, Any]): span_rows = list(self._model.set_schema(schema)) for span_row in span_rows: self.setFirstColumnSpanned(span_row, QtCore.QModelIndex(), True) @@ -598,11 +609,11 @@ def set_schema(self, schema: Dict[str, Any]): self.resizeColumnToContents(1) self.expandAll() - def set_properties(self, properties: Dict[str, Any]): + def set_properties(self, properties: dict[str, Any]): self._model.set_properties(properties) self.resizeColumnToContents(0) self.resizeColumnToContents(1) self.expandAll() - def get_properties(self) -> Dict[str, Any]: + def get_properties(self) -> dict[str, Any]: return self._model.get_properties()