feat: add field name editing on entries

This commit is contained in:
Travis Abendshien
2026-06-25 04:09:26 -07:00
parent c88cfc9968
commit d4f3f49d3d
28 changed files with 451 additions and 492 deletions

View File

@@ -1540,7 +1540,12 @@ class Library:
session.commit()
def update_text_field(
self, entry_ids: list[int] | int, field: TextField, value: str, is_multiline: bool
self,
entry_ids: list[int] | int,
field: TextField,
name: str,
value: str,
is_multiline: bool,
):
"""Update a TextField field on one or more Entries."""
if isinstance(entry_ids, int):
@@ -1552,7 +1557,7 @@ class Library:
update_stmt = (
update(field_type)
.where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids)))
.values(value=value, is_multiline=is_multiline)
.values(name=name, value=value, is_multiline=is_multiline)
)
session.execute(update_stmt)
@@ -1562,6 +1567,7 @@ class Library:
self,
entry_ids: list[int] | int,
field: DatetimeField,
name: str,
value: datetime,
):
"""Update a DatetimeField field on one or more Entries."""
@@ -1574,7 +1580,7 @@ class Library:
update_stmt = (
update(field_type)
.where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids)))
.values(value=value)
.values(name=name, value=value)
)
session.execute(update_stmt)

View File

@@ -0,0 +1,67 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from typing import override
import structlog
from tagstudio.qt.views.edit_text_view import EditTextView
logger = structlog.get_logger(__name__)
class EditText(EditTextView):
def __init__(self, name: str, text: str | None, is_multiline: bool = False):
super().__init__()
self.name_field.setText(name)
self.text = text
self.is_multiline: bool = is_multiline
self.multiline_checkbox.setChecked(is_multiline)
self.multiline_checkbox.clicked.connect(lambda checked: self.on_multiline_checked(checked))
if self.is_multiline:
self.text_line.hide()
self.text_line_stretch.hide()
self.text_box.setPlainText(self.text or "")
else:
self.text_box.hide()
self.text_line.setText(self.text or "")
def on_multiline_checked(self, checked: bool):
was_multiline = self.is_multiline
self.is_multiline = checked
if was_multiline:
self.text = self.text_box.toPlainText()
self.text_box.hide()
self.text_line.setText(self.text)
self.text_line.show()
self.text_line_stretch.show()
else:
self.text = self.text_line.text()
self.text_line.hide()
self.text_line_stretch.hide()
self.text_box.setPlainText(self.text)
self.text_box.show()
@override
def parent_post_init(self):
if self.is_multiline:
self.text_box.setFocus()
else:
self.text_line.setFocus()
@override
def saved_data(self) -> dict[str, str | bool]:
return {
"name": self.name_field.text(),
"value": self.text_box.toPlainText() if self.is_multiline else self.text_line.text(),
"is_multiline": self.is_multiline,
}
@override
def reset(self):
self.text_box.setPlainText(self.text or "")

View File

@@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-only
from collections.abc import Callable
from typing import override
from warnings import catch_warnings
@@ -27,8 +26,6 @@ class FieldTemplateSearchModal(PanelModal):
self,
library: Library,
is_field_template_chooser: bool = True,
done_callback: Callable[..., None] | None = None,
save_callback: Callable[..., None] | None = None,
has_save: bool = False,
) -> None:
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
@@ -39,9 +36,7 @@ class FieldTemplateSearchModal(PanelModal):
super().__init__(
self.search_panel,
Translations["field.add.plural"],
done_callback=done_callback,
save_callback=save_callback,
has_save=has_save,
is_savable=has_save,
)
@@ -87,7 +82,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
Translations["field_template.add"]
if add_to_entry
else Translations["field_template.new"],
has_save=True,
is_savable=True,
)
if query.strip():
@@ -104,7 +99,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
panel,
item.name,
Translations["field_template.edit"],
has_save=True,
is_savable=True,
)
modal.saved.connect(lambda: self.edit_item(panel))

View File

@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
from functools import partial
from typing import TYPE_CHECKING, override
import structlog
@@ -77,20 +78,20 @@ class TagBoxWidget(TagBoxWidgetView):
build_tag_panel,
self.__driver.lib.tag_display_name(tag),
"Edit Tag",
done_callback=self.on_update.emit,
has_save=True,
)
# TODO - this was update_tag()
edit_modal.saved.connect(
lambda: self.__driver.lib.update_tag(
build_tag_panel.build_tag(),
parent_ids=set(build_tag_panel.parent_ids),
alias_names=set(build_tag_panel.alias_names),
alias_ids=set(build_tag_panel.alias_ids),
)
is_savable=True,
)
edit_modal.saved.connect(partial(self._update_tag_callback, build_tag_panel))
edit_modal.show()
def _update_tag_callback(self, build_tag_panel: BuildTagPanel):
self.__driver.lib.update_tag(
build_tag_panel.build_tag(),
parent_ids=set(build_tag_panel.parent_ids),
alias_names=set(build_tag_panel.alias_names),
alias_ids=set(build_tag_panel.alias_ids),
)
self.on_update.emit()
@override
def _on_search(self, tag: Tag) -> None:
self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}")

View File

