|
class PreviewPanel(QWidget): |
|
"""The Preview Panel Widget.""" |
|
tags_updated = Signal() |
|
|
|
def __init__(self, library: Library, driver:'QtDriver'): |
|
super().__init__() |
|
self.lib = library |
|
self.driver:QtDriver = driver |
|
self.initialized = False |
|
self.isOpen: bool = False |
|
# self.filepath = None |
|
# self.item = None # DEPRECATED, USE self.selected |
|
self.common_fields = [] |
|
self.mixed_fields = [] |
|
self.selected: list[tuple[ItemType, int]] = [] # New way of tracking items |
|
self.tag_callback = None |
|
self.containers: list[QWidget] = [] |
|
|
|
self.img_button_size: tuple[int, int] = (266, 266) |
|
self.image_ratio: float = 1.0 |
|
|
|
root_layout = QHBoxLayout(self) |
|
root_layout.setContentsMargins(0, 0, 0, 0) |
|
|
|
self.image_container = QWidget() |
|
image_layout = QHBoxLayout(self.image_container) |
|
image_layout.setContentsMargins(0, 0, 0, 0) |
|
|
|
splitter = QSplitter() |
|
splitter.setOrientation(Qt.Orientation.Vertical) |
|
splitter.setHandleWidth(12) |
|
|
|
self.preview_img = QPushButton() |
|
self.preview_img.setMinimumSize(*self.img_button_size) |
|
self.preview_img.setFlat(True) |
|
self.tr = ThumbRenderer() |
|
self.tr.updated.connect(lambda ts, i, s: (self.preview_img.setIcon(i))) |
|
self.tr.updated_ratio.connect(lambda ratio: (self.set_image_ratio(ratio), |
|
self.update_image_size((self.image_container.size().width(), self.image_container.size().height()), ratio))) |
|
|
|
splitter.splitterMoved.connect(lambda: self.update_image_size((self.image_container.size().width(), self.image_container.size().height()))) |
|
splitter.addWidget(self.image_container) |
|
|
|
image_layout.addWidget(self.preview_img) |
|
image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter) |
|
|
|
self.file_label = QLabel('Filename') |
|
self.file_label.setWordWrap(True) |
|
self.file_label.setTextInteractionFlags( |
|
Qt.TextInteractionFlag.TextSelectableByMouse) |
|
self.file_label.setStyleSheet('font-weight: bold; font-size: 12px') |
|
|
|
self.dimensions_label = QLabel('Dimensions') |
|
self.dimensions_label.setWordWrap(True) |
|
# self.dim_label.setTextInteractionFlags( |
|
# Qt.TextInteractionFlag.TextSelectableByMouse) |
|
self.dimensions_label.setStyleSheet(ItemThumb.small_text_style) |
|
|
|
# small_text_style = ( |
|
# f'background-color:rgba(17, 15, 27, 192);' |
|
# f'font-family:Oxanium;' |
|
# f'font-weight:bold;' |
|
# f'font-size:12px;' |
|
# f'border-radius:3px;' |
|
# f'padding-top: 4px;' |
|
# f'padding-right: 1px;' |
|
# f'padding-bottom: 1px;' |
|
# f'padding-left: 1px;' |
|
# ) |
|
|
|
self.scroll_layout = QVBoxLayout() |
|
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) |
|
self.scroll_layout.setContentsMargins(6,1,6,6) |
|
|
|
scroll_container: QWidget = QWidget() |
|
scroll_container.setObjectName('entryScrollContainer') |
|
scroll_container.setLayout(self.scroll_layout) |
|
# scroll_container.setStyleSheet('background:#080716; border-radius:12px;') |
|
scroll_container.setStyleSheet( |
|
'background:#00000000;' |
|
'border-style:none;' |
|
f'QScrollBar::{{background:red;}}' |
|
) |
|
|
|
|
|
info_section = QWidget() |
|
info_layout = QVBoxLayout(info_section) |
|
info_layout.setContentsMargins(0,0,0,0) |
|
info_layout.setSpacing(6) |
|
self.setStyleSheet( |
|
'background:#00000000;' |
|
f'QScrollBar::{{background:red;}}' |
|
) |
|
|
|
scroll_area = QScrollArea() |
|
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) |
|
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) |
|
scroll_area.setWidgetResizable(True) |
|
scroll_area.setFrameShadow(QFrame.Shadow.Plain) |
|
scroll_area.setFrameShape(QFrame.Shape.NoFrame) |
|
scroll_area.setStyleSheet( |
|
'background:#55000000;' |
|
'border-radius:12px;' |
|
'border-style:solid;' |
|
'border-width:1px;' |
|
'border-color:#11FFFFFF;' |
|
# f'QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{border: none;background: none;}}' |
|
# f'QScrollBar::left-arrow:horizontal, QScrollBar::right-arrow:horizontal, QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{border: none;background: none;color: none;}}' |
|
f'QScrollBar::{{background:red;}}' |
|
) |
|
scroll_area.setWidget(scroll_container) |
|
|
|
info_layout.addWidget(self.file_label) |
|
info_layout.addWidget(self.dimensions_label) |
|
info_layout.addWidget(scroll_area) |
|
splitter.addWidget(info_section) |
|
|
|
root_layout.addWidget(splitter) |
|
splitter.setStretchFactor(1, 2) |
|
|
|
self.afb_container = QWidget() |
|
self.afb_layout = QVBoxLayout(self.afb_container) |
|
self.afb_layout.setContentsMargins(0,12,0,0) |
|
|
|
self.add_field_button = QPushButton() |
|
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor) |
|
self.add_field_button.setMinimumSize(96, 28) |
|
self.add_field_button.setMaximumSize(96, 28) |
|
self.add_field_button.setText('Add Field') |
|
self.add_field_button.setStyleSheet( |
|
f'QPushButton{{' |
|
# f'background: #1E1A33;' |
|
# f'color: #CDA7F7;' |
|
f'font-weight: bold;' |
|
# f"border-color: #2B2547;" |
|
f'border-radius: 6px;' |
|
f'border-style:solid;' |
|
# f'border-width:{math.ceil(1*self.devicePixelRatio())}px;' |
|
'background:#55000000;' |
|
'border-width:1px;' |
|
'border-color:#11FFFFFF;' |
|
# f'padding-top: 1.5px;' |
|
# f'padding-right: 4px;' |
|
# f'padding-bottom: 5px;' |
|
# f'padding-left: 4px;' |
|
f'font-size: 13px;' |
|
f'}}' |
|
f'QPushButton::hover' |
|
f'{{' |
|
f'background: #333333;' |
|
f'}}') |
|
self.afb_layout.addWidget(self.add_field_button) |
|
self.afm = AddFieldModal(self.lib) |
|
self.place_add_field_button() |
|
self.update_image_size((self.image_container.size().width(), self.image_container.size().height())) |
|
|
|
def resizeEvent(self, event: QResizeEvent) -> None: |
|
self.update_image_size((self.image_container.size().width(), self.image_container.size().height())) |
|
return super().resizeEvent(event) |
|
|
|
def get_preview_size(self) -> tuple[int, int]: |
|
return (self.image_container.size().width(), self.image_container.size().height()) |
|
|
|
def set_image_ratio(self, ratio:float): |
|
# logging.info(f'Updating Ratio to: {ratio} #####################################################') |
|
self.image_ratio = ratio |
|
|
|
def update_image_size(self, size:tuple[int, int], ratio:float = None): |
|
if ratio: |
|
self.set_image_ratio(ratio) |
|
# self.img_button_size = size |
|
# logging.info(f'') |
|
# self.preview_img.setMinimumSize(64,64) |
|
|
|
adj_width = size[0] |
|
adj_height = size[1] |
|
# Landscape |
|
if self.image_ratio > 1: |
|
# logging.info('Landscape') |
|
adj_height = size[0] * (1/self.image_ratio) |
|
# Portrait |
|
elif self.image_ratio <= 1: |
|
# logging.info('Portrait') |
|
adj_width = size[1] * self.image_ratio |
|
|
|
if adj_width > size[0]: |
|
adj_height = adj_height * (size[0]/adj_width) |
|
adj_width = size[0] |
|
elif adj_height > size[1]: |
|
adj_width = adj_width * (size[1]/adj_height) |
|
adj_height = size[1] |
|
|
|
# adj_width = min(adj_width, self.image_container.size().width()) |
|
# adj_height = min(adj_width, self.image_container.size().height()) |
|
|
|
# self.preview_img.setMinimumSize(s) |
|
# self.preview_img.setMaximumSize(s_max) |
|
adj_size = QSize(adj_width, adj_height) |
|
self.img_button_size = (adj_width, adj_height) |
|
self.preview_img.setMaximumSize(adj_size) |
|
self.preview_img.setIconSize(adj_size) |
|
# self.preview_img.setMinimumSize(adj_size) |
|
|
|
# if self.preview_img.iconSize().toTuple()[0] < self.preview_img.size().toTuple()[0] + 10: |
|
# if type(self.item) == Entry: |
|
# filepath = os.path.normpath(f'{self.lib.library_dir}/{self.item.path}/{self.item.filename}') |
|
# self.tr.render_big(time.time(), filepath, self.preview_img.size().toTuple(), self.devicePixelRatio()) |
|
|
|
# logging.info(f' Img Aspect Ratio: {self.image_ratio}') |
|
# logging.info(f' Max Button Size: {size}') |
|
# logging.info(f'Container Size: {(self.image_container.size().width(), self.image_container.size().height())}') |
|
# logging.info(f'Final Button Size: {(adj_width, adj_height)}') |
|
# logging.info(f'') |
|
# logging.info(f' Icon Size: {self.preview_img.icon().actualSize().toTuple()}') |
|
# logging.info(f'Button Size: {self.preview_img.size().toTuple()}') |
|
|
|
def place_add_field_button(self): |
|
self.scroll_layout.addWidget(self.afb_container) |
|
self.scroll_layout.setAlignment(self.afb_container, Qt.AlignmentFlag.AlignHCenter) |
|
|
|
try: |
|
self.afm.done.disconnect() |
|
self.add_field_button.clicked.disconnect() |
|
except RuntimeError: |
|
pass |
|
|
|
# self.afm.done.connect(lambda f: (self.lib.add_field_to_entry(self.selected[0][1], f), self.update_widgets())) |
|
self.afm.done.connect(lambda f: (self.add_field_to_selected(f), self.update_widgets())) |
|
self.add_field_button.clicked.connect(self.afm.show) |
|
|
|
def add_field_to_selected(self, field_id: int): |
|
"""Adds an entry field to one or more selected items.""" |
|
added = set() |
|
for item_pair in self.selected: |
|
if item_pair[0] == ItemType.ENTRY and item_pair[1] not in added: |
|
self.lib.add_field_to_entry(item_pair[1], field_id) |
|
added.add(item_pair[1]) |
|
|
|
|
|
# def update_widgets(self, item: Union[Entry, Collation, Tag]): |
|
def update_widgets(self): |
|
""" |
|
Renders the panel's widgets with the newest data from the Library. |
|
""" |
|
logging.info(f'[ENTRY PANEL] UPDATE WIDGETS ({self.driver.selected})' ) |
|
self.isOpen = True |
|
# self.tag_callback = tag_callback if tag_callback else None |
|
window_title = '' |
|
|
|
# 0 Selected Items |
|
if len(self.driver.selected) == 0: |
|
if len(self.selected) != 0 or not self.initialized: |
|
self.file_label.setText(f"No Items Selected") |
|
self.dimensions_label.setText("") |
|
ratio: float = self.devicePixelRatio() |
|
self.tr.render_big(time.time(), '', (512, 512), ratio, True) |
|
try: |
|
self.preview_img.clicked.disconnect() |
|
except RuntimeError: |
|
pass |
|
for i, c in enumerate(self.containers): |
|
c.setHidden(True) |
|
|
|
self.selected = list(self.driver.selected) |
|
self.add_field_button.setHidden(True) |
|
|
|
# 1 Selected Item |
|
elif len(self.driver.selected) == 1: |
|
|
|
# 1 Selected Entry |
|
if self.driver.selected[0][0] == ItemType.ENTRY: |
|
item: Entry = self.lib.get_entry(self.driver.selected[0][1]) |
|
# If a new selection is made, update the thumbnail and filepath. |
|
if (len(self.selected) == 0 |
|
or self.selected != self.driver.selected): |
|
filepath = os.path.normpath(f'{self.lib.library_dir}/{item.path}/{item.filename}') |
|
window_title = filepath |
|
ratio: float = self.devicePixelRatio() |
|
self.tr.render_big(time.time(), filepath, (512, 512), ratio) |
|
self.file_label.setText("\u200b".join(filepath)) |
|
|
|
# TODO: Do this somewhere else, this is just here temporarily. |
|
extension = os.path.splitext(filepath)[1][1:].lower() |
|
try: |
|
image = None |
|
if extension in IMAGE_TYPES: |
|
image = Image.open(filepath) |
|
if image.mode == 'RGBA': |
|
new_bg = Image.new('RGB', image.size, color='#222222') |
|
new_bg.paste(image, mask=image.getchannel(3)) |
|
image = new_bg |
|
if image.mode != 'RGB': |
|
image = image.convert(mode='RGB') |
|
elif extension in VIDEO_TYPES: |
|
video = cv2.VideoCapture(filepath) |
|
video.set(cv2.CAP_PROP_POS_FRAMES, |
|
(video.get(cv2.CAP_PROP_FRAME_COUNT) // 2)) |
|
success, frame = video.read() |
|
if not success: |
|
# Depending on the video format, compression, and frame |
|
# count, seeking halfway does not work and the thumb |
|
# must be pulled from the earliest available frame. |
|
video.set(cv2.CAP_PROP_POS_FRAMES, 0) |
|
success, frame = video.read() |
|
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) |
|
image = Image.fromarray(frame) |
|
|
|
# Stats for specific file types are displayed here. |
|
if extension in (IMAGE_TYPES + VIDEO_TYPES): |
|
self.dimensions_label.setText(f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}\n{image.width} x {image.height} px") |
|
else: |
|
self.dimensions_label.setText(f"{extension.upper()}") |
|
|
|
if not image: |
|
self.dimensions_label.setText(f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}") |
|
raise UnidentifiedImageError |
|
|
|
except (UnidentifiedImageError, FileNotFoundError, cv2.error): |
|
pass |
|
|
|
|
|
|
|
try: |
|
self.preview_img.clicked.disconnect() |
|
except RuntimeError: |
|
pass |
|
self.preview_img.clicked.connect( |
|
lambda checked=False, filepath=filepath: open_file(filepath)) |
|
|
|
self.selected = list(self.driver.selected) |
|
for i, f in enumerate(item.fields): |
|
self.write_container(i, f) |
|
|
|
# Hide leftover containers |
|
if len(self.containers) > len(item.fields): |
|
for i, c in enumerate(self.containers): |
|
if i > (len(item.fields) - 1): |
|
c.setHidden(True) |
|
|
|
self.add_field_button.setHidden(False) |
|
|
|
# 1 Selected Collation |
|
elif self.driver.selected[0][0] == ItemType.COLLATION: |
|
pass |
|
|
|
# 1 Selected Tag |
|
elif self.driver.selected[0][0] == ItemType.TAG_GROUP: |
|
pass |
|
|
|
# Multiple Selected Items |
|
elif len(self.driver.selected) > 1: |
|
if self.selected != self.driver.selected: |
|
self.file_label.setText(f"{len(self.driver.selected)} Items Selected") |
|
self.dimensions_label.setText("") |
|
ratio: float = self.devicePixelRatio() |
|
self.tr.render_big(time.time(), '', (512, 512), ratio, True) |
|
try: |
|
self.preview_img.clicked.disconnect() |
|
except RuntimeError: |
|
pass |
|
|
|
self.common_fields = [] |
|
self.mixed_fields = [] |
|
for i, item_pair in enumerate(self.driver.selected): |
|
if item_pair[0] == ItemType.ENTRY: |
|
item = self.lib.get_entry(item_pair[1]) |
|
if i == 0: |
|
for f in item.fields: |
|
self.common_fields.append(f) |
|
else: |
|
common_to_remove = [] |
|
for f in self.common_fields: |
|
# Common field found (Same ID, identical content) |
|
if f not in item.fields: |
|
common_to_remove.append(f) |
|
|
|
# Mixed field found (Same ID, different content) |
|
if self.lib.get_field_index_in_entry(item, self.lib.get_field_attr(f, 'id')): |
|
# if self.lib.get_field_attr(f, 'type') == ('tag_box'): |
|
# pass |
|
# logging.info(f) |
|
# logging.info(type(f)) |
|
f_stripped = {self.lib.get_field_attr(f, 'id'):None} |
|
if f_stripped not in self.mixed_fields and (f not in self.common_fields or f in common_to_remove): |
|
# and (f not in self.common_fields or f in common_to_remove) |
|
self.mixed_fields.append(f_stripped) |
|
self.common_fields = [f for f in self.common_fields if f not in common_to_remove] |
|
order: list[int] = ( |
|
[0] + |
|
[1, 2] + |
|
[9, 17, 18, 19, 20] + |
|
[8, 7, 6] + |
|
[4] + |
|
[3, 21] + |
|
[10, 14, 11, 12, 13, 22] + |
|
[5] |
|
) |
|
self.mixed_fields = sorted(self.mixed_fields, key=lambda x: order.index(self.lib.get_field_attr(x, 'id'))) |
|
|
|
|
|
|
|
self.selected = list(self.driver.selected) |
|
for i, f in enumerate(self.common_fields): |
|
logging.info(f'ci:{i}, f:{f}') |
|
self.write_container(i, f) |
|
for i, f in enumerate(self.mixed_fields, start = len(self.common_fields)): |
|
logging.info(f'mi:{i}, f:{f}') |
|
self.write_container(i, f, mixed=True) |
|
|
|
# Hide leftover containers |
|
if len(self.containers) > len(self.common_fields) + len(self.mixed_fields): |
|
for i, c in enumerate(self.containers): |
|
if i > (len(self.common_fields) + len(self.mixed_fields) - 1): |
|
c.setHidden(True) |
|
|
|
self.add_field_button.setHidden(False) |
|
|
|
self.initialized = True |
|
|
|
|
|
# # Uninitialized or New Item: |
|
# if not self.item or self.item.id != item.id: |
|
# # logging.info(f'Uninitialized or New Item ({item.id})') |
|
# if type(item) == Entry: |
|
# # New Entry: Render preview and update filename label |
|
# filepath = os.path.normpath(f'{self.lib.library_dir}/{item.path}/{item.filename}') |
|
# window_title = filepath |
|
# ratio: float = self.devicePixelRatio() |
|
# self.tr.render_big(time.time(), filepath, (512, 512), ratio) |
|
# self.file_label.setText("\u200b".join(filepath)) |
|
|
|
# # TODO: Deal with this later. |
|
# # https://stackoverflow.com/questions/64252654/pyqt5-drag-and-drop-into-system-file-explorer-with-delayed-encoding |
|
# # https://doc.qt.io/qtforpython-5/PySide2/QtCore/QMimeData.html#more |
|
# # drag = QDrag(self.preview_img) |
|
# # mime = QMimeData() |
|
# # mime.setUrls([filepath]) |
|
# # drag.setMimeData(mime) |
|
# # drag.exec_(Qt.DropAction.CopyAction) |
|
|
|
# try: |
|
# self.preview_img.clicked.disconnect() |
|
# except RuntimeError: |
|
# pass |
|
# self.preview_img.clicked.connect( |
|
# lambda checked=False, filepath=filepath: open_file(filepath)) |
|
|
|
# for i, f in enumerate(item.fields): |
|
# self.write_container(item, i, f) |
|
|
|
# self.item = item |
|
|
|
# # try: |
|
# # self.tags_updated.disconnect() |
|
# # except RuntimeError: |
|
# # pass |
|
# # if self.tag_callback: |
|
# # # logging.info(f'[UPDATE CONTAINER] Updating Callback for {item.id}: {self.tag_callback}') |
|
# # self.tags_updated.connect(self.tag_callback) |
|
|
|
|
|
|
|
# # Initialized, Updating: |
|
# elif self.item and self.item.id == item.id: |
|
# # logging.info(f'Initialized Item, Updating! ({item.id})') |
|
# for i, f in enumerate(item.fields): |
|
# self.write_container(item, i, f) |
|
|
|
# # Hide leftover containers |
|
# if len(self.containers) > len(self.item.fields): |
|
# for i, c in enumerate(self.containers): |
|
# if i > (len(self.item.fields) - 1): |
|
# c.setHidden(True) |
|
|
|
|
|
|
|
self.setWindowTitle(window_title) |
|
self.show() |
|
|
|
def set_tags_updated_slot(self, slot: object): |
|
""" |
|
Replacement for tag_callback. |
|
""" |
|
try: |
|
self.tags_updated.disconnect() |
|
except RuntimeError: |
|
pass |
|
logging.info(f'[UPDATE CONTAINER] Setting tags updated slot') |
|
self.tags_updated.connect(slot) |
|
|
|
# def write_container(self, item:Union[Entry, Collation, Tag], index, field): |
|
def write_container(self, index, field, mixed=False): |
|
"""Updates/Creates data for a FieldContainer.""" |
|
# logging.info(f'[ENTRY PANEL] WRITE CONTAINER') |
|
# Remove 'Add Field' button from scroll_layout, to be re-added later. |
|
self.scroll_layout.takeAt(self.scroll_layout.count()-1).widget() |
|
container: FieldContainer = None |
|
if len(self.containers) < (index + 1): |
|
container = FieldContainer() |
|
self.containers.append(container) |
|
self.scroll_layout.addWidget(container) |
|
else: |
|
container = self.containers[index] |
|
# container.inner_layout.removeItem(container.inner_layout.itemAt(1)) |
|
# container.setHidden(False) |
|
if self.lib.get_field_attr(field, 'type') == 'tag_box': |
|
# logging.info(f'WRITING TAGBOX FOR ITEM {item.id}') |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(False) |
|
container.set_inline(False) |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Tag Box)" |
|
if not mixed: |
|
item = self.lib.get_entry(self.selected[0][1]) # TODO TODO TODO: TEMPORARY |
|
if type(container.get_inner_widget()) == TagBoxWidget: |
|
inner_container: TagBoxWidget = container.get_inner_widget() |
|
inner_container.set_item(item) |
|
inner_container.set_tags(self.lib.get_field_attr(field, 'content')) |
|
try: |
|
inner_container.updated.disconnect() |
|
except RuntimeError: |
|
pass |
|
# inner_container.updated.connect(lambda f=self.filepath, i=item: self.write_container(item, index, field)) |
|
else: |
|
inner_container = TagBoxWidget(item, title, index, self.lib, self.lib.get_field_attr(field, 'content'), self.driver) |
|
|
|
container.set_inner_widget(inner_container) |
|
inner_container.field = field |
|
inner_container.updated.connect(lambda: (self.write_container(index, field), self.tags_updated.emit())) |
|
# if type(item) == Entry: |
|
# NOTE: Tag Boxes have no Edit Button (But will when you can convert field types) |
|
# f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
container.set_copy_callback(None) |
|
container.set_edit_callback(None) |
|
else: |
|
text = '<i>Mixed Data</i>' |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Wacky Tag Box)" |
|
inner_container = TextWidget(title, text) |
|
container.set_inner_widget(inner_container) |
|
container.set_copy_callback(None) |
|
container.set_edit_callback(None) |
|
container.set_remove_callback(None) |
|
|
|
|
|
self.tags_updated.emit() |
|
# self.dynamic_widgets.append(inner_container) |
|
elif self.lib.get_field_attr(field, 'type') in 'text_line': |
|
# logging.info(f'WRITING TEXTLINE FOR ITEM {item.id}') |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(True) |
|
container.set_inline(False) |
|
# Normalize line endings in any text content. |
|
text: str = '' |
|
if not mixed: |
|
text = self.lib.get_field_attr( |
|
field, 'content').replace('\r', '\n') |
|
else: |
|
text = '<i>Mixed Data</i>' |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Text Line)" |
|
inner_container = TextWidget(title, text) |
|
container.set_inner_widget(inner_container) |
|
# if type(item) == Entry: |
|
if not mixed: |
|
modal = PanelModal(EditTextLine(self.lib.get_field_attr(field, 'content')), |
|
title=title, |
|
window_title=f'Edit {self.lib.get_field_attr(field, "name")}', |
|
save_callback=(lambda content: (self.update_field(field, content), self.update_widgets())) |
|
) |
|
container.set_edit_callback(modal.show) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
container.set_copy_callback(None) |
|
else: |
|
container.set_edit_callback(None) |
|
container.set_copy_callback(None) |
|
container.set_remove_callback(None) |
|
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) |
|
|
|
elif self.lib.get_field_attr(field, 'type') in 'text_box': |
|
# logging.info(f'WRITING TEXTBOX FOR ITEM {item.id}') |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(True) |
|
container.set_inline(False) |
|
# Normalize line endings in any text content. |
|
text: str = '' |
|
if not mixed: |
|
text = self.lib.get_field_attr( |
|
field, 'content').replace('\r', '\n') |
|
else: |
|
text = '<i>Mixed Data</i>' |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Text Box)" |
|
inner_container = TextWidget(title, text) |
|
container.set_inner_widget(inner_container) |
|
# if type(item) == Entry: |
|
if not mixed: |
|
container.set_copy_callback(None) |
|
modal = PanelModal(EditTextBox(self.lib.get_field_attr(field, 'content')), |
|
title=title, |
|
window_title=f'Edit {self.lib.get_field_attr(field, "name")}', |
|
save_callback=(lambda content: (self.update_field(field, content), self.update_widgets())) |
|
) |
|
container.set_edit_callback(modal.show) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
else: |
|
container.set_edit_callback(None) |
|
container.set_copy_callback(None) |
|
container.set_remove_callback(None) |
|
elif self.lib.get_field_attr(field, 'type') == 'collation': |
|
# logging.info(f'WRITING COLLATION FOR ITEM {item.id}') |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(True) |
|
container.set_inline(False) |
|
collation = self.lib.get_collation(self.lib.get_field_attr(field, 'content')) |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Collation)" |
|
text: str = (f'{collation.title} ({len(collation.e_ids_and_pages)} Items)') |
|
if len(self.selected) == 1: |
|
text += f' - Page {collation.e_ids_and_pages[[x[0] for x in collation.e_ids_and_pages].index(self.selected[0][1])][1]}' |
|
inner_container = TextWidget(title, text) |
|
container.set_inner_widget(inner_container) |
|
# if type(item) == Entry: |
|
container.set_copy_callback(None) |
|
# container.set_edit_callback(None) |
|
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
elif self.lib.get_field_attr(field, 'type') == 'datetime': |
|
# logging.info(f'WRITING DATETIME FOR ITEM {item.id}') |
|
if not mixed: |
|
try: |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(False) |
|
container.set_inline(False) |
|
# TODO: Localize this and/or add preferences. |
|
date = dt.strptime(self.lib.get_field_attr( |
|
field, 'content'), '%Y-%m-%d %H:%M:%S') |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Date)" |
|
inner_container = TextWidget(title, date.strftime('%D - %r')) |
|
container.set_inner_widget(inner_container) |
|
except: |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(False) |
|
container.set_inline(False) |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Date) (Unknown Format)" |
|
inner_container = TextWidget(title, str(self.lib.get_field_attr(field, 'content'))) |
|
# if type(item) == Entry: |
|
container.set_copy_callback(None) |
|
container.set_edit_callback(None) |
|
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
else: |
|
text = '<i>Mixed Data</i>' |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Wacky Date)" |
|
inner_container = TextWidget(title, text) |
|
container.set_inner_widget(inner_container) |
|
container.set_copy_callback(None) |
|
container.set_edit_callback(None) |
|
container.set_remove_callback(None) |
|
else: |
|
# logging.info(f'[ENTRY PANEL] Unknown Type: {self.lib.get_field_attr(field, "type")}') |
|
container.set_title(self.lib.get_field_attr(field, 'name')) |
|
# container.set_editable(False) |
|
container.set_inline(False) |
|
title = f"{self.lib.get_field_attr(field, 'name')} (Unknown Field Type)" |
|
inner_container = TextWidget(title, str(self.lib.get_field_attr(field, 'content'))) |
|
container.set_inner_widget(inner_container) |
|
# if type(item) == Entry: |
|
container.set_copy_callback(None) |
|
container.set_edit_callback(None) |
|
# container.set_remove_callback(lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets(item))) |
|
prompt=f'Are you sure you want to remove this \"{self.lib.get_field_attr(field, "name")}\" field?' |
|
callback = lambda: (self.remove_field(field), self.update_widgets()) |
|
# callback = lambda: (self.lib.get_entry(item.id).fields.pop(index), self.update_widgets()) |
|
container.set_remove_callback(lambda: self.remove_message_box( |
|
prompt=prompt, |
|
callback=callback)) |
|
container.setHidden(False) |
|
self.place_add_field_button() |
|
|
|
def remove_field(self, field:object): |
|
"""Removes a field from all selected Entries, given a field object.""" |
|
for item_pair in self.selected: |
|
if item_pair[0] == ItemType.ENTRY: |
|
entry = self.lib.get_entry(item_pair[1]) |
|
try: |
|
index = entry.fields.index(field) |
|
updated_badges = False |
|
if 8 in entry.fields[index].keys() and (1 in entry.fields[index][8] or 0 in entry.fields[index][8]): |
|
updated_badges = True |
|
# TODO: Create a proper Library/Entry method to manage fields. |
|
entry.fields.pop(index) |
|
if updated_badges: |
|
self.driver.update_badges() |
|
except ValueError: |
|
logging.info(f'[PREVIEW PANEL][ERROR?] Tried to remove field from Entry ({entry.id}) that never had it') |
|
pass |
|
|
|
def update_field(self, field:object, content): |
|
"""Removes a field from all selected Entries, given a field object.""" |
|
field = dict(field) |
|
for item_pair in self.selected: |
|
if item_pair[0] == ItemType.ENTRY: |
|
entry = self.lib.get_entry(item_pair[1]) |
|
try: |
|
logging.info(field) |
|
index = entry.fields.index(field) |
|
self.lib.update_entry_field(entry.id, index, content, 'replace') |
|
except ValueError: |
|
logging.info(f'[PREVIEW PANEL][ERROR] Tried to update field from Entry ({entry.id}) that never had it') |
|
pass |
|
|
|
def remove_message_box(self, prompt:str, callback:FunctionType) -> int: |
|
remove_mb = QMessageBox() |
|
remove_mb.setText(prompt) |
|
remove_mb.setWindowTitle('Remove Field') |
|
remove_mb.setIcon(QMessageBox.Icon.Warning) |
|
cancel_button = remove_mb.addButton('&Cancel', QMessageBox.ButtonRole.DestructiveRole) |
|
remove_button = remove_mb.addButton('&Remove', QMessageBox.ButtonRole.RejectRole) |
|
# remove_mb.setStandardButtons(QMessageBox.StandardButton.Cancel) |
|
remove_mb.setDefaultButton(cancel_button) |
|
result = remove_mb.exec_() |
|
# logging.info(result) |
|
if result == 1: |
|
callback() |