refactor(ui): use Qt's .layout() method instead of _layout references

This commit is contained in:
Travis Abendshien
2026-07-19 11:24:23 -07:00
parent 4aa7b9e043
commit 111ea76884
7 changed files with 119 additions and 114 deletions
@@ -58,7 +58,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
"""
# NOTE: Unlike tags, creating new field templates will ALWAYS spawn an edit window
# since the user needs to decide what type of field it should be before it's created.
query: str = self._layout.search_field.text()
query: str = self.layout().search_field.text()
panel = EditFieldTemplateModal()
modal = PanelModal(
panel,
@@ -126,7 +126,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
self._clear_search_query()
edit_item_panel.hide()
self._on_search_query_changed(self._layout.search_field.text())
self._on_search_query_changed(self.layout().search_field.text())
@override
def _edit_item(self, edit_item_panel: PanelWidget) -> None:
@@ -136,18 +136,18 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
self._lib.update_field_template(
edit_item_panel.old_field_type, edit_item_panel.build_field_template()
)
self._update_items(self._layout.search_field.text())
self._update_items(self.layout().search_field.text())
@override
def _get_item_widget(self, index: int, library: Library | None) -> FieldTemplateWidget:
"""Gets the item widget at a specific index."""
# Create any new item widgets needed up to the given index
if self._layout.content_layout.count() <= index:
while self._layout.content_layout.count() <= index:
if self.layout().content_layout.count() <= index:
while self.layout().content_layout.count() <= index:
pad_field_template_widget = FieldTemplateWidget()
pad_field_template_widget.setHidden(True)
self._layout.content_layout.addWidget(pad_field_template_widget)
self.layout().content_layout.addWidget(pad_field_template_widget)
field_template_widget: QWidget = self._layout.content_layout.itemAt(index).widget()
field_template_widget: QWidget = self.layout().content_layout.itemAt(index).widget()
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
@@ -7,6 +7,7 @@ from datetime import datetime as dt
from enum import IntEnum
from functools import partial
from pathlib import Path
from typing import override
from warnings import catch_warnings
import structlog
@@ -47,7 +48,6 @@ class _ItemMode(IntEnum):
class PreviewPanel(QWidget):
def __init__(self, driver: "QtDriver") -> None:
super().__init__()
self._layout = PreviewPanelView(driver=driver, pixel_ratio=self.devicePixelRatio())
self._driver = driver
self._lib = self._driver.lib
self._selected: list[int]
@@ -68,77 +68,77 @@ class PreviewPanel(QWidget):
self,
)
self.setLayout(self._layout)
self.setLayout(PreviewPanelView(driver=driver, pixel_ratio=self.devicePixelRatio()))
self._set_item_mode(None)
self._connect_callbacks()
def _connect_callbacks(self) -> None:
# Tag Search
self._layout.add_tag_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.TAG))
self.layout().add_tag_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.TAG))
self._open_tag_search_action.activated.connect(self._open_tag_search_callback)
self._layout.tag_search_box.done.connect(self._tag_added_callback)
self._layout.containers.on_tags_update.connect(self._update_added_callback)
self.layout().tag_search_box.done.connect(self._tag_added_callback)
self.layout().containers.on_tags_update.connect(self._update_added_callback)
# Field Search
self._layout.add_field_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.FIELD))
self.layout().add_field_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.FIELD))
self._open_field_search_action.activated.connect(self._open_field_search_callback)
self._layout.field_search_box.done.connect(self._field_added_callback)
self.layout().field_search_box.done.connect(self._field_added_callback)
# Previews
self._layout.preview_thumb.stats_updated.connect(self._thumb_stats_updated_callback)
self._layout.preview_thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
self.layout().preview_thumb.stats_updated.connect(self._thumb_stats_updated_callback)
self.layout().preview_thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
def _set_item_mode(self, mode: _ItemMode | None):
def hide_and_disable_buttons():
self._layout.add_tag_button.setHidden(True)
self._layout.add_tag_button.setEnabled(False)
self._layout.add_field_button.setHidden(True)
self._layout.add_field_button.setEnabled(False)
self.layout().add_tag_button.setHidden(True)
self.layout().add_tag_button.setEnabled(False)
self.layout().add_field_button.setHidden(True)
self.layout().add_field_button.setEnabled(False)
def restore_buttons():
self._layout.add_tag_button.setHidden(False)
self._layout.add_tag_button.setEnabled(True)
self._layout.add_field_button.setHidden(False)
self._layout.add_field_button.setEnabled(True)
self.layout().add_tag_button.setHidden(False)
self.layout().add_tag_button.setEnabled(True)
self.layout().add_field_button.setHidden(False)
self.layout().add_field_button.setEnabled(True)
if mode == _ItemMode.TAG:
self._layout.tag_search_box.added = self._layout.containers.tags
self._layout.field_search_box.hide_and_reset()
self._layout.tag_search_box.setHidden(False)
self.layout().tag_search_box.added = self.layout().containers.tags
self.layout().field_search_box.hide_and_reset()
self.layout().tag_search_box.setHidden(False)
hide_and_disable_buttons()
elif mode == _ItemMode.FIELD:
self._layout.tag_search_box.hide_and_reset()
self._layout.field_search_box.setHidden(False)
self.layout().tag_search_box.hide_and_reset()
self.layout().field_search_box.setHidden(False)
hide_and_disable_buttons()
else:
self._layout.tag_search_box.hide_and_reset()
self._layout.field_search_box.hide_and_reset()
self.layout().tag_search_box.hide_and_reset()
self.layout().field_search_box.hide_and_reset()
restore_buttons()
def _open_tag_search_callback(self) -> None:
self._layout.add_tag_button.setFocus()
self._layout.add_tag_button.click()
self.layout().add_tag_button.setFocus()
self.layout().add_tag_button.click()
def _open_field_search_callback(self) -> None:
self._layout.add_field_button.setFocus()
self._layout.add_field_button.click()
self.layout().add_field_button.setFocus()
self.layout().add_field_button.click()
def _tag_added_callback(self):
self._set_item_mode(None)
self._layout.add_tag_button.setFocus()
self.layout().add_tag_button.setFocus()
def _field_added_callback(self):
self._set_item_mode(None)
self._layout.add_field_button.setFocus()
self.layout().add_field_button.setFocus()
def _update_added_callback(self):
self._layout.tag_search_box.added = self._layout.containers.tags
self.layout().tag_search_box.added = self.layout().containers.tags
def _thumb_stats_updated_callback(self, filepath: Path, stats: FileAttributeData) -> None:
if len(self._selected) != 1:
return
if filepath != self._layout.preview_thumb.current_file:
if filepath != self.layout().preview_thumb.current_file:
return
if self._current_stats is None:
@@ -151,18 +151,18 @@ class PreviewPanel(QWidget):
if stats.duration is not None:
self._current_stats.duration = stats.duration
self._layout.file_attrs.update_stats(filepath, self._current_stats)
self.layout().file_attrs.update_stats(filepath, self._current_stats)
def _set_selection_callback(self) -> None:
with catch_warnings(record=True):
self._layout.field_search_box.item_chosen.disconnect()
self._layout.tag_search_box.item_chosen.disconnect()
self.layout().field_search_box.item_chosen.disconnect()
self.layout().tag_search_box.item_chosen.disconnect()
self._layout.field_search_box.item_chosen.connect(self._add_field_to_selected)
self._layout.tag_search_box.item_chosen.connect(self._add_tag_to_selected)
self.layout().field_search_box.item_chosen.connect(self._add_field_to_selected)
self.layout().tag_search_box.item_chosen.connect(self._add_tag_to_selected)
def _add_field_to_selected(self, template: BaseFieldTemplate) -> None:
self._layout.containers.add_field_to_selected(template)
self.layout().containers.add_field_to_selected(template)
# TODO: Allow editing of fields across multiple entries at once.
if len(self._selected) == 1:
if self._driver.settings.edit_field_on_add:
@@ -175,7 +175,7 @@ class PreviewPanel(QWidget):
if entry_field is not None:
self._edit_field(entry.id, entry_field)
self._layout.containers.update_from_entry(self._selected[0])
self.layout().containers.update_from_entry(self._selected[0])
def _edit_field(self, entry_id: int, field: BaseField) -> None:
# TODO: A lot of this code is similar to or straight up shared with FieldContainers.
@@ -190,7 +190,7 @@ class PreviewPanel(QWidget):
inline_title=False,
)
edit_modal.saved_data.connect(
partial(self._layout.containers.update_text_field_callback, field, entry_id)
partial(self.layout().containers.update_text_field_callback, field, entry_id)
)
edit_modal.show()
elif type(field) is DatetimeField:
@@ -201,21 +201,21 @@ class PreviewPanel(QWidget):
inline_title=False,
)
edit_modal.saved_data.connect(
partial(self._layout.containers.update_datetime_field_callback, field, entry_id)
partial(self.layout().containers.update_datetime_field_callback, field, entry_id)
)
edit_modal.show()
def _add_tag_to_selected(self, tag_id: int) -> None:
self._layout.containers.add_tags_to_selected(tag_id)
self.layout().containers.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._layout.containers.update_from_entry(self._selected[0])
self.layout().containers.update_from_entry(self._selected[0])
def _toggle_ffmpeg_warning(self, enable_warning: bool = True) -> None:
if enable_warning and (not FfmpegStatus.which() or not FfprobeStatus.which()):
self._layout.warning_banner.show()
self.layout().warning_banner.show()
return
self._layout.warning_banner.hide()
self.layout().warning_banner.hide()
def set_selection(self, selected: list[int], update_preview: bool = True) -> None:
"""Render the panel widgets with the newest data from the Library.
@@ -230,13 +230,13 @@ class PreviewPanel(QWidget):
try:
# No Items Selected
if len(selected) == 0:
self._layout.preview_thumb.hide_preview()
self.layout().preview_thumb.hide_preview()
self._current_stats = None
self._layout.file_attrs.update_stats()
self._layout.file_attrs.update_date_label()
self._layout.containers.hide_containers()
self._layout.add_tag_button.setEnabled(False)
self._layout.add_field_button.setEnabled(False)
self.layout().file_attrs.update_stats()
self.layout().file_attrs.update_date_label()
self.layout().containers.hide_containers()
self.layout().add_tag_button.setEnabled(False)
self.layout().add_field_button.setEnabled(False)
# One Item Selected
elif len(selected) == 1:
@@ -244,25 +244,25 @@ class PreviewPanel(QWidget):
entry: Entry = unwrap(self._lib.get_entry(entry_id))
filepath: Path = unwrap(self._lib.library_dir) / entry.path
if filepath != self._layout.preview_thumb.current_file:
if filepath != self.layout().preview_thumb.current_file:
self._current_stats = None
if update_preview:
stats: FileAttributeData = self._layout.preview_thumb.display_file(filepath)
stats: FileAttributeData = self.layout().preview_thumb.display_file(filepath)
self._current_stats = stats
self._layout.file_attrs.update_stats(filepath, stats)
self._layout.file_attrs.update_date_label(filepath)
self._layout.containers.update_from_entry(entry_id)
self.layout().file_attrs.update_stats(filepath, stats)
self.layout().file_attrs.update_date_label(filepath)
self.layout().containers.update_from_entry(entry_id)
self._set_selection_callback()
# Multiple Selected Items
elif len(selected) > 1:
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
self._layout.preview_thumb.hide_preview() # TODO: Render mixed selection
self.layout().preview_thumb.hide_preview() # TODO: Render mixed selection
self._current_stats = None
self._layout.file_attrs.update_multi_selection(len(selected))
self._layout.file_attrs.update_date_label()
self._layout.containers.hide_containers() # TODO: Allow for mixed editing
self.layout().file_attrs.update_multi_selection(len(selected))
self.layout().file_attrs.update_date_label()
self.layout().containers.hide_containers() # TODO: Allow for mixed editing
self._set_selection_callback()
except Exception as e:
@@ -270,8 +270,13 @@ class PreviewPanel(QWidget):
def stop_media_playback(self) -> None:
"""Stop any media playback in the preview panel."""
self._layout.preview_thumb.media_player.stop()
self.layout().preview_thumb.media_player.stop()
@property
def containers(self) -> FieldContainers:
return self._layout.containers
return self.layout().containers
@override
def layout(self) -> PreviewPanelView:
"""Return the typed layout for this widget."""
return super().layout() # pyright: ignore[reportReturnType]
@@ -52,9 +52,9 @@ class SearchPanel[T](PanelWidget):
super().__init__()
self.view = view
self.is_chooser = is_chooser
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.addWidget(self.view)
self.setLayout(QVBoxLayout(self))
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().addWidget(self.view)
self.view.connect_callbacks(self)
self._driver: QtDriver | None = None
self.exclude: list[int] = exclude or []
+26 -26
View File
@@ -47,7 +47,6 @@ class SuggestBox[T](QWidget):
def __init__(self, driver: "QtDriver", view: SuggestBoxView) -> None:
super().__init__()
self._layout = view
self._driver = driver
self._limit = 5
self._is_shift_held = False
@@ -55,42 +54,42 @@ class SuggestBox[T](QWidget):
self.added: list[int] = []
self.excluded: list[int] = []
self.setLayout(self._layout)
self.setLayout(view)
self._connect_callbacks()
def hide_and_reset(self):
self.hide()
self._layout.search_field.setDisabled(True)
self.layout().search_field.setDisabled(True)
self._on_shift_held(held=False)
def _connect_callbacks(self) -> None:
self._layout.search_field.textChanged.connect(self._on_search_query_changed)
self._layout.search_field.editingFinished.connect(self._editing_finished_callback)
self._layout.search_field.return_pressed.connect(
lambda: self._on_search_query_submitted(self._layout.search_field.text())
self.layout().search_field.textChanged.connect(self._on_search_query_changed)
self.layout().search_field.editingFinished.connect(self._editing_finished_callback)
self.layout().search_field.return_pressed.connect(
lambda: self._on_search_query_submitted(self.layout().search_field.text())
)
self._layout.search_field.shift_return_pressed.connect(
self.layout().search_field.shift_return_pressed.connect(
lambda: self._on_search_query_submitted(
self._layout.search_field.text(), always_create=True
self.layout().search_field.text(), always_create=True
)
)
self._layout.search_field.shift_holding.connect(lambda held: self._on_shift_held(held))
self.layout().search_field.shift_holding.connect(lambda held: self._on_shift_held(held))
def _on_shift_held(self, held: bool):
if held:
self._is_shift_held = True
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
if self._layout.content_layout.count() > 0:
self._layout.content_layout.itemAt(0).widget().setGraphicsEffect(opacity_effect)
if self.layout().content_layout.count() > 0:
self.layout().content_layout.itemAt(0).widget().setGraphicsEffect(opacity_effect)
else:
self._is_shift_held = False
if self._layout.content_layout.count() > 0:
self._layout.content_layout.itemAt(0).widget().setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
if self.layout().content_layout.count() > 0:
self.layout().content_layout.itemAt(0).widget().setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
def _clear_search_query(self) -> None:
self._layout.search_field.setText("")
self.layout().search_field.setText("")
def _get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore
raise NotImplementedError()
@@ -106,7 +105,7 @@ class SuggestBox[T](QWidget):
self.hide_and_reset()
return
elif not self.isHidden():
self._layout.search_field.setFocus()
self.layout().search_field.setFocus()
# Create and add item if no search results
if (len(self._search_results) <= 0) or always_create:
@@ -173,14 +172,14 @@ class SuggestBox[T](QWidget):
item: T | None = all_results[i] if i < len(all_results) else None
self._set_item_widget(item=item, index=i)
if self._layout.content_layout.isEmpty():
self._layout.scroll_area.setHidden(True)
self._layout.content_layout.setContentsMargins(0, 0, 0, 0)
self._layout.search_field.setStyleSheet(autofill_line_edit_style())
if self.layout().content_layout.isEmpty():
self.layout().scroll_area.setHidden(True)
self.layout().content_layout.setContentsMargins(0, 0, 0, 0)
self.layout().search_field.setStyleSheet(autofill_line_edit_style())
else:
self._layout.scroll_area.setHidden(False)
self._layout.content_layout.setContentsMargins(6, 6, 6, 6)
self._layout.search_field.setStyleSheet(autofill_line_edit_top_style())
self.layout().scroll_area.setHidden(False)
self.layout().content_layout.setContentsMargins(6, 6, 6, 6)
self.layout().search_field.setStyleSheet(autofill_line_edit_top_style())
def _search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@@ -189,7 +188,7 @@ class SuggestBox[T](QWidget):
raise NotImplementedError()
def _editing_finished_callback(self):
if self._layout.search_field.text() == "":
if self.layout().search_field.text() == "":
self.done.emit()
self.hide_and_reset()
@@ -203,13 +202,14 @@ class SuggestBox[T](QWidget):
def showEvent(self, event: QShowEvent) -> None:
self._update_items()
self._on_shift_held(held=False)
self._layout.search_field.setDisabled(False)
self.layout().search_field.setDisabled(False)
self._clear_search_query()
return super().showEvent(event)
@override
def layout(self) -> SuggestBoxView:
return self._layout
"""Return the typed layout for this widget."""
return super().layout() # pyright: ignore[reportReturnType]
@override
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
@@ -64,7 +64,7 @@ class TagSuggestBox(SuggestBox[Tag]):
Args:
add_to_entry (bool): Should this item be added to currently selected entries?
"""
query: str = self._layout.search_field.text()
query: str = self.layout().search_field.text()
if self._driver.settings.edit_tag_on_create:
panel: BuildTagPanel = BuildTagPanel(self._lib)
@@ -150,7 +150,7 @@ class TagSuggestBox(SuggestBox[Tag]):
self._clear_search_query()
edit_item_panel.hide()
self._on_search_query_changed(self._layout.search_field.text())
self._on_search_query_changed(self.layout().search_field.text())
@override
def _edit_item(self, edit_item_panel: PanelWidget) -> None:
@@ -162,19 +162,19 @@ class TagSuggestBox(SuggestBox[Tag]):
parent_ids=edit_item_panel.parent_ids,
aliases=edit_item_panel.aliases,
)
self._update_items(self._layout.search_field.text())
self._update_items(self.layout().search_field.text())
@override
def _get_item_widget(self, index: int, library: Library | None) -> TagWidget:
"""Gets the item widget at a specific index."""
# Create any new item widgets needed up to the given index
if self._layout.content_layout.count() <= index:
while self._layout.content_layout.count() <= index:
if self.layout().content_layout.count() <= index:
while self.layout().content_layout.count() <= index:
tag_widget = TagWidget(tag=None, has_edit=True, has_remove=True, library=library)
tag_widget.on_remove.connect(self._update_items)
tag_widget.setHidden(True)
self._layout.content_layout.addWidget(tag_widget)
self.layout().content_layout.addWidget(tag_widget)
tag_widget: QWidget = self._layout.content_layout.itemAt(index).widget()
tag_widget: QWidget = self.layout().content_layout.itemAt(index).widget()
assert isinstance(tag_widget, TagWidget)
return tag_widget
+2 -2
View File
@@ -74,7 +74,7 @@ def test_file_path_display(
entry = qt_driver.lib.get_entry(2)
assert isinstance(entry, Entry)
filename = entry.path
panel._layout.file_attrs.update_stats(filepath=unwrap(qt_driver.lib.library_dir) / filename)
panel.layout().file_attrs.update_stats(filepath=unwrap(qt_driver.lib.library_dir) / filename)
# Generate the expected file string.
# This is copied directly from the file_attributes.py file
@@ -92,7 +92,7 @@ def test_file_path_display(
file_str += f"<b>{'\u200b'.join(part_)}</b>"
# Assert the file path is displayed correctly
assert panel._layout.file_attrs.file_label.text() == file_str
assert panel.layout().file_attrs.file_label.text() == file_str
@pytest.mark.parametrize(
+7 -7
View File
@@ -17,10 +17,10 @@ def test_update_selection_empty(qt_driver: QtDriver):
panel.set_selection(qt_driver.selected)
# Panel should disable UI that allows for entry modification
assert panel._layout.add_tag_button.isEnabled() == panel._layout.add_field_button.isEnabled()
assert panel.layout().add_tag_button.isEnabled() == panel.layout().add_field_button.isEnabled()
assert (
not panel._layout.add_tag_button.isEnabled()
and not panel._layout.add_field_button.isEnabled()
not panel.layout().add_tag_button.isEnabled()
and not panel.layout().add_field_button.isEnabled()
)
@@ -32,8 +32,8 @@ def test_update_selection_single(qt_driver: QtDriver, entry_full: Entry):
panel.set_selection(qt_driver.selected)
# Panel should enable UI that allows for entry modification
assert panel._layout.add_tag_button.isEnabled() == panel._layout.add_field_button.isEnabled()
assert panel._layout.add_tag_button.isEnabled() and panel._layout.add_field_button.isEnabled()
assert panel.layout().add_tag_button.isEnabled() == panel.layout().add_field_button.isEnabled()
assert panel.layout().add_tag_button.isEnabled() and panel.layout().add_field_button.isEnabled()
def test_update_selection_multiple(qt_driver: QtDriver):
@@ -45,5 +45,5 @@ def test_update_selection_multiple(qt_driver: QtDriver):
panel.set_selection(qt_driver.selected)
# Panel should enable UI that allows for entry modification
assert panel._layout.add_tag_button.isEnabled() == panel._layout.add_field_button.isEnabled()
assert panel._layout.add_tag_button.isEnabled() and panel._layout.add_field_button.isEnabled()
assert panel.layout().add_tag_button.isEnabled() == panel.layout().add_field_button.isEnabled()
assert panel.layout().add_tag_button.isEnabled() and panel.layout().add_field_button.isEnabled()