@@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-only
from collections.abc import Callable
from typing import TYPE_CHECKING, override
from warnings import catch_warnings
@@ -34,8 +33,6 @@ class TagSearchModal(PanelModal):
library: Library,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
done_callback: Callable[..., None] | None = None,
save_callback: Callable[..., None] | None = None,
has_save: bool = False,
):
self.tsp = TagSearchPanel(
@@ -47,9 +44,7 @@ class TagSearchModal(PanelModal):
super().__init__(
self.tsp,
Translations["tag.add.plural"],
done_callback=done_callback,
save_callback=save_callback,
has_save=has_save,
is_savable=has_save,
)
@@ -94,7 +89,7 @@ class TagSearchPanel(SearchPanel[Tag]):
panel,
Translations["tag.new"],
Translations["tag.add"] if add_to_entry else Translations["tag.new"],
has_save=True,
is_savable=True,
)
if query.strip():
@@ -113,7 +108,7 @@ class TagSearchPanel(SearchPanel[Tag]):
edit_tag_panel,
self.__lib.tag_display_name(item),
Translations["tag.edit"],
has_save=True,
is_savable=True,
)
edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel))

View File

@@ -32,6 +32,7 @@ from tagstudio.qt.mixed.tag_widget import (
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.panel_modal import PanelWidget
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
logger = structlog.get_logger(__name__)
@@ -135,37 +136,7 @@ class BuildColorPanel(PanelWidget):
self.border_layout.addWidget(self.border_checkbox)
self.border_label = QLabel(Translations["color.color_border"])
self.border_layout.addWidget(self.border_label)
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
border_color = get_border_color(primary_color)
highlight_color = get_highlight_color(primary_color)
text_color: QColor = get_text_color(primary_color, highlight_color)
self.border_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)
self.border_checkbox.setStyleSheet(checkbox_style())
# Add Widgets to Layout ================================================
self.root_layout.addWidget(self.preview_widget)

View File

@@ -25,7 +25,6 @@ from PySide6.QtWidgets import (
QWidget,
)
from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
from tagstudio.core.utils.types import unwrap
@@ -39,9 +38,10 @@ from tagstudio.qt.mixed.tag_widget import (
get_primary_color,
get_text_color,
)
from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
logger = structlog.get_logger(__name__)
@@ -201,9 +201,9 @@ class BuildTagPanel(PanelWidget):
self.tag_color_selection,
chose_tag_color_title,
chose_tag_color_title,
done_callback=lambda: self.choose_color_callback(
self.tag_color_selection.selected_color
),
)
self.choose_color_modal.done.connect(
lambda: self.choose_color_callback(self.tag_color_selection.selected_color)
)
self.color_button.button.clicked.connect(self.choose_color_modal.show)
self.color_layout.addWidget(self.color_button)
@@ -218,38 +218,7 @@ class BuildTagPanel(PanelWidget):
self.cat_title = QLabel(Translations["tag.is_category"])
self.cat_checkbox = QCheckBox()
self.cat_checkbox.setFixedSize(22, 22)
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
border_color = get_border_color(primary_color)
highlight_color = get_highlight_color(primary_color)
text_color: QColor = get_text_color(primary_color, highlight_color)
self.cat_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)
self.cat_checkbox.setStyleSheet(checkbox_style())
self.cat_layout.addWidget(self.cat_checkbox)
self.cat_layout.addWidget(self.cat_title)
@@ -263,33 +232,7 @@ class BuildTagPanel(PanelWidget):
self.hidden_title = QLabel(Translations["tag.is_hidden"])
self.hidden_checkbox = QCheckBox()
self.hidden_checkbox.setFixedSize(22, 22)
self.hidden_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)
self.hidden_checkbox.setStyleSheet(checkbox_style())
self.hidden_layout.addWidget(self.hidden_checkbox)
self.hidden_layout.addWidget(self.hidden_title)

View File

@@ -134,7 +134,7 @@ class ColorBoxWidget(FieldWidget):
self.edit_modal = PanelModal(
build_color_panel,
"Edit Color",
has_save=True,
is_savable=True,
)
self.edit_modal.saved.connect(

View File

@@ -3,12 +3,11 @@
import typing
from collections.abc import Callable
from datetime import datetime as dt
from typing import cast
from typing import cast, override
from PySide6.QtCore import QDateTime
from PySide6.QtWidgets import QDateTimeEdit, QVBoxLayout
from PySide6.QtWidgets import QDateTimeEdit, QLineEdit, QVBoxLayout
from tagstudio.qt.views.panel_modal import PanelWidget
@@ -40,11 +39,16 @@ def qdtf2dtf(dtf: str) -> str:
class DatetimePicker(PanelWidget):
def __init__(self, driver: "QtDriver", datetime: dt | str):
def __init__(self, driver: "QtDriver", name: str, datetime: dt | str):
super().__init__()
self.setMinimumSize(300, 60)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.name_field = QLineEdit()
self.name_field.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px")
self.name_field.setText(name)
if isinstance(datetime, str):
datetime = DatetimePicker.string2dt(datetime)
self.datetime_edit = QDateTimeEdit()
@@ -55,20 +59,24 @@ class DatetimePicker(PanelWidget):
self.datetime_edit.setDisplayFormat(qdtf2dtf(driver.settings.datetime_format))
self.initial_value = datetime
self.root_layout.addWidget(self.name_field)
self.root_layout.addWidget(self.datetime_edit)
def get_content(self):
return DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime()))
@override
def saved_data(self) -> dict[str, str]:
return {
"name": self.name_field.text(),
"value": DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime())),
}
@override
def parent_post_init(self):
self.datetime_edit.setFocus()
@override
def reset(self):
self.datetime_edit.setDateTime(DatetimePicker.dt2qdt(self.initial_value))
def add_callback(self, callback: Callable, event: str = "returnPressed"):
if event == "returnPressed":
pass
else:
raise ValueError(f"unknown event type: {event}")
@staticmethod
def qdt2dt(qdt: QDateTime) -> dt:
return cast(dt, qdt.toPython())

