import validators
import os
import logging
import re
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QSize, QUrl
from PyQt5.QtGui import QPixmap, QResizeEvent, QKeyEvent, QStandardItemModel, QStandardItem, QMovie, QDesktopServices, \
QIcon, QCursor
from PyQt5 import QtMultimedia
from PyQt5.QtMultimediaWidgets import QVideoWidget
from ArtNet.gui.windows.importer.picture_importer import Ui_MainWindow
from ArtNet.gui.windows.artnet_mainwindow import ArtnetMainWindow
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.gui.dialogs.link_input_dialog.link_dialog import LinkInputDialog
from ArtNet.gui.dialogs.collection_select_dialog.collection_sel_dialog import CollectionSelectDialog
from ArtNet.gui.dialogs.collection_modify_dialog.collection_dialog import CollectionModDialog
from ArtNet.web.link_generator import LinkGenerator
class ImporterWindow(ArtnetMainWindow):
UNKNOWN_TITLE = "-Title Unknown-"
UNKNOWN_PRESENCE = "(Not in Database)"
UNKNOWN_LINK = "No Source Available"
NO_COLLECTION = "No Collections"
def __init__(self, main):
super().__init__(main)
self.__pixmap: QPixmap = None
self.__video: QVideoWidget = None
self.__player: QtMultimedia.QMediaPlayer = None
self.__text_player: QtWidgets.QTextEdit = None
self.__showing_video: bool = False
self.__showing_text: 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_implied_tags: list = list()
self.curr_tag_aliases: list = list()
self.curr_collections: list = list()
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.actionArtNet_Browser.triggered.connect(self.on_browser_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.collections_button.clicked.connect(self.on_collection_button_clicked)
self.ui.delete_button.clicked.connect(self.on_delete_image_clicked)
self.ui.link_button.clicked.connect(self.on_source_link_button_clicked)
self.ui.link_label.linkActivated.connect(self.on_link_label_activated)
self.ui.image_file_label.linkActivated.connect(self.on_link_file_label_activated)
self.ui.image_author_label.linkActivated.connect(self.on_image_author_label_activated)
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.description_edit.textChanged.connect(self.on_description_change)
self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK)
self.ui.description_edit.setReadOnly(False)
self.ui.image_label.setContextMenuPolicy(Qt.CustomContextMenu)
self.ui.image_label.customContextMenuRequested.connect(self.on_custom_context_menu_requested)
self.on_tag_search_change()
self.center()
if os.path.isfile("./application_icon.png"):
self.setWindowIcon(QIcon("./application_icon.png"))
else:
logging.warning("Didn't find application icon!")
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(int((screen.width() - size.width()) / 3), int((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:
"""
new_title = self.curr_image_title
new_description = self.ui.description_edit.toPlainText().strip()
if new_title == self.curr_file_name or len(new_title) == 0 or new_title == ImporterWindow.UNKNOWN_TITLE:
new_title = None
if new_description is None or len(new_description) == 0:
new_description = None
else:
new_description = new_description.strip()
image_data = {
"ID": self.curr_art_id,
"title": new_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),
"description": new_description,
"collections": self.curr_collections,
}
for presence in self.curr_presences:
if presence[-1] == ImporterWindow.UNKNOWN_PRESENCE:
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
for coll_id, coll_name in self.curr_collections:
msg = QtWidgets.QMessageBox()
msg.setWindowTitle("Could not save collection!")
msg.setInformativeText("The saving and proper selection of collections is not supported as of now!")
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.exec_()
break
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"], desc=image_data["description"],
collections=image_data["collections"])
self.set_temporary_status_message("Saved {0} ({1}) to ArtNet DB!"
.format(image_data["title"], image_data["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 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 == ImporterWindow.UNKNOWN_PRESENCE:
presences.remove((name, domain))
elif len(presences) == 0:
presences = [(self.curr_art_path.split("/")[0], ImporterWindow.UNKNOWN_PRESENCE)]
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_current_collections(self) -> list:
"""
Get the collections currently associated with the current art
"""
return self.curr_collections
def set_current_collections(self, collections: list):
"""
Set the collections associated with the current art
:param collections: List[Tuple[coll_id, coll_name]]
"""
self.curr_collections = collections
if len(collections) == 0:
self.ui.collections_label.setText(ImporterWindow.NO_COLLECTION)
else:
s = ""
for coll_id, coll_name, coll_desc in collections:
s += "{1}".format("", f"#{coll_id}:{coll_name}")
# currently not linking to anything
s += "|"
s = s[:-1]
self.ui.collections_label.setText(s)
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 create_collection(self, collection_name: str, description: str = None) -> int:
"""
Create a new collection with the given name and description
Returns the assigned ID of the collection
"""
return self.main.db_connection.create_collection(collection_name, description)
def get_collection(self, collection_id: int):
"""
Fetch the collection given by the id. If not present None is returned.
"""
return self.main.db_connection.get_collection(collection_id)
def delete_collection(self, collection_id: int):
"""
Delete the collection given by the id from the database.
"""
self.main.db_connection.delete_collection(collection_id)
def search_collections(self, collection_name: str = None, presence_name: str = None,
presence_domain: str = None, art_name: str = None):
"""
Search for collections that fit the given parameters. All parameters are searched fuzzy.
"""
return self.main.db_connection.search_collections(collection_name=collection_name,
presence_name=presence_name, presence_domain=presence_domain,
art_name=art_name)
def get_image_link_from_label(self) -> str:
"""
Gets the image link from the label if it is a valid link.
Otherwise an empty string
:return:
"""
result = re.match('[ a-zA-Z<=>"]*((https|http)://[a-zA-Z0-9]+\.[a-zA-Z0-9]+/[a-zA-Z0-9/]+)',
self.ui.link_label.text())
if result is not None:
result = result.groups()[0]
else:
result = ""
return result
def set_presence_label_text(self, presences: list):
"""
Set the label listing all current presences and include links if possible.
:param presences:
:return:
"""
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, link: str) -> str:
"""
Sets the Image title to a link if there is link data given for this image.
:return: Returns the result that has been set to see if a successful link was constructed
"""
self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK)
if link is not None and validators.url(link):
self.curr_link = link
self.data_changed = True
hyperlink = "{1}".format(link, "Source")
self.ui.link_label.setText(hyperlink)
self.ui.link_label.setToolTip(link)
return link
elif link is None or len(link) == 0:
return ""
else:
self.ui.link_label.setText(ImporterWindow.UNKNOWN_LINK)
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_by_name(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_tag_search_result_list(self, tags: list):
"""
Set the tags in the search result list to tags
:param tags:
:return:
"""
tags.sort(key=lambda x: x["name"])
item_model = QStandardItemModel(self.ui.search_result_list)
for tag in tags:
tag_name = tag["name"]
item = QStandardItem(tag_name)
flags = Qt.ItemIsEnabled
if tag_name not in self.curr_implied_tags:
# new tag and not implied yet
item.setData(Qt.Unchecked, Qt.CheckStateRole)
item.setToolTip(f"Tag ID: {tag['id']}\n\n"+tag["description"]+f"\nCategory: {tag['category']}")
flags |= Qt.ItemIsUserCheckable
if self.curr_tags is not None and tag_name in (self.curr_tags + self.curr_implied_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_description_text(self, text: str):
"""
Set the text for the description field
"""
self.ui.description_edit.setText(text)
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: not used
:return:
"""
tags.sort(key=lambda x: x["name"])
self.curr_tags = tags
item_model = QStandardItemModel(self.ui.tag_list)
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_by_name(x["name"])
self.set_implied_list(implied_tags)
for tag in tags:
tag_name = tag["name"]
item = QStandardItem(tag_name)
flags = Qt.ItemIsEnabled
if tag_name not in self.curr_implied_tags:
# new tag and not implied yet
item.setData(Qt.Unchecked, Qt.CheckStateRole)
item.setToolTip(f"Tag ID: {tag['id']}\n\n" + tag["description"] + f"\nCategory: {tag['category']}")
flags |= Qt.ItemIsUserCheckable
if set_checked:
if tag_name not in self.curr_implied_tags:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
item.setFlags(flags)
item_model.appendRow(item)
item_model.itemChanged.connect(self.on_tag_item_changed)
self.ui.tag_list.setModel(item_model)
self.data_changed = True
def set_implied_list(self, tags: list):
"""
Sets the implied tags in the imply list
:param tags:
:return:
"""
self.curr_implied_tags = tags
item_model = QStandardItemModel(self.ui.implied_tag_list)
done = []
for tag in tags:
tag_name = tag["name"]
if tag_name in done:
continue
else:
done.append(tag_name)
item = QStandardItem(tag_name)
item.setToolTip(f"Tag ID: {tag['id']}\n\n"+tag["description"]+f"\nCategory: {tag['category']}")
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, description: str, collections: list):
"""
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:
:param description:
:param collections:
: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_collections(collections)
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 self.__showing_text: # remove text are from image layout
self.__text_player.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", "mp4", "mov", "mkv"]:
self.__showing_video = True
if isinstance(self.__video, QVideoWidget):
self.__video.show()
else:
self.__video = QVideoWidget() if self.__video is None else self.__video
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)
elif file_ending in ["txt"]: # for stories or text files
self.__showing_text = True
self.ui.image_label.hide()
self.__text_player = QtWidgets.QTextEdit() if self.__text_player is None else self.__text_player
with open(full_path, "r") as text_file:
story = text_file.read()
text_file.close()
self.__text_player.setText(story)
self.__text_player.setReadOnly(True)
self.ui.image_frame.layout().addWidget(self.__text_player)
self.__text_player.show()
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.image_file_label.setText("{1}"
.format(f"file:///{full_path[:-1 * len(self.curr_file_name)]}",
file_name))
self.ui.image_file_label.setToolTip(relative_path)
self.ui.description_edit.setText(description)
self.set_image_title_link(link)
self.set_image_id_spinbox()
self.data_changed = False # reset any triggered change detection
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(ImporterWindow, 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:
logging.debug("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)
elif QtWidgets.QApplication.focusWidget() == self.ui.tag_search_bar: # quick select for the search bar
if self.ui.search_result_list.model().rowCount() == 1: # only 1 search result left
model_index = self.ui.search_result_list.model().index(0, 0)
item_data = self.ui.search_result_list.model().itemData(model_index)
if item_data[0] not in self.curr_tags: # add/remove new selected tag to the lists
self.curr_tags.append(item_data[0])
else:
self.curr_tags.remove(item_data[0])
self.set_tag_list(self.curr_tags) # update relevant lists
self.set_tag_search_result_list([item_data[0]])
logging.debug(item_data)
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):
logging.info("Clicked Save!")
self.save_changes()
def on_import_tags_clicked(self):
logging.info("Clicked Import!")
dialog = TagImportDialog(self)
if len(self.get_image_link_from_label()) == 0 \
or self.get_image_link_from_label() == ImporterWindow.UNKNOWN_LINK:
url = LinkGenerator.get_instance().construct_link(self.curr_file_name,
LinkGenerator.get_instance()
.predict_domain(self.curr_file_name))
self.set_image_title_link(url) # Update no link to the predicted link
else:
url = self.get_image_link_from_label()
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.set_image_title_link(url)
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(result)
self.set_tag_list(self.curr_tags)
def on_next_clicked(self):
logging.info("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.refresh_shown_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):
logging.info("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.refresh_shown_image()
if self.presence_docker_open:
self.toggle_presence_docker()
self.on_tag_search_change()
def toggle_presence_docker(self):
if not self.presence_docker_open:
logging.info("Opened presence docker!")
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.setArrowType(Qt.RightArrow)
self.presence_docker_open = True
else:
logging.info("Closed presence docker!")
self.presence_docker.setParent(None)
self.ui.presence_docker_button.setArrowType(Qt.LeftArrow)
self.presence_docker.destroy()
self.presence_docker = None
self.presence_docker_open = False
def on_artnet_root_change_clicked(self):
logging.info("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):
logging.info("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
try:
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"])
except ValueError as e: # details were wrong (probably)
QtWidgets.QMessageBox.warning(self, "Wrong Credentials?", f"Error: {e}")
def on_tag_creation_clicked(self):
logging.info("Clicked Tag Creation!")
dialog = TagModifyDialog(self, create_tag=True)
tag_data: dict = dialog.exec_()
logging.debug(f"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()
implied_tags = [] # refresh implied tags, the edit/addition might have changed smth
for x in self.curr_tags:
# collect all implied tags into a list
implied_tags += self.main.db_connection.get_all_tag_implications_by_name(x)
def on_tag_deletion_clicked(self):
logging.info("Clicked Tag Deletion!")
dialog = TagSelectDialog(self, delete_tag=True)
tag = dialog.exec_()
logging.debug("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_by_name(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_()
logging.debug(f"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):
logging.info("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_by_name(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 = self.main.db_connection.get_category_by_ID(tag["category_id"])[1]
edit_dialog.set_all_categories()
tag_data = edit_dialog.exec_()
logging.debug(f"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(tag_id=tag["ID"],
name=tag_data["name"], description=tag_data["description"],
aliases=tag_data["aliases"], implications=tag_data["implications"],
category_id=self.main.db_connection.get_category_by_name(tag_data["category"])[
0])
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())
aliases = self.main.db_connection.get_tag_aliases_by_name(item.text())
for alias in aliases:
self.curr_tags.append(alias)
implications = self.main.db_connection.get_all_tag_implications_by_name(item.text())
for implication in implications:
self.curr_tags.append(implication)
if item.checkState() == Qt.Unchecked:
if item.text() in self.curr_tags:
self.curr_tags.remove(item.text())
aliases = self.main.db_connection.get_tag_aliases_by_name(item.text())
for alias in aliases:
if alias in self.curr_tags:
self.curr_tags.remove(alias)
implications = self.main.db_connection.get_all_tag_implications_by_name(item.text())
for implication in implications:
self.curr_tags.remove(implication)
else:
raise Exception("Something went terribly wrong!")
self.set_tag_list(self.curr_tags)
self.on_tag_search_change()
def on_tag_item_changed(self, item: QStandardItem):
logging.debug("Item {0} has changed!".format(item.text()))
if item.checkState() == Qt.Unchecked:
if item.text() in self.curr_tags:
aliases = self.main.db_connection.get_tag_aliases_by_name(item.text())
if len(aliases) > 0: # tag has aliases, might need to also remove them
for alias in aliases:
self.curr_tags.remove(alias)
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, tag_id in tags:
result.append({"name": tag_name, "description": tag_desc, "category": tag_category, "id": tag_id})
self.set_tag_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_prev_unknown_image_clicked(self):
unknown_image_index = self.main.get_prev_unknown_image()
logging.info("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.refresh_shown_image()
def on_next_unknown_image_clicked(self):
unknown_image_index = self.main.get_next_unknown_image()
logging.info("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.refresh_shown_image()
def on_image_id_spinbox_changed(self, v: int):
if self.__tmp_imageid_spinbox == v:
logging.info("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.refresh_shown_image()
self.__tmp_imageid_spinbox: int = v
def on_delete_image_clicked(self):
logging.info("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:
logging.debug("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:
logging.info(f"deleting image data of \"{self.curr_image_title}\"")
self.main.db_connection.remove_image(art_hash)
else:
return
else:
logging.debug("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:
logging.info(f"deleting image file {self.curr_art_path}")
self.main.delete_image(self.curr_art_path)
else:
return
self.main.refresh_shown_image()
def on_link_label_activated(self, link: str):
logging.debug(f"Source link activated! {link}")
QDesktopServices.openUrl(QUrl(link))
def on_image_author_label_activated(self, link: str):
logging.debug(f"Image author link activated! {link}")
QDesktopServices.openUrl(QUrl(link))
def on_link_file_label_activated(self, link: str):
logging.debug(f"File label link activated! {link}")
QDesktopServices.openUrl(QUrl(link))
def on_description_change(self):
self.data_changed = True
def on_browser_clicked(self):
logging.debug("Clicked on open ArtNet browser!")
self.main.switch_to_browser()
def on_source_link_button_clicked(self):
logging.debug("Clicked on link button!")
dialog = LinkInputDialog(self, self.curr_link)
link = dialog.exec_()
if link is None: # dialog was cancelled
logging.debug("Cancelled link dialog.")
return
logging.info(f"Setting source link to \"{link}\"")
self.set_image_title_link(link)
def on_collection_button_clicked(self):
logging.debug("Clicked on collection button!")
QtWidgets.QMessageBox.information(self, "Not Implemented", "This feature has not been implemented yet!")
selected_collections = CollectionSelectDialog(self).exec_(self.curr_collections)
selected_collections = [self.get_collection(collection_id=c[0]) for c in selected_collections] # TODO verify that it returns the art-collection details if there are any
for selected_collection in selected_collections: # allow editing the collection rankings
result = CollectionModDialog(self, db_connection=self.main.db_connection, edit_collection=False)\
.exec_(selected_collection)
print()
self.set_current_collections(list(set(list(self.curr_collections + selected_collections))))
def on_custom_context_menu_requested(self):
action = QtWidgets.QAction('Copy URL to clipboard')
action.triggered.connect(self.on_copy_url_to_clipboard)
menu = QtWidgets.QMenu()
menu.addAction(action)
menu.exec_(QCursor.pos())
def on_copy_url_to_clipboard(self):
QtWidgets.QApplication.clipboard().setText(os.path.join(self.main.config.data['file_root'], self.curr_art_path))