import logging import os.path from typing import List, Tuple from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.QtGui import QStandardItem, QStandardItemModel from ArtNet.gui.dialogs.collection_modify_dialog.collection_modify_dialog import Ui_Dialog from ArtNet.gui.dialogs.image_select_dialog.image_sel_dialog import ImageSelectDialog from ArtNet.db.db_adapter import DBAdapter class CollectionModDialog(QtWidgets.QDialog): """ Dialog to edit and modify a collection and its entries' ranking """ # TODO add way to increase and lower ranking for an entry (up and down buttons?) # TODO add way to delete entry (button) # TODO can I catch which item in a list is selected? def __init__(self, parent, db_connection: DBAdapter, edit_collection: bool = False): super().__init__(parent) self.parent = parent self.__db = db_connection self.__curr_entries: List[Tuple[str, dict]] = list() # [(entry_str, Dict[title, path, ranking])] self.data = dict() self.setWindowTitle("Creating Collection") self.ui = Ui_Dialog() self.ui.setupUi(self) if not edit_collection: self.ui.collection_name_line.setReadOnly(True) self.ui.description_browser.setReadOnly(True) self.ui.add_image_button.clicked.connect(self.on_add_art_clicked) self.ui.optimize_ranking_button.clicked.connect(self.on_optimize_ranking_clicked) @staticmethod def create_entry_string(art_data: dict) -> str: """ Creates a string representation of the given art in the collection including its ranking string """ title: str = art_data["title"] if art_data["title"] is not None else os.path.basename(art_data["path"]) if len(title) > 20: title = title[-20:] elif len(title) < 20: while len(title) < 20: title += " " entry_string = title + " | " + art_data["ranking"] return entry_string def append_collection_entry(self, art_data: dict): """ Append a new art entry into the collection """ if len(self.__curr_entries) > 0: # need to create even lower entry lowest_r = None for s, data in self.__curr_entries: r = data["ranking"] if lowest_r is None or lowest_r < r: lowest_r = r if lowest_r[-1] < "z": # lower and not 'z' ranking = lowest_r ranking = ranking[:-1] ranking += chr(ord(lowest_r[-1]) + (ord('z')-ord(lowest_r[-1]))//2 + 1) # last character + 1 else: ranking = lowest_r if lowest_r is not None else "" ranking += "m" else: # first to add ranking = "m" # the middle art_data["ranking"] = ranking title: str = art_data["title"] if art_data["title"] is not None else os.path.basename(art_data["path"]) if len(title) > 20: title = title[-20:] elif len(title) < 20: while len(title) < 20: title += " " entry_string = title + " | " + art_data["ranking"] self.__curr_entries.append((entry_string, art_data)) self.__curr_entries.sort(key=lambda x: x[1]["ranking"]) logging.debug(f"New collection list is: {self.__curr_entries}") self.set_collection_entry_list(self.__curr_entries) def insert_collection_entry(self, art_data: dict, index: int): """ Insert the art entry into the collection at position index. This might shift other entries in their index but tries to minimize ranking string changes. """ assert(index >= 0) if len(self.__curr_entries)-1 > index: # inserts between entries or in front of entries conflicting_art = self.__curr_entries[index][1] following_art = self.__curr_entries[index+1][1] if len(conflicting_art["ranking"]) == len(following_art["ranking"]): # can I squeeze it in? if following_art["ranking"][-1] < conflicting_art["ranking"][-1] < "z": # both are not same and not "z" -> there might be space char_diff = ord(conflicting_art["ranking"][-1]) - ord(following_art["ranking"][-1]) if char_diff > 1: # can squeeze a letter between them ranking = conflicting_art["ranking"][:-1] ranking += chr(ord(conflicting_art["ranking"][-1]) + char_diff//2) else: ranking = conflicting_art["ranking"] + "m" else: ranking = conflicting_art["ranking"] + "m" elif conflicting_art["ranking"][-1] < "z": # just insert it the plain way ranking = conflicting_art["ranking"] ranking = chr(ord(ranking[-1])+1) else: ranking = conflicting_art["ranking"] + "m" art_data["ranking"] = ranking entry_string = CollectionModDialog.create_entry_string(art_data) self.__curr_entries.append((entry_string, art_data)) self.__curr_entries.sort(key=lambda x: x[1]["ranking"]) logging.debug(f"New collection list is: {self.__curr_entries}") self.set_collection_entry_list(self.__curr_entries) else: # inserts at the end self.append_collection_entry(art_data) def collect_collection_details(self): """ Collect the entered details about the collection and save them as self.data """ self.data["name"] = self.ui.collection_name_line.text() self.data["description"] = self.ui.description_browser.toPlainText().strip() self.data["entries"] = self.__curr_entries def set_collection_entry_list(self, entries: list): """ Set the entries in the list :param entries: :return: """ item_model = QStandardItemModel(self.ui.entry_list) entries.sort(key=lambda x: x[1]["ranking"]) for entry, _ in entries: item = QStandardItem(entry) flags = Qt.ItemIsEnabled item.setFlags(flags) item.setData(Qt.Unchecked, Qt.CheckStateRole) item.setCheckState(Qt.Checked) # can just be checked, otherwise not in the entry list item_model.appendRow(item) item_model.itemChanged.connect(self.on_collection_entry_changed) self.ui.entry_list.setModel(item_model) def exec_(self, collection_details: dict = None) -> dict: if collection_details is not None: self.ui.collection_name_line.setText(collection_details["name"]) self.ui.description_browser.setText(collection_details["description"]) self.__curr_entries = collection_details["entries"] self.set_collection_entry_list([CollectionModDialog.create_entry_string(entry) for entry in collection_details["entries"]]) if super(CollectionModDialog, self).exec_() == QtWidgets.QDialog.Rejected: return None self.collect_collection_details() return self.data # callback method def on_add_art_clicked(self): logging.info("Clicked add art for collection") dialog = ImageSelectDialog(db_connection=self.__db, parent=self) art_data = dialog.exec_() logging.info(f"Received selected art: {art_data}") if art_data is None: # nothing was selected return self.append_collection_entry(art_data) def on_optimize_ranking_clicked(self): logging.info("Clicked optimize ranking for collection") # TODO use some optimization algo to change the ranking strings to be minimal in length, with max distance def on_collection_entry_changed(self, item: QStandardItem): logging.info(f"Removing entry {item.text()}") entry_id = item.text().split(":")[0] self.__curr_entries.remove(entry_id)