import os import typing from qtpy import QtCore as QC from qtpy import QtWidgets as QW from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusSerialisable from hydrus.client import ClientApplicationCommand as CAC from hydrus.client import ClientConstants as CC from hydrus.client import ClientParsing from hydrus.client import ClientSearch from hydrus.client import ClientThreading from hydrus.client.gui import ClientGUIDialogs from hydrus.client.gui import QtPorting as QP from hydrus.client.gui.lists import ClientGUIListBoxes from hydrus.client.gui.lists import ClientGUIListBoxesData from hydrus.client.gui.parsing import ClientGUIParsingLegacy from hydrus.client.gui.widgets import ClientGUICommon from hydrus.client.media import ClientMedia from hydrus.client.metadata import ClientTags from hydrus.client.metadata import ClientTagSorting def FilterSuggestedPredicatesForMedia( predicates: typing.Sequence[ ClientSearch.Predicate ], medias: typing.Collection[ ClientMedia.Media ], service_key: bytes ) -> typing.List[ ClientSearch.Predicate ]: tags = [ predicate.GetValue() for predicate in predicates ] filtered_tags = FilterSuggestedTagsForMedia( tags, medias, service_key ) predicates = [ predicate for predicate in predicates if predicate.GetValue() in filtered_tags ] return predicates def FilterSuggestedTagsForMedia( tags: typing.Sequence[ str ], medias: typing.Collection[ ClientMedia.Media ], service_key: bytes ) -> typing.List[ str ]: num_media = len( medias ) useful_tags_set = set( tags ) ( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediasTagCount( medias, service_key, ClientTags.TAG_DISPLAY_STORAGE ) current_tags_to_count.update( pending_tags_to_count ) # TODO: figure out a nicer way to filter out siblings and parents here # maybe have to wait for when tags always know their siblings # then we could also filter out worse/better siblings of the same count # this is a sync way for now: # db_tags_to_ideals_and_parents = HG.client_controller.Read( 'tag_display_decorators', service_key, tags_to_lookup ) for ( tag, count ) in current_tags_to_count.items(): if count == num_media: useful_tags_set.discard( tag ) tags_filtered = [ tag for tag in tags if tag in useful_tags_set ] return tags_filtered class ListBoxTagsSuggestionsFavourites( ClientGUIListBoxes.ListBoxTagsStrings ): def __init__( self, parent, service_key, activate_callable, sort_tags = True ): ClientGUIListBoxes.ListBoxTagsStrings.__init__( self, parent, service_key = service_key, sort_tags = sort_tags, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) self._activate_callable = activate_callable width = HG.client_controller.new_options.GetInteger( 'suggested_tags_width' ) if width is not None: self.setMinimumWidth( width ) def _Activate( self, ctrl_down, shift_down ) -> bool: if len( self._selected_terms ) > 0: tags = set( self._GetTagsFromTerms( self._selected_terms ) ) self._activate_callable( tags, only_add = True ) self._RemoveSelectedTerms() self._DataHasChanged() return True return False def _Sort( self ): self._RegenTermsToIndices() def ActivateAll( self ): self._activate_callable( self.GetTags(), only_add = True ) def TakeFocusForUser( self ): self.SelectTopItem() self.setFocus( QC.Qt.OtherFocusReason ) class ListBoxTagsSuggestionsRelated( ClientGUIListBoxes.ListBoxTagsPredicates ): def __init__( self, parent, service_key, activate_callable ): ClientGUIListBoxes.ListBoxTagsPredicates.__init__( self, parent, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) self._activate_callable = activate_callable width = HG.client_controller.new_options.GetInteger( 'suggested_tags_width' ) self.setMinimumWidth( width ) def _Activate( self, ctrl_down, shift_down ) -> bool: if len( self._selected_terms ) > 0: tags = set( self._GetTagsFromTerms( self._selected_terms ) ) self._activate_callable( tags, only_add = True ) self._RemoveSelectedTerms() self._DataHasChanged() return True return False def _GenerateTermFromPredicate( self, predicate: ClientSearch.Predicate ) -> ClientGUIListBoxesData.ListBoxItemPredicate: predicate = predicate.GetCountlessCopy() return ClientGUIListBoxesData.ListBoxItemPredicate( predicate ) def _Sort( self ): self._RegenTermsToIndices() def TakeFocusForUser( self ): self.SelectTopItem() self.setFocus( QC.Qt.OtherFocusReason ) class FavouritesTagsPanel( QW.QWidget ): mouseActivationOccurred = QC.Signal() def __init__( self, parent, service_key, media, activate_callable ): QW.QWidget.__init__( self, parent ) self._service_key = service_key self._media = media vbox = QP.VBoxLayout() self._favourite_tags = ListBoxTagsSuggestionsFavourites( self, self._service_key, activate_callable, sort_tags = False ) QP.AddToLayout( vbox, self._favourite_tags, CC.FLAGS_EXPAND_BOTH_WAYS ) self.setLayout( vbox ) self._UpdateTagDisplay() self._favourite_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) def _UpdateTagDisplay( self ): favourites = list( HG.client_controller.new_options.GetSuggestedTagsFavourites( self._service_key ) ) ClientTagSorting.SortTags( HG.client_controller.new_options.GetDefaultTagSort(), favourites ) tags = FilterSuggestedTagsForMedia( favourites, self._media, self._service_key ) self._favourite_tags.SetTags( tags ) def MediaUpdated( self ): self._UpdateTagDisplay() def SetMedia( self, media ): self._media = media self._UpdateTagDisplay() def TakeFocusForUser( self ): self._favourite_tags.TakeFocusForUser() class RecentTagsPanel( QW.QWidget ): mouseActivationOccurred = QC.Signal() def __init__( self, parent, service_key, media, activate_callable ): QW.QWidget.__init__( self, parent ) self._service_key = service_key self._media = media self._last_fetched_tags = [] self._new_options = HG.client_controller.new_options vbox = QP.VBoxLayout() clear_button = QW.QPushButton( 'clear', self ) clear_button.clicked.connect( self.EventClear ) self._recent_tags = ListBoxTagsSuggestionsFavourites( self, self._service_key, activate_callable, sort_tags = False ) QP.AddToLayout( vbox, clear_button, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._recent_tags, CC.FLAGS_EXPAND_BOTH_WAYS ) self.setLayout( vbox ) self._RefreshRecentTags() self._recent_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) def _RefreshRecentTags( self ): def do_it( service_key ): def qt_code( recent_tags ): if not self or not QP.isValid( self ): return self._last_fetched_tags = recent_tags self._UpdateTagDisplay() if len( self._recent_tags.GetTags() ) > 0: self._recent_tags.SelectTopItem() recent_tags = HG.client_controller.Read( 'recent_tags', service_key ) QP.CallAfter( qt_code, recent_tags ) HG.client_controller.CallToThread( do_it, self._service_key ) def _UpdateTagDisplay( self ): tags = FilterSuggestedTagsForMedia( self._last_fetched_tags, self._media, self._service_key ) self._recent_tags.SetTags( tags ) def EventClear( self ): from hydrus.client.gui import ClientGUIDialogsQuick result = ClientGUIDialogsQuick.GetYesNo( self, 'Clear recent tags?' ) if result == QW.QDialog.Accepted: HG.client_controller.Write( 'push_recent_tags', self._service_key, None ) self._last_fetched_tags = [] self._UpdateTagDisplay() def RefreshRecentTags( self ): self._RefreshRecentTags() def MediaUpdated( self ): self._UpdateTagDisplay() def SetMedia( self, media ): self._media = media self._UpdateTagDisplay() self._RefreshRecentTags() def TakeFocusForUser( self ): self._recent_tags.TakeFocusForUser() class RelatedTagsPanel( QW.QWidget ): mouseActivationOccurred = QC.Signal() def __init__( self, parent, service_key, media, activate_callable ): QW.QWidget.__init__( self, parent ) self._service_key = service_key self._media = media self._last_fetched_predicates = [] self._have_done_search_with_this_media = False self._selected_tags = set() self._new_options = HG.client_controller.new_options vbox = QP.VBoxLayout() self._status_label = ClientGUICommon.BetterStaticText( self, label = 'ready' ) self._just_do_local_files = ClientGUICommon.OnOffButton( self, on_label = 'just for my files', off_label = 'for all known files', start_on = True ) self._just_do_local_files.setToolTip( 'Select how big the search is. Searching across all known files on a repository produces high quality results but takes a long time.' ) tt = 'If you select some tags, this will search using only those as reference!' self._button_1 = QW.QPushButton( 'quick', self ) self._button_1.clicked.connect( self.RefreshQuick ) self._button_1.setMinimumWidth( 30 ) self._button_1.setToolTip( tt ) self._button_2 = QW.QPushButton( 'medium', self ) self._button_2.clicked.connect( self.RefreshMedium ) self._button_2.setMinimumWidth( 30 ) self._button_2.setToolTip( tt ) self._button_3 = QW.QPushButton( 'thorough', self ) self._button_3.clicked.connect( self.RefreshThorough ) self._button_3.setMinimumWidth( 30 ) self._button_3.setToolTip( tt ) if HG.client_controller.services_manager.GetServiceType( self._service_key ) == HC.LOCAL_TAG: self._just_do_local_files.setVisible( False ) self._related_tags = ListBoxTagsSuggestionsRelated( self, service_key, activate_callable ) button_hbox = QP.HBoxLayout() QP.AddToLayout( button_hbox, self._button_1, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) QP.AddToLayout( button_hbox, self._button_2, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) QP.AddToLayout( button_hbox, self._button_3, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) QP.AddToLayout( vbox, self._status_label, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._just_do_local_files, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._related_tags, CC.FLAGS_EXPAND_BOTH_WAYS ) self.setLayout( vbox ) self._related_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) def _FetchRelatedTagsNew( self, max_time_to_take ): def do_it( file_service_key, tag_service_key, search_tags ): def qt_code( predicates, num_done, num_to_do, total_time_took ): if not self or not QP.isValid( self ): return if num_to_do == len( search_tags ): tags_s = 'tags' else: tags_s = 'tags ({} had no relations)'.format( HydrusData.ToHumanInt( len( search_tags ) - num_to_do ) ) if num_done == len( search_tags ): num_done_s = 'Searched all {} {} in '.format( HydrusData.ToHumanInt( num_done ), tags_s ) elif num_done == num_to_do: num_done_s = 'Searched {} {} in '.format( HydrusData.ToHumanInt( num_done ), tags_s ) else: num_done_s = '{} {} searched fully in '.format( HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ), tags_s ) label = '{}{}.'.format( num_done_s, HydrusData.TimeDeltaToPrettyTimeDelta( total_time_took ) ) self._status_label.setText( label ) self._last_fetched_predicates = predicates self._UpdateTagDisplay() self._have_done_search_with_this_media = True start_time = HydrusData.GetNowPrecise() ( num_done, num_to_do, predicates ) = HG.client_controller.Read( 'related_tags', file_service_key, tag_service_key, search_tags, max_time_to_take = max_time_to_take ) total_time_took = HydrusData.GetNowPrecise() - start_time predicates = ClientSearch.SortPredicates( predicates ) QP.CallAfter( qt_code, predicates, num_done, num_to_do, total_time_took ) self._related_tags.SetPredicates( [] ) if len( self._selected_tags ) == 0: search_tags = ClientMedia.GetMediasTags( self._media, self._service_key, ClientTags.TAG_DISPLAY_STORAGE, ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING ) ) else: search_tags = self._selected_tags if len( search_tags ) == 0: self._status_label.setVisible( False ) return self._status_label.setVisible( True ) self._status_label.setText( 'searching\u2026' ) if self._just_do_local_files.IsOn(): file_service_key = CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY else: file_service_key = CC.COMBINED_FILE_SERVICE_KEY tag_service_key = self._service_key HG.client_controller.CallToThread( do_it, file_service_key, tag_service_key, search_tags ) def _UpdateTagDisplay( self ): predicates = FilterSuggestedPredicatesForMedia( self._last_fetched_predicates, self._media, self._service_key ) self._related_tags.SetPredicates( predicates ) def RefreshQuick( self ): max_time_to_take = self._new_options.GetInteger( 'related_tags_search_1_duration_ms' ) / 1000.0 self._FetchRelatedTagsNew( max_time_to_take ) def RefreshMedium( self ): max_time_to_take = self._new_options.GetInteger( 'related_tags_search_2_duration_ms' ) / 1000.0 self._FetchRelatedTagsNew( max_time_to_take ) def RefreshThorough( self ): max_time_to_take = self._new_options.GetInteger( 'related_tags_search_3_duration_ms' ) / 1000.0 self._FetchRelatedTagsNew( max_time_to_take ) def MediaUpdated( self ): self._UpdateTagDisplay() def NotifyUserLooking( self ): if not self._have_done_search_with_this_media: self.RefreshQuick() def SetMedia( self, media ): self._media = media self._status_label.setText( 'ready' ) self._related_tags.SetPredicates( [] ) self._have_done_search_with_this_media = False def SetSelectedTags( self, tags ): self._selected_tags = tags def TakeFocusForUser( self ): self._related_tags.TakeFocusForUser() class FileLookupScriptTagsPanel( QW.QWidget ): mouseActivationOccurred = QC.Signal() def __init__( self, parent, service_key, media, activate_callable ): QW.QWidget.__init__( self, parent ) self._service_key = service_key self._media = media self._last_fetched_tags = [] self._script_choice = ClientGUICommon.BetterChoice( self ) self._script_choice.setEnabled( False ) self._have_fetched = False self._fetch_button = ClientGUICommon.BetterButton( self, 'fetch tags', self.FetchTags ) self._fetch_button.setEnabled( False ) self._script_management = ClientGUIParsingLegacy.ScriptManagementControl( self ) self._tags = ListBoxTagsSuggestionsFavourites( self, self._service_key, activate_callable, sort_tags = True ) self._add_all = ClientGUICommon.BetterButton( self, 'add all', self._tags.ActivateAll ) vbox = QP.VBoxLayout() QP.AddToLayout( vbox, self._script_choice, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._fetch_button, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._script_management, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._add_all, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._tags, CC.FLAGS_EXPAND_BOTH_WAYS ) self._SetTags( [] ) self.setLayout( vbox ) self._FetchScripts() self._tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) def _FetchScripts( self ): def do_it(): def qt_code(): if not self or not QP.isValid( self ): return script_names_to_scripts = { script.GetName() : script for script in scripts } for ( name, script ) in list(script_names_to_scripts.items()): self._script_choice.addItem( script.GetName(), script ) new_options = HG.client_controller.new_options favourite_file_lookup_script = new_options.GetNoneableString( 'favourite_file_lookup_script' ) if favourite_file_lookup_script in script_names_to_scripts: self._script_choice.SetValue( script_names_to_scripts[ favourite_file_lookup_script ] ) else: self._script_choice.setCurrentIndex( 0 ) self._script_choice.setEnabled( True ) self._fetch_button.setEnabled( True ) scripts = HG.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP ) QP.CallAfter( qt_code ) HG.client_controller.CallToThread( do_it ) def _SetTags( self, tags ): self._last_fetched_tags = tags self._UpdateTagDisplay() def _UpdateTagDisplay( self ): tags = FilterSuggestedTagsForMedia( self._last_fetched_tags, self._media, self._service_key ) self._tags.SetTags( tags ) if len( tags ) == 0: self._add_all.setEnabled( False ) else: self._add_all.setEnabled( True ) def FetchTags( self ): script = self._script_choice.GetValue() if script.UsesUserInput(): message = 'Enter the custom input for the file lookup script.' with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg: if dlg.exec() != QW.QDialog.Accepted: return file_identifier = dlg.GetValue() else: ( m, ) = self._media file_identifier = script.ConvertMediaToFileIdentifier( m ) stop_time = HydrusData.GetNow() + 30 job_key = ClientThreading.JobKey( cancellable = True, stop_time = stop_time ) self._script_management.SetJobKey( job_key ) self._SetTags( [] ) HG.client_controller.CallToThread( self.THREADFetchTags, script, job_key, file_identifier ) def MediaUpdated( self ): self._UpdateTagDisplay() def SetMedia( self, media ): self._media = media self._UpdateTagDisplay() def TakeFocusForUser( self ): if self._have_fetched: self._tags.TakeFocusForUser() else: self._fetch_button.setFocus( QC.Qt.OtherFocusReason ) def THREADFetchTags( self, script, job_key, file_identifier ): def qt_code( tags ): if not self or not QP.isValid( self ): return self._SetTags( tags ) self._have_fetched = True parse_results = script.DoQuery( job_key, file_identifier ) tags = list( ClientParsing.GetTagsFromParseResults( parse_results ) ) tag_sort = ClientTagSorting.TagSort( ClientTagSorting.SORT_BY_HUMAN_TAG, sort_order = CC.SORT_ASC ) ClientTagSorting.SortTags( tag_sort, tags ) QP.CallAfter( qt_code, tags ) class SuggestedTagsPanel( QW.QWidget ): mouseActivationOccurred = QC.Signal() def __init__( self, parent, service_key, media, activate_callable ): QW.QWidget.__init__( self, parent ) self._service_key = service_key self._media = media self._new_options = HG.client_controller.new_options layout_mode = self._new_options.GetNoneableString( 'suggested_tags_layout' ) self._notebook = None if layout_mode == 'notebook': self._notebook = ClientGUICommon.BetterNotebook( self ) panel_parent = self._notebook else: panel_parent = self panels = [] self._favourite_tags = None favourites = HG.client_controller.new_options.GetSuggestedTagsFavourites( self._service_key ) if len( favourites ) > 0: self._favourite_tags = FavouritesTagsPanel( panel_parent, service_key, media, activate_callable ) self._favourite_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) panels.append( ( 'favourites', self._favourite_tags ) ) self._related_tags = None if self._new_options.GetBoolean( 'show_related_tags' ): self._related_tags = RelatedTagsPanel( panel_parent, service_key, media, activate_callable ) self._related_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) panels.append( ( 'related', self._related_tags ) ) self._file_lookup_script_tags = None if self._new_options.GetBoolean( 'show_file_lookup_script_tags' ) and len( media ) == 1: self._file_lookup_script_tags = FileLookupScriptTagsPanel( panel_parent, service_key, media, activate_callable ) self._file_lookup_script_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) panels.append( ( 'file lookup scripts', self._file_lookup_script_tags ) ) self._recent_tags = None if self._new_options.GetNoneableInteger( 'num_recent_tags' ) is not None: self._recent_tags = RecentTagsPanel( panel_parent, service_key, media, activate_callable ) self._recent_tags.mouseActivationOccurred.connect( self.mouseActivationOccurred ) panels.append( ( 'recent', self._recent_tags ) ) hbox = QP.HBoxLayout() if layout_mode == 'notebook': for ( name, panel ) in panels: self._notebook.addTab( panel, name ) QP.AddToLayout( hbox, self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS ) name_to_page_dict = { 'favourites' : self._favourite_tags, 'related' : self._related_tags, 'file_lookup_scripts' : self._file_lookup_script_tags, 'recent' : self._recent_tags } default_suggested_tags_notebook_page = self._new_options.GetString( 'default_suggested_tags_notebook_page' ) choice = name_to_page_dict.get( default_suggested_tags_notebook_page, None ) if choice is not None: self._notebook.setCurrentWidget( choice ) self._notebook.currentChanged.connect( self._PageChanged ) elif layout_mode == 'columns': for ( name, panel ) in panels: box_panel = ClientGUICommon.StaticBox( self, name ) box_panel.Add( panel, CC.FLAGS_EXPAND_BOTH_WAYS ) QP.AddToLayout( hbox, box_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) self.setLayout( hbox ) if len( panels ) == 0: self.hide() else: self._PageChanged() def _PageChanged( self ): if self._notebook is None: self._related_tags.NotifyUserLooking() return current_page = self._notebook.currentWidget() if current_page == self._related_tags: self._related_tags.NotifyUserLooking() def MediaUpdated( self ): if self._favourite_tags is not None: self._favourite_tags.MediaUpdated() if self._recent_tags is not None: self._recent_tags.MediaUpdated() if self._file_lookup_script_tags is not None: self._file_lookup_script_tags.MediaUpdated() if self._related_tags is not None: self._related_tags.MediaUpdated() def RefreshRelatedThorough( self ): if self._related_tags is not None: self._related_tags.RefreshThorough() def SetMedia( self, media ): self._media = media if self._favourite_tags is not None: self._favourite_tags.SetMedia( media ) if self._recent_tags is not None: self._recent_tags.SetMedia( media ) if self._file_lookup_script_tags is not None: self._file_lookup_script_tags.SetMedia( media ) if self._related_tags is not None: self._related_tags.SetMedia( media ) self._PageChanged() def SetSelectedTags( self, tags ): if self._related_tags is not None: self._related_tags.SetSelectedTags( tags ) def TakeFocusForUser( self, command ): if command == CAC.SIMPLE_SHOW_AND_FOCUS_MANAGE_TAGS_FAVOURITE_TAGS: panel = self._favourite_tags elif command == CAC.SIMPLE_SHOW_AND_FOCUS_MANAGE_TAGS_RELATED_TAGS: panel = self._related_tags elif command == CAC.SIMPLE_SHOW_AND_FOCUS_MANAGE_TAGS_FILE_LOOKUP_SCRIPT_TAGS: panel = self._file_lookup_script_tags elif command == CAC.SIMPLE_SHOW_AND_FOCUS_MANAGE_TAGS_RECENT_TAGS: panel = self._recent_tags if panel is not None: if self._notebook is not None: self._notebook.SelectPage( panel ) panel.TakeFocusForUser()