View File

@@ -2,10 +2,10 @@
# SPDX-License-Identifier: GPL-3.0-only
import sys
import typing
from collections.abc import Callable
from datetime import datetime as dt
from functools import partial
from warnings import catch_warnings
import structlog
@@ -31,13 +31,12 @@ from tagstudio.core.library.alchemy.fields import (
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.edit_text_controller import EditText
from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
from tagstudio.qt.mixed.field_widget import FieldContainer
from tagstudio.qt.mixed.text_field import TextWidget
from tagstudio.qt.mixed.text_field import TextContainerWidget
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
from tagstudio.qt.views.edit_text_box_modal import EditTextBox
from tagstudio.qt.views.edit_text_line_modal import EditTextLine
from tagstudio.qt.views.panel_modal import PanelModal
if typing.TYPE_CHECKING:
@@ -47,7 +46,7 @@ logger = structlog.get_logger(__name__)
class FieldContainers(QWidget):
"""The Preview Panel Widget."""
"""Widget for the tag and field containers displayed inside the Preview Panel."""
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__()
@@ -102,6 +101,11 @@ class FieldContainers(QWidget):
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.addWidget(self.scroll_area)
@property
def top_entry_id(self) -> int:
"""Get the topmost entry ID in the (cached) selected entries."""
return self.cached_entries[0].id
def update_from_entry(self, entry_id: int, update_badges: bool = True) -> None:
"""Update tags and fields from a single Entry source."""
logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id)
@@ -130,7 +134,7 @@ class FieldContainers(QWidget):
# Write field container(s)
for index, field in enumerate(entry_fields, start=container_index):
self.write_container(index, field, is_mixed=False)
self.write_field_container(index, field, is_mixed=False)
# Hide leftover container(s)
if len(self.containers) > container_len:
@@ -247,22 +251,128 @@ class FieldContainers(QWidget):
)
self.driver.add_tags_to_selected_callback(tag_ids)
def write_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None:
"""Update/Create data for a FieldContainer.
def write_field_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None:
"""Update/Create data for a field FieldContainer.
Args:
index(int): The container index.
field(BaseField): The type of field to write to.
field(BaseField): The field to write in this container.
is_mixed(bool): Relevant when multiple items are selected.
If True, field is not present in all selected items.
If True, field is not present in all selected items.
"""
def update_text_field_callback(
field: TextField, entry_id: int, content: dict[str, str | bool]
) -> None:
"""Callback called when a text field has updated data."""
self._update_text_field(
field, str(content["name"]), str(content["value"]), bool(content["is_multiline"])
)
self.update_from_entry(entry_id)
def update_datetime_field_callback(
field: DatetimeField, entry_id: int, content: dict[str, str]
) -> None:
"""Callback called when a datetime field has updated data."""
self.update_datetime_field(field, str(content["name"]), str(content["value"]))
self.update_from_entry(entry_id)
def remove_field_callback(field: BaseField, entry_id: int) -> None:
"""Callback called when a field needs to be removed from an entry."""
self._remove_field(field)
self.update_from_entry(entry_id)
def write_text_container(
container: FieldContainer, field: TextField, title: str, is_mixed: bool
):
container.set_title(field.name)
# Normalize line endings in any text content.
if not is_mixed:
assert isinstance(field.value, str | type(None))
text = (field.value or "").replace("\r", "\n")
else:
text = f"<i>{Translations['field.mixed_data']}</i>"
inner_widget = TextContainerWidget(title, text)
container.set_inner_widget(inner_widget)
if not is_mixed:
edit_modal = PanelModal(
EditText(field.name, field.value, field.is_multiline),
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
is_savable=True,
inline_title=False,
)
edit_modal.saved_data.connect(
partial(update_text_field_callback, field, self.top_entry_id)
)
container.set_edit_callback(edit_modal.show)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(title),
callback=partial(remove_field_callback, field, self.top_entry_id),
)
)
def write_datetime_container(
container: FieldContainer, field: DatetimeField, title: str, is_mixed: bool
):
container.set_title(field.name)
if not is_mixed:
try:
assert field.value is not None
text = self.driver.settings.format_datetime(
DatetimePicker.string2dt(field.value)
)
except (ValueError, AssertionError):
text = str(field.value)
else:
text = f"<i>{Translations['field.mixed_data']}</i>"
inner_widget = TextContainerWidget(title, text)
container.set_inner_widget(inner_widget)
if not is_mixed:
edit_modal = PanelModal(
DatetimePicker(self.driver, field.name, field.value or dt.now()),
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
is_savable=True,
inline_title=False,
)
edit_modal.saved_data.connect(
partial(update_datetime_field_callback, field, self.top_entry_id)
)
container.set_edit_callback(edit_modal.show)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.name),
callback=partial(remove_field_callback, field, self.top_entry_id),
)
)
def write_unknown_container():
container.set_title(field.name)
inner_widget = TextContainerWidget(title, field.name)
container.set_inner_widget(inner_widget)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.name),
callback=partial(remove_field_callback, field, self.top_entry_id),
)
)
logger.info(
"[FieldContainers][write_container]",
index=index,
name=field.name,
type=field.class_name,
)
# Create new containers if necessary
if len(self.containers) < (index + 1):
container = FieldContainer()
self.containers.append(container)
@@ -274,156 +384,27 @@ class FieldContainers(QWidget):
field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown")
title = f"{field.name} ({Translations[field_name_key]})"
# Single-line Text
if type(field) is TextField and not field.is_multiline:
container.set_title(field.name)
container.set_inline(False)
# Normalize line endings in any text content.
if not is_mixed:
assert isinstance(field.value, str | type(None))
text = field.value or ""
else:
text = "<i>Mixed Data</i>" # TODO: Localize this
inner_widget = TextWidget(title, text)
container.set_inner_widget(inner_widget)
if not is_mixed:
modal = PanelModal(
EditTextLine(field.value),
title=title,
window_title=f"Edit {field.name}", # TODO: Localize this
save_callback=( # pyright: ignore[reportArgumentType]
lambda content: (
self.update_text_field(field, content, is_multiline=False),
self.update_from_entry(self.cached_entries[0].id),
)
),
)
if "pytest" in sys.modules:
# for better testability
container.modal = modal # pyright: ignore[reportAttributeAccessIssue]
container.set_edit_callback(modal.show)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(title),
callback=lambda: (
self.remove_field(field),
self.update_from_entry(self.cached_entries[0].id),
),
)
)
# Multiline Text
elif type(field) is TextField and field.is_multiline:
container.set_title(field.name)
container.set_inline(False)
# Normalize line endings in any text content.
if not is_mixed:
assert isinstance(field.value, str | type(None))
text = (field.value or "").replace("\r", "\n")
else:
text = "<i>Mixed Data</i>" # TODO: Localize this
inner_widget = TextWidget(title, text)
container.set_inner_widget(inner_widget)
if not is_mixed:
modal = PanelModal(
EditTextBox(field.value),
title=title,
window_title=f"Edit {field.name}", # TODO: Localize this
save_callback=( # pyright: ignore[reportArgumentType]
lambda content: (
self.update_text_field(field, content, is_multiline=True),
self.update_from_entry(self.cached_entries[0].id),
)
),
)
container.set_edit_callback(modal.show)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.name),
callback=lambda: (
self.remove_field(field),
self.update_from_entry(self.cached_entries[0].id),
),
)
)
# Write containers
if type(field) is TextField:
write_text_container(container, field, title, is_mixed)
elif type(field) is DatetimeField:
logger.info("[FieldContainers][write_container] Datetime Field", field=field)
if not is_mixed:
container.set_title(field.name)
container.set_inline(False)
try:
assert field.value is not None
text = self.driver.settings.format_datetime(
DatetimePicker.string2dt(field.value)
)
except (ValueError, AssertionError):
text = str(field.value)
inner_widget = TextWidget(title, text)
container.set_inner_widget(inner_widget)
modal = PanelModal(
DatetimePicker(self.driver, field.value or dt.now()),
title=f"Edit {field.name}",
save_callback=( # pyright: ignore[reportArgumentType]
lambda content: (
self.update_datetime_field(field, content),
self.update_from_entry(self.cached_entries[0].id),
)
),
)
container.set_edit_callback(modal.show)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.name),
callback=lambda: (
self.remove_field(field),
self.update_from_entry(self.cached_entries[0].id),
),
)
)
else:
text = "<i>Mixed Data</i>" # TODO: Localize this
inner_widget = TextWidget(title, text)
container.set_inner_widget(inner_widget)
write_datetime_container(container, field, title, is_mixed)
else:
logger.warning(
"[FieldContainers][write_container] Unknown Field", field=field
) # TODO: Localize this
container.set_title(field.name)
container.set_inline(False)
inner_widget = TextWidget(title, field.name)
container.set_inner_widget(inner_widget)
container.set_remove_callback(
lambda: self.remove_message_box(
prompt=self.remove_field_prompt(field.name),
callback=lambda: (
self.remove_field(field),
self.update_from_entry(self.cached_entries[0].id),
),
)
)
write_unknown_container()
container.setHidden(False)
def write_tag_container(
self, index: int, tags: set[Tag], category_tag: Tag | None = None, is_mixed: bool = False
) -> None:
"""Update/Create tag data for a FieldContainer.
"""Update/Create tag data for a tag FieldContainer.
Args:
index(int): The container index.
tags(set[Tag]): The list of tags for this container.
category_tag(Tag|None): The category tag this container represents.
is_mixed(bool): Relevant when multiple items are selected.
If True, field is not present in all selected items.
If True, field is not present in all selected items.
"""
logger.info("[FieldContainers][write_tag_container]", index=index)
if len(self.containers) < (index + 1):
@@ -433,10 +414,7 @@ class FieldContainers(QWidget):
else:
container = self.containers[index]
container.set_title(
"Tags" if not category_tag else category_tag.name
) # TODO: Localize this
container.set_inline(False)
container.set_title(Translations["entries.tags"] if not category_tag else category_tag.name)
if not is_mixed:
inner_widget = container.get_inner_widget()
@@ -446,10 +424,7 @@ class FieldContainers(QWidget):
inner_widget.on_update.disconnect()
else:
inner_widget = TagBoxWidget(
"Tags", # TODO: Localize this
self.driver,
)
inner_widget = TagBoxWidget(Translations["entries.tags"], self.driver)
container.set_inner_widget(inner_widget)
inner_widget.set_entries([e.id for e in self.cached_entries])
inner_widget.set_tags(tags)
@@ -458,15 +433,15 @@ class FieldContainers(QWidget):
lambda: self.update_from_entry(self.cached_entries[0].id, update_badges=True)
)
else:
text = "<i>Mixed Data</i>"
inner_widget = TextWidget("Mixed Tags", text)
text = f"<i>{Translations['field.mixed_data']}</i>"
inner_widget = TextContainerWidget("Mixed Tags", text)
container.set_inner_widget(inner_widget)
container.set_edit_callback()
container.set_remove_callback()
container.setHidden(False)
def remove_field(self, field: BaseField) -> None:
def _remove_field(self, field: BaseField) -> None:
"""Remove a field from all selected Entries."""
logger.info(
"[FieldContainers] Removing Field",
@@ -476,24 +451,26 @@ class FieldContainers(QWidget):
entry_ids = [e.id for e in self.cached_entries]
self.lib.remove_entry_field(field, entry_ids)
def update_text_field(self, field: TextField, value: str, is_multiline: bool) -> None:
def _update_text_field(
self, field: TextField, name: str, value: str, is_multiline: bool
) -> None:
"""Update a text field across selected entries."""
entry_ids = [e.id for e in self.cached_entries]
assert entry_ids, "No entries selected"
self.lib.update_text_field(entry_ids, field, value, is_multiline)
self.lib.update_text_field(entry_ids, field, name, value, is_multiline)
def update_datetime_field(self, field: DatetimeField, value: str) -> None:
def update_datetime_field(self, field: DatetimeField, name: str, value: str) -> None:
"""Update a datetime field across selected entries."""
entry_ids = [e.id for e in self.cached_entries]
assert entry_ids, "No entries selected"
self.lib.update_datetime_field(entry_ids, field, dt.fromisoformat(value))
self.lib.update_datetime_field(entry_ids, field, name, dt.fromisoformat(value))
def remove_message_box(self, prompt: str, callback: Callable) -> None:
def remove_message_box(self, prompt: str, callback: Callable[..., None]) -> None:
remove_mb = QMessageBox()
remove_mb.setText(prompt)
remove_mb.setWindowTitle("Remove Field") # TODO: Localize
remove_mb.setWindowTitle(Translations["Remove Field"])
remove_mb.setIcon(QMessageBox.Icon.Warning)
cancel_button = remove_mb.addButton(
Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole

View File

@@ -53,7 +53,6 @@ class FieldContainer(QWidget):
super().__init__()
self.setObjectName("fieldContainer")
self.title: str = title
self.inline: bool = inline
self.copy_callback: Callable[[], None] | None = None
self.edit_callback: Callable[[], None] | None = None
self.remove_callback: Callable[[], None] | None = None
@@ -170,9 +169,6 @@ class FieldContainer(QWidget):
self.title = self.title = f"<h4>{title}</h4>"
self.title_widget.setText(self.title)
def set_inline(self, inline: bool) -> None:
self.inline = inline
@override
def enterEvent(self, event: QEnterEvent) -> None:
# NOTE: You could pass the hover event to the FieldWidget if needed.

View File

@@ -356,9 +356,9 @@ class SettingsPanel(PanelWidget):
modal = PanelModal(
widget=settings_panel,
window_title=Translations["settings.title"],
done_callback=lambda: settings_panel.update_settings(driver),
has_save=True,
is_savable=True,
)
modal.done.connect(lambda: settings_panel.update_settings(driver))
modal.title_widget.setVisible(False)
return modal

View File

@@ -176,7 +176,7 @@ class TagColorManager(QWidget):
self.create_namespace_modal = PanelModal(
build_namespace_panel,
Translations["namespace.create.title"],
has_save=True,
is_savable=True,
)
self.create_namespace_modal.saved.connect(

View File

@@ -10,8 +10,8 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel
from tagstudio.qt.mixed.field_widget import FieldWidget
class TextWidget(FieldWidget):
def __init__(self, title, text: str) -> None:
class TextContainerWidget(FieldWidget):
def __init__(self, title: str, text: str) -> None:
super().__init__(title)
self.setObjectName("textBox")
self.base_layout = QHBoxLayout()

View File

@@ -24,7 +24,7 @@ from typing import TypeVar
from warnings import catch_warnings
import structlog
from humanfriendly import format_size, format_timespan
from humanfriendly import format_size, format_timespan # pyright: ignore[reportUnknownVariableType]
from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal
from PySide6.QtGui import (
QColor,
@@ -45,7 +45,8 @@ from PySide6.QtWidgets import (
QScrollArea,
)
import tagstudio.qt.resources_rc # noqa: F401
# This import has side-effect of importing PySide resources
import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport]
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH
from tagstudio.core.driver import DriverMixin
from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption
@@ -64,11 +65,7 @@ from tagstudio.core.utils.str_formatting import is_version_outdated
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox
from tagstudio.qt.controllers.field_template_search_panel_controller import (
FieldTemplateSearchPanel,
)
# this import has side-effect of import PySide resources
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchPanel
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
@@ -376,10 +373,12 @@ class QtDriver(DriverMixin, QObject):
view=TagSearchPanelView(is_tag_chooser=False),
),
title=Translations["tag_manager.title"],
done_callback=lambda checked=False: self.main_window.preview_panel.set_selection(
is_savable=False,
)
self.tag_manager_panel.done.connect(
lambda checked=False: self.main_window.preview_panel.set_selection(
self.selected, update_preview=False
),
has_save=False,
)
)
# Initialize the Color Group Manager panel
@@ -393,10 +392,12 @@ class QtDriver(DriverMixin, QObject):
view=FieldTemplateSearchPanelView(is_field_template_chooser=False),
),
title=Translations["field_template_manager.title"],
done_callback=lambda checked=False: self.main_window.preview_panel.set_selection(
is_savable=False,
)
self.field_template_manager_panel.done.connect(
lambda checked=False: self.main_window.preview_panel.set_selection(
self.selected, update_preview=False
),
has_save=False,
)
)
# Initialize the Tag Search panel
@@ -741,7 +742,7 @@ class QtDriver(DriverMixin, QObject):
self.ignore_modal = PanelModal(
panel,
Translations["menu.edit.ignore_files"],
has_save=True,
is_savable=True,
)
self.ignore_modal.saved.connect(panel.save)
self.main_window.menu_bar.ignore_modal_action.triggered.connect(self.ignore_modal.show)
@@ -880,7 +881,7 @@ class QtDriver(DriverMixin, QObject):
panel,
Translations["tag.new"],
Translations["tag.add"],
has_save=True,
is_savable=True,
)
self.modal.saved.connect(

View File

@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
from typing import override
from typing import Any, override
from PySide6.QtCore import Signal
from PySide6.QtGui import QMouseEvent
@@ -14,8 +14,8 @@ class ClickableLabel(QLabel):
clicked = Signal()
def __init__(self):
super().__init__()
def __init__(self, *args: Any, **kwarg: Any): # pyright: ignore[reportExplicitAny]
super().__init__(*args, **kwarg)
@override
def mousePressEvent(self, ev: QMouseEvent):

View File

@@ -29,31 +29,31 @@ class EditFieldTemplateModalView(PanelWidget):
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Field Name -------------------------------------------------------------------------------
self.__name_widget = QWidget()
self.__name_layout = QVBoxLayout(self.__name_widget)
self.__name_layout.setStretch(1, 1)
self.__name_layout.setContentsMargins(0, 0, 0, 0)
self.__name_layout.setSpacing(0)
self.__name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.__name_title = QLabel(Translations["field.name"])
self.__name_layout.addWidget(self.__name_title)
self._name_widget = QWidget()
self._name_layout = QVBoxLayout(self._name_widget)
self._name_layout.setStretch(1, 1)
self._name_layout.setContentsMargins(0, 0, 0, 0)
self._name_layout.setSpacing(0)
self._name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self._name_title = QLabel(Translations["field.name"])
self._name_layout.addWidget(self._name_title)
self.name_field = QLineEdit()
self.name_field.setFixedHeight(24)
self.name_field.setPlaceholderText(Translations["field.field_name_required"])
self.__name_layout.addWidget(self.name_field)
self._name_layout.addWidget(self.name_field)
# Field Type -------------------------------------------------------------------------------
self.__type_widget = QWidget()
self.__type_layout = QVBoxLayout(self.__type_widget)
self.__type_layout.setStretch(1, 1)
self.__type_layout.setContentsMargins(0, 0, 0, 0)
self.__type_layout.setSpacing(0)
self.__type_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.__type_title = QLabel(Translations["field.type"])
self.__type_layout.addWidget(self.__type_title)
self._type_widget = QWidget()
self._type_layout = QVBoxLayout(self._type_widget)
self._type_layout.setStretch(1, 1)
self._type_layout.setContentsMargins(0, 0, 0, 0)
self._type_layout.setSpacing(0)
self._type_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self._type_title = QLabel(Translations["field.type"])
self._type_layout.addWidget(self._type_title)
self._type_combobox = QComboBox()
self.__type_layout.addWidget(self._type_combobox)
self._type_layout.addWidget(self._type_combobox)
# Add Widgets to Layout ====================================================================
self.root_layout.addWidget(self.__name_widget)
self.root_layout.addWidget(self.__type_widget)
self.root_layout.addWidget(self._name_widget)
self.root_layout.addWidget(self._type_widget)

View File

@@ -1,25 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtWidgets import QPlainTextEdit, QVBoxLayout
from tagstudio.qt.views.panel_modal import PanelWidget
class EditTextBox(PanelWidget):
def __init__(self, text):
super().__init__()
self.setMinimumSize(480, 480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.text = text
self.text_edit = QPlainTextEdit()
self.text_edit.setPlainText(text)
self.root_layout.addWidget(self.text_edit)
def get_content(self) -> str:
return self.text_edit.toPlainText()
def reset(self):
self.text_edit.setPlainText(self.text)

View File

@@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from collections.abc import Callable
from PySide6.QtWidgets import QLineEdit, QVBoxLayout
from tagstudio.qt.views.panel_modal import PanelWidget
class EditTextLine(PanelWidget):
def __init__(self, text):
super().__init__()
self.setMinimumWidth(480)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.text = text
self.text_edit = QLineEdit()
self.text_edit.setText(text)
self.root_layout.addWidget(self.text_edit)
def get_content(self) -> str:
return self.text_edit.text()
def reset(self):
self.text_edit.setText(self.text)
def add_callback(self, callback: Callable, event: str = "returnPressed"):
if event == "returnPressed":
self.text_edit.returnPressed.connect(callback)
else:
raise ValueError(f"unknown event type: {event}")

View File

@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtGui import Qt
from PySide6.QtWidgets import (
QCheckBox,
QHBoxLayout,
QLineEdit,
QPlainTextEdit,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.clickable_label import ClickableLabel
from tagstudio.qt.views.panel_modal import PanelWidget
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
class EditTextView(PanelWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(480, 240)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.name_field = QLineEdit()
self.name_field.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px")
self.text_box = QPlainTextEdit()
self.text_line = QLineEdit()
self.text_line_stretch = QWidget()
self.text_line_stretch.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
# Is Multiline
self.multiline_widget = QWidget()
self.multiline_layout = QHBoxLayout(self.multiline_widget)
self.multiline_layout.setStretch(1, 1)
self.multiline_layout.setContentsMargins(0, 0, 0, 0)
self.multiline_layout.setSpacing(6)
self.multiline_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.multiline_title = ClickableLabel(Translations["field.text.is_multiline"])
self.multiline_checkbox = QCheckBox()
self.multiline_checkbox.setFixedSize(22, 22)
self.multiline_checkbox.setStyleSheet(checkbox_style())
self.multiline_title.clicked.connect(self.multiline_checkbox.click)
self.multiline_layout.addWidget(self.multiline_checkbox)
self.multiline_layout.addWidget(self.multiline_title)
self.root_layout.addWidget(self.name_field)
self.root_layout.addWidget(self.text_box)
self.root_layout.setStretch(2, 1)
self.root_layout.addWidget(self.text_line)
self.root_layout.addWidget(self.text_line_stretch)
self.root_layout.setStretch(4, 1)
self.root_layout.addWidget(self.multiline_widget)

View File

@@ -1,6 +1,8 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from typing import override
from PySide6.QtWidgets import QWidget
from tagstudio.core.library.alchemy.library import Library
@@ -16,6 +18,7 @@ class FieldTemplateSearchPanelView(SearchPanelView):
self.search_field.setPlaceholderText(Translations["home.search_field_templates"])
self.create_button.setText(Translations["field_template.create"])
@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

View File

@@ -10,7 +10,7 @@ import structlog
from PIL import Image, ImageQt
from PySide6 import QtCore
from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt
from PySide6.QtGui import QAction, QColor, QPixmap
from PySide6.QtGui import QAction, QPixmap
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
@@ -35,18 +35,17 @@ from PySide6.QtWidgets import (
)
from tagstudio.core.enums import ShowFilepathOption
from tagstudio.core.library.alchemy.enums import SortingModeEnum, TagColorEnum
from tagstudio.core.library.alchemy.enums import SortingModeEnum
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
from tagstudio.qt.mixed.landing import LandingWidget
from tagstudio.qt.mixed.pagination import Pagination
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
from tagstudio.qt.mnemonics import assign_mnemonics
from tagstudio.qt.models.palette import ColorType, get_tag_color
from tagstudio.qt.platform_strings import trash_term
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.thumb_grid_layout import ThumbGridLayout
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
@@ -589,11 +588,6 @@ class MainWindow(QMainWindow):
self.extra_input_layout = QHBoxLayout()
self.extra_input_layout.setObjectName("extra_input_layout")
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
border_color = get_border_color(primary_color)
highlight_color = get_highlight_color(primary_color)
text_color: QColor = get_text_color(primary_color, highlight_color)
## Show hidden entries checkbox
self.show_hidden_entries_widget = QWidget()
self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget)
@@ -604,33 +598,7 @@ class MainWindow(QMainWindow):
self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"])
self.show_hidden_entries_checkbox = QCheckBox()
self.show_hidden_entries_checkbox.setFixedSize(22, 22)
self.show_hidden_entries_checkbox.setStyleSheet(
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)
self.show_hidden_entries_checkbox.setStyleSheet(checkbox_style())
self.show_hidden_entries_checkbox.setChecked(False) # Default: No

View File

@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-only
from collections.abc import Callable
from typing import override
import contextlib
from typing import Any, override
import structlog
from PySide6 import QtCore, QtGui
@@ -16,18 +16,19 @@ logger = structlog.get_logger(__name__)
class PanelModal(QWidget):
saved = Signal()
"""A generic reusable modal panel widget."""
done = Signal()
saved = Signal()
saved_data = Signal(type(Any))
# TODO: Separate callbacks from the buttons you want, and just generally
# figure out what you want from this.
def __init__(
self,
widget: "PanelWidget",
title: str = "",
window_title: str | None = None,
done_callback: Callable[..., None] | None = None,
save_callback: Callable[..., None] | None = None,
has_save: bool = False,
is_savable: bool = False,
inline_title: bool = True,
):
# [Done]
# - OR -
@@ -37,37 +38,24 @@ class PanelModal(QWidget):
self.setWindowTitle(title if window_title is None else window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 6)
self.title_widget = QLabel()
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px")
self.title_widget.setText(title)
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.root_layout.setContentsMargins(6, 0 if inline_title else 12, 6, 6)
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
# self.cancel_button = QPushButton()
# self.cancel_button.setText('Cancel')
if not (save_callback or has_save):
if not is_savable:
self.done_button = QPushButton(Translations["generic.done"])
self.done_button.setAutoDefault(True)
self.done_button.clicked.connect(self.hide)
if done_callback:
self.done_button.clicked.connect(done_callback)
self.done_button.clicked.connect(self.done.emit)
self.widget.panel_done_button = self.done_button
self.button_layout.addWidget(self.done_button)
if save_callback or has_save:
else:
self.cancel_button = QPushButton(Translations["generic.cancel"])
self.cancel_button.clicked.connect(self.hide)
self.cancel_button.clicked.connect(widget.reset)
# self.cancel_button.clicked.connect(cancel_callback)
self.widget.panel_cancel_button = self.cancel_button
self.button_layout.addWidget(self.cancel_button)
@@ -75,23 +63,19 @@ class PanelModal(QWidget):
self.save_button.setAutoDefault(True)
self.save_button.clicked.connect(self.hide)
self.save_button.clicked.connect(self.saved.emit)
self.save_button.clicked.connect(lambda: self.saved_data.emit(widget.saved_data()))
self.widget.panel_save_button = self.save_button
if done_callback:
self.save_button.clicked.connect(done_callback)
if save_callback:
self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.save_button)
# trigger save button actions when pressing enter in the widget
self.widget.add_callback(lambda: self.save_button.click())
if inline_title:
self.title_widget = QLabel()
self.title_widget.setObjectName("fieldTitle")
self.title_widget.setWordWrap(True)
self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px")
self.title_widget.setText(title)
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.root_layout.addWidget(self.title_widget)
if save_callback is not None:
widget.done.connect(lambda: save_callback(widget.get_content()))
self.root_layout.addWidget(self.title_widget)
self.root_layout.addWidget(widget)
widget.parent_modal = self
self.root_layout.setStretch(1, 2)
@@ -100,9 +84,9 @@ class PanelModal(QWidget):
@override
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
if self.cancel_button:
with contextlib.suppress(AttributeError):
self.cancel_button.click()
elif self.done_button:
with contextlib.suppress(AttributeError):
self.done_button.click()
event.accept()
@@ -110,7 +94,6 @@ class PanelModal(QWidget):
class PanelWidget(QWidget):
"""Used for widgets that go in a modal panel, ex. for editing or searching."""
done = Signal()
parent_modal: PanelModal | None = None
panel_save_button: QPushButton | None = None
panel_cancel_button: QPushButton | None = None
@@ -119,8 +102,8 @@ class PanelWidget(QWidget):
def __init__(self):
super().__init__()
def get_content(self) -> str:
return ""
def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny]
return None
def reset(self) -> None:
pass
@@ -128,9 +111,6 @@ class PanelWidget(QWidget):
def parent_post_init(self) -> None:
pass
def add_callback(self, callback: Callable[..., None], event: str = "returnPressed"): # pyright: ignore[reportUnusedParameter]
logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}")
@override
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
if event.key() == QtCore.Qt.Key.Key_Escape:

