diff --git a/brainframe_qt/ui/dialogs/task_configuration/task_configuration.py b/brainframe_qt/ui/dialogs/task_configuration/task_configuration.py
index cae4e2b6a..08c99611e 100644
--- a/brainframe_qt/ui/dialogs/task_configuration/task_configuration.py
+++ b/brainframe_qt/ui/dialogs/task_configuration/task_configuration.py
@@ -4,7 +4,7 @@
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QInputDialog
from PyQt5.uic import loadUi
-from brainframe.api import bf_codecs
+from brainframe.api import bf_codecs, bf_errors
from brainframe_qt.api_utils import api
from brainframe_qt.ui.dialogs import AlarmCreationDialog
@@ -58,6 +58,7 @@ def _init_signals(self) -> None:
self.zone_list.initiate_zone_edit.connect(self._edit_zone_by_id)
self.zone_list.zone_delete.connect(self.delete_zone)
self.zone_list.alarm_delete.connect(self.delete_alarm)
+ self.zone_list.zone_name_change.connect(self.change_zone_name)
self.dialog_button_box.accepted.connect(self.accept)
self.dialog_button_box.rejected.connect(self.reject)
@@ -171,6 +172,48 @@ def cancel_zone_edit(self) -> None:
if None in self.zone_list.zones:
self.zone_list.remove_zone(None)
+ def change_zone_name(self, zone_id: int, zone_name: str) -> None:
+ def update_zone_name() -> Zone:
+ zone = api.get_zone(zone_id)
+ zone.name = zone_name
+
+ updated_zone = Zone.from_api_zone(api.set_zone(zone))
+ return updated_zone
+
+ def on_success(updated_zone: Zone) -> None:
+ self.zone_list.update_zone(updated_zone)
+
+ def on_error(error: Exception) -> None:
+ dialog: Optional[BrainFrameMessage] = None
+ title = self.tr("Unable to rename Zone")
+
+ if isinstance(error, bf_errors.ZoneNotFoundError):
+ message = self.tr(
+ f"Attempted to rename Zone {zone_id} to {zone_name} but the Zone "
+ f"no longer exists."
+ )
+ try:
+ self.zone_list.remove_zone(zone_id)
+ except KeyError:
+ # Zone must already be gone
+ pass
+
+ dialog = BrainFrameMessage.warning(
+ parent=self,
+ title=title,
+ warning=message,
+ )
+
+ if dialog is not None:
+ dialog.exec()
+
+ QTAsyncWorker(
+ self,
+ update_zone_name,
+ on_success=on_success,
+ on_error=on_error,
+ ).start()
+
def delete_alarm(self, alarm_id: int) -> None:
api.delete_zone_alarm(alarm_id)
self.zone_list.remove_alarm(alarm_id)
diff --git a/brainframe_qt/ui/dialogs/task_configuration/task_configuration.ui b/brainframe_qt/ui/dialogs/task_configuration/task_configuration.ui
index ef7efbebe..c620e7726 100644
--- a/brainframe_qt/ui/dialogs/task_configuration/task_configuration.ui
+++ b/brainframe_qt/ui/dialogs/task_configuration/task_configuration.ui
@@ -150,6 +150,27 @@
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 35
+
+
+
+ Double click a zone to rename it
+
+
+ Qt::AlignCenter
+
+
+
diff --git a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list.py b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list.py
index bf0235867..19640b929 100644
--- a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list.py
+++ b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list.py
@@ -1,4 +1,4 @@
-from typing import Dict, Callable, List
+from typing import Callable, Dict, List, Optional
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout
@@ -14,6 +14,8 @@ class ZoneList(QWidget):
zone_delete = pyqtSignal(int)
alarm_delete = pyqtSignal(int)
+ zone_name_change = pyqtSignal(int, str)
+
layout: Callable[..., QVBoxLayout]
def __init__(self, parent=None):
@@ -39,7 +41,7 @@ def _init_style(self) -> None:
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setSpacing(0)
- def add_zone(self, zone: Zone) -> ZoneListZoneItem:
+ def add_zone(self, zone: Zone, index: Optional[int] = None) -> ZoneListZoneItem:
"""Creates and returns the new ZoneListItem using the Zone"""
zone_item = ZoneListZoneItem(zone, parent=self)
@@ -47,11 +49,14 @@ def add_zone(self, zone: Zone) -> ZoneListZoneItem:
zone_item.zone_edit.connect(self.initiate_zone_edit)
zone_item.zone_delete.connect(self.zone_delete)
+ zone_item.zone_name_change.connect(self.zone_name_change)
- self.layout().addWidget(zone_item)
+ if index is not None:
+ self.layout().insertWidget(index, zone_item)
+ else:
+ self.layout().addWidget(zone_item)
- for alarm in zone.alarms:
- self.add_alarm(zone, alarm)
+ self._add_alarm_widgets_for_zone(zone)
return zone_item
@@ -61,6 +66,13 @@ def confirm_zone(self, zone: Zone) -> None:
self.remove_zone(None)
self.add_zone(zone)
+ def update_zone(self, zone: Zone) -> None:
+ old_zone_widget = self.zones[zone.id]
+ old_index = self.layout().indexOf(old_zone_widget)
+
+ self.remove_zone(zone.id)
+ self.add_zone(zone, old_index)
+
def add_alarm(self, zone: Zone, alarm: bf_codecs.ZoneAlarm):
alarm_widget = ZoneListAlarmItem(alarm, parent=self)
@@ -74,6 +86,7 @@ def remove_alarm(self, alarm_id: int) -> None:
alarm_widget = self.alarms.pop(alarm_id)
self.layout().removeWidget(alarm_widget)
+ alarm_widget.deleteLater()
def remove_zone(self, zone_id: int) -> None:
zone_widget: ZoneListZoneItem = self.zones.pop(zone_id)
@@ -81,11 +94,12 @@ def remove_zone(self, zone_id: int) -> None:
alarm_widgets = self._find_alarm_widgets_for_zone(zone_widget)
# Remove zone and all child alarms
- self.layout().removeWidget(zone_widget)
for alarm_widget in alarm_widgets:
# Uses private attribute for now, but this is temporary until zone widgets
# contain alarm widgets inside of them
self.remove_alarm(alarm_widget._alarm.id)
+ self.layout().removeWidget(zone_widget)
+ zone_widget.deleteLater()
def _add_alarm_widget(self, alarm_widget: ZoneListAlarmItem, zone_id: int) -> None:
"""Add an alarm widget to the correct zone"""
@@ -103,6 +117,10 @@ def _add_alarm_widget(self, alarm_widget: ZoneListAlarmItem, zone_id: int) -> No
self.layout().insertWidget(insert_index, alarm_widget)
+ def _add_alarm_widgets_for_zone(self, zone: Zone) -> None:
+ for alarm in zone.alarms:
+ self.add_alarm(zone, alarm)
+
def _find_alarm_widgets_for_zone(
self, zone_widget: ZoneListZoneItem
) -> List[ZoneListAlarmItem]:
diff --git a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item.py b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item.py
index 045db46e7..4c3a8a753 100644
--- a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item.py
+++ b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item.py
@@ -1,4 +1,7 @@
+from typing import Tuple
+
from PyQt5.QtCore import QObject, pyqtSignal
+from PyQt5.QtGui import QValidator
from PyQt5.QtWidgets import QWidget
from brainframe.api import bf_codecs
@@ -11,6 +14,7 @@ class ZoneListZoneItem(ZoneListItemUI):
zone_delete = pyqtSignal(int)
zone_edit = pyqtSignal(int)
+ zone_name_change = pyqtSignal(int, str)
def __init__(self, zone: Zone, *, parent: QObject):
super().__init__(parent=parent)
@@ -20,24 +24,38 @@ def __init__(self, zone: Zone, *, parent: QObject):
self.entry_name = zone.name
self.entry_type = self._get_entry_type(zone)
+ self._init_validators()
self._init_signals()
- self._configure_buttons()
+
+ self._handle_full_frame_zone()
def _init_signals(self) -> None:
self.trash_button.clicked.connect(self._on_trash_button_click)
self.edit_button.clicked.connect(self._on_edit_button_click)
+ self.name_label.text_changed.connect(self._on_zone_name_change)
+
+ def _init_validators(self) -> None:
+ validator = self._ZoneNameValidator()
+
+ self.name_label.validator = validator
- def _configure_buttons(self) -> None:
+ def _handle_full_frame_zone(self) -> None:
+ """Disable some functionality if the Zone is the full-frame zone"""
if self._zone.name == bf_codecs.Zone.FULL_FRAME_ZONE_NAME:
self.trash_button.setDisabled(True)
self.edit_button.setDisabled(True)
+ self.name_label.editable = False
+
def _on_edit_button_click(self, _clicked: bool) -> None:
self.zone_edit.emit(self._zone.id)
def _on_trash_button_click(self, _clicked: bool) -> None:
self.zone_delete.emit(self._zone.id)
+ def _on_zone_name_change(self, zone_name: str) -> None:
+ self.zone_name_change.emit(self._zone.id, zone_name)
+
@staticmethod
def _get_entry_type(zone: Zone) -> ZoneListType:
if isinstance(zone, Line):
@@ -47,10 +65,20 @@ def _get_entry_type(zone: Zone) -> ZoneListType:
else:
return ZoneListType.UNKNOWN
+ class _ZoneNameValidator(QValidator):
+ def validate(self, input_: str, pos: int) -> Tuple[QValidator.State, str, int]:
+ if input_ == bf_codecs.Zone.FULL_FRAME_ZONE_NAME:
+ state = QValidator.Intermediate
+ else:
+ state = QValidator.Acceptable
+
+ return state, input_, pos
+
class ZoneListAlarmItem(ZoneListItemUI):
"""Temporary until ZoneListZoneItem holds Alarm widgets"""
alarm_delete = pyqtSignal(int)
+ alarm_edit = pyqtSignal(int)
def __init__(self, alarm: bf_codecs.ZoneAlarm, *, parent: QObject):
super().__init__(parent=parent)
@@ -64,8 +92,11 @@ def __init__(self, alarm: bf_codecs.ZoneAlarm, *, parent: QObject):
self._init_signals()
+ self._disable_editing()
+
def _init_signals(self) -> None:
self.trash_button.clicked.connect(self._on_trash_button_click)
+ self.edit_button.clicked.connect(self._on_edit_button_click)
def _init_padding_widget(self) -> QWidget:
"""Temporary solution to indent alarm widgets a bit.
@@ -81,3 +112,15 @@ def _init_padding_widget(self) -> QWidget:
def _on_trash_button_click(self, _clicked: bool) -> None:
self.alarm_delete.emit(self._alarm.id)
+
+ def _on_edit_button_click(self, _clicked: bool) -> None:
+ self.alarm_edit.emit(self._alarm.id)
+
+ def _disable_editing(self) -> None:
+ """Editing of alarms is not currently supported"""
+ self.edit_button.setDisabled(True)
+ self.edit_button.setToolTip(self.tr(
+ "Editing of alarms is not currently not supported"
+ ))
+
+ self.name_label.editable = False
diff --git a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item_ui.py b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item_ui.py
index 638e50c50..fc2b339c2 100644
--- a/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item_ui.py
+++ b/brainframe_qt/ui/dialogs/task_configuration/widgets/zone_list_item_ui.py
@@ -2,10 +2,11 @@
from PyQt5.QtCore import QObject, Qt
from PyQt5.QtGui import QIcon
-from PyQt5.QtWidgets import QPushButton, QWidget, QHBoxLayout, QLabel
+from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QLineEdit
from brainframe_qt.ui.resources.ui_elements.buttons import IconButton
from brainframe_qt.ui.resources.ui_elements.widgets import AspectRatioSVGWidget
+from brainframe_qt.ui.resources.ui_elements.widgets.text_edit import EditableLabel
class ZoneListType(Enum):
@@ -48,8 +49,11 @@ def _init_entry_icon(self) -> AspectRatioSVGWidget:
return icon
- def _init_name_label(self) -> QLabel:
+ def _init_name_label(self) -> EditableLabel:
label = QLabel(self._entry_name, parent=self)
+ line_edit = QLineEdit(parent=self)
+
+ label = EditableLabel(label, line_edit, parent=self)
return label
@@ -93,7 +97,7 @@ def entry_name(self) -> str:
@entry_name.setter
def entry_name(self, entry_name: str) -> None:
self._entry_name = entry_name
- self.name_label.setText(entry_name)
+ self.name_label.text = entry_name
@property
def entry_type(self) -> ZoneListType:
diff --git a/brainframe_qt/ui/resources/i18n/brainframe_zh.ts b/brainframe_qt/ui/resources/i18n/brainframe_zh.ts
index 06ff3a2cf..5f1e10c25 100644
--- a/brainframe_qt/ui/resources/i18n/brainframe_zh.ts
+++ b/brainframe_qt/ui/resources/i18n/brainframe_zh.ts
@@ -1284,22 +1284,22 @@ Please recheck the entered server address.
TaskConfiguration
-
+
Add points until done, then press "Confirm" button
添加点直到完成,然后按“确认”按钮
-
+
Item Name Already Exists
名称已存在
-
+
Item {} already exists in Stream
项目{}已存在于视频流中
-
+
Please use another name.
请使用另一个名称。
@@ -1334,35 +1334,50 @@ Please recheck the entered server address.
已有任务
-
+
Confirm
确认
-
+
Cancel
取消
-
+
New Line
新检测线段
-
+
Name for new line:
新检测线段名称:
-
+
New Region
新检测区域
-
+
Name for new region:
新检测区域名称:
+
+
+ Unable to rename Zone
+ 无法重命名区域
+
+
+
+ Attempted to rename Zone {zone_id} to {zone_name} but the Zone no longer exists.
+ 尝试将区域 {zone_id} 重命名为 {zone_name},但该区域不再存在。
+
+
+
+ Double click a zone to rename it
+ 双击区域以重命名
+
TextLicenseEditor
@@ -1420,6 +1435,14 @@ Please recheck the entered server address.
这将删除所有此视频流相关的区域、检测结果、警报、报警信息等,并且无法撤消。这个流程会在后台发生<br><br>可能需要几分钟。
+
+ ZoneListAlarmItem
+
+
+ Editing of alarms is not currently not supported
+ 目前不支持编辑预警
+
+
ZoneStatusLabelItem
diff --git a/brainframe_qt/ui/resources/images/icons/edit_pencil.svg b/brainframe_qt/ui/resources/images/icons/edit_pencil.svg
index 276f59f81..743759708 100644
--- a/brainframe_qt/ui/resources/images/icons/edit_pencil.svg
+++ b/brainframe_qt/ui/resources/images/icons/edit_pencil.svg
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df71cec30d2dd80997cf80d90873e688e7942ccb53b2a785ff57436ae34a4713
-size 2917
+oid sha256:07034304fb15e15e31cc69ca8b6574d1dc3f0fd056b5bacae840eae56186fb19
+size 3819
diff --git a/brainframe_qt/ui/resources/images/icons/edit_pencil_inkscape.svg b/brainframe_qt/ui/resources/images/icons/edit_pencil_inkscape.svg
index 289d0fa3b..aa2a3a138 100644
--- a/brainframe_qt/ui/resources/images/icons/edit_pencil_inkscape.svg
+++ b/brainframe_qt/ui/resources/images/icons/edit_pencil_inkscape.svg
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:993af1943c4bac5bb7e9d8486b31b957da8bbb0174ff0bd427a9d76ab51dcd0c
-size 4221
+oid sha256:7bb7f6c27ebdb4352963287197656d0d3b1067319608e0f071ba06cbe812f3e0
+size 4663
diff --git a/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/__init__.py b/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/__init__.py
index b55cab74a..b24e11fce 100644
--- a/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/__init__.py
+++ b/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/__init__.py
@@ -1,2 +1,3 @@
from .drag_and_drop_text_editor import DragAndDropTextEditor
+from .editable_label import EditableLabel
from .placeholder_text_edit import PlaceholderTextEdit
diff --git a/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/editable_label.py b/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/editable_label.py
new file mode 100644
index 000000000..b5ee320bd
--- /dev/null
+++ b/brainframe_qt/ui/resources/ui_elements/widgets/text_edit/editable_label.py
@@ -0,0 +1,100 @@
+from PyQt5.QtCore import QObject, Qt, pyqtSignal
+from PyQt5.QtGui import QMouseEvent, QKeyEvent, QValidator
+from PyQt5.QtWidgets import QStackedWidget, QLabel, QLineEdit
+
+
+class EditableLabel(QStackedWidget):
+ text_changed = pyqtSignal(str)
+
+ def __init__(self, label: QLabel, line_edit: QLineEdit, *, parent: QObject):
+ super().__init__(parent=parent)
+
+ self._label = label
+ self._line_edit = line_edit
+
+ self.editable = True
+ """If this is False, editing text is disabled"""
+ self._editing = False
+
+ self._init_layout()
+ self._init_style()
+
+ self._init_signals()
+
+ def _init_layout(self) -> None:
+ self.addWidget(self._label)
+ self.addWidget(self._line_edit)
+
+ def _init_style(self) -> None:
+ self.setAttribute(Qt.WA_StyledBackground, True)
+
+ def _init_signals(self) -> None:
+ self._line_edit.editingFinished.connect(self.finish_edit)
+
+ def keyPressEvent(self, event: QKeyEvent) -> None:
+ if self._editing and event.key() == Qt.Key_Escape:
+ self.cancel_edit()
+
+ def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
+ if not self.editable:
+ return
+
+ if not self._editing:
+ self.start_edit()
+
+ @property
+ def text(self) -> str:
+ return self._label.text()
+
+ @text.setter
+ def text(self, text: str) -> None:
+ self._label.setText(text)
+
+ @property
+ def validator(self) -> QValidator:
+ return self._line_edit.validator()
+
+ @validator.setter
+ def validator(self, validator: QValidator) -> None:
+ self._line_edit.setValidator(validator)
+
+ def cancel_edit(self) -> None:
+ if not self._editing:
+ return
+
+ # Block signals to prevent out-focus event from self._line_edit triggering
+ # self.finish_edit
+ self._line_edit.blockSignals(True)
+ self.setCurrentWidget(self._label)
+ self._line_edit.blockSignals(False)
+
+ self._editing = False
+
+ def finish_edit(self) -> None:
+ if not self.editable:
+ self.cancel_edit()
+ return
+
+ if not self._editing:
+ return
+
+ self.text = self._line_edit.text()
+
+ self.setCurrentWidget(self._label)
+
+ self._editing = False
+
+ self.text_changed.emit(self.text)
+
+ def start_edit(self) -> None:
+ if not self.editable or self._editing:
+ return
+
+ self._line_edit.setText(self.text)
+
+ self._line_edit.selectAll()
+ self._line_edit.setFocus()
+
+ self.setCurrentWidget(self._line_edit)
+
+ self._editing = True