From 99069ba77d235a85f2d5729c1b56e41ae91d3646 Mon Sep 17 00:00:00 2001 From: Peery Date: Sat, 17 Apr 2021 18:20:27 +0200 Subject: [PATCH] ArtNet App Moved ArtNet from another repo here --- ArtNet/__init__.py | 0 ArtNet/artnet_manager.py | 376 +++++++ ArtNet/db/__init__.py | 0 ArtNet/db/db_adapter.py | 853 +++++++++++++++ ArtNet/file/__init__.py | 0 ArtNet/file/config_reader.py | 135 +++ ArtNet/file/file_reader.py | 45 + ArtNet/gui/__init__.py | 0 ArtNet/gui/dialogs/__init__.py | 0 .../dialogs/artist_modify_dialog/__init__.py | 0 .../artist_modify_dialog/artist_mod_dialog.py | 57 + .../artist_modify_dialog.py | 101 ++ .../artist_modify_dialog.ui | 154 +++ .../category_mod_dialog.py | 69 ++ .../category_modify_dialog.py | 92 ++ .../category_modify_dialog.ui | 137 +++ .../dialogs/db_connection_dialog/__init__.py | 0 .../db_connection_dialog.py | 147 +++ .../db_connection_dialog.ui | 216 ++++ .../dialogs/db_connection_dialog/db_dialog.py | 31 + .../presence_modify_dialog/__init__.py | 0 .../presence_mod_dialog.py | 200 ++++ .../presence_modify_dialog.py | 149 +++ .../presence_modify_dialog.ui | 222 ++++ .../presence_selection_dialog/__init__.py | 0 .../presence_select_dialog.py | 70 ++ .../presence_selection_dialog.py | 101 ++ .../presence_selection_dialog.ui | 153 +++ .../tag_import_dialog/tag_imp_dialog.py | 122 +++ .../tag_import_dialog/tag_import_dialog.py | 143 +++ .../tag_import_dialog/tag_import_dialog.ui | 239 +++++ .../gui/dialogs/tag_modify_dialog/__init__.py | 0 .../tag_modify_dialog/tag_mod_dialog.py | 285 +++++ .../tag_modify_dialog/tag_modify_dialog.py | 226 ++++ .../tag_modify_dialog/tag_modify_dialog.ui | 375 +++++++ .../gui/dialogs/tag_select_dialog/__init__.py | 0 .../tag_select_dialog/tag_select_dialog.py | 77 ++ .../tag_select_dialog/tag_selection_dialog.py | 70 ++ .../tag_select_dialog/tag_selection_dialog.ui | 104 ++ ArtNet/gui/dockers/__init__.py | 0 ArtNet/gui/dockers/presence/__init__.py | 0 ArtNet/gui/dockers/presence/presence_dock.py | 146 +++ .../gui/dockers/presence/presence_docker.py | 143 +++ .../gui/dockers/presence/presence_docker.ui | 190 ++++ ArtNet/gui/manual_picture_importer.py | 104 ++ ArtNet/gui/picture_importer.py | 349 ++++++ ArtNet/gui/picture_importer.ui | 564 ++++++++++ ArtNet/gui/window.py | 999 ++++++++++++++++++ ArtNet/web/__init__.py | 0 ArtNet/web/link_generator.py | 169 +++ README.md | 90 ++ __main__.py | 10 + 52 files changed, 7713 insertions(+) create mode 100644 ArtNet/__init__.py create mode 100644 ArtNet/artnet_manager.py create mode 100644 ArtNet/db/__init__.py create mode 100644 ArtNet/db/db_adapter.py create mode 100644 ArtNet/file/__init__.py create mode 100644 ArtNet/file/config_reader.py create mode 100644 ArtNet/file/file_reader.py create mode 100644 ArtNet/gui/__init__.py create mode 100644 ArtNet/gui/dialogs/__init__.py create mode 100644 ArtNet/gui/dialogs/artist_modify_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/artist_modify_dialog/artist_mod_dialog.py create mode 100644 ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py create mode 100644 ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui create mode 100644 ArtNet/gui/dialogs/category_modify_dialog/category_mod_dialog.py create mode 100644 ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.py create mode 100644 ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.ui create mode 100644 ArtNet/gui/dialogs/db_connection_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.py create mode 100644 ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.ui create mode 100644 ArtNet/gui/dialogs/db_connection_dialog/db_dialog.py create mode 100644 ArtNet/gui/dialogs/presence_modify_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/presence_modify_dialog/presence_mod_dialog.py create mode 100644 ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.py create mode 100644 ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.ui create mode 100644 ArtNet/gui/dialogs/presence_selection_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/presence_selection_dialog/presence_select_dialog.py create mode 100644 ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.py create mode 100644 ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.ui create mode 100644 ArtNet/gui/dialogs/tag_import_dialog/tag_imp_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.ui create mode 100644 ArtNet/gui/dialogs/tag_modify_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/tag_modify_dialog/tag_mod_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.ui create mode 100644 ArtNet/gui/dialogs/tag_select_dialog/__init__.py create mode 100644 ArtNet/gui/dialogs/tag_select_dialog/tag_select_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.py create mode 100644 ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.ui create mode 100644 ArtNet/gui/dockers/__init__.py create mode 100644 ArtNet/gui/dockers/presence/__init__.py create mode 100644 ArtNet/gui/dockers/presence/presence_dock.py create mode 100644 ArtNet/gui/dockers/presence/presence_docker.py create mode 100644 ArtNet/gui/dockers/presence/presence_docker.ui create mode 100644 ArtNet/gui/manual_picture_importer.py create mode 100644 ArtNet/gui/picture_importer.py create mode 100644 ArtNet/gui/picture_importer.ui create mode 100644 ArtNet/gui/window.py create mode 100644 ArtNet/web/__init__.py create mode 100644 ArtNet/web/link_generator.py create mode 100644 README.md create mode 100644 __main__.py diff --git a/ArtNet/__init__.py b/ArtNet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/artnet_manager.py b/ArtNet/artnet_manager.py new file mode 100644 index 0000000..c5b4bab --- /dev/null +++ b/ArtNet/artnet_manager.py @@ -0,0 +1,376 @@ +import sys +import os +from hashlib import md5 + +from PyQt5.QtWidgets import QApplication + +from ArtNet.db.db_adapter import DBAdapter +from ArtNet.file.file_reader import FileReader +from ArtNet.file.config_reader import ConfigReader +from ArtNet.gui.window import Window +from ArtNet.gui.dialogs.db_connection_dialog.db_dialog import DBDialog +from ArtNet.web.link_generator import LinkGenerator + + +class ArtNetManager: + + def __init__(self, config_location: str = "."): + self.config = ConfigReader(config_location, "somePassword") + self.db_connection = None + self.__app = QApplication(sys.argv) + + while self.db_connection is None: + try: + self.db_connection = self.create_db_connection(user=self.config.data["db"]["user"], + password=self.config.data["db"]["password"], + host=self.config.data["db"]["host"], + port=self.config.data["db"]["port"], + database=self.config.data["db"]["database"]) + except ValueError as e: # db connection didn't work + print(e) + dialog = DBDialog() + prev_db_data = self.get_db_connection_details() + dialog.ui.user_line_edit.setText(prev_db_data["user"]) + dialog.ui.password_line_edit.setText(prev_db_data["password"]) + dialog.ui.host_line_edit.setText(prev_db_data["host"]) + dialog.ui.database_line_edit.setText(prev_db_data["database"]) + dialog.ui.port_line_edit.setText(str(prev_db_data["port"])) + + db_data: dict = dialog.exec_() + if len(db_data.keys()) == 0: + return + self.change_db_connection(host=db_data["host"], port=db_data["port"], + user=db_data["user"], password=db_data["password"], + database=db_data["database"]) + self.window = Window(self) + + # TODO prompt for overwriting config when password invalid + # TODO sane default location + # TODO tag editor, don't kill dialog if fields are missing + # TODO display Tag Category when searching for tags + + # TODO remove string limit restrictions on implications & similar + + self.__file_root: str = None + self.window.on_artnet_root_change_clicked() + self.__file_reader = FileReader(self.__file_root) + + self.curr_image_index: int = None + + self.all_images: list = None + self.update_all_images_list() + + self.window.set_temporary_status_message("Hello o7", 5000) + + def run(self): + if len(self.all_images) > 0: + self.curr_image_index = 0 + self.change_image() + + self.window.show() + + sys.exit(self.__app.exec_()) + + def update_all_images_list(self): + """ + Updates the internal list of all images in the artnet root + :return: + """ + images = [] + artist_list = self.__file_reader.list_artists() + artist_list.sort() + for artist in artist_list: + for image in self.__file_reader.get_files(artist): + images.append(image) + self.all_images = images + + self.known_image_amount = 0 + for image in self.all_images: + if self.db_connection.get_art_by_path(image) is not None: + self.known_image_amount += 1 + + def scrape_tags(self, file_name: str, art_ID: int, url: str=None): + """ + Scrape the tags from the given url and return which one's are new + :param file_name: + :param art_ID: + :param url: + :return: + """ + if url is None: + return None + + #tags = ArtNet.web.Scrap_Tags.scrap_tags(file_name, url, ArtNet.web.link_generator.predict_domain(file_name)) + tags = LinkGenerator.get_instance().scrape_tags(url, LinkGenerator.get_instance().predict_domain(file_name)) + if tags is None: + return None + + already_applied_tags = self.db_connection.get_art_tags_by_ID(art_ID) + for i in range(len(already_applied_tags)): + already_applied_tags[i] = self.db_connection.get_tag_by_ID(already_applied_tags[i])[0][1].strip() + + importable_tags = [] + importable_artists = [] + scraped_tags = [] + for i in tags.values(): + scraped_tags += i + + for tag in scraped_tags: + result = self.db_connection.get_tag_by_name(tag) + if len(result) == 0: # tag does not exist yet + if tag in tags['artists']: + importable_artists.append(tag) + continue + importable_tags.append(tag) + continue + + if tag in already_applied_tags: # tag is already applied + continue + + result = self.db_connection.get_tag_impliers(tag) + if len(result) != 0: # tag is implied by some other tag + continue + + result = self.db_connection.get_tag_aliases(tag) + if len(result) != 0: # tag is already tagged by an alias + continue + + if tag in tags['artists']: + importable_artists.append(tag) + continue + importable_tags.append(tag) # tag must be known and not tagged yet + + return importable_tags, importable_artists + + def import_tags(self, art_ID: int, tags): + """ + Add a list of given tags to the specified art_ID + :param art_ID: + :param tags: + :return: + """ + for tag in tags: + + if len(self.db_connection.get_tag_by_name(tag)) == 0: # tag doesn't exist yet + result = self.window.force_edit_tag_dialog(name=tag) + + if result is None: # tag creation was aborted + continue + + tag = result['name'] # overwrite with possibly new tag name + + tag = self.db_connection.get_tag_by_name(tag)[0][0] + self.window.curr_tags.append(tag) + self.window.data_changed = True + + def get_md5_of_image(self, path: str): + """ + Calculate the md5 hash of the image given by the path. + + :param path: assumed to be relative e.g. artist_A/image.jpg + :return: + """ + md5_hash = md5() + try: + with open(self.get_root() + os.path.sep + path, + "rb") as file: # read file part by part because of potential memory limits + for chunk in iter(lambda: file.read(4096), b""): + md5_hash.update(chunk) + + return md5_hash.hexdigest() + except FileNotFoundError: + self.update_all_images_list() + return None + + def delete_image(self, path: str, delete_instead_of_move: bool = False, trash_bin_folder_path: str = "trash"): + """ + Deletes the image from the root folder + :param path: + :param delete_instead_of_move: + :param trash_bin_folder_path: + :return: + """ + full_art_path = self.get_root() + os.path.sep + path + + if delete_instead_of_move: + print(f"Deleting the actual file {full_art_path} is disabled for now") + #os.remove(full_art_path) + #return + + trash_dst = trash_bin_folder_path + os.path.sep + path + t_splits = trash_dst.split(os.path.sep) + t_path = "" + for i in range(len(t_splits)-1): + t_path += os.path.sep + t_splits[i] + t_path = t_path[1:] + if not os.path.exists(t_path): + print(f"{t_path} did not exist and will be created!") + os.makedirs("."+os.path.sep+t_path) + os.rename(full_art_path, trash_dst) + print(f"Moving image {full_art_path} to {trash_dst}") + + self.update_all_images_list() + while self.curr_image_index >= len(self.all_images): + self.curr_image_index -= 1 + + + + @DeprecationWarning + def recalculate_hash_for_known_images(self): + """ + Calculate and write the md5 hash for every image known to the database. + + Important: For migration purposes only + :return: + """ + for i in range(len(self.all_images)): + image_db_result = self.db_connection.get_art_by_path(self.all_images[i]) + + if image_db_result is None: # unknown image, skip + continue + + hash_digest = self.get_md5_of_image(self.all_images[i]) + + authors = self.db_connection.get_authors_of_art(path=image_db_result["path"]) + tag_ids = self.db_connection.get_art_tags_by_ID(art_ID=image_db_result["ID"]) + tags = [] + for tag_id in tag_ids: + tags.append(self.db_connection.get_tag_by_ID(tag_id)[0][1].strip()) + + self.db_connection.save_image(ID=image_db_result["ID"], path=image_db_result["path"], + title=image_db_result["title"], + link=image_db_result["link"], authors=authors, md5_hash=hash_digest, + tags=tags) + + def get_next_unknown_image(self) -> int: + """ + Search all images for the next image not known to the database in ascending order. + + :return: index of the next unknown image, None if there is no unknown image + """ + next_unknown: int = None + curr_searched_image_index = self.curr_image_index + 1 + + finished_loop = False + while next_unknown is None and not finished_loop: + if curr_searched_image_index >= len(self.all_images): # wrap around to the start + curr_searched_image_index = 0 + if curr_searched_image_index == self.curr_image_index: # searched all images, nothing unknown + break + + image_db_result = self.db_connection.get_art_by_path(self.all_images[curr_searched_image_index]) + if image_db_result is None: + image_db_result = self.db_connection.get_art_by_hash( + self.get_md5_of_image(self.all_images[curr_searched_image_index]) + ) + + if image_db_result is None: # image is unknown to database + next_unknown = curr_searched_image_index + break + else: + curr_searched_image_index += 1 + + if next_unknown: + return curr_searched_image_index + else: + return None + + def get_prev_unknown_image(self) -> int: + """ + Search all images for the next image not known to the databse in descending order. + + :return:index of the next unknown image, None if there is no unknown image + """ + next_unknown: int = None + curr_searched_image_index = self.curr_image_index - 1 + + finished_loop = False + while next_unknown is None and not finished_loop: + if curr_searched_image_index <= 0: # wrap around to the end + curr_searched_image_index = len(self.all_images) - 1 + if curr_searched_image_index == self.curr_image_index: # searched all images, nothing unknown + break + + image_db_result = self.db_connection.get_art_by_path(self.all_images[curr_searched_image_index]) + + if image_db_result is None: # image is unknown to database + next_unknown = curr_searched_image_index + break + else: + curr_searched_image_index -= 1 + + if next_unknown: + return curr_searched_image_index + else: + return None + + def change_image(self): + """ + Refresh the image display to show the most current data according to the selected image + :return: + """ + self.window.setting_up_data = True + + image_db_result = self.db_connection.get_art_by_hash( + self.get_md5_of_image(self.all_images[self.curr_image_index]) + ) + #image_db_result = self.db_connection.get_art_by_path(self.all_images[self.curr_image_index]) + s = self.all_images[self.curr_image_index].split(os.path.sep) + if image_db_result is not None: + image_title = image_db_result["title"] if len(image_db_result["title"]) > 0 else "-Title Unknown-" + image_author = self.db_connection.get_authors_of_art_by_ID(image_db_result["ID"]) + image_link = image_db_result["link"] + art_ID = image_db_result["ID"] + if image_author is None or len(image_author) == 0: + image_author = [(self.all_images[self.curr_image_index].split(os.path.sep)[0], "(Not in Database)")] + else: + art_ID = None + image_author = [(s[0], "(Not in Database)")] + image_title = s[-1] + " (Not in Database)" + image_link = "(Unknown)" + + print("Displaying", self.all_images[self.curr_image_index]) + self.window.display_image(image_title, image_author, + os.path.join(self.__file_root, self.all_images[self.curr_image_index]), + self.all_images[self.curr_image_index], + art_ID, image_link, file_name=s[-1]) + self.window.set_tag_list([self.db_connection.get_tag_by_ID(x)[0][1].strip() for x in self.db_connection.get_art_tags_by_ID(art_ID)]) + + self.window.data_changed = False + self.window.setting_up_data = False + + def create_db_connection(self, host: str, port: int, database: str, user: str, password: str) -> DBAdapter: + print("Changing db connection to ".format(host, port, user, password)) + return DBAdapter(user=user, password=password, host=host, port=port, database=database) + + def get_root(self) -> str: + return self.__file_root + + def change_root(self, path: str): + """ + Change the Artnet root path and confirm it is working. + + Rewrite config there + :param path: + :return: + """ + if len(path) == 0: + exit(0) + print("Changing root to", path) + self.__file_root = path + + def get_db_connection_details(self) -> dict: + return self.config.data["db"] + + def change_db_connection(self, host: str, port: int, database: str, user: str, password: str): + self.config.data["db"]["user"] = user + self.config.data["db"]["password"] = password + self.config.data["db"]["host"] = host + self.config.data["db"]["database"] = database + self.config.data["db"]["port"] = str(port) + + self.config.update_config() + + self.db_connection = self.create_db_connection(host, port, database, user, password) + + diff --git a/ArtNet/db/__init__.py b/ArtNet/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/db/db_adapter.py b/ArtNet/db/db_adapter.py new file mode 100644 index 0000000..c43d4a3 --- /dev/null +++ b/ArtNet/db/db_adapter.py @@ -0,0 +1,853 @@ +import psycopg2 +from psycopg2.errors import * +from psycopg2.errorcodes import UNIQUE_VIOLATION + + +class DBAdapter: + + def __init__(self, user: str, password: str, host: str = "localhost", port: int = 5432, database: str = "artnet"): + self.db = None + self.db_cursor = None + try: + self.db = psycopg2.connect(host=host, port=port, database=database, user=user, password=password) + self.db_cursor = self.db.cursor() + + except psycopg2.OperationalError as e: + raise ValueError("Invalid DB credentials!") + print("DB connection to {0}:{1}/{2}".format(host, port, database)) + + def save_image(self, ID: str, title: str, authors: list, path: str, tags: list, link: str, md5_hash: str): + """ + Updates or saves the given image data to the DB + :param ID: + :param title: + :param authors: + :param path: + :param tags: + :param link: + :param md5_hash: md5 hash as a hex digest + :return: + """ + print("Saving Image {0}:{1} authors: {2} path: {3} tags: {4} link: {5} hash:{6}" + .format(ID, title, authors, path, tags, link, md5_hash)) + d = {"title": title, "path": path, "id": ID, "link": link, "hash": md5_hash} + if self.get_art_by_path(path) is None: + if ID is None: + self.db_cursor.execute( + "INSERT INTO art (path, title, link, md5_hash) VALUES (%(path)s, %(title)s, %(link)s, %(hash)s)", + d) + ID = self.get_art_by_path(path)["ID"] + d["id"] = ID + elif self.get_tag_by_ID(ID) is None: + self.db_cursor.execute( + "INSERT INTO art (path, title, link, md5_hash) VALUES (%(path)s, %(title)s, %(link)s, %(hash)s)", d) + else: + self.db_cursor.execute("UPDATE art SET path = %(path)s, title = %(title)s, link = %(link)s, " + + "md5_hash = %(hash)s WHERE id = %(id)s", d) + + if ID is None: + ID = self.get_art_by_path(path)["ID"] + assert(ID != None) + + old_tags = [self.get_tag_by_ID(x)[0][1].strip() for x in self.get_art_tags_by_ID(ID)] + for tag in tags: + if tag in old_tags: # already present + continue + d = {"id": ID, "tag": self.get_tag_ID(tag)} + try: + self.db_cursor.execute("INSERT INTO art_tag (art_id, tag_ID) VALUES (%(id)s, %(tag)s)", d) + except psycopg2.Error as e: + if e.pgcode == UNIQUE_VIOLATION: + print(e) + print("Skipping Unique Violation ...") + else: + raise e + + for old_tag in old_tags: + if old_tag not in tags: # need to remove old tag + self.remove_tag_from_image(art_ID=ID, tag_ID=self.get_tag_ID(old_tag)) + + old_authors = self.get_authors_of_art_by_ID(ID) + for author in authors: + if author in old_authors: + continue + d = {"id": ID, "author_name": author[0], "author_domain": author[1]} + self.db_cursor.execute("INSERT INTO art_author (presence_name, presence_domain, art_ID) " + + "VALUES (%(author_name)s, %(author_domain)s, %(id)s)", d) + for old_author_name, old_author_domain in old_authors: + if (old_author_name, old_author_domain) not in authors: # need to remove old author + self.remove_artist_from_image(art_ID=ID, presence_name=old_author_name, + presence_domain=old_author_domain) + + self.db.commit() + + def remove_image(self, hash: str): + """ + Removes an image completely from the database. + + Does not affect tags or artists but removes art-tag, art-artist relations + :param hash: md5 hash of the image to be deleted + :return: + """ + image_data = self.get_art_by_hash(hash) + art_ID = image_data["ID"] + print(f"Deleting image #{art_ID} {image_data['title']}") + tags = self.get_art_tags_by_ID(art_ID) + for tag_ID in tags: + pass + #self.remove_tag_from_image(art_ID=art_ID, tag_ID=tag_ID) + authors = self.get_authors_of_art_by_ID(art_ID) + for presence_name, presence_domain in authors: + pass + #self.remove_artist_from_image(art_ID=art_ID, presence_name=presence_name, presence_domain=presence_domain) + + #self.db_cursor.execute("DELETE FROM art WHERE md5_hash = %(hash)s", {"hash": hash}) + + + + def remove_artist_from_image(self, art_ID, presence_name, presence_domain): + """ + Removes the relationship between an artist and the image + :param art_ID: + :param presence_name: + :param presence_domain: + :return: + """ + d = {"name": presence_name, "domain": presence_domain, "id": art_ID} + self.db_cursor.execute("DELETE FROM art_author " + + "WHERE presence_name = %(name)s and " + + "presence_domain = %(domain)s and art_ID = %(id)s", + d) + + def remove_tag_from_image(self, art_ID, tag_ID): + """ + Remove the relationship between a tag and the image + :param art_ID: + :param tag_ID: + :return: + """ + d = {"id": art_ID, "tag": tag_ID} + self.db_cursor.execute("DELETE FROM art_tag WHERE art_id = %(id)s and tag_id = %(tag)s", d) + + def save_presence(self, name: str, domain: str, artist_ID: int, link: str): + """ + Save the presence entry with the given data. + + If it doesn't exist a new one will be created. + If the artist ID is unknown an exception will be thrown + :param name: + :param domain: + :param artist_ID: + :param link: + :return: + """ + print("Saving Presence {0}:{1} Artist: {2} Link: {3}".format(name, domain, artist_ID, link)) + artist = self.get_artist(artist_ID) + if artist is None or len(artist) == 0: + raise Exception("Unknown Artist to create/update Presence with!") + + presence = self.get_presence(name, domain) + d = {"name": name, "domain": domain, "artist": artist_ID, "link": link} + if len(presence) == 0: # presence does not exist yet + self.db_cursor.execute("INSERT INTO presence (name, domain, artist, link) " + + "VALUES (%(name)s, %(domain)s, %(artist)s, %(link)s)", d) + else: # presence exists, update it + self.db_cursor.execute("UPDATE presence SET artist = %(artist)s, link = %(link)s" + + "WHERE name = %(name)s and domain = %(domain)s", d) + + self.db.commit() + + def remove_presence(self, name: str, domain: str): + """ + Remove a presence from the database + :return: + """ + print("Removing Presence {0}:{1}".format(name, domain)) + d = {"name": name, "domain": domain} + self.db_cursor.execute("DELETE FROM presence WHERE name = %(name)s and domain = %(domain)s", d) + self.db.commit() + + def get_presence(self, name: str, domain: str): + """ + Queries the database for the presence and returns the result + + :param name: + :param domain: + :return: + """ + d = {"name": name, "domain": domain} + self.db_cursor.execute("SELECT name, domain, artist, link FROM presence " + + "WHERE name = %(name)s and domain = %(domain)s", + d) + rows = self.db_cursor.fetchall() + new_rows = [] + for row in rows: + new_rows.append((row[0].strip(), row[1].strip(), row[2], row[3])) + + return new_rows + + def save_artist(self, ID: int, description: str): + """ + Save (or update if ID is already taken) an artist to the DB + :param ID: + :param description: + :return: + """ + print("Saving artist {0}:{1}".format(ID, description)) + d = {"id": ID, "description": description} + if ID is None: # no ID given, auto generate it + self.db_cursor.execute("INSERT INTO artist (description) VALUES (%(description)s)", d) + elif len(self.get_artist(ID)) != 0: # artist exists already: + self.db_cursor.execute("UPDATE artist SET description = %(description)s WHERE id = %(id)s", d) + else: # artist needs to be created + self.db_cursor.execute("INSERT INTO artist (id, description) VALUES (%(id)s, %(description)s)", d) + self.db.commit() + + def remove_artist(self, ID: int): + """ + Deletes the artist from the DB + :param ID: + :return: + """ + print("Deleting artist {0}".format(ID)) + d = {"id": ID} + self.db_cursor.execute("DELETE FROM Artist WHERE ID = %(id)s", d) + self.db.commit() + + def save_category(self, name: str): + """ + Save the category to the DB. + + Does not check if the category already exists + :param name: + :return: + """ + print("Saving category {0}!".format(name)) + d = {"name": name} + self.db_cursor.execute("INSERT INTO tag_category (name) VALUES (%(name)s)", d) + + self.db.commit() + + def remove_category(self, name: str): + """ + Remove the category from the DB. + :param name: + :return: + """ + d = {"name": name} + self.db_cursor.execute("DELETE FROM tag_category WHERE name = %(name)s", d) + + self.db.commit() + + def get_artist(self, ID: int): + """ + Queries the database for the artist (not presence) and returns the result + :param ID: + :return: + """ + d = {"id": ID} + self.db_cursor.execute("SELECT id, description FROM artist WHERE id = %(id)s", d) + + return self.db_cursor.fetchall() + + def get_artist_presences(self, id: int): + """ + Queries the databse for all presences associated with the artist and returns the result + :param id: + :return: + """ + d = {"id": id} + self.db_cursor.execute("SELECT name, domain, artist FROM Presence WHERE artist = %(id)s", d) + + return self.db_cursor.fetchall() + + def get_all_artists(self): + """ + Lists all available artists (not presences) and returns the result + :return: + """ + self.db_cursor.execute("SELECT id, description FROM artist") + + def get_art_by_hash(self, file_hash: str) -> dict: + """ + Queries the database for the art via its hash and returns it if available. + + Currently uses an md5 hash + :param file_hash: + :return: + """ + d = {"hash": file_hash} + self.db_cursor.execute("SELECT id, path, title, link, md5_hash FROM art where md5_hash = %(hash)s", d) + + result = self.db_cursor.fetchall() + + if len(result) == 0: + return None + else: + result = result[0] + image_data = { + "ID": result[0], + "path": result[1], + "title": result[2], + "link": result[3], + "md5_hash": result[4], + } + return image_data + + def get_art_by_path(self, path: str) -> dict: + """ + Queries the database for the art via its path and returns it if available. + + Otherwise None + :param path: + :return: + """ + d = {"path": path} + self.db_cursor.execute("SELECT id, path, title, link, md5_hash FROM art where path = %(path)s", d) + + result = self.db_cursor.fetchall() + + if len(result) == 0: + return None + else: + result = result[0] + image_data = { + "ID": result[0], + "path": result[1], + "title": result[2], + "link": result[3], + "md5_hash": result[4], + } + return image_data + + @DeprecationWarning + def get_authors_of_art(self, path: str): + """ + Get the authors (presence and author) of the given art. + :param path: + :return: + """ + art_data = self.get_art_by_path(path) + art_id: int = None + if art_data is not None: + art_id = art_data["ID"] + if art_id is None: + return None + d = {"id": art_id} + + self.db_cursor.execute("SELECT presence_name, presence_domain FROM art_author WHERE art_ID = %(id)s", d) + presences = self.db_cursor.fetchall() + + result = [] + for name, domain in presences: + result.append((name.strip(), domain.strip())) + + return result + + def get_authors_of_art_by_ID(self, id: int): + """ + Get the authors (presence and author) of the given art. + :param id: + :return: + """ + d = {"id": id} + + self.db_cursor.execute("SELECT presence_name, presence_domain FROM art_author WHERE art_ID = %(id)s", d) + presences = self.db_cursor.fetchall() + + result = [] + for name, domain in presences: + result.append((name.strip(), domain.strip())) + + return result + + def get_art_tags_by_ID(self, art_ID: int): + """ + Query the database for all tags associated with a given art ID + :param art_ID: + :return: + """ + d = {"id": art_ID} + self.db_cursor.execute("SELECT tag_ID FROM art_tag WHERE art_id = %(id)s", d) + + rows = self.db_cursor.fetchall() + new_rows = [] + for row in rows: + new_rows.append(row[0]) + return new_rows + + def search_fuzzy_presence(self, name: str, domain: str, all_if_empty: bool = False): + """ + Search a list of presences fitting the (name, domain) fuzzy. + :param name: + :param domain: + :param all_if_empty: + :return: + """ + if all_if_empty and (name is None or len(name) == 0) and (domain is None or len(domain) == 0): + self.db_cursor.execute("SELECT name, domain, artist FROM presence") + else: + self.db_cursor.execute("SELECT name, domain, artist FROM presence WHERE LOWER(name) LIKE LOWER('%{0}%')".format(name) + + " AND domain LIKE '%{0}%'".format(domain)) + rows = self.db_cursor.fetchall() + + result = [] + for row in rows: + result.append((row[0].strip(), row[1].strip())) + return result + + def get_presences_art(self, name, domain): + """ + Query a list of all art that includes the given presence as their author + :param name: + :param domain: + :return: + """ + d = {"name": name, "domain": domain} + self.db_cursor.execute( + "SELECT art_ID FROM Art_Author WHERE presence_name = %(name)s and presence_domain = %(domain)s", d) + + return self.db_cursor.fetchall() + + def get_all_categories(self) -> list: + """ + Return all categories in the database. + :return: + """ + self.db_cursor.execute("SELECT name FROM tag_category") + + rows = [] + for row in self.db_cursor.fetchall(): + rows.append(row[0].strip()) + + return rows + + def search_fuzzy_categories(self, search: str) -> list: + """ + Search a list of categories fitting search fuzzy. + :param search: + :return: + """ + self.db_cursor.execute("SELECT name FROM tag_category WHERE LOWER(name) LIKE LOWER('%{0}%')".format(search)) + + rows = [] + for row in self.db_cursor.fetchall(): + rows.append(row[0]) + + return rows + + def search_fuzzy_tag(self, name: str, all_if_empty: bool = False) -> list: + """ + Search a list of tags fitting name fuzzy. + :param name: + :param all_if_empty: + :return: + """ + if all_if_empty and len(name) == 0: + self.db_cursor.execute("SELECT name, description, category FROM tag") + else: + self.db_cursor.execute("SELECT name, description, category FROM tag WHERE LOWER(name) LIKE LOWER('%{0}%')".format(name)) + + rows = self.db_cursor.fetchall() + new_rows = [] + for i in range(len(rows)): + new_rows.append([]) + for j in range(len(rows[i])): + if rows[i][j] is None: + new_rows[i].append("") + else: + new_rows[i].append(rows[i][j].strip()) + + return new_rows + + def search_fuzzy_artists(self, ID: int, description: str, all_if_empty: bool = False): + """ + Search a list of fitting artists fuzzy. + + If ID is None it will search using the description + :param ID: + :param description: + :param all_if_empty: + :return: + """ + if ID is not None: + self.db_cursor.execute("SELECT id, description FROM artist WHERE id = %(id)s", {"id": ID}) + elif all_if_empty and ID is None and len(description) == 0: + self.db_cursor.execute("SELECT id, description FROM artist") + else: + self.db_cursor.execute("SELECT id, description FROM artist WHERE LOWER(description) LIKE LOWER('%{0}%')" + .format(description)) + + return self.db_cursor.fetchall() + + def create_tag(self, name: str, description: str, aliases: list, implications: list, category: str = None): + name = name.strip() + if aliases is None: + aliases = [] + if implications is None: + implications = [] + d = {"name": name, "description": description, "category": category} + self.db_cursor.execute("INSERT INTO tag (name, description, category) " + + "VALUES (LOWER(%(name)s), %(description)s, %(category)s)", d) + + for alias in aliases: + self.add_alias(name, alias) + for implicant in implications: + self.add_implication(name, implicant) + + self.db.commit() + + def edit_tag(self, name: str, description: str, aliases: list=None, implications: list=None, category: str = None, + old_tag: str = None): + """ + Edit a tag with the new given data + :param name: unique tag name to apply edits to (distinct from old_tag) + :param description: new description to apply, set to None to omit + :param aliases: list of tag names to be the alias of this tag, set to None to omit + :param implications: list of tag names to be implied by this tag, set to None to omit + :param category: + :param old_tag: old tag name from which to transfer all data + :return: + """ + name = name.strip().lower() + d = { + "name": name, + "description": description, + "alias": aliases, + "implications": implications, + "category": category, + } + + if old_tag is not None and len(old_tag) > 0: + # tag name changed, need to transfer all data and remove old tag + old_tag = self.get_tag_by_name(old_tag) + d["ID"] = old_tag[0][-1] + self.db_cursor.execute("UPDATE tag SET name = %(name)s WHERE tag_ID = %(ID)s", d) + + if description is not None: + self.db_cursor.execute("UPDATE tag SET description = %(description)s, category = %(category)s " + + "WHERE name = %(name)s", d) + + if aliases is not None: + old_aliases = self.get_tag_aliases(name) + for alias in aliases: + if alias in old_aliases: # is this already set? + continue + self.add_alias(name, alias) + + for old_alias in old_aliases: + if old_alias not in aliases: # got to delete an alias? + self.remove_alias(name, old_alias) + + if implications is not None: + old_implicants = self.get_tag_implications(name) + for implicant in implications: + if implicant in old_implicants: # is this already set? + continue + self.add_implication(name, implicant) + + for old_implicant in old_implicants: + if old_implicant not in implications: # got to delete an implicant? + self.remove_implication(name, old_implicant) + + def add_alias(self, name: str, alias: str): + """ + Add the alias pair to the database + :param name: + :param alias: + :return: + """ + d = { + "name": self.get_tag_ID(name), + "alias": self.get_tag_ID(alias) + } + self.db_cursor.execute("INSERT INTO tag_alias (tag1, tag2) VALUES (%(name)s, %(alias)s)", d) + + def remove_alias(self, name: str, alias: str): + """ + Remove alias pair from the database + :param name: + :param alias: + :return: + """ + d = { + "ID": self.get_tag_ID(name), + "alias": self.get_tag_ID(alias) + } + self.db_cursor.execute("DELETE FROM tag_alias WHERE (tag1 = %(ID)s and tag2 = %(alias)s) " + + "or (tag1 = %(alias)s and tag2 = %(ID)s)", d) + + def add_implication(self, name: str, implicant: str): + """ + Add the implication to the database + :param name: + :param implicant: + :return: + """ + d = { + "name": self.get_tag_ID(name), + "implicant": self.get_tag_ID(implicant) + } + self.db_cursor.execute("INSERT INTO tag_implication (root_tag, implicate) VALUES (%(name)s, %(implicant)s)", + d) + self.db.commit() + + def remove_implication(self, name: str, implicant: str): + """ + Remove the implication pair from the database + :param name: + :param implicant: + :return: + """ + d = {"name": self.get_tag_ID(name), + "implicant": self.get_tag_ID(implicant)} + self.db_cursor.execute("DELETE FROM tag_implication WHERE root_tag = %(name)s " + + "and implicate = %(implicant)s", d) + self.db.commit() + + def remove_tag(self, name: str): + """ + Remove the given tag from the database + :param str: + :return: + """ + d = {"name": name} + self.db_cursor.execute("DELETE FROM tag WHERE name = %(name)s", d) + + self.db.commit() + + def get_tag_ID(self, name: str) -> int: + """ + Get the tag ID by querying it by its unique name + :param name: + :return: + """ + d = {"name": name} + self.db_cursor.execute("SELECT tag_ID, name FROM tag where LOWER(name) = LOWER(%(name)s)", d) + + rows = [] + for row in self.db_cursor.fetchall(): + rows.append(row[0]) + + if len(rows) > 1: + exit(1) # something went horribly horribly wrong! + elif len(rows) == 0: + return None + + return rows[0] + + def get_tag_by_name(self, name: str) -> list: + """ + Search the tag in the DB via its name. + + Note: name is constrained to be unique by the DB + :param name: + :return: Returns the tag's name, description, category, tag ID + """ + d = {"name": name} + self.db_cursor.execute("SELECT name, description, category, tag_ID FROM tag where LOWER(name) = LOWER(%(name)s)", d) + + rows = [] + for row in self.db_cursor.fetchall(): + new_row = [] + for value in row: + if value is None: + new_row.append("") + elif type(value) == str: + new_row.append(value.strip()) + else: + new_row.append(value) + rows.append(new_row) + return rows + + def get_tag_by_ID(self, ID: int) -> list: + """ + Search the tag in the DB via its ID. + + Note: name is constrained to be unique by the DB + :param ID: + :return: Returns the tag's ID, name, description, category + """ + d = {"ID": ID} + self.db_cursor.execute("SELECT tag_ID, name, description, category FROM tag where tag_ID = %(ID)s", d) + + return self.db_cursor.fetchall() + + def get_tag_aliases(self, name: str) -> list: + """ + Search for the tag's aliases and the tag's aliases + :param name: + :return: List of tag Names that are aliases of this one + """ + aliases = self.get_tag_aliases_by_ID(self.get_tag_ID(name)) + result = [] + for alias in aliases: + tag_data = self.get_tag_by_ID(alias) + if len(tag_data) > 0: + result.append(tag_data[0][1].strip()) + return result + + def get_tag_aliases_by_ID(self, tag_ID: int) -> list: + """ + Search for the tag's aliases and the tag's aliases + :param name: + :return: List of tag IDs that are aliases of this one + """ + marked = [] + to_search = [tag_ID] + while len(to_search) != 0: + curr_alias = to_search.pop() + found_aliases = self.__get_tag_aliases_no_recurse(curr_alias) + marked.append(curr_alias) + + for found_alias in found_aliases: + if found_alias not in marked: + to_search.append(found_alias) + + marked.remove(tag_ID) + return marked + + def get_all_tag_implications(self, name: str) -> list: + """ + Search for the tag's implications and those that are implied by them. + :param name: + :return: + """ + root_ID = self.get_tag_ID(name) + collected_tags = self.get_tag_implications_by_ID(root_ID) + collected_tags.append(root_ID) + to_search = self.get_tag_implications_by_ID(root_ID) + + while len(to_search) != 0: + curr_tag = to_search.pop() + found_implications = self.get_tag_implications_by_ID(curr_tag) + for found in found_implications: + if found in collected_tags: + continue + else: + collected_tags.append(found) + to_search.append(found) + + collected_tags.remove(root_ID) + + result = [] + for tag_ID in collected_tags: + tag_data = self.get_tag_by_ID(tag_ID) + if len(tag_data) != 0: + result.append(tag_data[0][1].strip()) + return result + + def get_tag_implications_by_ID(self, ID: int) -> list: + """ + Search for the tag's implications + :param ID: ID of the tag + :return: returns a list of IDs that are implied + """ + d = {"ID": ID} + if d["ID"] is None: + return [] + + self.db_cursor.execute("SELECT root_tag, implicate FROM Tag_Implication WHERE root_tag = %(ID)s", d) + + rows = self.db_cursor.fetchall() + if len(rows) == 0: + return [] + + r = [] + for row in rows: + r.append(row[1]) + + return r + + def get_tag_implications(self, name: str) -> list: + """ + Search for the tag's implications + :param name: + :param output_ID: Don't resolve the tag ids into names in the resulting list + :return: List of tag names + """ + d = {"ID": self.get_tag_ID(name)} + if d["ID"] is None: + return [] + + self.db_cursor.execute("SELECT root_tag, implicate FROM Tag_Implication WHERE root_tag = %(ID)s", d) + + rows = self.db_cursor.fetchall() + if len(rows) == 0: + return [] + + r = [] + for row in rows: + r.append(row[1]) + + result = [self.get_tag_by_ID(v) for v in r] + + r = [] + for value in result: + r.append(value[0][1].strip()) + + return r + + def get_tag_impliers(self, name: str) -> list: + """ + Search for tags that imply this one. + :param name: + :return: + """ + d = {"ID": self.get_tag_ID(name)} + self.db_cursor.execute("SELECT root_tag, implicate FROM Tag_Implication WHERE implicate = %(ID)s", d) + + rows = self.db_cursor.fetchall() + r = [] + for row in rows: + r.append(row[0]) + return r + + def __get_tag_aliases_no_recurse(self, tag_id: int) -> list: + """ + Search for the tag's aliases + :param tag_id: + :return: + """ + d = {"ID": tag_id} + if d["ID"] is None: + return [] + + self.db_cursor.execute("SELECT tag1, tag2 FROM Tag_Alias " + "WHERE tag1 = %(ID)s or tag2 = %(ID)s", d) + rows = self.db_cursor.fetchall() + r = [] + for row in rows: + if row[0] == tag_id: + r.append(row[1]) + elif row[1] == tag_id: + r.append(row[0]) + else: + raise Exception("Something went terribly wrong!") + + return r + + +if __name__ == "__main__": + db = DBAdapter(user="artnet_editor", password="G606Rm9sFEXe6wfTLxVu", + database="artnet", host="localhost", port=5432) + + search = 'Ajin/66699b4e41f533982db1766e8f72494a.gif2222' + print("Art search result:", search, db.get_art_by_path('Ajin/66699b4e41f533982db1766e8f72494a.gif')) + + search = "tag" + print("Fuzzy Search Result:", search, db.search_fuzzy_tag(search)) + + search = "tag1" + print("Search Result:", search, db.get_tag_by_name(search)) + + search = "tag1" + print("Alias Search Result:", search, db.get_tag_aliases(search)) + + search = "tag1" + print("Implication Search Result:", search, db.get_tag_implications(search)) + + target = "tag1" + db.edit_tag(target, "new description! ;D", aliases=["tag1_alias"], implications=["tag1_implicated"]) + print("Editing:", target, db.get_tag_by_name(target), db.get_tag_aliases(target), db.get_tag_implications(target)) + diff --git a/ArtNet/file/__init__.py b/ArtNet/file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/file/config_reader.py b/ArtNet/file/config_reader.py new file mode 100644 index 0000000..abdad1a --- /dev/null +++ b/ArtNet/file/config_reader.py @@ -0,0 +1,135 @@ +import os +import base64 +import copy + +import yaml +from cryptography.exceptions import AlreadyFinalized +from cryptography.exceptions import InvalidTag +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +class ConfigReader: + + kdf = None + nonce = None + + def __init__(self, root_folder: str, password: str): + if root_folder is None: + raise Exception("No root folder was defined!") + self.__key = None + self.__aesgcm = None + self.data = {} + + self.__password = password + self.__config_location = os.path.join(root_folder, ".artnet", "artnet.config") + + ConfigReader.__create_kdf() + self.__key = ConfigReader.kdf.derive(bytes(password.encode('utf-8'))) + self.__aesgcm = AESGCM(self.__key) + + if os.path.exists(self.__config_location) and os.path.isfile(self.__config_location): + self.read_config() + else: + if not os.path.exists(os.path.join(root_folder, ".artnet")): + os.mkdir(os.path.join(root_folder, ".artnet")) + self.create_default_config() + self.read_config() + + def update_config(self): + """ + Update the written config with the local settings + :return: + """ + file = open(self.__config_location, "w") + yaml.dump(stream=file, data=self.encrypt_sensitive_data(copy.deepcopy(self.data))) + + def read_config(self): + """ + Read the config from file and overwrite local settings + :return: + """ + print(os.path.isfile(self.__config_location)) + print(os.path.join(os.getcwd(), self.__config_location)) + file = open(self.__config_location, "r") + data = yaml.safe_load(stream=file) + self.data = self.decrypt_sensitive_data(data) + + def encrypt_sensitive_data(self, data: dict) -> dict: + """ + Encrypts the sensitive portions of the data + :return: + """ + new_data = data + new_data["db"]["password"] = self.encrypt_text(data["db"]["password"]) + new_data["db"]["user"] = self.encrypt_text(data["db"]["user"]) + + return new_data + + def decrypt_sensitive_data(self, data: dict) -> dict: + """ + Decrypts the sensitive portions of the data + :return: + """ + new_data = data + new_data["db"]["password"] = self.decrypt_text(data["db"]["password"]) + new_data["db"]["user"] = self.decrypt_text(data["db"]["user"]) + + return new_data + + def create_default_config(self): + """ + Create a default config, overwrites all settings and generates a new key + :return: + """ + self.data = { + "db": { + "host": "localhost", + "port": 5432, + "database": "artnet", + "user": "your_user", + "password": "enter_password_via_gui" + }, + + } + self.update_config() + + @staticmethod + def __create_kdf(): + ConfigReader.kdf = PBKDF2HMAC(algorithm=hashes.SHA512(), + length=32, + salt=bytes("ArtN3t.WhatElse?".encode('utf-8')), + iterations=10000, + backend=default_backend() + ) + ConfigReader.nonce = bytes("qt34nvßn".encode('utf-8')) + + def encrypt_text(self, text: str) -> str: + cipher_text_bytes = self.__aesgcm.encrypt(data=text.encode('utf-8'), + associated_data=None, + nonce=ConfigReader.nonce + ) + return base64.urlsafe_b64encode(cipher_text_bytes) + + def decrypt_text(self, cipher: str) -> str: + if ConfigReader.kdf is None: + ConfigReader.__create_kdf() + try: + decrypted_cipher_text_bytes = self.__aesgcm.decrypt( + nonce=ConfigReader.nonce, + data=base64.urlsafe_b64decode(cipher), + associated_data=None + ) + except InvalidTag: + raise Exception("Could not decrypt Text! Wrong Password?") + + return decrypted_cipher_text_bytes.decode('utf-8') + + +if __name__ == "__main__": + cr = ConfigReader(password="MySuperDuperKey", root_folder=".") + + print(cr.data) diff --git a/ArtNet/file/file_reader.py b/ArtNet/file/file_reader.py new file mode 100644 index 0000000..93e5ef9 --- /dev/null +++ b/ArtNet/file/file_reader.py @@ -0,0 +1,45 @@ +import os + + +class FileReader: + + def __init__(self, root: str): + if not os.path.isdir(root): + raise Exception("Root was not a valid directory! {0}".format(root)) + self.__root = root + + def list_artists(self) -> list: + """ + List all Artists within the root folder + :return: + """ + l = [os.path.basename(x) for x in os.listdir(self.__root)] + if ".artnet" in l: + l.remove(".artnet") + return l + + def get_files(self, artist: str) -> list: + """ + List all files within the given artists directory with their relative path (to the root). + :param artist: + :return: + """ + l = [] + + root = os.path.join(self.__root) + dirs = [artist] + + while len(dirs) != 0: + curr_dir = dirs.pop() + curr_full_dir = os.path.join(root, curr_dir) + l += [os.path.join(curr_dir, f) for f in os.listdir(curr_full_dir) if os.path.isfile(os.path.join(curr_full_dir, f))] + for d in [x for x in os.listdir(curr_full_dir) if os.path.isdir(os.path.join(curr_full_dir, x))]: + dirs.append(os.path.join(curr_dir, d)) + + return l + + +if __name__ == "__main__": + fr = FileReader("/home/peery/Software_Projects/ArtNet/App/Fake_Other_Artists") + print(fr.list_artists()) + print(fr.get_files("AkuDrache")) diff --git a/ArtNet/gui/__init__.py b/ArtNet/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/__init__.py b/ArtNet/gui/dialogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/__init__.py b/ArtNet/gui/dialogs/artist_modify_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/artist_mod_dialog.py b/ArtNet/gui/dialogs/artist_modify_dialog/artist_mod_dialog.py new file mode 100644 index 0000000..0801798 --- /dev/null +++ b/ArtNet/gui/dialogs/artist_modify_dialog/artist_mod_dialog.py @@ -0,0 +1,57 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt + +from ArtNet.gui.dialogs.artist_modify_dialog.artist_modify_dialog import Ui_Artist_Mod_Dialog + + +class ArtistModDialog(QtWidgets.QDialog): + + def __init__(self, parent=None, create_artist: bool = False): + super().__init__(parent) + self.parent = parent + self.ui = Ui_Artist_Mod_Dialog() + self.ui.setupUi(self) + + self.data = {"id": None, + "description": None} + + if not create_artist: + self.set_available_artists(self.parent.parent.parent.get_all_artists()) + else: + self.ui.id_list = QtWidgets.QLineEdit(self) + self.ui.id_spinbox.setParent(None) + self.ui.id_spinbox.destroy() + self.ui.id_layout.addWidget(self.ui.id_list) + + self.ui.description_line.textChanged.connect(self.on_description_changed) + + def set_available_artists(self, artists: list): + if artists is None: + self.ui.id_spinbox.setParent(None) + self.ui.id_spinbox.destroy() + self.ui.id_label.setText("Artist ID (auto-generated)") + return + + self.ui.id_spinbox.setMinimum(0) + self.ui.id_spinbox.setMaximum(len(artists)) + self.ui.id_spinbox.valueChanged.connect(self.on_id_spinbox_changed) + + def on_id_spinbox_changed(self): + id = self.ui.id_spinbox.value() + artist = self.parent.parent.parent.get_artist(id) + if len(artist) != 0: + self.ui.description_line.setText(artist[0][1]) + else: + self.ui.description_line.setText("Warning! unknown Artist ID!") + + self.data["id"] = id + + def on_description_changed(self): + self.data["description"] = self.ui.description_line.text() + + def exec_(self): + result = super(ArtistModDialog, self).exec_() + if result == QtWidgets.QDialog.Rejected: + return None + + return self.data diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py new file mode 100644 index 0000000..ddc7de8 --- /dev/null +++ b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/artist_modify_dialog/artist_modify_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Artist_Mod_Dialog(object): + def setupUi(self, Artist_Mod_Dialog): + Artist_Mod_Dialog.setObjectName("Artist_Mod_Dialog") + Artist_Mod_Dialog.resize(311, 205) + self.verticalLayout = QtWidgets.QVBoxLayout(Artist_Mod_Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.dialog_frame = QtWidgets.QFrame(Artist_Mod_Dialog) + self.dialog_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.dialog_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.dialog_frame.setObjectName("dialog_frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.dialog_frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.dialog_topic = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_topic.sizePolicy().hasHeightForWidth()) + self.dialog_topic.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(11) + font.setBold(True) + font.setWeight(75) + self.dialog_topic.setFont(font) + self.dialog_topic.setAlignment(QtCore.Qt.AlignCenter) + self.dialog_topic.setObjectName("dialog_topic") + self.verticalLayout_2.addWidget(self.dialog_topic) + self.frame = QtWidgets.QFrame(self.dialog_frame) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.id_label = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.id_label.sizePolicy().hasHeightForWidth()) + self.id_label.setSizePolicy(sizePolicy) + self.id_label.setObjectName("id_label") + self.verticalLayout_5.addWidget(self.id_label) + self.id_layout = QtWidgets.QVBoxLayout() + self.id_layout.setObjectName("id_layout") + self.id_spinbox = QtWidgets.QDoubleSpinBox(self.frame) + self.id_spinbox.setDecimals(0) + self.id_spinbox.setMaximum(10000.0) + self.id_spinbox.setObjectName("id_spinbox") + self.id_layout.addWidget(self.id_spinbox) + self.verticalLayout_5.addLayout(self.id_layout) + self.description_label = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.description_label.sizePolicy().hasHeightForWidth()) + self.description_label.setSizePolicy(sizePolicy) + self.description_label.setObjectName("description_label") + self.verticalLayout_5.addWidget(self.description_label) + self.description_line = QtWidgets.QLineEdit(self.frame) + self.description_line.setObjectName("description_line") + self.verticalLayout_5.addWidget(self.description_line) + self.verticalLayout_2.addWidget(self.frame) + self.verticalLayout.addWidget(self.dialog_frame) + self.buttonBox = QtWidgets.QDialogButtonBox(Artist_Mod_Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Artist_Mod_Dialog) + self.buttonBox.accepted.connect(Artist_Mod_Dialog.accept) + self.buttonBox.rejected.connect(Artist_Mod_Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Artist_Mod_Dialog) + + def retranslateUi(self, Artist_Mod_Dialog): + _translate = QtCore.QCoreApplication.translate + Artist_Mod_Dialog.setWindowTitle(_translate("Artist_Mod_Dialog", "Dialog")) + self.dialog_topic.setText(_translate("Artist_Mod_Dialog", "Artist Topic")) + self.id_label.setText(_translate("Artist_Mod_Dialog", "Artist ID:")) + self.description_label.setText(_translate("Artist_Mod_Dialog", "Artist Description:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + Artist_Mod_Dialog = QtWidgets.QDialog() + ui = Ui_Artist_Mod_Dialog() + ui.setupUi(Artist_Mod_Dialog) + Artist_Mod_Dialog.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui new file mode 100644 index 0000000..85cf331 --- /dev/null +++ b/ArtNet/gui/dialogs/artist_modify_dialog/artist_modify_dialog.ui @@ -0,0 +1,154 @@ + + + Artist_Mod_Dialog + + + + 0 + 0 + 311 + 205 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 11 + 75 + true + + + + Artist Topic + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + Artist ID: + + + + + + + + + 0 + + + 10000.000000000000000 + + + + + + + + + + 0 + 0 + + + + Artist Description: + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Artist_Mod_Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Artist_Mod_Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/category_modify_dialog/category_mod_dialog.py b/ArtNet/gui/dialogs/category_modify_dialog/category_mod_dialog.py new file mode 100644 index 0000000..77f3a2b --- /dev/null +++ b/ArtNet/gui/dialogs/category_modify_dialog/category_mod_dialog.py @@ -0,0 +1,69 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QStandardItem, QStandardItemModel + +from ArtNet.gui.dialogs.category_modify_dialog.category_modify_dialog import Ui_category_modify_dialog + + +class CategoryModDialog(QtWidgets.QDialog): + + def __init__(self, parent=None, delete_category: bool = False): + super().__init__(parent) + self.parent = parent + + self.ui = Ui_category_modify_dialog() + self.ui.setupUi(self) + self.name_valid: bool = False + self.all_categories: list = None + + self.default_style = self.ui.category_name_line.styleSheet() + + self.delete_mode: bool = delete_category + if delete_category: + self.ui.dialog_topic.setText("Delete Category") + else: + self.ui.dialog_topic.setText("Create Category") + + self.ui.category_name_line.textChanged.connect(self.on_category_name_change) + self.set_all_categories() + + def set_all_categories(self): + """ + Populate the category list with all categories if name line is empty. + Otherwise fuzzy search for fitting categories + :return: + """ + self.all_categories = self.parent.get_categories(self.ui.category_name_line.text(), all_if_empty=True) + categories = [] + for i in range(len(self.all_categories)): + categories.append(self.all_categories[i]) + self.all_categories = categories + + item_model = QStandardItemModel(self.ui.other_categories_list) + + for category in self.all_categories: + item = QStandardItem(category) + item_model.appendRow(item) + + self.ui.other_categories_list.setModel(item_model) + + def on_category_name_change(self): + # TODO mark input red if it is a taken name (or if it isn't in deletion mode) + self.set_all_categories() + self.ui.category_name_line.setStyleSheet(self.default_style) + self.name_valid = True + + if self.delete_mode: + if self.ui.category_name_line.text() not in self.all_categories: + self.ui.category_name_line.setStyleSheet("color: red") + self.name_valid = False + else: + if self.ui.category_name_line.text() in self.all_categories: + self.ui.category_name_line.setStyleSheet("color: red") + self.name_valid = False + + def exec_(self) -> dict: + if super(CategoryModDialog, self).exec_() == QtWidgets.QDialog.Rejected or not self.name_valid: + return None + + return {"name": self.ui.category_name_line.text()} diff --git a/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.py b/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.py new file mode 100644 index 0000000..2bb7e8e --- /dev/null +++ b/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/category_modify_dialog/category_modify_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_category_modify_dialog(object): + def setupUi(self, category_modify_dialog): + category_modify_dialog.setObjectName("category_modify_dialog") + category_modify_dialog.resize(398, 298) + self.verticalLayout = QtWidgets.QVBoxLayout(category_modify_dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(category_modify_dialog) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.dialog_topic = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_topic.sizePolicy().hasHeightForWidth()) + self.dialog_topic.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(12) + font.setBold(True) + font.setWeight(75) + self.dialog_topic.setFont(font) + self.dialog_topic.setAlignment(QtCore.Qt.AlignCenter) + self.dialog_topic.setObjectName("dialog_topic") + self.verticalLayout_2.addWidget(self.dialog_topic) + self.frame_2 = QtWidgets.QFrame(self.frame) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.frame_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.category_name_label = QtWidgets.QLabel(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.category_name_label.sizePolicy().hasHeightForWidth()) + self.category_name_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.category_name_label.setFont(font) + self.category_name_label.setObjectName("category_name_label") + self.verticalLayout_3.addWidget(self.category_name_label) + self.category_name_line = QtWidgets.QLineEdit(self.frame_2) + self.category_name_line.setObjectName("category_name_line") + self.verticalLayout_3.addWidget(self.category_name_line) + self.other_categories_list = QtWidgets.QListView(self.frame_2) + self.other_categories_list.setObjectName("other_categories_list") + self.verticalLayout_3.addWidget(self.other_categories_list) + self.verticalLayout_2.addWidget(self.frame_2) + self.verticalLayout.addWidget(self.frame) + self.buttonBox = QtWidgets.QDialogButtonBox(category_modify_dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(category_modify_dialog) + self.buttonBox.accepted.connect(category_modify_dialog.accept) + self.buttonBox.rejected.connect(category_modify_dialog.reject) + QtCore.QMetaObject.connectSlotsByName(category_modify_dialog) + + def retranslateUi(self, category_modify_dialog): + _translate = QtCore.QCoreApplication.translate + category_modify_dialog.setWindowTitle(_translate("category_modify_dialog", "Dialog")) + self.dialog_topic.setText(_translate("category_modify_dialog", "Category Topic")) + self.category_name_label.setText(_translate("category_modify_dialog", "Category Name:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + category_modify_dialog = QtWidgets.QDialog() + ui = Ui_category_modify_dialog() + ui.setupUi(category_modify_dialog) + category_modify_dialog.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.ui b/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.ui new file mode 100644 index 0000000..34a447f --- /dev/null +++ b/ArtNet/gui/dialogs/category_modify_dialog/category_modify_dialog.ui @@ -0,0 +1,137 @@ + + + category_modify_dialog + + + + 0 + 0 + 398 + 298 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 12 + 75 + true + + + + Category Topic + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 10 + 75 + true + + + + Category Name: + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + category_modify_dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + category_modify_dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/db_connection_dialog/__init__.py b/ArtNet/gui/dialogs/db_connection_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.py b/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.py new file mode 100644 index 0000000..0e577ed --- /dev/null +++ b/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/db_connection_dialog/db_connection_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_DBConnection(object): + def setupUi(self, DBConnection): + DBConnection.setObjectName("DBConnection") + DBConnection.resize(328, 262) + self.verticalLayout = QtWidgets.QVBoxLayout(DBConnection) + self.verticalLayout.setObjectName("verticalLayout") + self.dialog_frame = QtWidgets.QFrame(DBConnection) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_frame.sizePolicy().hasHeightForWidth()) + self.dialog_frame.setSizePolicy(sizePolicy) + self.dialog_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.dialog_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.dialog_frame.setObjectName("dialog_frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.dialog_frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.title_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.title_label.sizePolicy().hasHeightForWidth()) + self.title_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.title_label.setFont(font) + self.title_label.setLayoutDirection(QtCore.Qt.LeftToRight) + self.title_label.setAlignment(QtCore.Qt.AlignCenter) + self.title_label.setObjectName("title_label") + self.verticalLayout_2.addWidget(self.title_label) + self.host_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.host_label.sizePolicy().hasHeightForWidth()) + self.host_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.host_label.setFont(font) + self.host_label.setObjectName("host_label") + self.verticalLayout_2.addWidget(self.host_label) + self.host_line_edit = QtWidgets.QLineEdit(self.dialog_frame) + self.host_line_edit.setObjectName("host_line_edit") + self.verticalLayout_2.addWidget(self.host_line_edit) + self.port_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.port_label.sizePolicy().hasHeightForWidth()) + self.port_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.port_label.setFont(font) + self.port_label.setObjectName("port_label") + self.verticalLayout_2.addWidget(self.port_label) + self.port_line_edit = QtWidgets.QLineEdit(self.dialog_frame) + self.port_line_edit.setObjectName("port_line_edit") + self.verticalLayout_2.addWidget(self.port_line_edit) + self.database_label = QtWidgets.QLabel(self.dialog_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.database_label.setFont(font) + self.database_label.setObjectName("database_label") + self.verticalLayout_2.addWidget(self.database_label) + self.database_line_edit = QtWidgets.QLineEdit(self.dialog_frame) + self.database_line_edit.setObjectName("database_line_edit") + self.verticalLayout_2.addWidget(self.database_line_edit) + self.user_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.user_label.sizePolicy().hasHeightForWidth()) + self.user_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.user_label.setFont(font) + self.user_label.setObjectName("user_label") + self.verticalLayout_2.addWidget(self.user_label) + self.user_line_edit = QtWidgets.QLineEdit(self.dialog_frame) + self.user_line_edit.setObjectName("user_line_edit") + self.verticalLayout_2.addWidget(self.user_line_edit) + self.password_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.password_label.sizePolicy().hasHeightForWidth()) + self.password_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.password_label.setFont(font) + self.password_label.setObjectName("password_label") + self.verticalLayout_2.addWidget(self.password_label) + self.password_line_edit = QtWidgets.QLineEdit(self.dialog_frame) + self.password_line_edit.setEchoMode(QtWidgets.QLineEdit.Password) + self.password_line_edit.setObjectName("password_line_edit") + self.verticalLayout_2.addWidget(self.password_line_edit) + self.verticalLayout.addWidget(self.dialog_frame) + self.buttonBox = QtWidgets.QDialogButtonBox(DBConnection) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(DBConnection) + self.buttonBox.accepted.connect(DBConnection.accept) + self.buttonBox.rejected.connect(DBConnection.reject) + QtCore.QMetaObject.connectSlotsByName(DBConnection) + + def retranslateUi(self, DBConnection): + _translate = QtCore.QCoreApplication.translate + DBConnection.setWindowTitle(_translate("DBConnection", "Dialog")) + self.title_label.setText(_translate("DBConnection", "Edit PostgreSQL Connection")) + self.host_label.setText(_translate("DBConnection", "Host:")) + self.port_label.setText(_translate("DBConnection", "Port:")) + self.database_label.setText(_translate("DBConnection", "Database:")) + self.user_label.setText(_translate("DBConnection", "User:")) + self.password_label.setText(_translate("DBConnection", "Password:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + DBConnection = QtWidgets.QDialog() + ui = Ui_DBConnection() + ui.setupUi(DBConnection) + DBConnection.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.ui b/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.ui new file mode 100644 index 0000000..0b41636 --- /dev/null +++ b/ArtNet/gui/dialogs/db_connection_dialog/db_connection_dialog.ui @@ -0,0 +1,216 @@ + + + DBConnection + + + + 0 + 0 + 328 + 262 + + + + Dialog + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 10 + 75 + true + + + + Qt::LeftToRight + + + Edit PostgreSQL Connection + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Host: + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Port: + + + + + + + + + + + 75 + true + + + + Database: + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + User: + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + DBConnection + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DBConnection + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/db_connection_dialog/db_dialog.py b/ArtNet/gui/dialogs/db_connection_dialog/db_dialog.py new file mode 100644 index 0000000..79ecde7 --- /dev/null +++ b/ArtNet/gui/dialogs/db_connection_dialog/db_dialog.py @@ -0,0 +1,31 @@ +from PyQt5 import QtWidgets + +from ArtNet.gui.dialogs.db_connection_dialog.db_connection_dialog import Ui_DBConnection + + +class DBDialog(QtWidgets.QDialog): + + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_DBConnection() + self.ui.setupUi(self) + self.data = {} + + self.ui.buttonBox.accepted.connect(self.getConnectionDetails) + + def getConnectionDetails(self): + self.data = dict() + + self.data["host"] = self.ui.host_line_edit.text() + self.data["port"] = self.ui.port_line_edit.text() + self.data["database"] = self.ui.database_line_edit.text() + self.data["user"] = self.ui.user_line_edit.text() + self.data["password"] = self.ui.password_line_edit.text() + + self.accept() + + def exec_(self) -> dict: + if super(DBDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + return self.data + diff --git a/ArtNet/gui/dialogs/presence_modify_dialog/__init__.py b/ArtNet/gui/dialogs/presence_modify_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/presence_modify_dialog/presence_mod_dialog.py b/ArtNet/gui/dialogs/presence_modify_dialog/presence_mod_dialog.py new file mode 100644 index 0000000..ecbd557 --- /dev/null +++ b/ArtNet/gui/dialogs/presence_modify_dialog/presence_mod_dialog.py @@ -0,0 +1,200 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QStandardItem, QStandardItemModel + +from ArtNet.gui.dialogs.presence_modify_dialog.presence_modify_dialog import Ui_PresenceModDialog +from ArtNet.gui.dialogs.artist_modify_dialog.artist_mod_dialog import ArtistModDialog + + +class PresenceModDialog(QtWidgets.QDialog): + + def __init__(self, parent=None, edit_presence: bool = False): + super().__init__(parent) + self.parent = parent + self.ui = Ui_PresenceModDialog() + self.ui.setupUi(self) + + self.curr_artist: tuple = None + self.__curr_searched_artists: list = [] + + self.data: dict = None + + if edit_presence: + self.ui.dialog_topic_label.setText("Edit Presence") + self.ui.presence_domain_line.setReadOnly(True) + self.ui.presence_name_line.setReadOnly(True) + else: + self.ui.dialog_topic_label.setText("Create Presence") + self.ui.presence_domain_line.setReadOnly(False) + self.ui.presence_name_line.setReadOnly(False) + + self.ui.artist_search_line.textChanged.connect(self.on_artist_search_changed) + + self.ui.create_artist_button.clicked.connect(self.on_create_artist_clicked) + self.ui.edit_artist_button.clicked.connect(self.on_edit_artist_clicked) + self.ui.delete_artist_button.clicked.connect(self.on_delete_artist_clicked) + + def set_presence_name(self, name: str): + self.ui.presence_name_line.setText(name) + + def set_presence_domain(self, name: str): + self.ui.presence_domain_line.setText(name) + + def on_create_artist_clicked(self): + print("Clicked Create artist!") + dialog = ArtistModDialog(self) + + data = dialog.exec_() + if data is None: + return + + self.parent.parent.create_artist(ID=data["id"], description=data["description"]) + + def on_edit_artist_clicked(self): + print("Clicked Edit artist!") + + def on_delete_artist_clicked(self): + print("Clicked Delete artist!") + + if self.curr_artist is None: + QtWidgets.QMessageBox.information(self, "No Artist Selected", "There is no artist selected to delete!") + return + + answer = QtWidgets.QMessageBox.question(self, "Delete this Artist?", + "Do you really want to delete the Artist \"{0}\"?" + .format(self.curr_artist[1])) + if answer == QtWidgets.QMessageBox.Yes: + pass # TODO check if presences are associated & warn + presences = self.parent.parent.get_artist_presences(self.curr_artist[0]) + + msg = "" + affected_art = 0 + for presence in presences: + msg += presence[0]+":"+presence[1] + "\n" + + arts = self.parent.parent.get_presences_art(presence[0], presence[1]) + affected_art += len(arts) + + answer = QtWidgets.QMessageBox.question(self, "Are you sure?", + ("Do you really wish to delete the Artist \"{0}\"?\n" + + "following Presences are associated with " + + "it and deleted also:\n\n" + + "{1}\n" + + "This will also remove this presence from following " + + "amount of art pieces:" + + "\n{2}\n") + .format(self.curr_artist[1], msg, affected_art) + ) + if answer == QtWidgets.QMessageBox.Yes: + pass + + self.parent.parent.remove_artist(self.curr_artist[0]) + for presence in presences: + self.parent.parent.remove_presence(presence[0], presence[1]) + self.close() + + else: + return + else: + return + + def on_artist_search_changed(self): + if len(self.ui.artist_search_line.text()) == 0: # nothing to search for + self.set_artist_result_list([]) + return + artists = self.parent.parent.get_artists(self.ui.artist_search_line.text()) + + result = [] + for ID, desc in artists: + result.append(str(ID)+":"+desc) + + self.set_artist_result_list(result) + + def set_artist_result_list(self, artists: list): + """ + Set the list of search result artists. + :param artists: [(id, desc)] + :return: + """ + item_model = QStandardItemModel(self.ui.artist_result_list) + + self.__curr_searched_artists = [] + for artist in artists: + self.__curr_searched_artists.append(artist) + + item = QStandardItem(artist) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if self.curr_artist is not None: + curr_artist = str(self.curr_artist[0]) + ":" + self.curr_artist[1] + if artist == curr_artist: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_search_artist_item_changed) + self.ui.artist_result_list.setModel(item_model) + + def set_artist_selected_list(self, artist: tuple): + """ + Set the list of selected artist + :param artists: [(id, desc)] + :return: + """ + if artist is None: + return + + item_model = QStandardItemModel(self.ui.artist_selection_list) + + item = QStandardItem(str(artist[0])+":"+artist[1]) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_selected_artist_item_changed) + self.ui.artist_selection_list.setModel(item_model) + + def on_search_artist_item_changed(self, item: QStandardItem): + artist = item.text().split(":") + artist = (artist[0], artist[1]) + if item.checkState() == Qt.Checked: + self.curr_artist = artist + elif item.checkState() == Qt.Unchecked: + self.curr_artist = None + self.set_artist_result_list(self.__curr_searched_artists) + self.set_artist_selected_list(self.curr_artist) + + def on_selected_artist_item_changed(self, item: QStandardItem): + artist = item.text().split(":") + artist = (artist[0].strip(), artist[1].strip()) + if item.checkState() == Qt.Checked: + self.curr_artist = artist + elif item.checkState() == Qt.Unchecked: + self.curr_artist = artist + self.set_artist_result_list(self.__curr_searched_artists) + self.set_artist_selected_list(self.curr_artist) + + def collect_presence_details(self): + self.data = { + "name": self.ui.presence_name_line.text(), + "domain": self.ui.presence_domain_line.text(), + "artist": self.curr_artist, + "link": self.ui.presence_link_list.text(), + } + + is_null = True + for value in self.data.values(): + if value is not None and len(value) != 0: + is_null = False + if is_null: + self.data = None + + def exec_(self) -> dict: + if super(PresenceModDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + self.collect_presence_details() + + return self.data diff --git a/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.py b/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.py new file mode 100644 index 0000000..460f460 --- /dev/null +++ b/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/presence_modify_dialog/presence_modify_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_PresenceModDialog(object): + def setupUi(self, PresenceModDialog): + PresenceModDialog.setObjectName("PresenceModDialog") + PresenceModDialog.resize(376, 405) + self.verticalLayout = QtWidgets.QVBoxLayout(PresenceModDialog) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(PresenceModDialog) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.dialog_topic_label = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_topic_label.sizePolicy().hasHeightForWidth()) + self.dialog_topic_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(11) + font.setBold(True) + font.setWeight(75) + self.dialog_topic_label.setFont(font) + self.dialog_topic_label.setAlignment(QtCore.Qt.AlignCenter) + self.dialog_topic_label.setObjectName("dialog_topic_label") + self.verticalLayout_2.addWidget(self.dialog_topic_label) + self.frame_2 = QtWidgets.QFrame(self.frame) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.frame_2) + self.verticalLayout_3.setSpacing(5) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.presence_name_label = QtWidgets.QLabel(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.presence_name_label.sizePolicy().hasHeightForWidth()) + self.presence_name_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.presence_name_label.setFont(font) + self.presence_name_label.setObjectName("presence_name_label") + self.verticalLayout_3.addWidget(self.presence_name_label) + self.presence_name_line = QtWidgets.QLineEdit(self.frame_2) + self.presence_name_line.setObjectName("presence_name_line") + self.verticalLayout_3.addWidget(self.presence_name_line) + self.presence_domain_label = QtWidgets.QLabel(self.frame_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.presence_domain_label.sizePolicy().hasHeightForWidth()) + self.presence_domain_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.presence_domain_label.setFont(font) + self.presence_domain_label.setObjectName("presence_domain_label") + self.verticalLayout_3.addWidget(self.presence_domain_label) + self.presence_domain_line = QtWidgets.QLineEdit(self.frame_2) + self.presence_domain_line.setObjectName("presence_domain_line") + self.verticalLayout_3.addWidget(self.presence_domain_line) + self.presence_link_label = QtWidgets.QLabel(self.frame_2) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.presence_link_label.setFont(font) + self.presence_link_label.setObjectName("presence_link_label") + self.verticalLayout_3.addWidget(self.presence_link_label) + self.presence_link_list = QtWidgets.QLineEdit(self.frame_2) + self.presence_link_list.setObjectName("presence_link_list") + self.verticalLayout_3.addWidget(self.presence_link_list) + self.artist_label = QtWidgets.QLabel(self.frame_2) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.artist_label.setFont(font) + self.artist_label.setObjectName("artist_label") + self.verticalLayout_3.addWidget(self.artist_label) + self.artist_search_line = QtWidgets.QLineEdit(self.frame_2) + self.artist_search_line.setObjectName("artist_search_line") + self.verticalLayout_3.addWidget(self.artist_search_line) + self.artist_result_list = QtWidgets.QListView(self.frame_2) + self.artist_result_list.setObjectName("artist_result_list") + self.verticalLayout_3.addWidget(self.artist_result_list) + self.artist_selection_list = QtWidgets.QListView(self.frame_2) + self.artist_selection_list.setObjectName("artist_selection_list") + self.verticalLayout_3.addWidget(self.artist_selection_list) + self.artist_edit_layout = QtWidgets.QHBoxLayout() + self.artist_edit_layout.setObjectName("artist_edit_layout") + self.create_artist_button = QtWidgets.QPushButton(self.frame_2) + self.create_artist_button.setObjectName("create_artist_button") + self.artist_edit_layout.addWidget(self.create_artist_button) + self.edit_artist_button = QtWidgets.QPushButton(self.frame_2) + self.edit_artist_button.setObjectName("edit_artist_button") + self.artist_edit_layout.addWidget(self.edit_artist_button) + self.delete_artist_button = QtWidgets.QPushButton(self.frame_2) + self.delete_artist_button.setObjectName("delete_artist_button") + self.artist_edit_layout.addWidget(self.delete_artist_button) + self.verticalLayout_3.addLayout(self.artist_edit_layout) + self.verticalLayout_2.addWidget(self.frame_2) + self.verticalLayout.addWidget(self.frame) + self.buttonBox = QtWidgets.QDialogButtonBox(PresenceModDialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(PresenceModDialog) + self.buttonBox.accepted.connect(PresenceModDialog.accept) + self.buttonBox.rejected.connect(PresenceModDialog.reject) + QtCore.QMetaObject.connectSlotsByName(PresenceModDialog) + + def retranslateUi(self, PresenceModDialog): + _translate = QtCore.QCoreApplication.translate + PresenceModDialog.setWindowTitle(_translate("PresenceModDialog", "Dialog")) + self.dialog_topic_label.setText(_translate("PresenceModDialog", "Presence Topic")) + self.presence_name_label.setText(_translate("PresenceModDialog", "Presence Name:")) + self.presence_domain_label.setText(_translate("PresenceModDialog", "Presence Domain:")) + self.presence_link_label.setText(_translate("PresenceModDialog", "Presence Link:")) + self.artist_label.setText(_translate("PresenceModDialog", "Artist:")) + self.create_artist_button.setText(_translate("PresenceModDialog", "Create Artist")) + self.edit_artist_button.setText(_translate("PresenceModDialog", "Edit Artist")) + self.delete_artist_button.setText(_translate("PresenceModDialog", "Delete Artist")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + PresenceModDialog = QtWidgets.QDialog() + ui = Ui_PresenceModDialog() + ui.setupUi(PresenceModDialog) + PresenceModDialog.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.ui b/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.ui new file mode 100644 index 0000000..d805953 --- /dev/null +++ b/ArtNet/gui/dialogs/presence_modify_dialog/presence_modify_dialog.ui @@ -0,0 +1,222 @@ + + + PresenceModDialog + + + + 0 + 0 + 376 + 405 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 11 + 75 + true + + + + Presence Topic + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 5 + + + + + + 0 + 0 + + + + + 75 + true + + + + Presence Name: + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Presence Domain: + + + + + + + + + + + 75 + true + + + + Presence Link: + + + + + + + + + + + 10 + 75 + true + + + + Artist: + + + + + + + + + + + + + + + + + + Create Artist + + + + + + + Edit Artist + + + + + + + Delete Artist + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + PresenceModDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PresenceModDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/presence_selection_dialog/__init__.py b/ArtNet/gui/dialogs/presence_selection_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/presence_selection_dialog/presence_select_dialog.py b/ArtNet/gui/dialogs/presence_selection_dialog/presence_select_dialog.py new file mode 100644 index 0000000..8910cdb --- /dev/null +++ b/ArtNet/gui/dialogs/presence_selection_dialog/presence_select_dialog.py @@ -0,0 +1,70 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QStandardItem, QStandardItemModel + +from ArtNet.gui.dialogs.presence_selection_dialog.presence_selection_dialog import Ui_presence_selection_dialog + + +class PresenceSelectDialog(QtWidgets.QDialog): + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.ui = Ui_presence_selection_dialog() + self.ui.setupUi(self) + + self.__curr_presence: list = None + self.__curr_search_result: list = None + + self.ui.presence_name_search_line.textChanged.connect(self.on_search_change) + self.ui.presence_domain_search_line.textChanged.connect(self.on_search_change) + + def set_search_result_list(self, result: list): + self.__curr_search_result = result + item_model = QStandardItemModel(self.ui.presence_search_list) + + for presence in result: + item = QStandardItem(presence) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if self.__curr_presence is not None: + curr_presence = self.__curr_presence[0] + ":" + self.__curr_presence[1] + if presence == curr_presence: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_search_presence_item_changed) + self.ui.presence_search_list.setModel(item_model) + + def on_search_change(self): + name = self.ui.presence_name_search_line.text() + domain = self.ui.presence_domain_search_line.text() + + if len(name) == 0 and len(domain) == 0: # nothing to search for + self.set_search_result_list([]) + return + presences = self.parent.parent.get_authors(name, domain) + + result = [] + for ID, desc in presences: + result.append(ID+":"+desc) + + self.set_search_result_list(result) + + def on_search_presence_item_changed(self, item: QStandardItem): + name, domain = item.text().split(":") + if item.checkState() == Qt.Checked: + self.__curr_presence = self.parent.parent.get_presence(name, domain) + if self.__curr_presence is not None: + self.__curr_presence = self.__curr_presence[0] # unpack from list + elif item.checkState() == Qt.Unchecked: + self.__curr_presence = None + self.set_search_result_list(self.__curr_search_result) + + def exec_(self) -> list: + if super(PresenceSelectDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + + return self.__curr_presence diff --git a/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.py b/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.py new file mode 100644 index 0000000..7bf2683 --- /dev/null +++ b/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/presence_selection_dialog/presence_selection_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_presence_selection_dialog(object): + def setupUi(self, presence_selection_dialog): + presence_selection_dialog.setObjectName("presence_selection_dialog") + presence_selection_dialog.resize(330, 260) + self.verticalLayout = QtWidgets.QVBoxLayout(presence_selection_dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(presence_selection_dialog) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.label = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setPointSize(11) + font.setBold(True) + font.setWeight(75) + self.label.setFont(font) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setObjectName("label") + self.verticalLayout_2.addWidget(self.label) + self.frame_2 = QtWidgets.QFrame(self.frame) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_2) + self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_4 = QtWidgets.QVBoxLayout() + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.label_2 = QtWidgets.QLabel(self.frame_2) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.label_2.setFont(font) + self.label_2.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.label_2.setObjectName("label_2") + self.verticalLayout_4.addWidget(self.label_2) + self.presence_name_search_line = QtWidgets.QLineEdit(self.frame_2) + self.presence_name_search_line.setObjectName("presence_name_search_line") + self.verticalLayout_4.addWidget(self.presence_name_search_line) + self.horizontalLayout.addLayout(self.verticalLayout_4) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.label_3 = QtWidgets.QLabel(self.frame_2) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.verticalLayout_3.addWidget(self.label_3) + self.presence_domain_search_line = QtWidgets.QLineEdit(self.frame_2) + self.presence_domain_search_line.setObjectName("presence_domain_search_line") + self.verticalLayout_3.addWidget(self.presence_domain_search_line) + self.horizontalLayout.addLayout(self.verticalLayout_3) + self.verticalLayout_2.addWidget(self.frame_2) + self.presence_search_list = QtWidgets.QListView(self.frame) + self.presence_search_list.setObjectName("presence_search_list") + self.verticalLayout_2.addWidget(self.presence_search_list) + self.verticalLayout.addWidget(self.frame) + self.buttonBox = QtWidgets.QDialogButtonBox(presence_selection_dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(presence_selection_dialog) + self.buttonBox.accepted.connect(presence_selection_dialog.accept) + self.buttonBox.rejected.connect(presence_selection_dialog.reject) + QtCore.QMetaObject.connectSlotsByName(presence_selection_dialog) + + def retranslateUi(self, presence_selection_dialog): + _translate = QtCore.QCoreApplication.translate + presence_selection_dialog.setWindowTitle(_translate("presence_selection_dialog", "Dialog")) + self.label.setText(_translate("presence_selection_dialog", "Select Presence")) + self.label_2.setText(_translate("presence_selection_dialog", "Name:")) + self.label_3.setText(_translate("presence_selection_dialog", "Domain:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + presence_selection_dialog = QtWidgets.QDialog() + ui = Ui_presence_selection_dialog() + ui.setupUi(presence_selection_dialog) + presence_selection_dialog.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.ui b/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.ui new file mode 100644 index 0000000..c4f847a --- /dev/null +++ b/ArtNet/gui/dialogs/presence_selection_dialog/presence_selection_dialog.ui @@ -0,0 +1,153 @@ + + + presence_selection_dialog + + + + 0 + 0 + 330 + 260 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 11 + 75 + true + + + + Select Presence + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 10 + 75 + true + + + + Name: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + + + + + + 10 + 75 + true + + + + Domain: + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + presence_selection_dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + presence_selection_dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/tag_import_dialog/tag_imp_dialog.py b/ArtNet/gui/dialogs/tag_import_dialog/tag_imp_dialog.py new file mode 100644 index 0000000..bab2e06 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_import_dialog/tag_imp_dialog.py @@ -0,0 +1,122 @@ +from PyQt5 import QtWidgets +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtCore import Qt + +from ArtNet.gui.dialogs.tag_import_dialog.tag_import_dialog import Ui_Dialog + + +class TagImportDialog(QtWidgets.QDialog): + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.parent = parent + self.ui = Ui_Dialog() + self.ui.setupUi(self) + self.ui.import_all_button.clicked.connect(self.on_import_all_clicked) + self.ui.remove_all_button.clicked.connect(self.on_remove_all_clicked) + + self.to_import = [] + self.not_import = [] + + self.data = dict() + + def set_import_tag_list(self, tags: list): + """ + Set the tags in the imported list + :param tags: + :return: + """ + set_checked = True + item_model = QStandardItemModel(self.ui.import_list) + self.data = tags + + for tag in tags: + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_import_item_changed) + self.ui.import_list.setModel(item_model) + + def set_detected_artists(self, artists: list): + """ + Set the label for detected artists + :param artists: + :return: + """ + s = "" + for artist in artists: + s += artist + " | " + s = s[:-3] + + self.ui.detected_artists_label.setText(s) + + def set_used_link(self, link: str): + """ + Set the label to display the predicted link + :param link: + :return: + """ + self.ui.used_link_label.setText(link) + + def set_ignore_tag_list(self, tags: list): + """ + Set the tags in the ignore list + :param tags: + :return: + """ + set_checked = False + item_model = QStandardItemModel(self.ui.ignored_list) + + for tag in tags: + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_ignore_item_changed) + self.ui.ignored_list.setModel(item_model) + + def on_ignore_item_changed(self, item: QStandardItem): + if item.checkState() == Qt.Checked: + tag = item.text() + self.to_import.append(tag) + self.not_import.remove(tag) + + self.set_import_tag_list(self.to_import) + self.set_ignore_tag_list(self.not_import) + + def on_import_item_changed(self, item: QStandardItem): + if item.checkState() == Qt.Unchecked: + tag = item.text() + self.to_import.remove(tag) + self.not_import.append(tag) + + self.set_import_tag_list(self.to_import) + self.set_ignore_tag_list(self.not_import) + + def on_import_all_clicked(self): + self.to_import += self.not_import + self.not_import = [] + + self.set_import_tag_list(self.to_import) + self.set_ignore_tag_list(self.not_import) + + def on_remove_all_clicked(self): + self.not_import += self.to_import + self.to_import = [] + + self.set_import_tag_list(self.to_import) + self.set_ignore_tag_list(self.not_import) + + def exec_(self) -> int: + if super(TagImportDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + return self.data diff --git a/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.py b/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.py new file mode 100644 index 0000000..6798c28 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/tag_import_dialog/tag_import_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(775, 300) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.label = QtWidgets.QLabel(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(12) + font.setBold(True) + font.setWeight(75) + self.label.setFont(font) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.frame_4 = QtWidgets.QFrame(Dialog) + self.frame_4.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_4.setObjectName("frame_4") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_4) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.label_4 = QtWidgets.QLabel(self.frame_4) + font = QtGui.QFont() + font.setPointSize(9) + font.setBold(True) + font.setWeight(75) + self.label_4.setFont(font) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.verticalLayout_4.addWidget(self.label_4) + self.detected_artists_label = QtWidgets.QLabel(self.frame_4) + font = QtGui.QFont() + font.setPointSize(8) + self.detected_artists_label.setFont(font) + self.detected_artists_label.setAlignment(QtCore.Qt.AlignCenter) + self.detected_artists_label.setObjectName("detected_artists_label") + self.verticalLayout_4.addWidget(self.detected_artists_label) + self.used_link_label = QtWidgets.QLabel(self.frame_4) + self.used_link_label.setAlignment(QtCore.Qt.AlignCenter) + self.used_link_label.setObjectName("used_link_label") + self.verticalLayout_4.addWidget(self.used_link_label) + self.verticalLayout.addWidget(self.frame_4) + self.frame = QtWidgets.QFrame(Dialog) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame) + self.horizontalLayout.setObjectName("horizontalLayout") + self.frame_2 = QtWidgets.QFrame(self.frame) + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.frame_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.label_2 = QtWidgets.QLabel(self.frame_2) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.label_2.setFont(font) + self.label_2.setObjectName("label_2") + self.verticalLayout_3.addWidget(self.label_2) + self.import_list = QtWidgets.QListView(self.frame_2) + self.import_list.setMinimumSize(QtCore.QSize(300, 0)) + self.import_list.setObjectName("import_list") + self.verticalLayout_3.addWidget(self.import_list) + self.remove_all_button = QtWidgets.QPushButton(self.frame_2) + self.remove_all_button.setObjectName("remove_all_button") + self.verticalLayout_3.addWidget(self.remove_all_button) + self.horizontalLayout.addWidget(self.frame_2) + self.frame_3 = QtWidgets.QFrame(self.frame) + self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_3.setObjectName("frame_3") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame_3) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.label_3 = QtWidgets.QLabel(self.frame_3) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.verticalLayout_2.addWidget(self.label_3) + self.ignored_list = QtWidgets.QListView(self.frame_3) + self.ignored_list.setMinimumSize(QtCore.QSize(300, 0)) + self.ignored_list.setObjectName("ignored_list") + self.verticalLayout_2.addWidget(self.ignored_list) + self.import_all_button = QtWidgets.QPushButton(self.frame_3) + self.import_all_button.setObjectName("import_all_button") + self.verticalLayout_2.addWidget(self.import_all_button) + self.horizontalLayout.addWidget(self.frame_3) + self.verticalLayout.addWidget(self.frame) + self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "Detected Tags")) + self.label_4.setText(_translate("Dialog", "Detected Artists")) + self.detected_artists_label.setText(_translate("Dialog", "(None)")) + self.used_link_label.setText(_translate("Dialog", "-No Link Used-")) + self.label_2.setText(_translate("Dialog", "To be Imported:")) + self.remove_all_button.setText(_translate("Dialog", "Remove All")) + self.label_3.setText(_translate("Dialog", "To Be Ignored:")) + self.import_all_button.setText(_translate("Dialog", "Import All")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + Dialog = QtWidgets.QDialog() + ui = Ui_Dialog() + ui.setupUi(Dialog) + Dialog.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.ui b/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.ui new file mode 100644 index 0000000..ae00ada --- /dev/null +++ b/ArtNet/gui/dialogs/tag_import_dialog/tag_import_dialog.ui @@ -0,0 +1,239 @@ + + + Dialog + + + + 0 + 0 + 775 + 300 + + + + Dialog + + + + + + + 0 + 0 + + + + + 12 + 75 + true + + + + Detected Tags + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 9 + 75 + true + + + + Detected Artists + + + Qt::AlignCenter + + + + + + + + 8 + + + + (None) + + + Qt::AlignCenter + + + + + + + -No Link Used- + + + Qt::AlignCenter + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 10 + 75 + true + + + + To be Imported: + + + + + + + + 300 + 0 + + + + + + + + Remove All + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 10 + 75 + true + + + + To Be Ignored: + + + + + + + + 300 + 0 + + + + + + + + Import All + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/tag_modify_dialog/__init__.py b/ArtNet/gui/dialogs/tag_modify_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/tag_modify_dialog/tag_mod_dialog.py b/ArtNet/gui/dialogs/tag_modify_dialog/tag_mod_dialog.py new file mode 100644 index 0000000..a315293 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_modify_dialog/tag_mod_dialog.py @@ -0,0 +1,285 @@ +from PyQt5 import QtWidgets +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtCore import Qt + +from ArtNet.gui.dialogs.tag_modify_dialog.tag_modify_dialog import Ui_TagModify + + +class TagModifyDialog(QtWidgets.QDialog): + + def __init__(self, parent=None, create_tag: bool = True): + super().__init__(parent) + self.parent = parent + self.ui = Ui_TagModify() + self.ui.setupUi(self) + + self.data = dict() + self.original_tag_name: str = None + self.create_tag = create_tag + + self.alias_selection: list = None + self.implication_selection: list = None + self.tag_alias_search_result: list = None + self.tag_implication_search_result: list = None + self.category_selection: str = None + + if create_tag: + self.ui.dialog_topic.setText("Create Tag") + else: + self.ui.dialog_topic.setText("Edit Tag") + self.ui.tag_name_line.setReadOnly(False) + + self.ui.buttonBox.accepted.connect(self.getTagDetails) + + self.ui.tag_alias_search_line.textChanged.connect(self.on_tag_alias_search_line_changed) + self.ui.tag_implication_search_line.textChanged.connect(self.on_tag_implication_search_line_changed) + self.set_all_categories() + + def set_search_alias_tags(self, aliases: list, set_checked: bool = False): + """ + Set the tags in the search result list to tags + :param aliases: + :param set_checked: + :return: + """ + if aliases is None: + return + item_model = QStandardItemModel(self.ui.tag_alias_search_list) + + for tag in aliases: + tag = tag[0].strip() + if tag == self.ui.tag_name_line.text(): + continue + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_search_alias_tag_item_changed) + self.ui.tag_alias_search_list.setModel(item_model) + + def set_search_implicated_tags(self, implications: list, set_checked: bool=False): + """ + Set the tags in the search result list to tags + :param implications: + :param set_checked: + :return: + """ + item_model = QStandardItemModel(self.ui.tag_implication_search_list) + + for tag in implications: + tag = tag[0].strip() + if tag == self.ui.tag_name_line.text(): + continue + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_search_implicated_tag_item_changed) + self.ui.tag_implication_search_list.setModel(item_model) + + def set_all_categories(self): + """ + Populate the categories list and only check the one selection + :return: + """ + categories = self.parent.get_categories("", all_if_empty=True) + item_model = QStandardItemModel(self.ui.category_list) + + for category in categories: + category = category.strip() + + item = QStandardItem(category) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if category == self.category_selection: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_category_item_changed) + self.ui.category_list.setModel(item_model) + + def set_selected_alias_tags(self, aliases: list, set_checked: bool=False): + """ + Set the tags in the search result list to tags + :param aliases: + :param set_checked: + :return: + """ + item_model = QStandardItemModel(self.ui.tag_alias_selection_list) + + for tag in aliases: + tag = tag.strip() + if tag == self.ui.tag_name_line.text(): + continue + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_selected_alias_tag_item_changed) + self.ui.tag_alias_selection_list.setModel(item_model) + + def set_selected_implicated_tags(self, implications: list, set_checked: bool=False): + """ + Set the tags in the search result list to tags + :param implications: + :param set_checked: + :return: + """ + item_model = QStandardItemModel(self.ui.tag_implication_search_list) + + for tag in implications: + tag = tag.strip() + if tag == self.ui.tag_name_line.text(): + continue + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_selected_implicated_tag_item_changed) + self.ui.tag_implication_selection_list.setModel(item_model) + + def on_tag_alias_search_line_changed(self): + search_text = self.ui.tag_alias_search_line.text() + if len(search_text) == 0: + tags = [] + else: + tags = self.parent.get_tag_search_result(search_text) + + if tags is None: + return + + self.tag_alias_search_result = tags + self.set_search_alias_tags(tags) + + def on_tag_implication_search_line_changed(self): + search_text = self.ui.tag_implication_search_line.text() + if len(search_text) == 0: + tags = [] + else: + tags = self.parent.get_tag_search_result(search_text) + + if tags is None: + return + self.tag_implication_search_result = tags + self.set_search_implicated_tags(tags) + + def on_search_implicated_tag_item_changed(self, item: QStandardItem): + if item.checkState() == Qt.Checked: + if self.implication_selection is None: + self.implication_selection = [] + self.implication_selection.append(item.text()) + elif item.checkState() == Qt.Unchecked: + if item.text() in self.implication_selection: + self.implication_selection.remove(item.text()) + + self.set_selected_implicated_tags(self.implication_selection, set_checked=True) + + def on_search_alias_tag_item_changed(self, item: QStandardItem): + if item.checkState() == Qt.Checked: + if self.alias_selection is None: + self.alias_selection = [] + self.alias_selection.append(item.text()) + elif item.checkState() == Qt.Unchecked: + if item.text() in self.alias_selection: + self.alias_selection.remove(item.text()) + + self.set_selected_alias_tags(self.alias_selection, set_checked=True) + + def on_selected_implicated_tag_item_changed(self, item: QStandardItem): + if self.implication_selection is None: + self.implication_selection = [] + self.implication_selection.remove(item.text()) + self.set_selected_implicated_tags(self.implication_selection, set_checked=True) + + if self.tag_implication_search_result is not None: + self.set_search_implicated_tags(self.tag_implication_search_result) + + def on_selected_alias_tag_item_changed(self, item: QStandardItem): + if self.alias_selection is None: + self.alias_selection = [] + self.alias_selection.remove(item.text()) + self.set_selected_alias_tags(self.alias_selection, set_checked=True) + + self.set_search_alias_tags(self.tag_alias_search_result) + + def on_category_item_changed(self, item: QStandardItem): + if self.category_selection is None: + self.category_selection = [] + + if item.checkState() == Qt.Checked: + self.category_selection = item.text() + elif item.checkState() == Qt.Unchecked: + self.category_selection = None + + self.set_all_categories() + + def has_critical_info(self) -> bool: + """ + Checks if all required fields are filled + :return: + """ + ok = True + if len(self.ui.tag_name_line.text()) == 0: + ok = False + if self.category_selection is None: + ok = False + + if not ok: + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Missing Fields") + msg.setIcon(QtWidgets.QMessageBox.Critical) + msg.setInformativeText( + "Either the tag name or the category selection is empty! Can't save the tag this way!") + + msg.exec_() + return ok + + def getTagDetails(self): + self.data = dict() + + self.data["name"] = self.ui.tag_name_line.text() + self.data["description"] = self.ui.tag_description_area.toPlainText() + + self.data["aliases"] = self.alias_selection + self.data["implications"] = self.implication_selection + + self.data["category"] = self.category_selection + + if not self.has_critical_info(): + self.data = None + self.reject() + return + + self.accept() + + def exec_(self) -> dict: + self.original_tag_name = self.ui.tag_name_line.text() + + if super(TagModifyDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + + if not self.create_tag: + if self.data["name"] != self.original_tag_name: + self.data["old_tag_name"] = self.original_tag_name + + return self.data diff --git a/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.py b/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.py new file mode 100644 index 0000000..53ad4e0 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/tag_modify_dialog/tag_modify_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_TagModify(object): + def setupUi(self, TagModify): + TagModify.setObjectName("TagModify") + TagModify.resize(587, 846) + self.verticalLayout = QtWidgets.QVBoxLayout(TagModify) + self.verticalLayout.setObjectName("verticalLayout") + self.dialog_frame = QtWidgets.QFrame(TagModify) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_frame.sizePolicy().hasHeightForWidth()) + self.dialog_frame.setSizePolicy(sizePolicy) + self.dialog_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.dialog_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.dialog_frame.setObjectName("dialog_frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.dialog_frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.dialog_topic = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dialog_topic.sizePolicy().hasHeightForWidth()) + self.dialog_topic.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.dialog_topic.setFont(font) + self.dialog_topic.setAlignment(QtCore.Qt.AlignCenter) + self.dialog_topic.setObjectName("dialog_topic") + self.verticalLayout_2.addWidget(self.dialog_topic) + self.tag_name_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_name_label.sizePolicy().hasHeightForWidth()) + self.tag_name_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.tag_name_label.setFont(font) + self.tag_name_label.setObjectName("tag_name_label") + self.verticalLayout_2.addWidget(self.tag_name_label) + self.tag_name_line = QtWidgets.QLineEdit(self.dialog_frame) + self.tag_name_line.setObjectName("tag_name_line") + self.verticalLayout_2.addWidget(self.tag_name_line) + self.tag_description_label = QtWidgets.QLabel(self.dialog_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_description_label.sizePolicy().hasHeightForWidth()) + self.tag_description_label.setSizePolicy(sizePolicy) + self.tag_description_label.setObjectName("tag_description_label") + self.verticalLayout_2.addWidget(self.tag_description_label) + self.tag_description_area = QtWidgets.QTextEdit(self.dialog_frame) + self.tag_description_area.setMaximumSize(QtCore.QSize(16777215, 100)) + self.tag_description_area.setObjectName("tag_description_area") + self.verticalLayout_2.addWidget(self.tag_description_area) + self.category_label = QtWidgets.QLabel(self.dialog_frame) + self.category_label.setObjectName("category_label") + self.verticalLayout_2.addWidget(self.category_label) + self.category_list = QtWidgets.QListView(self.dialog_frame) + self.category_list.setMaximumSize(QtCore.QSize(16777215, 100)) + self.category_list.setObjectName("category_list") + self.verticalLayout_2.addWidget(self.category_list) + self.tag_alias_frame = QtWidgets.QFrame(self.dialog_frame) + self.tag_alias_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_alias_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_alias_frame.setObjectName("tag_alias_frame") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tag_alias_frame) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.tag_alias_label = QtWidgets.QLabel(self.tag_alias_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.tag_alias_label.setFont(font) + self.tag_alias_label.setAlignment(QtCore.Qt.AlignCenter) + self.tag_alias_label.setObjectName("tag_alias_label") + self.verticalLayout_3.addWidget(self.tag_alias_label) + self.tag_alias_search_line = QtWidgets.QLineEdit(self.tag_alias_frame) + self.tag_alias_search_line.setObjectName("tag_alias_search_line") + self.verticalLayout_3.addWidget(self.tag_alias_search_line) + self.tag_alias_label_frame = QtWidgets.QFrame(self.tag_alias_frame) + self.tag_alias_label_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_alias_label_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_alias_label_frame.setObjectName("tag_alias_label_frame") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.tag_alias_label_frame) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.tag_alias_search_label = QtWidgets.QLabel(self.tag_alias_label_frame) + self.tag_alias_search_label.setObjectName("tag_alias_search_label") + self.horizontalLayout_2.addWidget(self.tag_alias_search_label) + self.tag_alias_selection_label = QtWidgets.QLabel(self.tag_alias_label_frame) + self.tag_alias_selection_label.setObjectName("tag_alias_selection_label") + self.horizontalLayout_2.addWidget(self.tag_alias_selection_label) + self.verticalLayout_3.addWidget(self.tag_alias_label_frame) + self.tag_alias_list_frame = QtWidgets.QFrame(self.tag_alias_frame) + self.tag_alias_list_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_alias_list_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_alias_list_frame.setObjectName("tag_alias_list_frame") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.tag_alias_list_frame) + self.horizontalLayout.setObjectName("horizontalLayout") + self.tag_alias_search_list = QtWidgets.QListView(self.tag_alias_list_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_alias_search_list.sizePolicy().hasHeightForWidth()) + self.tag_alias_search_list.setSizePolicy(sizePolicy) + self.tag_alias_search_list.setMaximumSize(QtCore.QSize(16777215, 100)) + self.tag_alias_search_list.setObjectName("tag_alias_search_list") + self.horizontalLayout.addWidget(self.tag_alias_search_list) + self.tag_alias_selection_list = QtWidgets.QListView(self.tag_alias_list_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_alias_selection_list.sizePolicy().hasHeightForWidth()) + self.tag_alias_selection_list.setSizePolicy(sizePolicy) + self.tag_alias_selection_list.setMaximumSize(QtCore.QSize(16777215, 100)) + self.tag_alias_selection_list.setObjectName("tag_alias_selection_list") + self.horizontalLayout.addWidget(self.tag_alias_selection_list) + self.verticalLayout_3.addWidget(self.tag_alias_list_frame) + self.verticalLayout_2.addWidget(self.tag_alias_frame) + self.tag_implication_frame = QtWidgets.QFrame(self.dialog_frame) + self.tag_implication_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_implication_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_implication_frame.setObjectName("tag_implication_frame") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tag_implication_frame) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.tag_implication_label = QtWidgets.QLabel(self.tag_implication_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.tag_implication_label.setFont(font) + self.tag_implication_label.setAlignment(QtCore.Qt.AlignCenter) + self.tag_implication_label.setObjectName("tag_implication_label") + self.verticalLayout_4.addWidget(self.tag_implication_label) + self.tag_implication_search_line = QtWidgets.QLineEdit(self.tag_implication_frame) + self.tag_implication_search_line.setObjectName("tag_implication_search_line") + self.verticalLayout_4.addWidget(self.tag_implication_search_line) + self.tag_implication_label_frame = QtWidgets.QFrame(self.tag_implication_frame) + self.tag_implication_label_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_implication_label_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_implication_label_frame.setObjectName("tag_implication_label_frame") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.tag_implication_label_frame) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.tag_implication_search_label = QtWidgets.QLabel(self.tag_implication_label_frame) + self.tag_implication_search_label.setObjectName("tag_implication_search_label") + self.horizontalLayout_3.addWidget(self.tag_implication_search_label) + self.tag_implication_selection_label = QtWidgets.QLabel(self.tag_implication_label_frame) + self.tag_implication_selection_label.setObjectName("tag_implication_selection_label") + self.horizontalLayout_3.addWidget(self.tag_implication_selection_label) + self.verticalLayout_4.addWidget(self.tag_implication_label_frame) + self.tag_implication_list_frame = QtWidgets.QFrame(self.tag_implication_frame) + self.tag_implication_list_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.tag_implication_list_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.tag_implication_list_frame.setObjectName("tag_implication_list_frame") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.tag_implication_list_frame) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.tag_implication_search_list = QtWidgets.QListView(self.tag_implication_list_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_implication_search_list.sizePolicy().hasHeightForWidth()) + self.tag_implication_search_list.setSizePolicy(sizePolicy) + self.tag_implication_search_list.setMaximumSize(QtCore.QSize(16777215, 100)) + self.tag_implication_search_list.setObjectName("tag_implication_search_list") + self.horizontalLayout_4.addWidget(self.tag_implication_search_list) + self.tag_implication_selection_list = QtWidgets.QListView(self.tag_implication_list_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_implication_selection_list.sizePolicy().hasHeightForWidth()) + self.tag_implication_selection_list.setSizePolicy(sizePolicy) + self.tag_implication_selection_list.setMinimumSize(QtCore.QSize(0, 0)) + self.tag_implication_selection_list.setMaximumSize(QtCore.QSize(16777215, 100)) + self.tag_implication_selection_list.setObjectName("tag_implication_selection_list") + self.horizontalLayout_4.addWidget(self.tag_implication_selection_list) + self.verticalLayout_4.addWidget(self.tag_implication_list_frame) + self.verticalLayout_2.addWidget(self.tag_implication_frame) + self.verticalLayout.addWidget(self.dialog_frame) + self.buttonBox = QtWidgets.QDialogButtonBox(TagModify) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(TagModify) + self.buttonBox.accepted.connect(TagModify.accept) + self.buttonBox.rejected.connect(TagModify.reject) + QtCore.QMetaObject.connectSlotsByName(TagModify) + + def retranslateUi(self, TagModify): + _translate = QtCore.QCoreApplication.translate + TagModify.setWindowTitle(_translate("TagModify", "Dialog")) + self.dialog_topic.setText(_translate("TagModify", "Tag Topic")) + self.tag_name_label.setText(_translate("TagModify", "Tag Name:")) + self.tag_description_label.setText(_translate("TagModify", "Description:")) + self.category_label.setText(_translate("TagModify", "Category:")) + self.tag_alias_label.setText(_translate("TagModify", "Tag Aliases")) + self.tag_alias_search_label.setText(_translate("TagModify", "Search Result:")) + self.tag_alias_selection_label.setText(_translate("TagModify", "Selection:")) + self.tag_implication_label.setText(_translate("TagModify", "Tag Implications")) + self.tag_implication_search_label.setText(_translate("TagModify", "Search Result:")) + self.tag_implication_selection_label.setText(_translate("TagModify", "Selection:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + TagModify = QtWidgets.QDialog() + ui = Ui_TagModify() + ui.setupUi(TagModify) + TagModify.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.ui b/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.ui new file mode 100644 index 0000000..cb57cb1 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_modify_dialog/tag_modify_dialog.ui @@ -0,0 +1,375 @@ + + + TagModify + + + + 0 + 0 + 587 + 846 + + + + Dialog + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 10 + 75 + true + + + + Tag Topic + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Tag Name: + + + + + + + + + + + 0 + 0 + + + + Description: + + + + + + + + 16777215 + 100 + + + + + + + + Category: + + + + + + + + 16777215 + 100 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Tag Aliases + + + Qt::AlignCenter + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Search Result: + + + + + + + Selection: + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Tag Implications + + + Qt::AlignCenter + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Search Result: + + + + + + + Selection: + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TagModify + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TagModify + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dialogs/tag_select_dialog/__init__.py b/ArtNet/gui/dialogs/tag_select_dialog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dialogs/tag_select_dialog/tag_select_dialog.py b/ArtNet/gui/dialogs/tag_select_dialog/tag_select_dialog.py new file mode 100644 index 0000000..a762aad --- /dev/null +++ b/ArtNet/gui/dialogs/tag_select_dialog/tag_select_dialog.py @@ -0,0 +1,77 @@ +from PyQt5 import QtWidgets +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtCore import Qt + +from ArtNet.gui.dialogs.tag_select_dialog.tag_selection_dialog import Ui_TagSelection + + +class TagSelectDialog(QtWidgets.QDialog): + + def __init__(self, parent=None, delete_tag: bool=False): + super().__init__(parent) + self.parent = parent + self.ui = Ui_TagSelection() + self.ui.setupUi(self) + + if delete_tag: + self.ui.select_tag_label.setText("Select Tag to delete") + + self.selected_item: QStandardItem = None + self.selection: str = None + + self.ui.buttonBox.accepted.connect(self.getTagSelection) + self.ui.tag_search_line.textChanged.connect(self.getSearchResult) + self.getSearchResult() + + def getSearchResult(self): + search_text = self.ui.tag_search_line.text() + tags = self.parent.get_tag_search_result(search_text) + + if tags is None: + return + self.set_search_result_list(tags) + + def getTagSelection(self) -> str: + if self.selected_item is None: + return + self.selection = self.selected_item.text() + + def on_tag_item_changed(self, item: QStandardItem): + if self.selected_item is not None: + self.selected_item.setCheckState(Qt.Unchecked) + self.selected_item = item + + def set_search_result_list(self, tags: list): + """ + Set the tags in the search result list to tags + :param tags: + :return: + """ + item_model = QStandardItemModel(self.ui.search_result_list) + + for tag in tags: + item = QStandardItem(tag[0]) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_tag_item_changed) + self.ui.search_result_list.setModel(item_model) + + def exec_(self) -> dict: + if super(TagSelectDialog, self).exec_() == QtWidgets.QDialog.Rejected: + return None + + if self.selection is None: + return None + tag = self.parent.get_tag(self.selection)[0] + tag_data = { + "ID": tag[3], + "name": tag[0], + "description": tag[1], + "aliases": self.parent.get_tag_aliases(tag[0]), + "implications": self.parent.get_tag_implications(tag[0]), + "category": tag[2] + } + return tag_data diff --git a/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.py b/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.py new file mode 100644 index 0000000..cd885a7 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dialogs/tag_select_dialog/tag_selection_dialog.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_TagSelection(object): + def setupUi(self, TagSelection): + TagSelection.setObjectName("TagSelection") + TagSelection.resize(400, 300) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(TagSelection) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.top_frame = QtWidgets.QFrame(TagSelection) + self.top_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.top_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.top_frame.setObjectName("top_frame") + self.verticalLayout = QtWidgets.QVBoxLayout(self.top_frame) + self.verticalLayout.setObjectName("verticalLayout") + self.select_tag_label = QtWidgets.QLabel(self.top_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.select_tag_label.sizePolicy().hasHeightForWidth()) + self.select_tag_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.select_tag_label.setFont(font) + self.select_tag_label.setAlignment(QtCore.Qt.AlignCenter) + self.select_tag_label.setObjectName("select_tag_label") + self.verticalLayout.addWidget(self.select_tag_label) + self.tag_search_line = QtWidgets.QLineEdit(self.top_frame) + self.tag_search_line.setObjectName("tag_search_line") + self.verticalLayout.addWidget(self.tag_search_line) + self.search_result_list = QtWidgets.QListView(self.top_frame) + self.search_result_list.setObjectName("search_result_list") + self.verticalLayout.addWidget(self.search_result_list) + self.verticalLayout_2.addWidget(self.top_frame) + self.buttonBox = QtWidgets.QDialogButtonBox(TagSelection) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout_2.addWidget(self.buttonBox) + + self.retranslateUi(TagSelection) + self.buttonBox.accepted.connect(TagSelection.accept) + self.buttonBox.rejected.connect(TagSelection.reject) + QtCore.QMetaObject.connectSlotsByName(TagSelection) + + def retranslateUi(self, TagSelection): + _translate = QtCore.QCoreApplication.translate + TagSelection.setWindowTitle(_translate("TagSelection", "Dialog")) + self.select_tag_label.setText(_translate("TagSelection", "Select Tag")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + TagSelection = QtWidgets.QDialog() + ui = Ui_TagSelection() + ui.setupUi(TagSelection) + TagSelection.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.ui b/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.ui new file mode 100644 index 0000000..5f79d98 --- /dev/null +++ b/ArtNet/gui/dialogs/tag_select_dialog/tag_selection_dialog.ui @@ -0,0 +1,104 @@ + + + TagSelection + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 75 + true + + + + Select Tag + + + Qt::AlignCenter + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TagSelection + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TagSelection + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ArtNet/gui/dockers/__init__.py b/ArtNet/gui/dockers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dockers/presence/__init__.py b/ArtNet/gui/dockers/presence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/gui/dockers/presence/presence_dock.py b/ArtNet/gui/dockers/presence/presence_dock.py new file mode 100644 index 0000000..b9cdd70 --- /dev/null +++ b/ArtNet/gui/dockers/presence/presence_dock.py @@ -0,0 +1,146 @@ +from PyQt5 import QtWidgets +from PyQt5.QtGui import QStandardItem, QStandardItemModel +from PyQt5.QtCore import Qt + +from ArtNet.gui.dockers.presence.presence_docker import Ui_presence_docker +from ArtNet.gui.dialogs.presence_modify_dialog.presence_mod_dialog import PresenceModDialog +from ArtNet.gui.dialogs.presence_selection_dialog.presence_select_dialog import PresenceSelectDialog + + +class PresenceDocker(QtWidgets.QFrame): + + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + + self.ui = Ui_presence_docker() + self.ui.setupUi(self) + + self.ui.name_line.textChanged.connect(self.on_search_bar_change) + self.ui.domain_line.textChanged.connect(self.on_search_bar_change) + + self.ui.create_presence_button.clicked.connect(self.on_create_presence_clicked) + self.ui.edit_presence_button.clicked.connect(self.on_edit_presence_clicked) + self.ui.delete_presence_button.clicked.connect(self.on_delete_presence_clicked) + + self.on_search_bar_change() + + def set_presence_list(self, presences: list): + item_model = QStandardItemModel(self.ui.search_author_list) + + for name, domain in presences: + item = QStandardItem(name+":"+domain) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + item_model.appendRow(item) + if (name, domain) in self.parent.get_current_presences(): + item.setCheckState(Qt.Checked) + + item_model.itemChanged.connect(self.on_presence_item_change) + self.ui.search_author_list.setModel(item_model) + + def set_selected_presences_list(self, presences: list): + item_model = QStandardItemModel(self.ui.selected_presence_list) + + for name, domain in presences: + item = QStandardItem(name + ":" + domain) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + if domain == "(Not in Database)": + continue + + item_model.appendRow(item) + if (name, domain) in self.parent.get_current_presences(): + item.setCheckState(Qt.Checked) + + item_model.itemChanged.connect(self.on_selected_presence_item_change) + self.ui.selected_presence_list.setModel(item_model) + + def on_search_bar_change(self): + authors = self.parent.get_authors(self.ui.name_line.text(), self.ui.domain_line.text()) + self.set_presence_list(authors) + + def on_presence_item_change(self, item: QStandardItem): + if item.checkState() == Qt.Checked: + s = item.text().split(":") + s = (s[0], s[1]) + if self.parent.get_current_presences() is None: + self.parent.set_current_presences([s]) + self.set_selected_presences_list([s]) + else: + self.parent.set_current_presences(self.parent.get_current_presences() + [s]) + self.set_selected_presences_list(self.parent.get_current_presences()) + elif item.checkState() == Qt.Unchecked: + presences = self.parent.get_current_presences() + if presences is None: + return + s = item.text().split(":") + s = (s[0], s[1]) + presences.remove(s) + self.parent.set_current_presences(presences) + self.set_selected_presences_list(presences) + + def on_selected_presence_item_change(self, item: QStandardItem): + item_presence = item.text().split(":") + item_presence = (item_presence[0], item_presence[1]) + if item.checkState() == Qt.Unchecked: + presences = self.parent.get_current_presences() + presences.remove(item_presence) + self.parent.set_current_presences(presences) + + self.set_selected_presences_list(presences) + self.on_search_bar_change() + + def on_create_presence_clicked(self): + dialog = PresenceModDialog(self, edit_presence=False) + data = dialog.exec_() + if data is None: # canceled? + self.on_search_bar_change() + return + self.parent.create_presence(name=data["name"], domain=data["domain"], artist=data["artist"], + link=data["link"]) + self.on_search_bar_change() + + def on_edit_presence_clicked(self): + select_dialog = PresenceSelectDialog(self) + selected_presence = select_dialog.exec_() + if selected_presence is None: + return + + dialog = PresenceModDialog(self, edit_presence=True) + dialog.ui.presence_name_line.setText(selected_presence[0]) + dialog.ui.presence_domain_line.setText(selected_presence[1]) + + if len(selected_presence) > 2: + if len(selected_presence) >= 4: + if selected_presence[3] is not None: + dialog.ui.presence_link_list.setText(selected_presence[3]) + + artist = self.parent.get_artists(selected_presence[2])[0] + dialog.curr_artist = artist + dialog.set_artist_selected_list(dialog.curr_artist) + + data = dialog.exec_() + if data is None: + self.on_search_bar_change() + return + + self.parent.create_presence(name=data["name"], domain=data["domain"], artist=data["artist"], link=data["link"]) + self.parent.set_temporary_status_message("Saved Presence {0}:{1} Artist:{2} link:{3} to DB!" + .format(data["name"], data["domain"], data["artist"], data["link"]), + 3000) + self.on_search_bar_change() + + def on_delete_presence_clicked(self): + print("Clicked delete presence button!") + select_dialog = PresenceSelectDialog(self) + selected_presence = select_dialog.exec_() + if selected_presence is None: + self.on_search_bar_change() + return + + self.parent.remove_presence(selected_presence[0], selected_presence[1]) + + self.on_search_bar_change() diff --git a/ArtNet/gui/dockers/presence/presence_docker.py b/ArtNet/gui/dockers/presence/presence_docker.py new file mode 100644 index 0000000..2c56b3e --- /dev/null +++ b/ArtNet/gui/dockers/presence/presence_docker.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dockers/presence/presence_docker.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_presence_docker(object): + def setupUi(self, presence_docker): + presence_docker.setObjectName("presence_docker") + presence_docker.resize(338, 576) + self.verticalLayout = QtWidgets.QVBoxLayout(presence_docker) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(presence_docker) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.docker_topic_label = QtWidgets.QLabel(self.frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.docker_topic_label.sizePolicy().hasHeightForWidth()) + self.docker_topic_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(13) + font.setBold(True) + font.setWeight(75) + self.docker_topic_label.setFont(font) + self.docker_topic_label.setAlignment(QtCore.Qt.AlignCenter) + self.docker_topic_label.setObjectName("docker_topic_label") + self.verticalLayout_2.addWidget(self.docker_topic_label) + self.search_author_label = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.search_author_label.setFont(font) + self.search_author_label.setObjectName("search_author_label") + self.verticalLayout_2.addWidget(self.search_author_label) + self.search_frame = QtWidgets.QFrame(self.frame) + self.search_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.search_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.search_frame.setObjectName("search_frame") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.search_frame) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.name_frame = QtWidgets.QFrame(self.search_frame) + self.name_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.name_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.name_frame.setObjectName("name_frame") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.name_frame) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.name_label = QtWidgets.QLabel(self.name_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.name_label.setFont(font) + self.name_label.setObjectName("name_label") + self.verticalLayout_3.addWidget(self.name_label) + self.name_line = QtWidgets.QLineEdit(self.name_frame) + self.name_line.setObjectName("name_line") + self.verticalLayout_3.addWidget(self.name_line) + self.horizontalLayout_3.addWidget(self.name_frame) + self.domain_frame = QtWidgets.QFrame(self.search_frame) + self.domain_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.domain_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.domain_frame.setObjectName("domain_frame") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.domain_frame) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.domain_label = QtWidgets.QLabel(self.domain_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.domain_label.setFont(font) + self.domain_label.setObjectName("domain_label") + self.verticalLayout_4.addWidget(self.domain_label) + self.domain_line = QtWidgets.QLineEdit(self.domain_frame) + self.domain_line.setObjectName("domain_line") + self.verticalLayout_4.addWidget(self.domain_line) + self.horizontalLayout_3.addWidget(self.domain_frame) + self.verticalLayout_2.addWidget(self.search_frame) + self.search_author_list = QtWidgets.QListView(self.frame) + self.search_author_list.setObjectName("search_author_list") + self.verticalLayout_2.addWidget(self.search_author_list) + self.selected_presence_label = QtWidgets.QLabel(self.frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.selected_presence_label.setFont(font) + self.selected_presence_label.setObjectName("selected_presence_label") + self.verticalLayout_2.addWidget(self.selected_presence_label) + self.selected_presence_list = QtWidgets.QListView(self.frame) + self.selected_presence_list.setObjectName("selected_presence_list") + self.verticalLayout_2.addWidget(self.selected_presence_list) + self.presence_docker_button_frame = QtWidgets.QFrame(self.frame) + self.presence_docker_button_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.presence_docker_button_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.presence_docker_button_frame.setObjectName("presence_docker_button_frame") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.presence_docker_button_frame) + self.horizontalLayout.setObjectName("horizontalLayout") + self.create_presence_button = QtWidgets.QPushButton(self.presence_docker_button_frame) + self.create_presence_button.setObjectName("create_presence_button") + self.horizontalLayout.addWidget(self.create_presence_button) + self.edit_presence_button = QtWidgets.QPushButton(self.presence_docker_button_frame) + self.edit_presence_button.setObjectName("edit_presence_button") + self.horizontalLayout.addWidget(self.edit_presence_button) + self.delete_presence_button = QtWidgets.QPushButton(self.presence_docker_button_frame) + self.delete_presence_button.setObjectName("delete_presence_button") + self.horizontalLayout.addWidget(self.delete_presence_button) + self.verticalLayout_2.addWidget(self.presence_docker_button_frame) + self.verticalLayout.addWidget(self.frame) + + self.retranslateUi(presence_docker) + QtCore.QMetaObject.connectSlotsByName(presence_docker) + + def retranslateUi(self, presence_docker): + _translate = QtCore.QCoreApplication.translate + presence_docker.setWindowTitle(_translate("presence_docker", "Form")) + self.docker_topic_label.setText(_translate("presence_docker", "Author (Presence)")) + self.search_author_label.setText(_translate("presence_docker", "Search Authors:")) + self.name_label.setText(_translate("presence_docker", "Name:")) + self.domain_label.setText(_translate("presence_docker", "Domain:")) + self.selected_presence_label.setText(_translate("presence_docker", "Selected Presences:")) + self.create_presence_button.setText(_translate("presence_docker", "Create Presence")) + self.edit_presence_button.setText(_translate("presence_docker", "Edit Presence")) + self.delete_presence_button.setText(_translate("presence_docker", "Delete Presence")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + presence_docker = QtWidgets.QWidget() + ui = Ui_presence_docker() + ui.setupUi(presence_docker) + presence_docker.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/dockers/presence/presence_docker.ui b/ArtNet/gui/dockers/presence/presence_docker.ui new file mode 100644 index 0000000..c081f71 --- /dev/null +++ b/ArtNet/gui/dockers/presence/presence_docker.ui @@ -0,0 +1,190 @@ + + + presence_docker + + + + 0 + 0 + 338 + 576 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 13 + 75 + true + + + + Author (Presence) + + + Qt::AlignCenter + + + + + + + + 10 + 75 + true + + + + Search Authors: + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Name: + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 75 + true + + + + Domain: + + + + + + + + + + + + + + + + + + + + 75 + true + + + + Selected Presences: + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Create Presence + + + + + + + Edit Presence + + + + + + + Delete Presence + + + + + + + + + + + + + + diff --git a/ArtNet/gui/manual_picture_importer.py b/ArtNet/gui/manual_picture_importer.py new file mode 100644 index 0000000..30cfcb0 --- /dev/null +++ b/ArtNet/gui/manual_picture_importer.py @@ -0,0 +1,104 @@ +from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QDesktopWidget, QToolBar, QPushButton, QGroupBox, QGridLayout +from PyQt5.QtWidgets import QVBoxLayout +from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import Qt + + +class PictureImporter(QMainWindow): + + def __init__(self, parent=None, max_relative_resize_width: float = 0.8, max_relative_resize_height: float = 0.8,): + super().__init__(parent) + + # Menubar + self.__menu_bar = self.menuBar() + self.__menu_bar.addAction("Action1") + self.__menu_bar.addAction("Action2") + + # Toolbar + open = QToolBar() + open.addAction("Open") + self.addToolBar(open) + close = QToolBar() + close.addAction("Close") + self.addToolBar(close) + + # layout + self.__layout = QGridLayout() + + # Central Component Settings + self.__window = QWidget() + + self.setCentralWidget(self.__window) + + # Statusbar Settings + self.__status_bar = self.statusBar() + self.__status_bar.showMessage("Welcome to ArtNet! :)", 5000) + label = QLabel("ArtNet - v0.1") + self.__status_bar.addPermanentWidget(label) + + self.resize(800, 600) + self.setWindowTitle("ArtNet - Picture Importer") + self.center() + + def center(self): + """ + Centers the window in the middle of the screen + + :return: + """ + screen = QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2) + + @DeprecationWarning + def display_geometry(self): + x = self.x() + y = self.y() + print("x: {0}, y: {1}".format(x, y)) + x = self.pos().x() + y = self.pos().y() + print("x: {0}, y: {1}".format(x, y)) + x = self.frameGeometry().x() + y = self.frameGeometry().y() + print("x: {0}, y: {1}".format(x, y)) + x = self.geometry().x() + y = self.geometry().y() + print("x: {0}, y: {1}".format(x, y)) + print("geometry: ", self.geometry()) + print("frameGeometry: ", self.frameGeometry()) + + def set_temporary_status_message(self, text: str, duration: int): + """ + Set a temporary status message (bottom left) for the given duration in milliseconds. + :param text: + :param duration: + :return: + """ + self.__status_bar.showMessage("Welcome to ArtNet! :)", 5000) + + def display_image(self, full_path: str): + """ + Display an image in the central widget + :param full_path: + :return: + """ + label = QLabel(self) + pixmap = self.__image_resize(QPixmap(full_path)) + + label.setPixmap(pixmap) + label.setAlignment(Qt.AlignCenter) + + self.setCentralWidget(label) + self.center() + + def __image_resize(self, pixmap: QPixmap) -> QPixmap: + """ + Resize the given pixmap so that we're not out of the desktop. + + :return: new scaled QPixmap + """ + return pixmap + screen_rect = QDesktopWidget().screenGeometry() + print("Resizing pixmap to", int(screen_rect.width()*0.4), int(screen_rect.height()*0.6)) + return pixmap.scaled(int(screen_rect.width()*0.6), int(screen_rect.height()*0.7), Qt.KeepAspectRatio) + diff --git a/ArtNet/gui/picture_importer.py b/ArtNet/gui/picture_importer.py new file mode 100644 index 0000000..3bc8f48 --- /dev/null +++ b/ArtNet/gui/picture_importer.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'picture_importer.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(827, 777) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setEnabled(True) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.horizontalLayout.setObjectName("horizontalLayout") + self.left_frame = QtWidgets.QFrame(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.left_frame.sizePolicy().hasHeightForWidth()) + self.left_frame.setSizePolicy(sizePolicy) + self.left_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.left_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.left_frame.setObjectName("left_frame") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.left_frame) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.presence_docker_button = QtWidgets.QToolButton(self.left_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.presence_docker_button.sizePolicy().hasHeightForWidth()) + self.presence_docker_button.setSizePolicy(sizePolicy) + self.presence_docker_button.setObjectName("presence_docker_button") + self.horizontalLayout_2.addWidget(self.presence_docker_button) + self.presence_docker_layout = QtWidgets.QVBoxLayout() + self.presence_docker_layout.setObjectName("presence_docker_layout") + self.horizontalLayout_2.addLayout(self.presence_docker_layout) + self.left_layout = QtWidgets.QVBoxLayout() + self.left_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.left_layout.setObjectName("left_layout") + self.prev_image_button = QtWidgets.QToolButton(self.left_frame) + self.prev_image_button.setObjectName("prev_image_button") + self.left_layout.addWidget(self.prev_image_button) + self.horizontalLayout_2.addLayout(self.left_layout) + self.center_frame = QtWidgets.QFrame(self.left_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.center_frame.sizePolicy().hasHeightForWidth()) + self.center_frame.setSizePolicy(sizePolicy) + self.center_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.center_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.center_frame.setObjectName("center_frame") + self.verticalLayout = QtWidgets.QVBoxLayout(self.center_frame) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(self.center_frame) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.frame) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.image_title_line = QtWidgets.QLineEdit(self.frame) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.image_title_line.setFont(font) + self.image_title_line.setAutoFillBackground(False) + self.image_title_line.setAlignment(QtCore.Qt.AlignCenter) + self.image_title_line.setReadOnly(False) + self.image_title_line.setObjectName("image_title_line") + self.horizontalLayout_4.addWidget(self.image_title_line) + self.imageNumberSpinBox = QtWidgets.QSpinBox(self.frame) + self.imageNumberSpinBox.setObjectName("imageNumberSpinBox") + self.horizontalLayout_4.addWidget(self.imageNumberSpinBox) + self.verticalLayout.addWidget(self.frame) + self.image_file_label = QtWidgets.QLabel(self.center_frame) + self.image_file_label.setAlignment(QtCore.Qt.AlignCenter) + self.image_file_label.setObjectName("image_file_label") + self.verticalLayout.addWidget(self.image_file_label) + self.author_by = QtWidgets.QLabel(self.center_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.author_by.sizePolicy().hasHeightForWidth()) + self.author_by.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(7) + font.setBold(False) + font.setWeight(50) + self.author_by.setFont(font) + self.author_by.setAlignment(QtCore.Qt.AlignCenter) + self.author_by.setObjectName("author_by") + self.verticalLayout.addWidget(self.author_by) + self.image_author_label = QtWidgets.QLabel(self.center_frame) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(False) + font.setItalic(False) + font.setWeight(50) + self.image_author_label.setFont(font) + self.image_author_label.setAlignment(QtCore.Qt.AlignCenter) + self.image_author_label.setObjectName("image_author_label") + self.verticalLayout.addWidget(self.image_author_label) + self.image_frame = QtWidgets.QFrame(self.center_frame) + self.image_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.image_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.image_frame.setObjectName("image_frame") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.image_frame) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.image_label = QtWidgets.QLabel(self.image_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.image_label.sizePolicy().hasHeightForWidth()) + self.image_label.setSizePolicy(sizePolicy) + self.image_label.setAlignment(QtCore.Qt.AlignCenter) + self.image_label.setObjectName("image_label") + self.verticalLayout_2.addWidget(self.image_label) + self.verticalLayout.addWidget(self.image_frame) + self.link_label = QtWidgets.QLabel(self.center_frame) + font = QtGui.QFont() + font.setPointSize(8) + font.setBold(True) + font.setWeight(75) + self.link_label.setFont(font) + self.link_label.setAlignment(QtCore.Qt.AlignCenter) + self.link_label.setObjectName("link_label") + self.verticalLayout.addWidget(self.link_label) + self.label_5 = QtWidgets.QLabel(self.center_frame) + self.label_5.setObjectName("label_5") + self.verticalLayout.addWidget(self.label_5) + self.link_line = QtWidgets.QLineEdit(self.center_frame) + font = QtGui.QFont() + font.setPointSize(7) + self.link_line.setFont(font) + self.link_line.setObjectName("link_line") + self.verticalLayout.addWidget(self.link_line) + self.horizontalLayout_2.addWidget(self.center_frame) + self.right_layout = QtWidgets.QVBoxLayout() + self.right_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.right_layout.setObjectName("right_layout") + self.next_image_button = QtWidgets.QToolButton(self.left_frame) + self.next_image_button.setObjectName("next_image_button") + self.right_layout.addWidget(self.next_image_button) + self.horizontalLayout_2.addLayout(self.right_layout) + self.horizontalLayout.addWidget(self.left_frame) + self.right_frame = QtWidgets.QFrame(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.right_frame.sizePolicy().hasHeightForWidth()) + self.right_frame.setSizePolicy(sizePolicy) + self.right_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.right_frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.right_frame.setObjectName("right_frame") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.right_frame) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.tag_layout = QtWidgets.QVBoxLayout() + self.tag_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.tag_layout.setObjectName("tag_layout") + self.label = QtWidgets.QLabel(self.right_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label.setFont(font) + self.label.setObjectName("label") + self.tag_layout.addWidget(self.label) + self.tag_list_layout = QtWidgets.QVBoxLayout() + self.tag_list_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.tag_list_layout.setObjectName("tag_list_layout") + self.tag_search_bar = QtWidgets.QLineEdit(self.right_frame) + self.tag_search_bar.setMaximumSize(QtCore.QSize(400, 16777215)) + self.tag_search_bar.setObjectName("tag_search_bar") + self.tag_list_layout.addWidget(self.tag_search_bar) + self.search_result_list = QtWidgets.QListView(self.right_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.search_result_list.sizePolicy().hasHeightForWidth()) + self.search_result_list.setSizePolicy(sizePolicy) + self.search_result_list.setMinimumSize(QtCore.QSize(0, 0)) + self.search_result_list.setMaximumSize(QtCore.QSize(400, 16777215)) + self.search_result_list.setObjectName("search_result_list") + self.tag_list_layout.addWidget(self.search_result_list) + self.label_2 = QtWidgets.QLabel(self.right_frame) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_2.setFont(font) + self.label_2.setObjectName("label_2") + self.tag_list_layout.addWidget(self.label_2) + self.tag_list = QtWidgets.QListView(self.right_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tag_list.sizePolicy().hasHeightForWidth()) + self.tag_list.setSizePolicy(sizePolicy) + self.tag_list.setMinimumSize(QtCore.QSize(100, 0)) + self.tag_list.setMaximumSize(QtCore.QSize(400, 16777215)) + self.tag_list.setObjectName("tag_list") + self.tag_list_layout.addWidget(self.tag_list) + self.label_3 = QtWidgets.QLabel(self.right_frame) + font = QtGui.QFont() + font.setPointSize(9) + font.setBold(True) + font.setWeight(75) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.tag_list_layout.addWidget(self.label_3) + self.implied_tag_list = QtWidgets.QListView(self.right_frame) + self.implied_tag_list.setMinimumSize(QtCore.QSize(100, 0)) + self.implied_tag_list.setMaximumSize(QtCore.QSize(400, 16777215)) + self.implied_tag_list.setObjectName("implied_tag_list") + self.tag_list_layout.addWidget(self.implied_tag_list) + self.tag_layout.addLayout(self.tag_list_layout) + self.tag_button_layout = QtWidgets.QHBoxLayout() + self.tag_button_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.tag_button_layout.setSpacing(5) + self.tag_button_layout.setObjectName("tag_button_layout") + self.save_button = QtWidgets.QPushButton(self.right_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.save_button.sizePolicy().hasHeightForWidth()) + self.save_button.setSizePolicy(sizePolicy) + self.save_button.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.save_button.setObjectName("save_button") + self.tag_button_layout.addWidget(self.save_button) + self.import_button = QtWidgets.QPushButton(self.right_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.import_button.sizePolicy().hasHeightForWidth()) + self.import_button.setSizePolicy(sizePolicy) + self.import_button.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.import_button.setObjectName("import_button") + self.tag_button_layout.addWidget(self.import_button) + self.prev_unknown_image_button = QtWidgets.QPushButton(self.right_frame) + self.prev_unknown_image_button.setObjectName("prev_unknown_image_button") + self.tag_button_layout.addWidget(self.prev_unknown_image_button) + self.next_unknown_image_button = QtWidgets.QPushButton(self.right_frame) + self.next_unknown_image_button.setObjectName("next_unknown_image_button") + self.tag_button_layout.addWidget(self.next_unknown_image_button) + self.delete_button = QtWidgets.QPushButton(self.right_frame) + self.delete_button.setObjectName("delete_button") + self.tag_button_layout.addWidget(self.delete_button) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.tag_button_layout.addItem(spacerItem) + self.tag_layout.addLayout(self.tag_button_layout) + self.horizontalLayout_3.addLayout(self.tag_layout) + self.horizontalLayout.addWidget(self.right_frame) + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.menuBar = QtWidgets.QMenuBar(MainWindow) + self.menuBar.setGeometry(QtCore.QRect(0, 0, 827, 19)) + self.menuBar.setObjectName("menuBar") + self.menuArtNet = QtWidgets.QMenu(self.menuBar) + self.menuArtNet.setObjectName("menuArtNet") + self.menuTags = QtWidgets.QMenu(self.menuBar) + self.menuTags.setObjectName("menuTags") + self.menuCategory = QtWidgets.QMenu(self.menuBar) + self.menuCategory.setObjectName("menuCategory") + MainWindow.setMenuBar(self.menuBar) + self.actionChange_Connection_Details = QtWidgets.QAction(MainWindow) + self.actionChange_Connection_Details.setObjectName("actionChange_Connection_Details") + self.actionChange_ArtNet_Root_Folder = QtWidgets.QAction(MainWindow) + self.actionChange_ArtNet_Root_Folder.setObjectName("actionChange_ArtNet_Root_Folder") + self.actionCreate_New_Tag = QtWidgets.QAction(MainWindow) + self.actionCreate_New_Tag.setObjectName("actionCreate_New_Tag") + self.actionDelete_a_Tag = QtWidgets.QAction(MainWindow) + self.actionDelete_a_Tag.setObjectName("actionDelete_a_Tag") + self.actionEdit_a_Tag = QtWidgets.QAction(MainWindow) + self.actionEdit_a_Tag.setObjectName("actionEdit_a_Tag") + self.actionCreate_New_Category = QtWidgets.QAction(MainWindow) + self.actionCreate_New_Category.setObjectName("actionCreate_New_Category") + self.actionDelete_a_Category = QtWidgets.QAction(MainWindow) + self.actionDelete_a_Category.setObjectName("actionDelete_a_Category") + self.actionCreate_New_Category_2 = QtWidgets.QAction(MainWindow) + self.actionCreate_New_Category_2.setObjectName("actionCreate_New_Category_2") + self.actionDelete_a_Category_2 = QtWidgets.QAction(MainWindow) + self.actionDelete_a_Category_2.setObjectName("actionDelete_a_Category_2") + self.menuArtNet.addAction(self.actionChange_Connection_Details) + self.menuArtNet.addAction(self.actionChange_ArtNet_Root_Folder) + self.menuTags.addAction(self.actionCreate_New_Tag) + self.menuTags.addAction(self.actionDelete_a_Tag) + self.menuTags.addAction(self.actionEdit_a_Tag) + self.menuCategory.addAction(self.actionCreate_New_Category_2) + self.menuCategory.addAction(self.actionDelete_a_Category_2) + self.menuBar.addAction(self.menuArtNet.menuAction()) + self.menuBar.addAction(self.menuTags.menuAction()) + self.menuBar.addAction(self.menuCategory.menuAction()) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.presence_docker_button.setText(_translate("MainWindow", "<")) + self.prev_image_button.setText(_translate("MainWindow", "<")) + self.image_title_line.setText(_translate("MainWindow", "Image_Title")) + self.image_file_label.setText(_translate("MainWindow", "file_name")) + self.author_by.setText(_translate("MainWindow", "by")) + self.image_author_label.setText(_translate("MainWindow", "author_name")) + self.image_label.setText(_translate("MainWindow", "No Image")) + self.link_label.setText(_translate("MainWindow", "Source_Link")) + self.label_5.setText(_translate("MainWindow", "Link:")) + self.next_image_button.setText(_translate("MainWindow", ">")) + self.label.setText(_translate("MainWindow", "Search Tags")) + self.label_2.setText(_translate("MainWindow", "Selected Tags:")) + self.label_3.setText(_translate("MainWindow", "Implied Tags:")) + self.save_button.setText(_translate("MainWindow", "Save")) + self.import_button.setText(_translate("MainWindow", "Import Tags")) + self.prev_unknown_image_button.setText(_translate("MainWindow", "prev Unknown")) + self.next_unknown_image_button.setText(_translate("MainWindow", "next Unknown")) + self.delete_button.setText(_translate("MainWindow", "Delete")) + self.menuArtNet.setTitle(_translate("MainWindow", "ArtNet")) + self.menuTags.setTitle(_translate("MainWindow", "Tags")) + self.menuCategory.setTitle(_translate("MainWindow", "Category")) + self.actionChange_Connection_Details.setText(_translate("MainWindow", "Change DB Connection")) + self.actionChange_ArtNet_Root_Folder.setText(_translate("MainWindow", "Change ArtNet Root")) + self.actionCreate_New_Tag.setText(_translate("MainWindow", "Create New Tag")) + self.actionDelete_a_Tag.setText(_translate("MainWindow", "Delete a Tag")) + self.actionEdit_a_Tag.setText(_translate("MainWindow", "Edit a Tag")) + self.actionCreate_New_Category.setText(_translate("MainWindow", "Create New Category")) + self.actionDelete_a_Category.setText(_translate("MainWindow", "Delete a Category")) + self.actionCreate_New_Category_2.setText(_translate("MainWindow", "Create New Category")) + self.actionDelete_a_Category_2.setText(_translate("MainWindow", "Delete a Category")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec_()) diff --git a/ArtNet/gui/picture_importer.ui b/ArtNet/gui/picture_importer.ui new file mode 100644 index 0000000..22fbf14 --- /dev/null +++ b/ArtNet/gui/picture_importer.ui @@ -0,0 +1,564 @@ + + + MainWindow + + + + 0 + 0 + 827 + 777 + + + + MainWindow + + + + true + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + < + + + + + + + + + + QLayout::SetMinimumSize + + + + + < + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 10 + 75 + true + + + + false + + + Image_Title + + + Qt::AlignCenter + + + false + + + + + + + + + + + + + file_name + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 7 + 50 + false + + + + by + + + Qt::AlignCenter + + + + + + + + 10 + 50 + false + false + + + + author_name + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + No Image + + + Qt::AlignCenter + + + + + + + + + + + 8 + 75 + true + + + + Source_Link + + + Qt::AlignCenter + + + + + + + Link: + + + + + + + + 7 + + + + + + + + + + + QLayout::SetMinimumSize + + + + + > + + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QLayout::SetMinimumSize + + + + + + 75 + true + + + + Search Tags + + + + + + + QLayout::SetMinimumSize + + + + + + 400 + 16777215 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 400 + 16777215 + + + + + + + + + 75 + true + + + + Selected Tags: + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 400 + 16777215 + + + + + + + + + 9 + 75 + true + + + + Implied Tags: + + + + + + + + 100 + 0 + + + + + 400 + 16777215 + + + + + + + + + + 5 + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Save + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Import Tags + + + + + + + prev Unknown + + + + + + + next Unknown + + + + + + + Delete + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + 0 + 0 + 827 + 19 + + + + + ArtNet + + + + + + + Tags + + + + + + + + Category + + + + + + + + + + + Change DB Connection + + + + + Change ArtNet Root + + + + + Create New Tag + + + + + Delete a Tag + + + + + Edit a Tag + + + + + Create New Category + + + + + Delete a Category + + + + + Create New Category + + + + + Delete a Category + + + + + + diff --git a/ArtNet/gui/window.py b/ArtNet/gui/window.py new file mode 100644 index 0000000..16b6c09 --- /dev/null +++ b/ArtNet/gui/window.py @@ -0,0 +1,999 @@ +import validators, os + +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt, QSize, QUrl +from PyQt5.QtGui import QPixmap, QResizeEvent, QKeyEvent, QStandardItemModel, QStandardItem, QMovie +from PyQt5 import QtMultimedia +from PyQt5.QtMultimediaWidgets import QVideoWidget + +from ArtNet.gui.picture_importer import Ui_MainWindow +from ArtNet.gui.dialogs.db_connection_dialog.db_dialog import DBDialog +from ArtNet.gui.dialogs.tag_modify_dialog.tag_mod_dialog import TagModifyDialog +from ArtNet.gui.dialogs.tag_select_dialog.tag_select_dialog import TagSelectDialog +from ArtNet.gui.dockers.presence.presence_dock import PresenceDocker +from ArtNet.gui.dialogs.category_modify_dialog.category_mod_dialog import CategoryModDialog +from ArtNet.gui.dialogs.tag_import_dialog.tag_imp_dialog import TagImportDialog + +from ArtNet.web.link_generator import LinkGenerator + + +class Window(QtWidgets.QMainWindow): + + def __init__(self, main): + super(Window, self).__init__() + + self.__main = main + + self.__pixmap: QPixmap = None + self.__video: QVideoWidget = None + self.__player: QtMultimedia.QMediaPlayer = None + self.__showing_video: bool = False + + self.__tmp_imageid_spinbox: int = None + + self.presence_docker_open: bool = False + self.presence_docker: PresenceDocker = None + self.curr_art_id: int = None + self.curr_image_title: str = None + self.curr_link: str = None + self.curr_art_path: str = None + self.curr_file_name: str = None + self.curr_presences: list = list() + self.curr_tags: list = list() + self.curr_imply_tags: list = list() + self.curr_tag_aliases: list = list() + + self.setting_up_data: bool = True + self.__data_changed: bool = False + + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + self.main_title = "ArtNet Picture Importer" + + self.ui.actionChange_ArtNet_Root_Folder.triggered.connect(self.on_artnet_root_change_clicked) + self.ui.actionChange_Connection_Details.triggered.connect(self.on_db_connection_change_clicked) + self.ui.actionCreate_New_Tag.triggered.connect(self.on_tag_creation_clicked) + self.ui.actionEdit_a_Tag.triggered.connect(self.on_tag_edit_clicked) + self.ui.actionDelete_a_Tag.triggered.connect(self.on_tag_deletion_clicked) + self.ui.actionCreate_New_Category_2.triggered.connect(self.on_category_creation_clicked) + self.ui.actionDelete_a_Category_2.triggered.connect(self.on_category_deletion_clicked) + + self.ui.imageNumberSpinBox.valueChanged.connect(self.on_image_id_spinbox_changed) + + self.ui.next_image_button.clicked.connect(self.on_next_clicked) + self.ui.prev_image_button.clicked.connect(self.on_previous_clicked) + self.ui.save_button.clicked.connect(self.on_save_clicked) + self.ui.import_button.clicked.connect(self.on_import_tags_clicked) + self.ui.prev_unknown_image_button.clicked.connect(self.on_prev_unknown_image_clicked) + self.ui.next_unknown_image_button.clicked.connect(self.on_next_unknown_image_clicked) + self.ui.delete_button.clicked.connect(self.on_delete_image_clicked) + + self.ui.presence_docker_button.clicked.connect(self.toggle_presence_docker) + + self.ui.tag_search_bar.textChanged.connect(self.on_tag_search_change) + self.ui.image_title_line.textChanged.connect(self.on_image_title_change) + self.ui.link_line.textChanged.connect(self.on_link_line_change) + + self.ui.link_label.setText("No Source Available") + self.ui.image_file_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.set_image_title_link() + + self.on_tag_search_change() + self.center() + + @property + def data_changed(self): + return self.__data_changed + + @data_changed.setter + def data_changed(self, v: bool): + self.__data_changed = v + + if self.curr_image_title is None: + return + if " (Not in Database)" in self.curr_image_title and v and not self.setting_up_data: + self.curr_image_title = self.curr_image_title.replace(" (Not in Database)", "") + self.setting_up_data = True + self.ui.image_title_line.setText(self.curr_image_title) + self.setting_up_data = False + + def center(self): + """ + Centers the window in the middle of the screen + + Note: actually not the center but a good position due to images changing size! + :return: + """ + screen = QtWidgets.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width() - size.width()) / 3, (screen.height() - size.height()) / 5) + + def check_save_changes(self): + """ + Check if there were changes to image settings. If yes ask for confirmation to save them. + + :return: + """ + if self.data_changed: + answer = QtWidgets.QMessageBox.question(self, "Save Changes?", + "There have been changes. Do you wish to save them?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) + if answer == QtWidgets.QMessageBox.Yes: + if not self.save_changes(): + return False + return True + + def save_changes(self) -> bool: + """ + Save the changes to image data to the DB. + :return: + """ + image_data = { + "ID": self.curr_art_id, + "title": self.curr_image_title, + "authors": self.curr_presences, + "path": self.curr_art_path, + "tags": self.curr_tags, + "link": self.curr_link, + "md5_hash": self.__main.get_md5_of_image(self.curr_art_path) + } + + for presence in self.curr_presences: + if presence[-1] == "(Not in Database)": + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Invalid Presence Domain") + msg.setInformativeText("You've tried to save with a not working presence entry! " + + "Please add one from the database!") + msg.setIcon(QtWidgets.QMessageBox.Warning) + msg.exec_() + return False + + self.__main.db_connection.save_image(ID=image_data["ID"], title=image_data["title"], + authors=image_data["authors"], path=image_data["path"], + tags=image_data["tags"], link=image_data["link"], + md5_hash=image_data["md5_hash"]) + + self.set_temporary_status_message("Saved {0} ({1}) to ArtNet DB!" + .format(self.curr_image_title, self.curr_art_id), 5000) + self.update_window_title() + self.data_changed = False + return True + + def set_temporary_status_message(self, text: str, duration: int): + """ + Set a temporary status message (bottom left) for the given duration in milliseconds. + :param text: + :param duration: + :return: + """ + self.ui.statusbar.showMessage(text, duration) + + def create_presence(self, name: str, domain: str, artist: tuple, link: str): + """ + Create a new Presence Entry with the given data + :param name: + :param domain: + :param artist: + :param link: + :return: + """ + if len(name) == 0 or len(domain) == 0 or artist is None: + return + self.__main.db_connection.save_presence(name=name, domain=domain, artist_ID=artist[0], link=link) + + def remove_presence(self, name: str, domain: str): + """ + Remove the presence from the DB + :param name: + :param domain: + :return: + """ + self.__main.db_connection.remove_presence(name, domain) + + def get_authors(self, presence_name: str, presence_domain: str) -> list: + """ + Query a search for the authors fitting the given strings + :param presence_name: + :param presence_domain: + :return: a list of tuples of (presence_name, presence_domain) + """ + return self.__main.db_connection.search_fuzzy_presence(presence_name, presence_domain, all_if_empty=True) + + def create_artist(self, ID: int, description: str): + """ + Create a new artist with the given data (or update an exisitng one if ID is already taken + :param ID: + :param description: + :return: + """ + self.__main.db_connection.save_artist(ID, description) + self.set_temporary_status_message("Created Artist {0}!".format(description), 3000) + + def get_artists(self, search: str) -> list: + """ + Query a search for the artists fitting the given data best. Search is fuzzy. + :param search: either an ID (int) or the description + :return: + """ + try: + ID_int = int(search) + description = None + except ValueError: + ID_int = None + description = search + + return self.__main.db_connection.search_fuzzy_artists(ID_int, description) + + def get_artist(self, id: int) -> list: + """ + Query for the artist matching id. Returns None if the data does not exactly fit. + :param id: + :return: + """ + return self.__main.db_connection.get_artist(id) + + def remove_artist(self, id: int): + """ + Delte the given artist from the database. + :param id: + :return: + """ + self.__main.db_connection.remove_artist(id) + + def get_artist_presences(self, id: int) -> list: + """ + Query for all presences associated with the given artist. + :param id: + :return: + """ + return self.__main.db_connection.get_artist_presences(id) + + def get_all_artists(self) -> list: + """ + Queries the database for a list of all available arists (not presences). + :return: + """ + return self.__main.db_connection.get_all_artists() + + def get_presence(self, name: str, domain: str): + """ + Query a search for the presence fitting the data + :param name: + :param domain: + :return: + """ + result = self.__main.db_connection.get_presence(name, domain) + + return result if len(result) != 0 else None + + def remove_presence(self, name: str, domain: str): + """ + Deletes a presence from the database and removes all Art_Author entries containing this presence. + :param name: + :param domain: + :return: + """ + self.__main.db_connection.remove_presence(name, domain) + + def get_presences_art(self, name: str, domain: str): + """ + Query a list of art owned by the given presence + :param name: + :param domain: + :return: + """ + return self.__main.db_connection.get_presences_art(name, domain) + + def get_current_presences(self) -> list: + """ + Get the presences currently associated with the current art + :return: + """ + return self.curr_presences + + def set_current_presences(self, presences: list): + """ + Set the presences associated with the current art + :param presences: list of tuples of (name, domain) + """ + if len(presences) > 1: + for name, domain in presences: + if domain == "(Not in Database)": + presences.remove((name, domain)) + elif len(presences) == 0: + presences = [(self.curr_art_path.split("/")[0], "(Not in Database)")] + self.curr_presences = presences + + if self.curr_presences is not None: + self.set_presence_label_text(self.curr_presences) + + self.data_changed = True + + def get_categories(self, search: str, all_if_empty: bool = False): + """ + Fuzzy Query for categories in the database. + + all_if_empty causes an empty search to return all categories instead of none + :param search: + :param all_if_empty: + :return: + """ + if all_if_empty and len(search) == 0: + return self.__main.db_connection.get_all_categories() + + return self.__main.db_connection.search_fuzzy_categories(search) + + def get_image_link_from_line(self) -> str: + """ + Gets the image link from the QLineEdit if it is a valid link. + + Otherwise an empty string + :return: + """ + return self.ui.link_line.text() + + def set_presence_label_text(self, presences: list): + """ + Set the label listing all current presences and include links if possible. + :param presences: + :return: + """ + links = [] + s = "" + for name, domain in presences: + full_data = self.get_presence(name, domain) + if full_data is None: + link = "" + else: + name, domain, _, link = full_data[0] + text = name + ":" + domain + if link is None or len(link) == 0: # no link, then just do plain text + hyperlink = text + else: + hyperlink = "{1}".format(link, text) + + s += hyperlink + s += "|" + + s = s[:-1] + self.ui.image_author_label.setText(s) + + def set_image_title_link(self) -> str: + """ + Sets the Image title to a link if there is link data given for this image. + :return: + """ + self.ui.link_label.setText("No Source Available") + link = self.ui.link_line.text() + + if validators.url(link): + self.curr_link = link + self.data_changed = True + hyperlink = "{1}".format(link, "Source") + self.ui.link_label.setText(hyperlink) + return link + elif len(link) == 0: + return "" + else: + self.ui.link_label.setText("No Source Available") + self.set_temporary_status_message("Invalid link \"{0}\" detected!".format(link), 5000) + return "" + + def get_tag(self, name: str) -> list: + """ + Query a search for the tag to the DB and return the result + :param name: + :return: + """ + return self.__main.db_connection.get_tag_by_name(name) + + def get_tag_aliases(self, name: str) -> list: + """ + Query a search for the tag's aliases to the DB + + Note: Returns all aliases as a list of their IDs + :param name: + :return: + """ + return self.__main.db_connection.get_tag_aliases(name) + + def get_tag_implications(self, name: str) -> list: + """ + Query a search for the tag's implications to the DB + :param name: + :return: + """ + return self.__main.db_connection.get_tag_implications(name) + + def get_tag_search_result(self, name: str) -> list: + """ + Query a search for tags to the DB that are like name + :return: + """ + return self.__main.db_connection.search_fuzzy_tag(name, all_if_empty=True) + + def set_search_result_list(self, tags: list): + """ + Set the tags in the search result list to tags + :param tags: + :return: + """ + item_model = QStandardItemModel(self.ui.search_result_list) + + for tag in tags: + item = QStandardItem(tag) + flags = Qt.ItemIsEnabled + + if tag not in self.curr_imply_tags+self.curr_tag_aliases and tag not in self.curr_tags: + item.setData(Qt.Unchecked, Qt.CheckStateRole) + flags |= Qt.ItemIsUserCheckable + if self.curr_tags is not None and tag in (self.curr_tags+self.curr_imply_tags+self.curr_tag_aliases): + # already selected, implied or aliased tags + item.setCheckState(Qt.Checked) + item.setFlags(flags) + item_model.appendRow(item) + + item_model.itemChanged.connect(self.on_tag_search_item_changed) + self.ui.search_result_list.setModel(item_model) + + def set_tag_list(self, tags: list, set_checked: bool = True, no_implication: bool = False): + """ + Set the tags in the tag list to this. + Also updates the tag implication list if no_implication is False + :param tags: + :param set_checked: + :param no_implication: bool indicating if the implication list should also be updated + :return: + """ + self.curr_tags = tags + item_model = QStandardItemModel(self.ui.tag_list) + + for tag in tags: + item = QStandardItem(tag) + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setData(Qt.Unchecked, Qt.CheckStateRole) + + item_model.appendRow(item) + + if set_checked: + item.setCheckState(Qt.Checked) + + item_model.itemChanged.connect(self.on_tag_item_changed) + self.ui.tag_list.setModel(item_model) + + implied_tags = [] + for x in self.curr_tags: + # collect all implied tags into a list + implied_tags += self.__main.db_connection.get_all_tag_implications(x) + + self.set_implied_list(implied_tags) + self.curr_tag_aliases = list() + for tag in tags+implied_tags: + self.curr_tag_aliases += self.__main.db_connection.get_tag_aliases(tag) + + self.data_changed = True + + def set_implied_list(self, tags: list): + """ + Sets the implied tags in the imply list + :param tags: + :return: + """ + self.curr_imply_tags = tags + item_model = QStandardItemModel(self.ui.implied_tag_list) + done = [] + for tag in tags: + if tag in done: + continue + else: + done.append(tag) + item = QStandardItem(tag) + item_model.appendRow(item) + + self.ui.implied_tag_list.setModel(item_model) + + self.data_changed = True + + def display_image(self, image_title: str, image_authors: list, full_path: str, relative_path: str, art_ID: int, + link: str, file_name: str): + """ + Display an image in the central widget + :param image_authors: + :param image_title: + :param full_path: + :param relative_path: + :param art_ID: + :param link: + :param file_name: + :return: + """ + self.curr_art_id = art_ID + self.curr_art_path = relative_path + self.curr_image_title = image_title + self.curr_file_name = os.path.basename(full_path) + self.curr_link = link + self.set_current_presences(image_authors) + + file_ending = relative_path.split(".")[-1] + if self.__showing_video: # remove old video from image layout + #self.ui.image_frame.layout().removeWidget(self.__video) + self.__video.hide() + self.ui.image_label.show() + if file_ending in ["gif"]: + self.__showing_video = False + self.__pixmap = QMovie(full_path) + self.ui.image_label.setMovie(self.__pixmap) + self.__pixmap.start() + self.__pixmap.frameChanged.connect(self.on_movie_frame_changed) + elif file_ending in ["webm"]: + self.__showing_video = True + self.__video = QVideoWidget() + self.__player = QtMultimedia.QMediaPlayer(None, QtMultimedia.QMediaPlayer.VideoSurface) + self.__player.setVideoOutput(self.__video) + self.__player.setMedia(QtMultimedia.QMediaContent(QUrl.fromLocalFile(full_path))) + self.__player.play() + + self.ui.image_frame.layout().addWidget(self.__video) + self.ui.image_label.hide() + + self.__player.stateChanged.connect(self.on_movie_player_state_changed) + self.__player.positionChanged.connect(self.on_movie_position_changed) + else: + self.__showing_video = False + self.__pixmap = QPixmap(full_path) + self.ui.image_label.setPixmap(self.__pixmap) + + self.ui.image_label.setScaledContents(True) + self.ui.image_label.setFixedSize(0, 0) + self.__image_resize() + + self.ui.image_label.setAlignment(Qt.AlignCenter) + self.ui.image_title_line.setText(image_title) + self.update_window_title() + self.ui.link_line.setText(link) + self.ui.image_file_label.setText(file_name) + self.set_image_title_link() + self.set_image_id_spinbox() + + def update_window_title(self): + """ + Update the title of the window with the newest image title as given in text field + :return: + """ + image_title = self.ui.image_title_line.text() + self.setWindowTitle(self.main_title + " - " + image_title + + f" ({round(self.__main.known_image_amount/len(self.__main.all_images), 5)}%)") + + def set_image_id_spinbox(self): + """ + Sets the imageIDSpinBox to the image ID of the currently displayed image + :return: + """ + self.ui.imageNumberSpinBox.setMinimum(0) + self.ui.imageNumberSpinBox.setMaximum(len(self.__main.all_images)-1) + self.ui.imageNumberSpinBox.setValue(self.__main.curr_image_index) + + def __image_resize(self): + """ + Resize the given pixmap so that we're not out of the desktop. + + :return: new scaled QPixmap + """ + if self.ui.image_label.movie() is not None or self.__showing_video: # if QMovie was used instead of image + rect = self.geometry() + size = QSize(min(rect.width(), rect.height()), min(rect.width(), rect.height())) + if type(self.__pixmap) != QMovie: # using QVideoWidget? + pass + #self.__player.setScaledSize(size) + else: + self.__pixmap.setScaledSize(size) + return + + size = self.__pixmap.size() + screen_rect = QtWidgets.QDesktopWidget().screenGeometry() + + size.scale(int(screen_rect.width() * 0.6), int(screen_rect.height() * 0.6), + Qt.KeepAspectRatio) + self.ui.image_label.setFixedSize(size) + + def resizeEvent(self, a0: QResizeEvent) -> None: + self.__image_resize() + + def keyPressEvent(self, a0: QKeyEvent) -> None: + super(Window, self).keyPressEvent(a0) + if a0.key() == Qt.Key_Left: + self.on_previous_clicked() + elif a0.key() == Qt.Key_Right: + self.on_next_clicked() + elif a0.key() == Qt.Key_Return: + print("Pressed Enter!") + if self.__showing_video: + s = self.__player.state() + if self.__player.state() == QtMultimedia.QMediaPlayer.PlayingState: + self.__player.pause() + self.set_temporary_status_message("Paused the Video!", 3000) + elif self.__player.state() == QtMultimedia.QMediaPlayer.PausedState: + self.__player.play() + self.set_temporary_status_message("Started the Video!", 3000) + elif self.__player.state() == QtMultimedia.QMediaPlayer.StoppedState: + self.__player.play() + self.set_temporary_status_message("Restarted the Video!", 3000) + + elif type(self.__pixmap) == QMovie: + if self.__pixmap.state() == QMovie.Paused: + self.__pixmap.start() + self.set_temporary_status_message("Started the Video!", 3000) + elif self.__pixmap.state() == QMovie.Running: + self.__pixmap.setPaused(True) + self.set_temporary_status_message("Paused the Video!", 3000) + + def on_movie_player_state_changed(self, state: int): + self.__image_resize() + if QtMultimedia.QMediaPlayer.StoppedState == state: # player stopped + self.set_temporary_status_message("Reached end of Video!", 2000) + + def on_movie_position_changed(self, position): + pass + + def on_movie_frame_changed(self, frame_number: int): + if type(self.__pixmap) != QMovie: + return + if frame_number == 0: + self.set_temporary_status_message("Reached end of Video!", 2000) + self.__pixmap.setPaused(True) + + def on_save_clicked(self): + print("Clicked Save!") + self.set_image_title_link() + self.save_changes() + + def on_import_tags_clicked(self): + print("Clicked Import!") + dialog = TagImportDialog(self) + if len(self.get_image_link_from_line()) == 0 or self.get_image_link_from_line() == '(Unknown)': + url = LinkGenerator.get_instance().construct_link(self.curr_file_name, + LinkGenerator.get_instance() + .predict_domain(self.curr_file_name)) + self.ui.link_line.setText(url) # Update no link to the predicted link + else: + url = self.get_image_link_from_line() + + r = self.__main.scrape_tags(self.curr_file_name, url=url, + art_ID=self.curr_art_id) + + if r is None: + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Unsupported Domain") + msg.setInformativeText("Could not predict a supported domain!") + msg.setIcon(QtWidgets.QMessageBox.Warning) + msg.exec_() + return + + self.ui.link_line.setText(url) + self.set_image_title_link() + tags, artists = r + + i = 0 + while i < len(tags): # workaround for an issue with altering lists during iteration + r = self.__main.db_connection.get_tag_by_name(tags[i]) + if len(r) > 0: + self.curr_tags.append(tags[i]) + self.data_changed = True + tags.remove(tags[i]) + continue + else: + i += 1 + self.set_tag_list(self.curr_tags) + + if len(tags) == 0: + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Nothing to import!") + msg.setInformativeText("There were no tags to import for this art!") + msg.setIcon(QtWidgets.QMessageBox.Information) + msg.exec_() + return + dialog.set_import_tag_list(tags) + dialog.set_detected_artists(artists) + dialog.set_used_link(url) + dialog.to_import = tags + + result = dialog.exec_() + if result is None: + self.set_tag_list(self.curr_tags) + return + + self.__main.import_tags(self.curr_art_id, result) + self.set_tag_list(self.curr_tags) + + def on_next_clicked(self): + print("Clicked Next!") + if not self.check_save_changes(): + return + + self.__main.curr_image_index += 1 + if self.__main.curr_image_index >= len(self.__main.all_images): + self.__main.curr_image_index = 0 + self.__main.change_image() + + if self.presence_docker_open: + self.toggle_presence_docker() + self.on_tag_search_change() + + def on_image_title_change(self): + self.data_changed = True + self.curr_image_title = self.ui.image_title_line.text() + + def on_previous_clicked(self): + print("Clicked previous!") + if not self.check_save_changes(): + return + + self.__main.curr_image_index -= 1 + if self.__main.curr_image_index < 0: + self.__main.curr_image_index += len(self.__main.all_images) + + self.__main.change_image() + + if self.presence_docker_open: + self.toggle_presence_docker() + self.on_tag_search_change() + + def toggle_presence_docker(self): + print("Clicked presence docker button!") + if not self.presence_docker_open: + self.presence_docker = PresenceDocker(self) + self.ui.presence_docker_layout.addWidget(self.presence_docker) + self.presence_docker.set_selected_presences_list(self.get_current_presences()) + self.ui.presence_docker_button.setText(">") + self.presence_docker_open = True + else: + self.presence_docker.setParent(None) + self.ui.presence_docker_button.setText("<") + + self.presence_docker.destroy() + self.presence_docker = None + self.presence_docker_open = False + + def on_artnet_root_change_clicked(self): + print("Clicked changing ArtNet root!") + dialog = QtWidgets.QFileDialog(self, 'Choose new ArtNet root:') + dialog.setFileMode(QtWidgets.QFileDialog.Directory) + dialog.setOptions(QtWidgets.QFileDialog.ShowDirsOnly) + + directory = dialog.getExistingDirectory() + self.__main.change_root(directory) + + def on_db_connection_change_clicked(self): + print("Clicked db connection change!") + dialog = DBDialog(self) + prev_db_data = self.__main.get_db_connection_details() + dialog.ui.user_line_edit.setText(prev_db_data["user"]) + dialog.ui.password_line_edit.setText(prev_db_data["password"]) + dialog.ui.host_line_edit.setText(prev_db_data["host"]) + dialog.ui.database_line_edit.setText(prev_db_data["database"]) + dialog.ui.port_line_edit.setText(str(prev_db_data["port"])) + + db_data: dict = dialog.exec_() + if len(db_data.keys()) == 0: + return + self.__main.change_db_connection(host=db_data["host"], port=db_data["port"], + user=db_data["user"], password=db_data["password"], + database=db_data["database"]) + + def on_tag_creation_clicked(self): + print("Clicked Tag Creation!") + dialog = TagModifyDialog(self, create_tag=True) + + tag_data: dict = dialog.exec_() + print("Got Tag data", tag_data) + if tag_data is None or len(tag_data.keys()) == 0: # got canceled? + return + + if len(self.get_tag(tag_data["name"])) > 0: + QtWidgets.QMessageBox.information(self, "Duplicate Tag", "The tag \"{0}\" already exists in the db!" + .format(tag_data["name"])) + return + self.__main.db_connection.create_tag(name=tag_data["name"], description=tag_data["description"], + aliases=tag_data["aliases"], implications=tag_data["implications"], + category=tag_data["category"]) + self.on_tag_search_change() + + def on_tag_deletion_clicked(self): + print("Clicked Tag Deletion!") + dialog = TagSelectDialog(self, delete_tag=True) + + tag = dialog.exec_() + print("Got Tag", tag) + if tag is None or len(tag) == 0: + return + + confirmation_reply = QtWidgets.QMessageBox.question(self, "Delete Tag \"{0}\"".format(tag["name"]), + "Are you sure you want to delete this Tag?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No) + if confirmation_reply == QtWidgets.QMessageBox.No: + return + + self.__main.db_connection.remove_tag(tag["name"]) + self.on_tag_search_change() + + def force_edit_tag_dialog(self, name: str): + + edit_dialog = TagModifyDialog(self, create_tag=True) + edit_dialog.ui.tag_name_line.setText(name) + edit_dialog.ui.tag_description_area.setText("") + edit_dialog.set_selected_alias_tags([], set_checked=True) + edit_dialog.alias_selection = [] + edit_dialog.set_selected_implicated_tags([], set_checked=True) + edit_dialog.implication_selection = [] + edit_dialog.category_selection = [] + edit_dialog.set_all_categories() + + tag_data = edit_dialog.exec_() + print("Got Tag data", tag_data) + if tag_data is None or len(tag_data.keys()) == 0: + return None + if len(tag_data["category"]) == 0: + answer = QtWidgets.QMessageBox.information(self, "No Category", + "There has been no Category selected for this tag! " + "No tag is allowed without a category!") + return None + + if len(self.get_tag(tag_data['name'])) > 0: + QtWidgets.QMessageBox.information(self, "Tag already exists", + "The Tag \"{0}\" you wanted to create already exists! Skipping...") + else: + self.__main.db_connection.create_tag(name=tag_data["name"], description=tag_data["description"], + aliases=tag_data["aliases"], implications=tag_data["implications"], + category=tag_data["category"]) + self.on_tag_search_change() + return tag_data + + def on_tag_edit_clicked(self): + print("Clicked Tag Editing!") + select_dialog = TagSelectDialog(self, delete_tag=False) + + tag = select_dialog.exec_() + if tag is None or len(tag) == 0: + return + + tag['aliases'] = self.__main.db_connection.get_tag_aliases(tag["name"]) + tag['implications'] = self.__main.db_connection.get_tag_implications(tag["name"]) + + edit_dialog = TagModifyDialog(self, create_tag=False) + edit_dialog.ui.tag_name_line.setText(tag["name"]) + edit_dialog.ui.tag_description_area.setText(tag["description"]) + edit_dialog.set_selected_alias_tags(tag["aliases"], set_checked=True) + edit_dialog.alias_selection = tag["aliases"] + edit_dialog.set_selected_implicated_tags(tag["implications"], set_checked=True) + edit_dialog.implication_selection = tag["implications"] + edit_dialog.category_selection = tag["category"] + edit_dialog.set_all_categories() + + tag_data = edit_dialog.exec_() + print("Got Tag data", tag_data) + if tag_data is None or len(tag_data.keys()) == 0: + return + + if "old_tag_name" not in tag_data.keys(): + tag_data["old_tag_name"] = None + + self.__main.db_connection.edit_tag(name=tag_data["name"], description=tag_data["description"], + aliases=tag_data["aliases"], implications=tag_data["implications"], + category=tag_data["category"], old_tag=tag_data["old_tag_name"]) + self.on_tag_search_change() + + def on_tag_search_item_changed(self, item: QStandardItem): + if item.checkState() == Qt.Checked: + self.curr_tags.append(item.text()) + if item.checkState() == Qt.Unchecked: + if item.text() in self.curr_tags: + self.curr_tags.remove(item.text()) + else: + return + self.set_tag_list(self.curr_tags) + + def on_tag_item_changed(self, item: QStandardItem): + print("Item {0} has changed!".format(item.text())) + if item.checkState() == Qt.Unchecked: + if item.text() in self.curr_tags: + self.curr_tags.remove(item.text()) + self.set_tag_list(self.curr_tags) + self.on_tag_search_change() + else: + raise Exception("Something went terribly wrong!") + + def on_tag_search_change(self): + tags = self.__main.db_connection.search_fuzzy_tag(self.ui.tag_search_bar.text(), all_if_empty=True) + + result = [] + for tag_name, tag_desc, tag_category in tags: + result.append(tag_name) + + self.set_search_result_list(result) + + def on_category_creation_clicked(self): + dialog = CategoryModDialog(self, delete_category=False) + + data = dialog.exec_() + if data is None: + return + + self.__main.db_connection.save_category(data["name"]) + + def on_category_deletion_clicked(self): + dialog = CategoryModDialog(self, delete_category=True) + + data = dialog.exec_() + if data is None: + return + + self.__main.db_connection.remove_category(data["name"]) + + def on_link_line_change(self): + self.data_changed = True + + def on_prev_unknown_image_clicked(self): + unknown_image_index = self.__main.get_prev_unknown_image() + print("Previous unknown image clicked!") + + result = QtWidgets.QMessageBox.question(self, "Switch Image?", + "Do you really want to skip to image #{1} \"{0}\"?" + .format(self.__main.all_images[unknown_image_index], + unknown_image_index)) + if result == QtWidgets.QMessageBox.Yes: + self.__main.curr_image_index = unknown_image_index + self.__main.change_image() + + def on_next_unknown_image_clicked(self): + unknown_image_index = self.__main.get_next_unknown_image() + print("Next unknown image clicked!") + + result = QtWidgets.QMessageBox.question(self, "Switch Image?", + "Do you really want to skip to image #{1} \"{0}\"?" + .format(self.__main.all_images[unknown_image_index], + unknown_image_index)) + if result == QtWidgets.QMessageBox.Yes: + self.__main.curr_image_index = unknown_image_index + self.__main.change_image() + + def on_image_id_spinbox_changed(self, v: int): + if self.__tmp_imageid_spinbox == v: + print("SpinBox change detected!") + result = QtWidgets.QMessageBox.question(self, "Switch Image?", + "Do you really want to skip to image #{1} \"{0}\"?" + .format(self.__main.all_images[v], + v)) + if result == QtWidgets.QMessageBox.Yes: + self.__main.curr_image_index = v + self.__main.change_image() + + self.__tmp_imageid_spinbox: int = v + + def on_delete_image_clicked(self): + print("Delete clicked!") + art_hash = self.__main.get_md5_of_image(self.curr_art_path) + if self.__main.db_connection.get_art_by_hash(art_hash) is not None: + print("Delete on known image") + confirm_result = QtWidgets.QMessageBox.question(self, "Delete data?", "Do you really wish to delete all " + "data from the DB about this image?") + + if confirm_result == QtWidgets.QMessageBox.Yes: + print(f"deleting image data of \"{self.curr_image_title}\"") + self.__main.db_connection.remove_image(art_hash) + else: + return + + else: + print("Delete on unknown image") + confirm_result = QtWidgets.QMessageBox.question(self, "Delete image?", "Do you really wish to delete this " + "image?") + + if confirm_result == QtWidgets.QMessageBox.Yes: + print("deleting image file") + self.__main.delete_image(self.curr_art_path) + else: + return + + self.__main.change_image() diff --git a/ArtNet/web/__init__.py b/ArtNet/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ArtNet/web/link_generator.py b/ArtNet/web/link_generator.py new file mode 100644 index 0000000..5de42c6 --- /dev/null +++ b/ArtNet/web/link_generator.py @@ -0,0 +1,169 @@ +import re +import requests +import lxml.html +from urllib.parse import urlparse + +DOMAIN_UNKNOWN = -1 + + +class DomainIdentifier: + """ + Makeshift-automated enum class to allow runtime expansion of an enum. + + Create an instance to get a new unique domain identifier that can resolve to an int. + + TODO find better way :D + """ + + __domain_list = [] + + def __init__(self, name: str): + self.__name = name + self.__domain_number = len(DomainIdentifier.__domain_list) + + if DomainIdentifier.get_identifier(name) is not None: + raise Exception("The domain \"{0}\" is already in the list! Tried to create a duplicate!".format(name)) + + DomainIdentifier.__domain_list.append(self) + + @property + def name(self) -> str: + return self.__name + + @property + def identifier(self) -> int: + return self.__domain_number + + @staticmethod + def get_identifier(name: str) -> "DomainIdentifier": + for d in DomainIdentifier.__domain_list: + if d.name == name: + return d + return None + + +class DomainLinkGenerator: + + def __init__(self, domain: DomainIdentifier): + self.__identifier = domain + + def match_file_name(self, file_name) -> bool: + """ + Checks if a given file name is plausible to be used by the given domain + :param file_name: + :return: + """ + raise NotImplementedError + + def get_domain_name(self) -> str: + return self.__identifier.name + + def get_identifier(self) -> int: + """ + Return the Identifier for this Predictors domain. + :return: + """ + return self.__identifier.identifier + + def construct_link(self, file_name: str) -> str: + """ + Construct a link by inserting the file_name into the known link pattern + :param file_name: + :return: + """ + raise NotImplementedError + + def scrape_tags(self, url: str, headers: dict) -> list: + """ + Scrape the tags from the given url for all tags associated with the work. + :param url: + :param headers: + :return: + """ + raise NotImplementedError + + +class LinkGenerator: + """ + Predict and generate valid links to the file + by matching the given file names against known patterns of the origin domain. + """ + + __instance = None # Singleton holder + + def __init__(self): + self.__link_generators = [] + import ArtNet.web.domains # implements return_all_domains() which returns instances of all domains + # return_all_domains() is to return a list of all DomainLinkGenerator instances that are to be used + + for p in ArtNet.web.domains.return_all_domains(): + self.register_domain_predictor(p) + + @staticmethod + def get_instance() -> "LinkGenerator": + """ + Gets the current instance + :return: + """ + if LinkGenerator.__instance is None: + LinkGenerator.__instance = LinkGenerator() + return LinkGenerator.__instance + + def register_domain_predictor(self, predictor: DomainLinkGenerator): + """ + Register another DomainValidator to be used by this LinkPredictor + :param predictor: + :param domain: int identifier for the domain + :return: + """ + if predictor not in self.__link_generators: + self.__link_generators.append(predictor) + + def predict_domain(self, file_name: str) -> int: + """ + Predict the possible domains of the given file by guessing via the filename + :param file_name: + :return: + """ + for g in self.__link_generators: + try: + if g.match_file_name(file_name): # TODO stop accepting first match + return g.get_identifier() + except NotImplementedError: + pass + return DOMAIN_UNKNOWN + + def construct_link(self, file_name: str, domain: int) -> str: + """ + Construct a valid link to access the web page where the file name (hopefully) originated from. + :param file_name: + :param domain: + :return: + """ + for g in self.__link_generators: + if g.get_identifier() == domain: # TODO stop accepting first match + try: + return g.construct_link(file_name) + except NotImplementedError: + return None + + return None + + def scrape_tags(self, url: str, domain: int) -> dict: + """ + Scrapes the tags from the given url + :param url: + :param domain: + :return: + """ + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:76.0) Gecko/20100101 Firefox/76.0" + } + url_domain = urlparse(url).netloc + for g in self.__link_generators: + if g.get_identifier() == domain or g.get_domain_name() == url_domain: + try: + return g.scrape_tags(url, headers) + except NotImplementedError: + pass + return None diff --git a/README.md b/README.md new file mode 100644 index 0000000..7092126 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# ArtNet + +ArtNet is a database schema to tag and organize images, videos and +other media files for easy search & traceability. +Create your very own reference database! + +## Dependencies +The database schema has been developed with [PostgreSQL 13](https://www.postgresql.org/). + +The GUI runs with Python 3.9 as well as: + * PyQT5 + * PyYAML + * other dependencies listed in requirements.txt + +## Features + +### Editor GUI +The GUI allows easy editing, creation and deletion of tags, art entries, +presence entries and artists while viewing the file in question. + +The GUI can connect to different databases via +"ArtNet" > "Change DB Connection" . +The login credentials are saved in the directory `.artnet` +located in the current working directory. + +**Important:** Configuration Encryption is experimental and **unsafe**! + +While the GUI makes an attempt to encrypt the configuration +file, the password for encryption is, as of writing, inside the source code. +The login data should therefore to be treated as plain text and +additionally secured in better ways such as proper file permissions. + +### Tagging +**You can:** + * create, edit & delete your own tags + * imply tags with other tags (e.g. *banana* implies *fruit*) + * create aliases for different tags that mean the same (e.g. *posing* being an alias to *pose*) + * categorize your tags + * create modules to import tagging from known sources + +## Concepts & Names + +### ArtNet Root +The root folder in which your references are stored. +Ultimately the structure is up to you but it is assumed for +new entries to be somewhat like this: +``` +ArtNetRoot/ + presence_name_A/ + image_A.jpg + vid_B.mp4 + presence_name_B/ + image_D.jpg + presence_name_C/ + image_C.jpg +``` +A structure like this will allow to the GUI to show a correct guess +for the presence name when encountering an unknown art entry. + +### Presences & Authors +In ArtNet you can create a presence for every account an artist +has on different or the same website. These presences are connected to +an Artist entry and therefore allow the connection of many accounts to the same artist. + +A presence consists of the presence name and the domain it is used on as well as an +optional direct link to the account. + +Multiple presences can be associated with any given art allowing for correct +attribution for collaborations and similar. + +-- TODO add artist_presence_relation_example_diagram here -- + +### Art +Basically the files to be organized and tagged. +Each art piece is a file associated with many tags, a source link and +one or more presences. + +### Tag +A tag is keyword that is later to be associated with Art. + +#### Aliases +Each tag can have aliases which are to be considered equal to +the current tag and therefore also associated with the art. +This relation is bidirectional. + +#### Implications +Each tag can also imply other tags, meaning when +the current tag is associated with an art piece the implied tags +are to be considered associated too but not vice versa. +This relation is unidirectional. diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..80ae341 --- /dev/null +++ b/__main__.py @@ -0,0 +1,10 @@ +from ArtNet.artnet_manager import ArtNetManager + +# TODO fix DB bug +# TODO 1. Open known image +# TODO 2. edit a tag on the current image & save the edit +# TODO 3. attempt to save the current image + +if __name__ == "__main__": + am = ArtNetManager() + am.run()