View File

@@ -19,7 +19,7 @@ from tagstudio.qt.mixed.media_player import MediaPlayer
from tagstudio.qt.platform_strings import open_file_str, trash_term
from tagstudio.qt.previews.renderer import ThumbRenderer
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.styles.rounded_pixmap_style import RoundedPixmapStyle
from tagstudio.qt.views.stylesheets.rounded_pixmap_style import RoundedPixmapStyle
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver

View File

@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtGui import QColor
from tagstudio.core.library.alchemy.enums import TagColorEnum
from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color
from tagstudio.qt.models.palette import ColorType, get_tag_color
def checkbox_style() -> str:
primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT))
border_color = get_border_color(primary_color)
highlight_color = get_highlight_color(primary_color)
text_color: QColor = get_text_color(primary_color, highlight_color)
return (
f"QCheckBox{{"
f"background: rgba{primary_color.toTuple()};"
f"color: rgba{text_color.toTuple()};"
f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QCheckBox::indicator{{"
f"width: 10px;"
f"height: 10px;"
f"border-radius: 2px;"
f"margin: 4px;"
f"}}"
f"QCheckBox::indicator:checked{{"
f"background: rgba{text_color.toTuple()};"
f"}}"
f"QCheckBox::hover{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
f"QCheckBox::focus{{"
f"border-color: rgba{highlight_color.toTuple()};"
f"outline:none;"
f"}}"
)

View File

@@ -92,6 +92,7 @@
"field.name": "Name",
"field.paste": "Paste Field",
"field.remove": "Remove Field",
"field.text.is_multiline": "Multiline",
"field.type": "Type",
"file.date_added": "Date Added",
"file.date_created": "Date Created",

View File

@@ -225,7 +225,9 @@ def test_remove_text_field_entry_with_multiple_fields(library: Library, entry_fu
def test_update_entry_field(library: Library, entry_full: Entry):
title_field = entry_full.text_fields[0]
library.update_text_field(entry_full.id, title_field, "new value", title_field.is_multiline)
library.update_text_field(
entry_full.id, title_field, title_field.name, "new value", title_field.is_multiline
)
entry = next(library.all_entries(with_joins=True))
assert entry.text_fields[0].value == "new value"
@@ -241,7 +243,9 @@ def test_update_entry_with_multiple_identical_text_fields(library: Library, entr
library.add_field_to_entries(entry_full.id, field=empty_title)
# update one of the fields
library.update_text_field(entry_full.id, title_field, "new value", title_field.is_multiline)
library.update_text_field(
entry_full.id, title_field, title_field.name, "new value", title_field.is_multiline
)
# Then only one should be updated
entry = next(library.all_entries(with_joins=True))