4262 lines
160 KiB
Python
4262 lines
160 KiB
Python
from . import ClientConstants as CC
|
|
from . import ClientGUIACDropdown
|
|
from . import ClientGUICommon
|
|
from . import ClientGUIControls
|
|
from . import ClientGUICore as CGC
|
|
from . import ClientGUIDialogs
|
|
from . import ClientGUIDialogsQuick
|
|
from . import ClientGUIFunctions
|
|
from . import ClientGUIListBoxes
|
|
from . import ClientGUIListCtrl
|
|
from . import ClientGUIMenus
|
|
from . import ClientGUITopLevelWindows
|
|
from . import ClientGUIScrolledPanels
|
|
from . import ClientGUIScrolledPanelsReview
|
|
from . import ClientGUIShortcuts
|
|
from . import ClientGUITagSuggestions
|
|
from . import ClientManagers
|
|
from . import ClientMedia
|
|
from . import ClientTags
|
|
import collections
|
|
from . import HydrusConstants as HC
|
|
from . import HydrusData
|
|
from . import HydrusExceptions
|
|
from . import HydrusGlobals as HG
|
|
from . import HydrusNetwork
|
|
from . import HydrusSerialisable
|
|
from . import HydrusTags
|
|
from . import HydrusText
|
|
import itertools
|
|
import os
|
|
from . import QtPorting as QP
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
from qtpy import QtGui as QG
|
|
from . import QtPorting as QP
|
|
|
|
class EditTagDisplayManagerPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent, tag_display_manager ):
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._tag_services = ClientGUICommon.BetterNotebook( self )
|
|
|
|
min_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._tag_services, 100 )
|
|
|
|
self._tag_services.setMinimumWidth( min_width )
|
|
|
|
#
|
|
|
|
services = list( HG.client_controller.services_manager.GetServices( ( HC.COMBINED_TAG, HC.LOCAL_TAG, HC.TAG_REPOSITORY ) ) )
|
|
|
|
for service in services:
|
|
|
|
service_key = service.GetServiceKey()
|
|
name = service.GetName()
|
|
|
|
page = self._Panel( self._tag_services, tag_display_manager, service_key )
|
|
|
|
select = service_key == CC.COMBINED_TAG_SERVICE_KEY
|
|
|
|
self._tag_services.addTab( page, name )
|
|
if select: self._tag_services.setCurrentWidget( page )
|
|
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
intro = 'Please note this new system is under construction. It is neither completely functional nor as efficient as intended.'
|
|
|
|
st = ClientGUICommon.BetterStaticText( self, intro )
|
|
st.setWordWrap( True )
|
|
|
|
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._tag_services, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
tag_display_manager = ClientTags.TagDisplayManager()
|
|
|
|
for page in self._tag_services.GetPages():
|
|
|
|
( service_key, tag_display_types_to_tag_filters ) = page.GetValue()
|
|
|
|
for ( tag_display_type, tag_filter ) in tag_display_types_to_tag_filters.items():
|
|
|
|
tag_display_manager.SetTagFilter( tag_display_type, service_key, tag_filter )
|
|
|
|
|
|
|
|
return tag_display_manager
|
|
|
|
|
|
class _Panel( QW.QWidget ):
|
|
|
|
def __init__( self, parent, tag_display_manager, service_key ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
single_tag_filter = tag_display_manager.GetTagFilter( ClientTags.TAG_DISPLAY_SINGLE_MEDIA, service_key )
|
|
selection_tag_filter = tag_display_manager.GetTagFilter( ClientTags.TAG_DISPLAY_SELECTION_LIST, service_key )
|
|
|
|
self._service_key = service_key
|
|
|
|
#
|
|
|
|
message = 'This filters which tags will show on \'single\' file views such as the media viewer and thumbnail banners.'
|
|
|
|
self._single_tag_filter_button = TagFilterButton( self, message, single_tag_filter, label_prefix = 'tags shown: ' )
|
|
|
|
message = 'This filters which tags will show on \'selection\' file views such as the \'selection tags\' list on regular search pages.'
|
|
|
|
self._selection_tag_filter_button = TagFilterButton( self, message, selection_tag_filter, label_prefix = 'tags shown: ' )
|
|
|
|
#
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'Tag filter for single file views: ', self._single_tag_filter_button ) )
|
|
rows.append( ( 'Tag filter for multiple file views: ', self._selection_tag_filter_button ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
if self._service_key == CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
message = 'These filters apply to all tag services.'
|
|
|
|
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( self, message ), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
QP.AddToLayout( vbox, gridbox )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
tag_display_types_to_tag_filters = {}
|
|
|
|
tag_display_types_to_tag_filters[ ClientTags.TAG_DISPLAY_SINGLE_MEDIA ] = self._single_tag_filter_button.GetValue()
|
|
tag_display_types_to_tag_filters[ ClientTags.TAG_DISPLAY_SELECTION_LIST ] = self._selection_tag_filter_button.GetValue()
|
|
|
|
return ( self._service_key, tag_display_types_to_tag_filters )
|
|
|
|
|
|
|
|
class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
TEST_RESULT_DEFAULT = 'Enter a tag here to test if it passes the current filter:'
|
|
|
|
def __init__( self, parent, tag_filter, prefer_blacklist = False, namespaces = None, message = None ):
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._prefer_blacklist = prefer_blacklist
|
|
self._namespaces = namespaces
|
|
|
|
self._wildcard_replacements = {}
|
|
|
|
self._wildcard_replacements[ '*' ] = ''
|
|
self._wildcard_replacements[ '*:' ] = ':'
|
|
self._wildcard_replacements[ '*:*' ] = ':'
|
|
|
|
#
|
|
|
|
help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp )
|
|
|
|
help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', QG.QColor( 0, 0, 255 ) )
|
|
|
|
#
|
|
|
|
self._import_favourite = ClientGUICommon.BetterButton( self, 'import', self._ImportFavourite )
|
|
self._export_favourite = ClientGUICommon.BetterButton( self, 'export', self._ExportFavourite )
|
|
self._load_favourite = ClientGUICommon.BetterButton( self, 'load', self._LoadFavourite )
|
|
self._save_favourite = ClientGUICommon.BetterButton( self, 'save', self._SaveFavourite )
|
|
self._delete_favourite = ClientGUICommon.BetterButton( self, 'delete', self._DeleteFavourite )
|
|
|
|
#
|
|
|
|
self._notebook = ClientGUICommon.BetterNotebook( self )
|
|
|
|
#
|
|
|
|
self._advanced_panel = self._InitAdvancedPanel()
|
|
|
|
self._whitelist_panel = self._InitWhitelistPanel()
|
|
self._blacklist_panel = self._InitBlacklistPanel()
|
|
|
|
#
|
|
|
|
if self._prefer_blacklist:
|
|
|
|
self._notebook.addTab( self._blacklist_panel, 'blacklist' )
|
|
self._notebook.addTab( self._whitelist_panel, 'whitelist' )
|
|
|
|
else:
|
|
|
|
self._notebook.addTab( self._whitelist_panel, 'whitelist' )
|
|
self._notebook.addTab( self._blacklist_panel, 'blacklist' )
|
|
|
|
|
|
self._notebook.addTab( self._advanced_panel, 'advanced' )
|
|
|
|
#
|
|
|
|
self._redundant_st = ClientGUICommon.BetterStaticText( self, '', ellipsize_end = True )
|
|
|
|
self._current_filter_st = ClientGUICommon.BetterStaticText( self, 'currently keeping: ', ellipsize_end = True )
|
|
|
|
self._test_result_st = ClientGUICommon.BetterStaticText( self, self.TEST_RESULT_DEFAULT )
|
|
self._test_result_st.setAlignment( QC.Qt.AlignVCenter | QC.Qt.AlignRight )
|
|
|
|
self._test_input = QW.QPlainTextEdit( self )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_BUTTON_SIZER )
|
|
|
|
if message is not None:
|
|
|
|
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,message), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._import_favourite, CC.FLAGS_SMALL_INDENT )
|
|
QP.AddToLayout( hbox, self._export_favourite, CC.FLAGS_SMALL_INDENT )
|
|
QP.AddToLayout( hbox, self._load_favourite, CC.FLAGS_SMALL_INDENT )
|
|
QP.AddToLayout( hbox, self._save_favourite, CC.FLAGS_SMALL_INDENT )
|
|
QP.AddToLayout( hbox, self._delete_favourite, CC.FLAGS_SMALL_INDENT )
|
|
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_BUTTON_SIZER )
|
|
QP.AddToLayout( vbox, self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._redundant_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._current_filter_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._test_result_st, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
|
QP.AddToLayout( hbox, self._test_input, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
|
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._advanced_blacklist.listBoxChanged.connect( self._UpdateStatus )
|
|
self._advanced_whitelist.listBoxChanged.connect( self._UpdateStatus )
|
|
self._simple_whitelist_global_checkboxes.clicked.connect( self.EventSimpleWhitelistGlobalCheck )
|
|
self._simple_whitelist_namespace_checkboxes.clicked.connect( self.EventSimpleWhitelistNamespaceCheck )
|
|
self._simple_blacklist_global_checkboxes.clicked.connect( self.EventSimpleBlacklistGlobalCheck )
|
|
self._simple_blacklist_namespace_checkboxes.clicked.connect( self.EventSimpleBlacklistNamespaceCheck )
|
|
|
|
self._test_input.textChanged.connect( self._UpdateTest )
|
|
|
|
self.SetValue( tag_filter )
|
|
|
|
|
|
def _AdvancedAddBlacklist( self, tag_slice ):
|
|
|
|
tag_slice = self._CleanTagSliceInput( tag_slice )
|
|
|
|
if tag_slice in self._advanced_blacklist.GetTags():
|
|
|
|
self._advanced_blacklist.RemoveTags( ( tag_slice, ) )
|
|
|
|
else:
|
|
|
|
self._advanced_whitelist.RemoveTags( ( tag_slice, ) )
|
|
|
|
if self._CurrentlyBlocked( tag_slice ):
|
|
|
|
self._ShowRedundantError( ClientTags.ConvertTagSliceToString( tag_slice ) + ' is already blocked by a broader rule!' )
|
|
|
|
else:
|
|
|
|
self._advanced_blacklist.AddTags( ( tag_slice, ) )
|
|
|
|
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
def _AdvancedAddBlacklistButton( self ):
|
|
|
|
tag_slice = self._advanced_blacklist_input.GetValue()
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
def _AdvancedAddBlacklistMultiple( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
|
|
def _AdvancedAddWhitelist( self, tag_slice ):
|
|
|
|
tag_slice = self._CleanTagSliceInput( tag_slice )
|
|
|
|
if tag_slice in self._advanced_whitelist.GetTags():
|
|
|
|
self._advanced_whitelist.RemoveTags( ( tag_slice, ) )
|
|
|
|
else:
|
|
|
|
self._advanced_blacklist.RemoveTags( ( tag_slice, ) )
|
|
|
|
# if it is still blocked after that, it needs whitelisting explicitly
|
|
|
|
if self._CurrentlyBlocked( tag_slice ):
|
|
|
|
self._advanced_whitelist.AddTags( ( tag_slice, ) )
|
|
|
|
elif tag_slice not in ( '', ':' ):
|
|
|
|
self._ShowRedundantError( ClientTags.ConvertTagSliceToString( tag_slice ) + ' is already permitted by a broader rule!' )
|
|
|
|
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
def _AdvancedAddWhitelistButton( self ):
|
|
|
|
tag_slice = self._advanced_whitelist_input.GetValue()
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
def _AdvancedAddWhitelistMultiple( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
|
|
def _AdvancedBlacklistEverything( self ):
|
|
|
|
self._advanced_blacklist.SetTags( [] )
|
|
|
|
self._advanced_whitelist.RemoveTags( ( '', ':' ) )
|
|
|
|
self._advanced_blacklist.AddTags( ( '', ':' ) )
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
def _AdvancedDeleteBlacklist( self ):
|
|
|
|
selected_tag_slices = self._advanced_blacklist.GetSelectedTags()
|
|
|
|
if len( selected_tag_slices ) > 0:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._advanced_blacklist.RemoveTags( selected_tag_slices )
|
|
|
|
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
def _AdvancedDeleteWhitelist( self ):
|
|
|
|
selected_tag_slices = self._advanced_whitelist.GetSelectedTags()
|
|
|
|
if len( selected_tag_slices ) > 0:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._advanced_whitelist.RemoveTags( selected_tag_slices )
|
|
|
|
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
def _CleanTagSliceInput( self, tag_slice ):
|
|
|
|
while '**' in tag_slice:
|
|
|
|
tag_slice = tag_slice.replace( '**', '*' )
|
|
|
|
|
|
if tag_slice in self._wildcard_replacements:
|
|
|
|
tag_slice = self._wildcard_replacements[ tag_slice ]
|
|
|
|
|
|
if ':' in tag_slice:
|
|
|
|
( namespace, subtag ) = HydrusTags.SplitTag( tag_slice )
|
|
|
|
if subtag == '*':
|
|
|
|
tag_slice = '{}:'.format( namespace )
|
|
|
|
|
|
|
|
return tag_slice
|
|
|
|
|
|
def _CurrentlyBlocked( self, tag_slice ):
|
|
|
|
if tag_slice in ( '', ':' ):
|
|
|
|
test_slices = { tag_slice }
|
|
|
|
elif tag_slice.count( ':' ) == 1 and tag_slice.endswith( ':' ):
|
|
|
|
test_slices = { ':', tag_slice }
|
|
|
|
elif ':' in tag_slice:
|
|
|
|
( ns, st ) = HydrusTags.SplitTag( tag_slice )
|
|
|
|
test_slices = { ':', ns + ':', tag_slice }
|
|
|
|
else:
|
|
|
|
test_slices = { '', tag_slice }
|
|
|
|
|
|
blacklist = set( self._advanced_blacklist.GetTags() )
|
|
|
|
return not blacklist.isdisjoint( test_slices )
|
|
|
|
|
|
def _DeleteFavourite( self ):
|
|
|
|
def do_it( name ):
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
if name in names_to_tag_filters:
|
|
|
|
message = 'Delete "{}"?'.format( name )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
del names_to_tag_filters[ name ]
|
|
|
|
HG.client_controller.new_options.SetFavouriteTagFilters( names_to_tag_filters )
|
|
|
|
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
menu = QW.QMenu()
|
|
|
|
if len( names_to_tag_filters ) == 0:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( menu, 'no favourites set!' )
|
|
|
|
else:
|
|
|
|
for ( name, tag_filter ) in names_to_tag_filters.items():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, name, 'delete {}'.format( name ), do_it, name )
|
|
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
def _ExportFavourite( self ):
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
menu = QW.QMenu()
|
|
|
|
if len( names_to_tag_filters ) == 0:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( menu, 'no favourites set!' )
|
|
|
|
else:
|
|
|
|
for ( name, tag_filter ) in names_to_tag_filters.items():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, name, 'load {}'.format( name ), HG.client_controller.pub, 'clipboard', 'text', tag_filter.DumpToString() )
|
|
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
def _GetWhiteBlacklistsPossible( self ):
|
|
|
|
blacklist_tag_slices = self._advanced_blacklist.GetTags()
|
|
whitelist_tag_slices = self._advanced_whitelist.GetTags()
|
|
|
|
blacklist_is_only_simples = set( blacklist_tag_slices ).issubset( { '', ':' } )
|
|
|
|
nothing_is_whitelisted = len( whitelist_tag_slices ) == 0
|
|
|
|
whitelist_possible = blacklist_is_only_simples
|
|
blacklist_possible = nothing_is_whitelisted
|
|
|
|
return ( whitelist_possible, blacklist_possible )
|
|
|
|
|
|
def _ImportFavourite( self ):
|
|
|
|
try:
|
|
|
|
raw_text = HG.client_controller.GetClipboardText()
|
|
|
|
except HydrusExceptions.DataMissing as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', str(e) )
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
obj = HydrusSerialisable.CreateFromString( raw_text )
|
|
|
|
except Exception as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard' )
|
|
|
|
return
|
|
|
|
|
|
if not isinstance( obj, ClientTags.TagFilter ):
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'That object was not a Tag Filter! It seemed to be a "{}".'.format(type(obj)) )
|
|
|
|
return
|
|
|
|
|
|
tag_filter = obj
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, 'Enter a name for the favourite.' ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
name = dlg.GetValue()
|
|
|
|
if name in names_to_tag_filters:
|
|
|
|
message = '"{}" already exists! Overwrite?'.format( name )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
names_to_tag_filters[ name ] = tag_filter
|
|
|
|
HG.client_controller.new_options.SetFavouriteTagFilters( names_to_tag_filters )
|
|
|
|
self.SetValue( tag_filter )
|
|
|
|
|
|
|
|
|
|
def _InitAdvancedPanel( self ):
|
|
|
|
advanced_panel = QW.QWidget( self._notebook )
|
|
|
|
#
|
|
|
|
blacklist_panel = ClientGUICommon.StaticBox( advanced_panel, 'exclude these' )
|
|
|
|
self._advanced_blacklist = ClientGUIListBoxes.ListBoxTagsCensorship( blacklist_panel )
|
|
|
|
self._advanced_blacklist_input = ClientGUIControls.TextAndPasteCtrl( blacklist_panel, self._AdvancedAddBlacklistMultiple, allow_empty_input = True )
|
|
|
|
add_blacklist_button = ClientGUICommon.BetterButton( blacklist_panel, 'add', self._AdvancedAddBlacklistButton )
|
|
delete_blacklist_button = ClientGUICommon.BetterButton( blacklist_panel, 'delete', self._AdvancedDeleteBlacklist )
|
|
blacklist_everything_button = ClientGUICommon.BetterButton( blacklist_panel, 'block everything', self._AdvancedBlacklistEverything )
|
|
|
|
#
|
|
|
|
whitelist_panel = ClientGUICommon.StaticBox( advanced_panel, 'except for these' )
|
|
|
|
self._advanced_whitelist = ClientGUIListBoxes.ListBoxTagsCensorship( whitelist_panel )
|
|
|
|
self._advanced_whitelist_input = ClientGUIControls.TextAndPasteCtrl( whitelist_panel, self._AdvancedAddWhitelistMultiple, allow_empty_input = True )
|
|
|
|
self._advanced_add_whitelist_button = ClientGUICommon.BetterButton( whitelist_panel, 'add', self._AdvancedAddWhitelistButton )
|
|
delete_whitelist_button = ClientGUICommon.BetterButton( whitelist_panel, 'delete', self._AdvancedDeleteWhitelist )
|
|
|
|
#
|
|
|
|
button_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( button_hbox, self._advanced_blacklist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( button_hbox, add_blacklist_button, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, delete_blacklist_button, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, blacklist_everything_button, CC.FLAGS_VCENTER )
|
|
|
|
blacklist_panel.Add( self._advanced_blacklist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
blacklist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
button_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( button_hbox, self._advanced_whitelist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( button_hbox, self._advanced_add_whitelist_button, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, delete_whitelist_button, CC.FLAGS_VCENTER )
|
|
|
|
whitelist_panel.Add( self._advanced_whitelist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
whitelist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, blacklist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( hbox, whitelist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
advanced_panel.setLayout( hbox )
|
|
|
|
return advanced_panel
|
|
|
|
|
|
def _InitBlacklistPanel( self ):
|
|
|
|
blacklist_panel = QW.QWidget( self._notebook )
|
|
|
|
#
|
|
|
|
self._simple_blacklist_error_st = ClientGUICommon.BetterStaticText( blacklist_panel )
|
|
|
|
self._simple_blacklist_global_checkboxes = QP.CheckListBox( blacklist_panel )
|
|
|
|
self._simple_blacklist_global_checkboxes.Append( 'unnamespaced tags', '' )
|
|
self._simple_blacklist_global_checkboxes.Append( 'namespaced tags', ':' )
|
|
|
|
self._simple_blacklist_namespace_checkboxes = QP.CheckListBox( blacklist_panel )
|
|
|
|
for namespace in self._namespaces:
|
|
|
|
if namespace == '':
|
|
|
|
continue
|
|
|
|
|
|
self._simple_blacklist_namespace_checkboxes.Append( namespace, namespace + ':' )
|
|
|
|
|
|
self._simple_blacklist = ClientGUIListBoxes.ListBoxTagsCensorship( blacklist_panel, removed_callable = self._SimpleBlacklistRemoved )
|
|
|
|
self._simple_blacklist_input = ClientGUIControls.TextAndPasteCtrl( blacklist_panel, self._SimpleAddBlacklistMultiple, allow_empty_input = True )
|
|
|
|
#
|
|
|
|
left_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( left_vbox, self._simple_blacklist_global_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( left_vbox, (20,20), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( left_vbox, self._simple_blacklist_namespace_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
right_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( right_vbox, self._simple_blacklist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( right_vbox, self._simple_blacklist_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
main_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( main_hbox, right_vbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._simple_blacklist_error_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, main_hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
blacklist_panel.setLayout( vbox )
|
|
|
|
return blacklist_panel
|
|
|
|
|
|
def _InitWhitelistPanel( self ):
|
|
|
|
whitelist_panel = QW.QWidget( self._notebook )
|
|
|
|
#
|
|
|
|
self._simple_whitelist_error_st = ClientGUICommon.BetterStaticText( whitelist_panel )
|
|
|
|
self._simple_whitelist_global_checkboxes = QP.CheckListBox( whitelist_panel )
|
|
|
|
self._simple_whitelist_global_checkboxes.Append( 'unnamespaced tags', '' )
|
|
self._simple_whitelist_global_checkboxes.Append( 'namespaced tags', ':' )
|
|
|
|
self._simple_whitelist_namespace_checkboxes = QP.CheckListBox( whitelist_panel )
|
|
|
|
for namespace in self._namespaces:
|
|
|
|
if namespace == '':
|
|
|
|
continue
|
|
|
|
|
|
self._simple_whitelist_namespace_checkboxes.Append( namespace, namespace + ':' )
|
|
|
|
|
|
self._simple_whitelist = ClientGUIListBoxes.ListBoxTagsCensorship( whitelist_panel, removed_callable = self._SimpleWhitelistRemoved )
|
|
|
|
self._simple_whitelist_input = ClientGUIControls.TextAndPasteCtrl( whitelist_panel, self._SimpleAddWhitelistMultiple, allow_empty_input = True )
|
|
|
|
#
|
|
|
|
left_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( left_vbox, self._simple_whitelist_global_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( left_vbox, (20,20), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( left_vbox, self._simple_whitelist_namespace_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
right_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( right_vbox, self._simple_whitelist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( right_vbox, self._simple_whitelist_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
main_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( main_hbox, right_vbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._simple_whitelist_error_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, main_hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
whitelist_panel.setLayout( vbox )
|
|
|
|
return whitelist_panel
|
|
|
|
|
|
def _LoadFavourite( self ):
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
menu = QW.QMenu()
|
|
|
|
if len( names_to_tag_filters ) == 0:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( menu, 'no favourites set!' )
|
|
|
|
else:
|
|
|
|
for ( name, tag_filter ) in names_to_tag_filters.items():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, name, 'load {}'.format( name ), self.SetValue, tag_filter )
|
|
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
def _SaveFavourite( self ):
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, 'Enter a name for the favourite.' ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
names_to_tag_filters = HG.client_controller.new_options.GetFavouriteTagFilters()
|
|
|
|
name = dlg.GetValue()
|
|
tag_filter = self.GetValue()
|
|
|
|
if name in names_to_tag_filters:
|
|
|
|
message = '"{}" already exists! Overwrite?'.format( name )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
names_to_tag_filters[ name ] = tag_filter
|
|
|
|
HG.client_controller.new_options.SetFavouriteTagFilters( names_to_tag_filters )
|
|
|
|
|
|
|
|
|
|
def _ShowHelp( self ):
|
|
|
|
help = 'Here you can set rules to filter tags for one purpose or another. The default is typically to permit all tags. Check the current filter summary text at the bottom-left of the panel to ensure you have your logic correct.'
|
|
help += os.linesep * 2
|
|
help += 'The different tabs are multiple ways of looking at the filter--sometimes it is more useful to think about a filter as a whitelist (where only the listed contents are kept) or a blacklist (where everything _except_ the listed contents are kept), and there is also an advanced tab that lets you do a more complicated combination of the two.'
|
|
help += os.linesep * 2
|
|
help += 'As well as selecting broader categories of tags with the checkboxes, you can type or paste the individual tags directly--just hit enter to add each one--and double-click an existing entry in a list to remove it.'
|
|
help += os.linesep * 2
|
|
help += 'If you wish to manually type a special tag, use these shorthands:'
|
|
help += os.linesep * 2
|
|
help += '"namespace:" - all instances of that namespace'
|
|
help += os.linesep
|
|
help += '":" - all namespaced tags'
|
|
help += os.linesep
|
|
help += '"" (i.e. an empty string) - all unnamespaced tags'
|
|
|
|
QW.QMessageBox.information( self, 'Information', help )
|
|
|
|
|
|
def _ShowRedundantError( self, text ):
|
|
|
|
self._redundant_st.setText( text )
|
|
|
|
HG.client_controller.CallLaterQtSafe( self._redundant_st, 2, self._redundant_st.setText, '' )
|
|
|
|
|
|
def _SimpleAddBlacklistMultiple( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
|
|
def _SimpleAddWhitelistMultiple( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
if tag_slice in ( '', ':' ) and tag_slice in self._simple_whitelist.GetTags():
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
else:
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
|
|
|
|
def _SimpleBlacklistRemoved( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
|
|
def _SimpleBlacklistReset( self ):
|
|
|
|
pass
|
|
|
|
|
|
def _SimpleWhitelistRemoved( self, tag_slices ):
|
|
|
|
tag_slices = set( tag_slices )
|
|
|
|
for simple in ( '', ':' ):
|
|
|
|
if simple in tag_slices:
|
|
|
|
tag_slices.discard( simple )
|
|
|
|
self._AdvancedAddBlacklist( simple )
|
|
|
|
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
|
|
def _SimpleWhitelistReset( self ):
|
|
|
|
pass
|
|
|
|
|
|
def _UpdateStatus( self ):
|
|
|
|
( whitelist_possible, blacklist_possible ) = self._GetWhiteBlacklistsPossible()
|
|
|
|
whitelist_tag_slices = self._advanced_whitelist.GetTags()
|
|
blacklist_tag_slices = self._advanced_blacklist.GetTags()
|
|
|
|
if whitelist_possible:
|
|
|
|
self._simple_whitelist_error_st.setText( '' )
|
|
|
|
self._simple_whitelist.setEnabled( True )
|
|
self._simple_whitelist_global_checkboxes.setEnabled( True )
|
|
self._simple_whitelist_input.setEnabled( True )
|
|
|
|
whitelist_tag_slices = set( whitelist_tag_slices )
|
|
|
|
if not self._CurrentlyBlocked( '' ):
|
|
|
|
whitelist_tag_slices.add( '' )
|
|
|
|
|
|
if not self._CurrentlyBlocked( ':' ):
|
|
|
|
whitelist_tag_slices.add( ':' )
|
|
|
|
self._simple_whitelist_namespace_checkboxes.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._simple_whitelist_namespace_checkboxes.setEnabled( True )
|
|
|
|
|
|
self._simple_whitelist.SetTags( whitelist_tag_slices )
|
|
|
|
for index in range( self._simple_whitelist_global_checkboxes.count() ):
|
|
|
|
check = QP.GetClientData( self._simple_whitelist_global_checkboxes, index ) in whitelist_tag_slices
|
|
|
|
self._simple_whitelist_global_checkboxes.Check( index, check )
|
|
|
|
|
|
for index in range( self._simple_whitelist_namespace_checkboxes.count() ):
|
|
|
|
check = QP.GetClientData( self._simple_whitelist_namespace_checkboxes, index ) in whitelist_tag_slices
|
|
|
|
self._simple_whitelist_namespace_checkboxes.Check( index, check )
|
|
|
|
|
|
else:
|
|
|
|
self._simple_whitelist_error_st.setText( 'The filter is currently more complicated than a simple whitelist, so cannot be shown here.' )
|
|
|
|
self._simple_whitelist.setEnabled( False )
|
|
self._simple_whitelist_global_checkboxes.setEnabled( False )
|
|
self._simple_whitelist_namespace_checkboxes.setEnabled( False )
|
|
self._simple_whitelist_input.setEnabled( False )
|
|
|
|
self._simple_whitelist.SetTags( '' )
|
|
|
|
for index in range( self._simple_whitelist_global_checkboxes.count() ):
|
|
|
|
self._simple_whitelist_global_checkboxes.Check( index, False )
|
|
|
|
|
|
for index in range( self._simple_whitelist_namespace_checkboxes.count() ):
|
|
|
|
self._simple_whitelist_namespace_checkboxes.Check( index, False )
|
|
|
|
|
|
|
|
#
|
|
|
|
whitelist_tag_slices = self._advanced_whitelist.GetTags()
|
|
blacklist_tag_slices = self._advanced_blacklist.GetTags()
|
|
|
|
if blacklist_possible:
|
|
|
|
self._simple_blacklist_error_st.setText( '' )
|
|
|
|
self._simple_blacklist.setEnabled( True )
|
|
self._simple_blacklist_global_checkboxes.setEnabled( True )
|
|
self._simple_blacklist_input.setEnabled( True )
|
|
|
|
if self._CurrentlyBlocked( ':' ):
|
|
|
|
self._simple_blacklist_namespace_checkboxes.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._simple_blacklist_namespace_checkboxes.setEnabled( True )
|
|
|
|
|
|
self._simple_blacklist.SetTags( blacklist_tag_slices )
|
|
|
|
for index in range( self._simple_blacklist_global_checkboxes.count() ):
|
|
|
|
check = QP.GetClientData( self._simple_blacklist_global_checkboxes, index ) in blacklist_tag_slices
|
|
|
|
self._simple_blacklist_global_checkboxes.Check( index, check )
|
|
|
|
|
|
for index in range( self._simple_blacklist_namespace_checkboxes.count() ):
|
|
|
|
check = QP.GetClientData( self._simple_blacklist_namespace_checkboxes, index ) in blacklist_tag_slices
|
|
|
|
self._simple_blacklist_namespace_checkboxes.Check( index, check )
|
|
|
|
|
|
else:
|
|
|
|
self._simple_blacklist_error_st.setText( 'The filter is currently more complicated than a simple blacklist, so cannot be shown here.' )
|
|
|
|
self._simple_blacklist.setEnabled( False )
|
|
self._simple_blacklist_global_checkboxes.setEnabled( False )
|
|
self._simple_blacklist_namespace_checkboxes.setEnabled( False )
|
|
self._simple_blacklist_input.setEnabled( False )
|
|
|
|
self._simple_blacklist.SetTags( '' )
|
|
|
|
for index in range( self._simple_blacklist_global_checkboxes.count() ):
|
|
|
|
self._simple_blacklist_global_checkboxes.Check( index, False )
|
|
|
|
|
|
for index in range( self._simple_blacklist_namespace_checkboxes.count() ):
|
|
|
|
self._simple_blacklist_namespace_checkboxes.Check( index, False )
|
|
|
|
|
|
|
|
#
|
|
|
|
whitelist_tag_slices = self._advanced_whitelist.GetTags()
|
|
blacklist_tag_slices = self._advanced_blacklist.GetTags()
|
|
|
|
if len( blacklist_tag_slices ) == 0:
|
|
|
|
self._advanced_whitelist_input.setEnabled( False )
|
|
self._advanced_add_whitelist_button.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._advanced_whitelist_input.setEnabled( True )
|
|
self._advanced_add_whitelist_button.setEnabled( True )
|
|
|
|
|
|
#
|
|
|
|
tag_filter = self.GetValue()
|
|
|
|
pretty_tag_filter = tag_filter.ToPermittedString()
|
|
|
|
self._current_filter_st.setText( 'currently keeping: '+pretty_tag_filter )
|
|
|
|
self._UpdateTest()
|
|
|
|
|
|
def _UpdateTest( self ):
|
|
|
|
test_input = self._test_input.toPlainText()
|
|
|
|
if test_input == '':
|
|
|
|
text = self.TEST_RESULT_DEFAULT
|
|
|
|
colour = QP.GetSystemColour( QG.QPalette.WindowText )
|
|
|
|
else:
|
|
|
|
tag_filter = self.GetValue()
|
|
|
|
if tag_filter.TagOK( test_input ):
|
|
|
|
text = 'tag passes!'
|
|
|
|
self._test_result_st.setObjectName( 'HydrusValid' )
|
|
|
|
else:
|
|
|
|
text = 'tag blocked!'
|
|
|
|
self._test_result_st.setObjectName( 'HydrusInvalid' )
|
|
|
|
|
|
|
|
self._test_result_st.setText( text )
|
|
self._test_result_st.style().polish( self._test_result_st )
|
|
|
|
|
|
def EventSimpleBlacklistNamespaceCheck( self, index ):
|
|
|
|
index = index.row()
|
|
|
|
if index != -1:
|
|
|
|
tag_slice = QP.GetClientData( self._simple_blacklist_namespace_checkboxes, index )
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
|
|
def EventSimpleBlacklistGlobalCheck( self, index ):
|
|
|
|
index = index.row()
|
|
|
|
if index != -1:
|
|
|
|
tag_slice = QP.GetClientData( self._simple_blacklist_global_checkboxes, index )
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
|
|
|
|
def EventSimpleWhitelistNamespaceCheck( self, index ):
|
|
|
|
index = index.row()
|
|
|
|
if index != -1:
|
|
|
|
tag_slice = QP.GetClientData( self._simple_whitelist_namespace_checkboxes, index )
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
|
|
def EventSimpleWhitelistGlobalCheck( self, index ):
|
|
|
|
index = index.row()
|
|
|
|
if index != -1:
|
|
|
|
tag_slice = QP.GetClientData( self._simple_whitelist_global_checkboxes, index )
|
|
|
|
if tag_slice in ( '', ':' ) and tag_slice in self._simple_whitelist.GetTags():
|
|
|
|
self._AdvancedAddBlacklist( tag_slice )
|
|
|
|
else:
|
|
|
|
self._AdvancedAddWhitelist( tag_slice )
|
|
|
|
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
tag_filter = ClientTags.TagFilter()
|
|
|
|
for tag_slice in self._advanced_blacklist.GetTags():
|
|
|
|
tag_filter.SetRule( tag_slice, CC.FILTER_BLACKLIST )
|
|
|
|
|
|
for tag_slice in self._advanced_whitelist.GetTags():
|
|
|
|
tag_filter.SetRule( tag_slice, CC.FILTER_WHITELIST )
|
|
|
|
|
|
return tag_filter
|
|
|
|
|
|
def SetValue( self, tag_filter: ClientTags.TagFilter ):
|
|
|
|
blacklist_tag_slices = [ tag_slice for ( tag_slice, rule ) in tag_filter.GetTagSlicesToRules().items() if rule == CC.FILTER_BLACKLIST ]
|
|
whitelist_tag_slices = [ tag_slice for ( tag_slice, rule ) in tag_filter.GetTagSlicesToRules().items() if rule == CC.FILTER_WHITELIST ]
|
|
|
|
self._advanced_blacklist.SetTags( blacklist_tag_slices )
|
|
self._advanced_whitelist.SetTags( whitelist_tag_slices )
|
|
|
|
( whitelist_possible, blacklist_possible ) = self._GetWhiteBlacklistsPossible()
|
|
|
|
selection_tests = []
|
|
|
|
if self._prefer_blacklist:
|
|
|
|
selection_tests.append( ( blacklist_possible, self._blacklist_panel ) )
|
|
selection_tests.append( ( whitelist_possible, self._whitelist_panel ) )
|
|
selection_tests.append( ( True, self._advanced_panel ) )
|
|
|
|
else:
|
|
|
|
selection_tests.append( ( whitelist_possible, self._whitelist_panel ) )
|
|
selection_tests.append( ( blacklist_possible, self._blacklist_panel ) )
|
|
selection_tests.append( ( True, self._advanced_panel ) )
|
|
|
|
|
|
for ( test, page ) in selection_tests:
|
|
|
|
if test:
|
|
|
|
self._notebook.SelectPage( page )
|
|
|
|
break
|
|
|
|
|
|
|
|
self._UpdateStatus()
|
|
|
|
|
|
class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|
|
|
def __init__( self, parent, file_service_key, media, immediate_commit = False, canvas_key = None ):
|
|
|
|
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
|
|
|
|
self._file_service_key = file_service_key
|
|
|
|
self._immediate_commit = immediate_commit
|
|
self._canvas_key = canvas_key
|
|
|
|
media = ClientMedia.FlattenMedia( media )
|
|
|
|
self._current_media = [ m.Duplicate() for m in media ]
|
|
|
|
self._hashes = set()
|
|
|
|
for m in self._current_media:
|
|
|
|
self._hashes.update( m.GetHashes() )
|
|
|
|
|
|
self._tag_repositories = ClientGUICommon.BetterNotebook( self )
|
|
|
|
#
|
|
|
|
services = HG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
|
|
|
|
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
|
|
|
|
for service in services:
|
|
|
|
service_key = service.GetServiceKey()
|
|
name = service.GetName()
|
|
|
|
page = self._Panel( self._tag_repositories, self._file_service_key, service.GetServiceKey(), self._current_media, self._immediate_commit, canvas_key = self._canvas_key )
|
|
page._add_tag_box.selectUp.connect( self.EventSelectUp )
|
|
page._add_tag_box.selectDown.connect( self.EventSelectDown )
|
|
page._add_tag_box.showPrevious.connect( self.EventShowPrevious )
|
|
page._add_tag_box.showNext.connect( self.EventShowNext )
|
|
page.okSignal.connect( self.okSignal )
|
|
|
|
select = service_key == default_tag_repository_key
|
|
|
|
self._tag_repositories.addTab( page, name )
|
|
if select: self._tag_repositories.setCurrentIndex( self._tag_repositories.count() - 1 )
|
|
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._tag_repositories, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
if self._canvas_key is not None:
|
|
|
|
HG.client_controller.sub( self, 'CanvasHasNewMedia', 'canvas_new_display_media' )
|
|
|
|
|
|
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'global', 'media', 'main_gui' ] )
|
|
|
|
self._tag_repositories.currentChanged.connect( self.EventServiceChanged )
|
|
|
|
self._SetSearchFocus()
|
|
|
|
|
|
def _GetGroupsOfServiceKeysToContentUpdates( self ):
|
|
|
|
groups_of_service_keys_to_content_updates = []
|
|
|
|
for page in self._tag_repositories.GetPages():
|
|
|
|
( service_key, groups_of_content_updates ) = page.GetGroupsOfContentUpdates()
|
|
|
|
for content_updates in groups_of_content_updates:
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
service_keys_to_content_updates = { service_key : content_updates }
|
|
|
|
groups_of_service_keys_to_content_updates.append( service_keys_to_content_updates )
|
|
|
|
|
|
|
|
|
|
return groups_of_service_keys_to_content_updates
|
|
|
|
|
|
def _SetSearchFocus( self ):
|
|
|
|
page = self._tag_repositories.currentWidget()
|
|
|
|
if page is not None:
|
|
|
|
page.SetTagBoxFocus()
|
|
|
|
|
|
|
|
def CanvasHasNewMedia( self, canvas_key, new_media_singleton ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
if new_media_singleton is not None:
|
|
|
|
self._current_media = ( new_media_singleton.Duplicate(), )
|
|
|
|
for page in self._tag_repositories.GetPages():
|
|
|
|
page.SetMedia( self._current_media )
|
|
|
|
|
|
|
|
|
|
|
|
def CanCancel( self ):
|
|
|
|
groups_of_service_keys_to_content_updates = self._GetGroupsOfServiceKeysToContentUpdates()
|
|
|
|
if len( groups_of_service_keys_to_content_updates ) > 0:
|
|
|
|
message = 'Are you sure you want to cancel? You have uncommitted changes that will be lost.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
ClientGUIScrolledPanels.ManagePanel.CleanBeforeDestroy( self )
|
|
|
|
for page in self._tag_repositories.GetPages():
|
|
|
|
page.CleanBeforeDestroy()
|
|
|
|
|
|
|
|
def CommitChanges( self ):
|
|
|
|
groups_of_service_keys_to_content_updates = self._GetGroupsOfServiceKeysToContentUpdates()
|
|
|
|
for service_keys_to_content_updates in groups_of_service_keys_to_content_updates:
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
|
|
def EventSelectDown( self ):
|
|
|
|
self._tag_repositories.SelectRight()
|
|
|
|
self._SetSearchFocus()
|
|
|
|
|
|
def EventSelectUp( self ):
|
|
|
|
self._tag_repositories.SelectLeft()
|
|
|
|
self._SetSearchFocus()
|
|
|
|
|
|
def EventShowNext( self ):
|
|
|
|
if self._canvas_key is not None:
|
|
|
|
HG.client_controller.pub( 'canvas_show_next', self._canvas_key )
|
|
|
|
|
|
|
|
def EventShowPrevious( self ):
|
|
|
|
if self._canvas_key is not None:
|
|
|
|
HG.client_controller.pub( 'canvas_show_previous', self._canvas_key )
|
|
|
|
|
|
|
|
def EventServiceChanged( self, index ):
|
|
|
|
if not self or not QP.isValid( self ): # actually did get a runtime error here, on some Linux WM dialog shutdown
|
|
|
|
return
|
|
|
|
|
|
if self.sender() != self._tag_repositories:
|
|
|
|
return
|
|
|
|
|
|
page = self._tag_repositories.currentWidget()
|
|
|
|
if page is not None:
|
|
|
|
HG.client_controller.CallAfterQtSafe( page, page.SetTagBoxFocus )
|
|
|
|
|
|
|
|
def ProcessApplicationCommand( self, command ):
|
|
|
|
command_processed = True
|
|
|
|
command_type = command.GetCommandType()
|
|
data = command.GetData()
|
|
|
|
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
|
|
|
|
action = data
|
|
|
|
if action == 'manage_file_tags':
|
|
|
|
self._OKParent()
|
|
|
|
elif action == 'focus_media_viewer':
|
|
|
|
tlws = ClientGUIFunctions.GetTLWParents( self )
|
|
|
|
from . import ClientGUICanvas
|
|
|
|
command_processed = False
|
|
|
|
for tlw in tlws:
|
|
|
|
if isinstance( tlw, ClientGUICanvas.CanvasFrame ):
|
|
|
|
tlw.TakeFocusForUser()
|
|
|
|
command_processed = True
|
|
|
|
break
|
|
|
|
|
|
|
|
elif action == 'set_search_focus':
|
|
|
|
self._SetSearchFocus()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
class _Panel( QW.QWidget ):
|
|
|
|
okSignal = QC.Signal()
|
|
|
|
def __init__( self, parent, file_service_key, tag_service_key, media, immediate_commit, canvas_key = None ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._file_service_key = file_service_key
|
|
self._tag_service_key = tag_service_key
|
|
self._immediate_commit = immediate_commit
|
|
self._canvas_key = canvas_key
|
|
|
|
self._groups_of_content_updates = []
|
|
|
|
self._service = HG.client_controller.services_manager.GetService( self._tag_service_key )
|
|
|
|
self._i_am_local_tag_service = self._service.GetServiceType() == HC.LOCAL_TAG
|
|
|
|
self._tags_box_sorter = ClientGUICommon.StaticBoxSorterForListBoxTags( self, 'tags' )
|
|
|
|
self._tags_box = ClientGUIListBoxes.ListBoxTagsSelectionTagsDialog( self._tags_box_sorter, self.EnterTags, self.RemoveTags )
|
|
|
|
self._tags_box_sorter.SetTagsBox( self._tags_box )
|
|
|
|
#
|
|
|
|
self._new_options = HG.client_controller.new_options
|
|
|
|
if self._i_am_local_tag_service:
|
|
|
|
text = 'remove all/selected tags'
|
|
|
|
else:
|
|
|
|
text = 'petition all/selected tags'
|
|
|
|
|
|
self._remove_tags = ClientGUICommon.BetterButton( self._tags_box_sorter, text, self._RemoveTagsButton )
|
|
|
|
menu_items = []
|
|
|
|
call = HydrusData.Call( self._DoSiblingsAndParents, self._tag_service_key )
|
|
|
|
menu_items.append( ( 'normal', 'Hard-replace all applicable tags with their siblings and add missing parents. (Just this service\'s siblings and parents)', 'Fix siblings and parents.', call ) )
|
|
|
|
call = HydrusData.Call( self._DoSiblingsAndParents, CC.COMBINED_TAG_SERVICE_KEY )
|
|
|
|
menu_items.append( ( 'normal', 'Hard-replace all applicable tags with their siblings and add missing parents. (All service siblings and parents)', 'Fix siblings and parents.', call ) )
|
|
|
|
self._do_siblings_and_parents = ClientGUICommon.MenuBitmapButton( self._tags_box_sorter, CC.global_pixmaps().family, menu_items )
|
|
self._do_siblings_and_parents.setToolTip( 'Hard-replace all applicable tags with their siblings and add missing parents.' )
|
|
|
|
self._copy_button = ClientGUICommon.BetterBitmapButton( self._tags_box_sorter, CC.global_pixmaps().copy, self._Copy )
|
|
self._copy_button.setToolTip( 'Copy selected tags to the clipboard. If none are selected, copies all.' )
|
|
|
|
self._paste_button = ClientGUICommon.BetterBitmapButton( self._tags_box_sorter, CC.global_pixmaps().paste, self._Paste )
|
|
self._paste_button.setToolTip( 'Paste newline-separated tags from the clipboard into here.' )
|
|
|
|
self._show_deleted = False
|
|
|
|
menu_items = []
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerOptions( 'add_parents_on_manage_tags' )
|
|
|
|
menu_items.append( ( 'check', 'auto-add entered tags\' parents on add/pend action', 'If checked, adding any tag that has parents will also add those parents.', check_manager ) )
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerOptions( 'replace_siblings_on_manage_tags' )
|
|
|
|
menu_items.append( ( 'check', 'auto-replace entered siblings on add/pend action', 'If checked, adding any tag that has a sibling will instead add that sibling.', check_manager ) )
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerOptions( 'allow_remove_on_manage_tags_input' )
|
|
|
|
menu_items.append( ( 'check', 'allow remove/petition result on tag input for already existing tag', 'If checked, inputting a tag that already exists will try to remove it.', check_manager ) )
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerOptions( 'yes_no_on_remove_on_manage_tags' )
|
|
|
|
menu_items.append( ( 'check', 'confirm remove/petition tags on explicit delete actions', 'If checked, clicking the remove/petition tags button (or hitting the deleted key on the list) will first confirm the action with a yes/no dialog.', check_manager ) )
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerCalls( self._FlipShowDeleted, lambda: self._show_deleted )
|
|
|
|
menu_items.append( ( 'check', 'show deleted', 'Show deleted tags, if any.', check_manager ) )
|
|
|
|
menu_items.append( ( 'separator', 0, 0, 0 ) )
|
|
|
|
menu_items.append( ( 'normal', 'migrate tags for these files', 'Migrate the tags for the files used to launch this manage tags panel.', self._MigrateTags ) )
|
|
|
|
if not self._i_am_local_tag_service and self._service.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
menu_items.append( ( 'separator', 0, 0, 0 ) )
|
|
|
|
menu_items.append( ( 'normal', 'modify users who added the selected tags', 'Modify the users who added the selected tags.', self._ModifyMappers ) )
|
|
|
|
|
|
self._cog_button = ClientGUICommon.MenuBitmapButton( self._tags_box_sorter, CC.global_pixmaps().cog, menu_items )
|
|
|
|
#
|
|
|
|
expand_parents = self._new_options.GetBoolean( 'add_parents_on_manage_tags' )
|
|
|
|
self._add_tag_box = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.AddTags, expand_parents, self._file_service_key, self._tag_service_key, null_entry_callable = self.OK )
|
|
|
|
self._tags_box.ChangeTagService( self._tag_service_key )
|
|
|
|
self.SetMedia( media )
|
|
|
|
self._suggested_tags = ClientGUITagSuggestions.SuggestedTagsPanel( self, self._tag_service_key, self._media, self.AddTags, canvas_key = self._canvas_key )
|
|
|
|
button_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( button_hbox, self._remove_tags, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, self._do_siblings_and_parents, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, self._copy_button, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, self._paste_button, CC.FLAGS_VCENTER )
|
|
QP.AddToLayout( button_hbox, self._cog_button, CC.FLAGS_SIZER_CENTER )
|
|
|
|
self._tags_box_sorter.Add( button_hbox, CC.FLAGS_BUTTON_SIZER )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._tags_box_sorter, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._add_tag_box )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._suggested_tags, CC.FLAGS_EXPAND_BOTH_WAYS_POLITE )
|
|
QP.AddToLayout( hbox, vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'global', 'main_gui' ] )
|
|
|
|
self.setLayout( hbox )
|
|
|
|
if self._immediate_commit:
|
|
|
|
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
|
|
|
|
|
|
HG.client_controller.sub( self, 'CheckboxExpandParents', 'checkbox_manager_inverted' )
|
|
|
|
|
|
def _EnterTags( self, tags, only_add = False, only_remove = False, forced_reason = None ):
|
|
|
|
tags = HydrusTags.CleanTags( tags )
|
|
|
|
if not self._i_am_local_tag_service and self._service.HasPermission( HC.CONTENT_TYPE_MAPPINGS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
forced_reason = 'admin'
|
|
|
|
|
|
tags_managers = [ m.GetTagsManager() for m in self._media ]
|
|
|
|
currents = [ tags_manager.GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) for tags_manager in tags_managers ]
|
|
pendings = [ tags_manager.GetPending( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) for tags_manager in tags_managers ]
|
|
petitioneds = [ tags_manager.GetPetitioned( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) for tags_manager in tags_managers ]
|
|
|
|
num_files = len( self._media )
|
|
|
|
# let's figure out what these tags can mean for the media--add, remove, or what?
|
|
|
|
choices = collections.defaultdict( list )
|
|
|
|
for tag in tags:
|
|
|
|
num_current = sum( ( 1 for current in currents if tag in current ) )
|
|
|
|
if self._i_am_local_tag_service:
|
|
|
|
if not only_remove:
|
|
|
|
if num_current < num_files:
|
|
|
|
num_non_current = num_files - num_current
|
|
|
|
choices[ HC.CONTENT_UPDATE_ADD ].append( ( tag, num_non_current ) )
|
|
|
|
|
|
|
|
if not only_add:
|
|
|
|
if num_current > 0:
|
|
|
|
choices[ HC.CONTENT_UPDATE_DELETE ].append( ( tag, num_current ) )
|
|
|
|
|
|
|
|
else:
|
|
|
|
num_pending = sum( ( 1 for pending in pendings if tag in pending ) )
|
|
num_petitioned = sum( ( 1 for petitioned in petitioneds if tag in petitioned ) )
|
|
|
|
if not only_remove:
|
|
|
|
if num_current + num_pending < num_files:
|
|
|
|
num_pendable = num_files - ( num_current + num_pending )
|
|
|
|
choices[ HC.CONTENT_UPDATE_PEND ].append( ( tag, num_pendable ) )
|
|
|
|
|
|
|
|
if not only_add:
|
|
|
|
if num_current > num_petitioned and not only_add:
|
|
|
|
num_petitionable = num_current - num_petitioned
|
|
|
|
choices[ HC.CONTENT_UPDATE_PETITION ].append( ( tag, num_petitionable ) )
|
|
|
|
|
|
if num_pending > 0 and not only_add:
|
|
|
|
choices[ HC.CONTENT_UPDATE_RESCIND_PEND ].append( ( tag, num_pending ) )
|
|
|
|
|
|
|
|
if not only_remove:
|
|
|
|
if num_petitioned > 0:
|
|
|
|
choices[ HC.CONTENT_UPDATE_RESCIND_PETITION ].append( ( tag, num_petitioned ) )
|
|
|
|
|
|
|
|
|
|
|
|
if len( choices ) == 0:
|
|
|
|
return
|
|
|
|
|
|
# now we have options, let's ask the user what they want to do
|
|
|
|
if len( choices ) == 1:
|
|
|
|
[ ( choice_action, tag_counts ) ] = list(choices.items())
|
|
|
|
tags = { tag for ( tag, count ) in tag_counts }
|
|
|
|
else:
|
|
|
|
bdc_choices = []
|
|
|
|
preferred_order = [ HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE, HC.CONTENT_UPDATE_PEND, HC.CONTENT_UPDATE_RESCIND_PEND, HC.CONTENT_UPDATE_PETITION, HC.CONTENT_UPDATE_RESCIND_PETITION ]
|
|
|
|
choice_text_lookup = {}
|
|
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_ADD ] = 'add'
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_DELETE ] = 'delete'
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_PEND ] = 'pend'
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_PETITION ] = 'petition'
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_RESCIND_PEND ] = 'rescind pend'
|
|
choice_text_lookup[ HC.CONTENT_UPDATE_RESCIND_PETITION ] = 'rescind petition'
|
|
|
|
for choice_action in preferred_order:
|
|
|
|
if choice_action not in choices:
|
|
|
|
continue
|
|
|
|
|
|
choice_text_prefix = choice_text_lookup[ choice_action ]
|
|
|
|
tag_counts = choices[ choice_action ]
|
|
|
|
tags = { tag for ( tag, count ) in tag_counts }
|
|
|
|
if len( tags ) == 1:
|
|
|
|
[ ( tag, count ) ] = tag_counts
|
|
|
|
text = choice_text_prefix + ' "' + HydrusText.ElideText( tag, 64 ) + '" for ' + HydrusData.ToHumanInt( count ) + ' files'
|
|
|
|
else:
|
|
|
|
text = choice_text_prefix + ' ' + HydrusData.ToHumanInt( len( tags ) ) + ' tags'
|
|
|
|
|
|
data = ( choice_action, tags )
|
|
|
|
if len( tag_counts ) > 25:
|
|
|
|
t_c = tag_counts[:25]
|
|
|
|
t_c_lines = [ tag + ' - ' + HydrusData.ToHumanInt( count ) + ' files' for ( tag, count ) in t_c ]
|
|
|
|
t_c_lines.append( 'and ' + HydrusData.ToHumanInt( len( tag_counts ) - 25 ) + ' others' )
|
|
|
|
tooltip = os.linesep.join( t_c_lines )
|
|
|
|
else:
|
|
|
|
tooltip = os.linesep.join( ( tag + ' - ' + HydrusData.ToHumanInt( count ) + ' files' for ( tag, count ) in tag_counts ) )
|
|
|
|
|
|
bdc_choices.append( ( text, data, tooltip ) )
|
|
|
|
|
|
try:
|
|
|
|
( choice_action, tags ) = ClientGUIDialogsQuick.SelectFromListButtons( self, 'What would you like to do?', bdc_choices )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
|
|
reason = None
|
|
|
|
if choice_action == HC.CONTENT_UPDATE_PETITION:
|
|
|
|
if forced_reason is None:
|
|
|
|
# add the easy reason buttons here
|
|
|
|
if len( tags ) == 1:
|
|
|
|
( tag, ) = tags
|
|
|
|
tag_text = '"' + tag + '"'
|
|
|
|
else:
|
|
|
|
tag_text = 'the ' + HydrusData.ToHumanInt( len( tags ) ) + ' tags'
|
|
|
|
|
|
message = 'Enter a reason for ' + tag_text + ' to be removed. A janitor will review your petition.'
|
|
|
|
suggestions = []
|
|
|
|
suggestions.append( 'mangled parse/typo' )
|
|
suggestions.append( 'not applicable' )
|
|
suggestions.append( 'should be namespaced' )
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
else:
|
|
|
|
reason = forced_reason
|
|
|
|
|
|
|
|
# we have an action and tags, so let's effect the content updates
|
|
|
|
content_updates_group = []
|
|
|
|
recent_tags = set()
|
|
|
|
for tag in tags:
|
|
|
|
if choice_action == HC.CONTENT_UPDATE_ADD: media_to_affect = [ m for m in self._media if tag not in m.GetTagsManager().GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
elif choice_action == HC.CONTENT_UPDATE_DELETE: media_to_affect = [ m for m in self._media if tag in m.GetTagsManager().GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
elif choice_action == HC.CONTENT_UPDATE_PEND: media_to_affect = [ m for m in self._media if tag not in m.GetTagsManager().GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) and tag not in m.GetTagsManager().GetPending( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
elif choice_action == HC.CONTENT_UPDATE_PETITION: media_to_affect = [ m for m in self._media if tag in m.GetTagsManager().GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) and tag not in m.GetTagsManager().GetPetitioned( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
elif choice_action == HC.CONTENT_UPDATE_RESCIND_PEND: media_to_affect = [ m for m in self._media if tag in m.GetTagsManager().GetPending( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
elif choice_action == HC.CONTENT_UPDATE_RESCIND_PETITION: media_to_affect = [ m for m in self._media if tag in m.GetTagsManager().GetPetitioned( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) ]
|
|
|
|
hashes = set( itertools.chain.from_iterable( ( m.GetHashes() for m in media_to_affect ) ) )
|
|
|
|
if len( hashes ) > 0:
|
|
|
|
content_updates = []
|
|
|
|
if choice_action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_PEND ):
|
|
|
|
if self._new_options.GetBoolean( 'replace_siblings_on_manage_tags' ):
|
|
|
|
siblings_manager = HG.client_controller.tag_siblings_manager
|
|
|
|
tag = siblings_manager.CollapseTag( self._tag_service_key, tag )
|
|
|
|
|
|
recent_tags.add( tag )
|
|
|
|
if self._new_options.GetBoolean( 'add_parents_on_manage_tags' ):
|
|
|
|
tag_parents_manager = HG.client_controller.tag_parents_manager
|
|
|
|
parents = tag_parents_manager.GetParents( self._tag_service_key, tag )
|
|
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, choice_action, ( parent, hashes ) ) for parent in parents ) )
|
|
|
|
|
|
|
|
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, choice_action, ( tag, hashes ), reason = reason ) )
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
if not self._immediate_commit:
|
|
|
|
for m in media_to_affect:
|
|
|
|
mt = m.GetTagsManager()
|
|
|
|
for content_update in content_updates:
|
|
|
|
mt.ProcessContentUpdate( self._tag_service_key, content_update )
|
|
|
|
|
|
|
|
|
|
content_updates_group.extend( content_updates )
|
|
|
|
|
|
|
|
|
|
if len( recent_tags ) > 0 and HG.client_controller.new_options.GetNoneableInteger( 'num_recent_tags' ) is not None:
|
|
|
|
HG.client_controller.Write( 'push_recent_tags', self._tag_service_key, recent_tags )
|
|
|
|
|
|
if len( content_updates_group ) > 0:
|
|
|
|
if self._immediate_commit:
|
|
|
|
service_keys_to_content_updates = { self._tag_service_key : content_updates_group }
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
else:
|
|
|
|
self._groups_of_content_updates.append( content_updates_group )
|
|
|
|
|
|
|
|
self._tags_box.SetTagsByMedia( self._media, force_reload = True )
|
|
|
|
|
|
def _MigrateTags( self ):
|
|
|
|
hashes = set()
|
|
|
|
for m in self._media:
|
|
|
|
hashes.update( m.GetHashes() )
|
|
|
|
|
|
def do_it( tag_service_key, hashes ):
|
|
|
|
frame = ClientGUITopLevelWindows.FrameThatTakesScrollablePanel( HG.client_controller.gui, 'tag migration' )
|
|
|
|
panel = ClientGUIScrolledPanelsReview.MigrateTagsPanel( frame, self._tag_service_key, hashes )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
QP.CallAfter( do_it, self._tag_service_key, hashes )
|
|
|
|
self.OK()
|
|
|
|
|
|
def _Copy( self ):
|
|
|
|
tags = list( self._tags_box.GetSelectedTags() )
|
|
|
|
if len( tags ) == 0:
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediasTagCount( self._media, self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
tags = set( current_tags_to_count.keys() ).union( pending_tags_to_count.keys() )
|
|
|
|
|
|
if len( tags ) > 0:
|
|
|
|
tags = HydrusTags.SortNumericTags( tags )
|
|
|
|
text = os.linesep.join( tags )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _DoSiblingsAndParents( self, service_key ):
|
|
|
|
tag_siblings_manager = HG.client_controller.tag_siblings_manager
|
|
|
|
tag_parents_manager = HG.client_controller.tag_parents_manager
|
|
|
|
removee_tags_to_hashes = collections.defaultdict( list )
|
|
addee_tags_to_hashes = collections.defaultdict( list )
|
|
|
|
for m in self._media:
|
|
|
|
hash = m.GetHash()
|
|
|
|
tags = m.GetTagsManager().GetCurrentAndPending( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE )
|
|
|
|
sibling_correct_tags = tag_siblings_manager.CollapseTags( service_key, tags, service_strict = True )
|
|
|
|
sibling_and_parent_correct_tags = tag_parents_manager.ExpandTags( service_key, sibling_correct_tags, service_strict = True )
|
|
|
|
removee_tags = tags.difference( sibling_and_parent_correct_tags )
|
|
addee_tags = sibling_and_parent_correct_tags.difference( tags )
|
|
|
|
for tag in removee_tags:
|
|
|
|
removee_tags_to_hashes[ tag ].append( hash )
|
|
|
|
|
|
for tag in addee_tags:
|
|
|
|
addee_tags_to_hashes[ tag ].append( hash )
|
|
|
|
|
|
|
|
if len( removee_tags_to_hashes ) == 0 and len( addee_tags_to_hashes ) == 0:
|
|
|
|
QW.QMessageBox.information( self, 'Information', 'No replacements seem to be needed.' )
|
|
|
|
return
|
|
|
|
|
|
summary_message = 'The following changes will be made:'
|
|
|
|
if len( removee_tags_to_hashes ) > 0:
|
|
|
|
removee_tags_to_counts = { tag : len( hashes ) for ( tag, hashes ) in removee_tags_to_hashes.items() }
|
|
|
|
removee_tags = list( removee_tags_to_counts.keys() )
|
|
|
|
ClientTags.SortTags( CC.SORT_BY_INCIDENCE_DESC, removee_tags, removee_tags_to_counts )
|
|
|
|
removee_tag_statements = [ '{} ({})'.format( tag, removee_tags_to_counts[ tag ] ) for tag in removee_tags ]
|
|
|
|
if len( removee_tag_statements ) > 10:
|
|
|
|
removee_tag_statements = removee_tag_statements[:10]
|
|
|
|
removee_tag_statements.append( 'and more' )
|
|
|
|
|
|
summary_message += os.linesep * 2
|
|
summary_message += 'REMOVE: ' + ', '.join( removee_tag_statements )
|
|
|
|
|
|
if len( addee_tags_to_hashes ) > 0:
|
|
|
|
addee_tags_to_counts = { tag : len( hashes ) for ( tag, hashes ) in addee_tags_to_hashes.items() }
|
|
|
|
addee_tags = list( addee_tags_to_counts.keys() )
|
|
|
|
ClientTags.SortTags( CC.SORT_BY_INCIDENCE_DESC, addee_tags, addee_tags_to_counts )
|
|
|
|
addee_tag_statements = [ '{} ({})'.format( tag, addee_tags_to_counts[ tag ] ) for tag in addee_tags ]
|
|
|
|
if len( addee_tag_statements ) > 10:
|
|
|
|
addee_tag_statements = addee_tag_statements[:10]
|
|
|
|
addee_tag_statements.append( 'and more' )
|
|
|
|
|
|
summary_message += os.linesep * 2
|
|
summary_message += 'ADD: ' + ', '.join( addee_tag_statements )
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, summary_message, yes_label = 'do it', no_label = 'forget it' )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
# this no longer does pend/petition for repos, making it local-only
|
|
# therefore, clients' unusual siblings no longer affect the tag repo
|
|
|
|
addee_action = HC.CONTENT_UPDATE_ADD
|
|
removee_action = HC.CONTENT_UPDATE_DELETE
|
|
reason = None
|
|
|
|
content_updates = []
|
|
|
|
for ( tag, hashes ) in removee_tags_to_hashes.items():
|
|
|
|
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, removee_action, ( tag, hashes ), reason = reason ) )
|
|
|
|
|
|
for ( tag, hashes ) in addee_tags_to_hashes.items():
|
|
|
|
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, addee_action, ( tag, hashes ), reason = reason ) )
|
|
|
|
|
|
if not self._immediate_commit:
|
|
|
|
hashes_to_tag_managers = { m.GetHash() : m.GetTagsManager() for m in self._media }
|
|
|
|
for content_update in content_updates:
|
|
|
|
hashes = content_update.GetHashes()
|
|
|
|
for hash in hashes:
|
|
|
|
if hash in hashes_to_tag_managers:
|
|
|
|
hashes_to_tag_managers[ hash ].ProcessContentUpdate( self._tag_service_key, content_update )
|
|
|
|
|
|
|
|
|
|
self._groups_of_content_updates.append( content_updates )
|
|
|
|
else:
|
|
|
|
service_keys_to_content_updates = { self._tag_service_key : content_updates }
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
self._tags_box.SetTagsByMedia( self._media, force_reload = True )
|
|
|
|
|
|
def _FlipShowDeleted( self ):
|
|
|
|
self._show_deleted = not self._show_deleted
|
|
|
|
self._tags_box.SetShow( 'deleted', self._show_deleted )
|
|
|
|
|
|
def _ModifyMappers( self ):
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'this does not work yet!' )
|
|
|
|
return
|
|
|
|
contents = []
|
|
|
|
tags = self._tags_box.GetSelectedTags()
|
|
|
|
hashes = set( itertools.chain.from_iterable( ( m.GetHashes() for m in self._media ) ) )
|
|
|
|
for tag in tags:
|
|
|
|
contents.extend( [ HydrusNetwork.Content( HC.CONTENT_TYPE_MAPPING, ( tag, hash ) ) for hash in hashes ] )
|
|
|
|
|
|
if len( contents ) > 0:
|
|
|
|
subject_accounts = 'blah' # fetch subjects from the server using the contents
|
|
|
|
with ClientGUIDialogs.DialogModifyAccounts( self, self._tag_service_key, subject_accounts ) as dlg:
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
|
|
def _Paste( self ):
|
|
|
|
try:
|
|
|
|
text = HG.client_controller.GetClipboardText()
|
|
|
|
except HydrusExceptions.DataMissing as e:
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', str(e) )
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
tags = HydrusText.DeserialiseNewlinedTexts( text )
|
|
|
|
tags = HydrusTags.CleanTags( tags )
|
|
|
|
self.AddTags( tags, only_add = True )
|
|
|
|
except Exception as e:
|
|
|
|
QW.QMessageBox.warning( self, 'Warning', 'I could not understand what was in the clipboard' )
|
|
|
|
|
|
|
|
def _RemoveTagsButton( self ):
|
|
|
|
tags_managers = [ m.GetTagsManager() for m in self._media ]
|
|
|
|
removable_tags = set()
|
|
|
|
for tags_manager in tags_managers:
|
|
|
|
removable_tags.update( tags_manager.GetCurrent( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) )
|
|
removable_tags.update( tags_manager.GetPending( self._tag_service_key, ClientTags.TAG_DISPLAY_STORAGE ) )
|
|
|
|
|
|
selected_tags = list( self._tags_box.GetSelectedTags() )
|
|
|
|
if len( selected_tags ) == 0:
|
|
|
|
tags_to_remove = list( removable_tags )
|
|
|
|
else:
|
|
|
|
tags_to_remove = [ tag for tag in selected_tags if tag in removable_tags ]
|
|
|
|
|
|
tags_to_remove = HydrusTags.SortNumericTags( tags_to_remove )
|
|
|
|
self.RemoveTags( tags_to_remove )
|
|
|
|
|
|
def AddTags( self, tags, only_add = False ):
|
|
|
|
if not self._new_options.GetBoolean( 'allow_remove_on_manage_tags_input' ):
|
|
|
|
only_add = True
|
|
|
|
|
|
if len( tags ) > 0:
|
|
|
|
self.EnterTags( tags, only_add = only_add )
|
|
|
|
|
|
|
|
def CheckboxExpandParents( self ):
|
|
|
|
self._add_tag_box.SetExpandParents( self._new_options.GetBoolean( 'add_parents_on_manage_tags' ) )
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
self._add_tag_box.CancelCurrentResultsFetchJob()
|
|
|
|
|
|
def EnterTags( self, tags, only_add = False ):
|
|
|
|
if len( tags ) > 0:
|
|
|
|
self._EnterTags( tags, only_add = only_add )
|
|
|
|
|
|
|
|
def GetGroupsOfContentUpdates( self ):
|
|
|
|
return ( self._tag_service_key, self._groups_of_content_updates )
|
|
|
|
|
|
def HasChanges( self ):
|
|
|
|
return len( self._groups_of_content_updates ) > 0
|
|
|
|
|
|
def OK( self ):
|
|
|
|
self.okSignal.emit()
|
|
|
|
|
|
def ProcessApplicationCommand( self, command ):
|
|
|
|
command_processed = True
|
|
|
|
command_type = command.GetCommandType()
|
|
data = command.GetData()
|
|
|
|
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
|
|
|
|
action = data
|
|
|
|
if action == 'set_search_focus':
|
|
|
|
self.SetTagBoxFocus()
|
|
|
|
elif action in ( 'show_and_focus_manage_tags_favourite_tags', 'show_and_focus_manage_tags_related_tags', 'show_and_focus_manage_tags_file_lookup_script_tags', 'show_and_focus_manage_tags_recent_tags' ):
|
|
|
|
self._suggested_tags.TakeFocusForUser( action )
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
|
|
|
for ( service_key, content_updates ) in list(service_keys_to_content_updates.items()):
|
|
|
|
for content_update in content_updates:
|
|
|
|
for m in self._media:
|
|
|
|
if len( m.GetHashes().intersection( content_update.GetHashes() ) ) > 0:
|
|
|
|
m.GetMediaResult().ProcessContentUpdate( service_key, content_update )
|
|
|
|
|
|
|
|
|
|
|
|
self._tags_box.SetTagsByMedia( self._media, force_reload = True )
|
|
|
|
|
|
def RemoveTags( self, tags ):
|
|
|
|
if len( tags ) > 0:
|
|
|
|
if self._new_options.GetBoolean( 'yes_no_on_remove_on_manage_tags' ):
|
|
|
|
if len( tags ) < 10:
|
|
|
|
message = 'Are you sure you want to remove these tags:'
|
|
message += os.linesep * 2
|
|
message += os.linesep.join( ( HydrusText.ElideText( tag, 64 ) for tag in tags ) )
|
|
|
|
else:
|
|
|
|
message = 'Are you sure you want to remove these ' + HydrusData.ToHumanInt( len( tags ) ) + ' tags?'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._EnterTags( tags, only_remove = True )
|
|
|
|
|
|
|
|
def SetMedia( self, media ):
|
|
|
|
if media is None:
|
|
|
|
media = []
|
|
|
|
|
|
self._media = media
|
|
|
|
self._tags_box.SetTagsByMedia( self._media )
|
|
|
|
|
|
def SetTagBoxFocus( self ):
|
|
|
|
self._add_tag_box.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
|
|
|
|
class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|
|
|
def __init__( self, parent, tags = None ):
|
|
|
|
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
|
|
|
|
self._tag_repositories = ClientGUICommon.BetterNotebook( self )
|
|
|
|
#
|
|
|
|
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
|
|
|
|
services = list( HG.client_controller.services_manager.GetServices( ( HC.LOCAL_TAG, ) ) )
|
|
|
|
services.extend( [ service for service in HG.client_controller.services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) if service.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_PETITION ) ] )
|
|
|
|
for service in services:
|
|
|
|
name = service.GetName()
|
|
service_key = service.GetServiceKey()
|
|
|
|
page = self._Panel( self._tag_repositories, service_key, tags )
|
|
|
|
select = service_key == default_tag_repository_key
|
|
|
|
self._tag_repositories.addTab( page, name )
|
|
if select: self._tag_repositories.setCurrentWidget( page )
|
|
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._tag_repositories, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _SetSearchFocus( self ):
|
|
|
|
page = self._tag_repositories.currentWidget()
|
|
|
|
if page is not None:
|
|
|
|
page.SetTagBoxFocus()
|
|
|
|
|
|
|
|
def CommitChanges( self ):
|
|
|
|
if self._tag_repositories.currentWidget().HasUncommittedPair():
|
|
|
|
message = 'Are you sure you want to OK? You have an uncommitted pair.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
raise HydrusExceptions.VetoException( 'Cancelled OK due to uncommitted pair.' )
|
|
|
|
|
|
|
|
service_keys_to_content_updates = {}
|
|
|
|
for page in self._tag_repositories.GetPages():
|
|
|
|
( service_key, content_updates ) = page.GetContentUpdates()
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
service_keys_to_content_updates[ service_key ] = content_updates
|
|
|
|
|
|
|
|
if len( service_keys_to_content_updates ) > 0:
|
|
|
|
HG.client_controller.Write( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
class _Panel( QW.QWidget ):
|
|
|
|
def __init__( self, parent, service_key, tags = None ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._service_key = service_key
|
|
|
|
self._service = HG.client_controller.services_manager.GetService( self._service_key )
|
|
|
|
self._i_am_local_tag_service = self._service.GetServiceType() == HC.LOCAL_TAG
|
|
|
|
self._pairs_to_reasons = {}
|
|
|
|
self._original_statuses_to_pairs = collections.defaultdict( set )
|
|
self._current_statuses_to_pairs = collections.defaultdict( set )
|
|
|
|
self._show_all = QW.QCheckBox( self )
|
|
|
|
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
columns = [ ( '', 6 ), ( 'child', 25 ), ( 'parent', -1 ) ]
|
|
|
|
self._tag_parents = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'tag_parents', 8, 25, columns, self._ConvertPairToListCtrlTuples, delete_key_callback = self._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
|
|
|
|
listctrl_panel.SetListCtrl( self._tag_parents )
|
|
|
|
self._tag_parents.Sort( 2 )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'from clipboard', 'Load parents from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, False ) ) )
|
|
menu_items.append( ( 'normal', 'from clipboard (only add pairs--no deletions)', 'Load parents from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, True ) ) )
|
|
menu_items.append( ( 'normal', 'from .txt file', 'Load parents from a .txt file.', HydrusData.Call( self._ImportFromTXT, False ) ) )
|
|
menu_items.append( ( 'normal', 'from .txt file (only add pairs--no deletions)', 'Load parents from a .txt file.', HydrusData.Call( self._ImportFromTXT, True ) ) )
|
|
|
|
listctrl_panel.AddMenuButton( 'import', menu_items )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'to clipboard', 'Save selected parents to your clipboard.', self._ExportToClipboard ) )
|
|
menu_items.append( ( 'normal', 'to .txt file', 'Save selected parents to a .txt file.', self._ExportToTXT ) )
|
|
|
|
listctrl_panel.AddMenuButton( 'export', menu_items, enabled_only_on_selection = True )
|
|
|
|
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, show_sibling_text = False )
|
|
self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, show_sibling_text = False )
|
|
|
|
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._children, ( 12, 6 ) )
|
|
|
|
self._children.setMinimumHeight( preview_height )
|
|
self._parents.setMinimumHeight( preview_height )
|
|
|
|
expand_parents = True
|
|
|
|
self._child_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterChildren, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key, show_paste_button = True )
|
|
self._child_input.setEnabled( False )
|
|
|
|
self._parent_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterParents, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key, show_paste_button = True )
|
|
self._parent_input.setEnabled( False )
|
|
|
|
self._add = QW.QPushButton( 'add', self )
|
|
self._add.clicked.connect( self.EventAddButton )
|
|
self._add.setEnabled( False )
|
|
|
|
#
|
|
|
|
#
|
|
|
|
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' + os.linesep + '.' )
|
|
self._count_st = ClientGUICommon.BetterStaticText( self, '' )
|
|
|
|
tags_box = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( tags_box, self._children, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( tags_box, self._parents, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
input_box = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( input_box, self._child_input )
|
|
QP.AddToLayout( input_box, self._parent_input )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._status_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._add, CC.FLAGS_LONE_BUTTON )
|
|
QP.AddToLayout( vbox, tags_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, input_box )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._tag_parents.itemSelectionChanged.connect( self._SetButtonStatus )
|
|
|
|
self._children.listBoxChanged.connect( self._UpdateListCtrlData )
|
|
self._parents.listBoxChanged.connect( self._UpdateListCtrlData )
|
|
self._show_all.clicked.connect( self._UpdateListCtrlData )
|
|
|
|
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
|
|
|
|
|
|
def _AddPairs( self, pairs, add_only = False ):
|
|
|
|
pairs = list( pairs )
|
|
|
|
pairs.sort( key = lambda c_p: HydrusTags.ConvertTagToSortable( c_p[1] ) )
|
|
|
|
new_pairs = []
|
|
current_pairs = []
|
|
petitioned_pairs = []
|
|
pending_pairs = []
|
|
|
|
for pair in pairs:
|
|
|
|
if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
|
|
|
|
if not add_only:
|
|
|
|
pending_pairs.append( pair )
|
|
|
|
|
|
elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
|
|
|
|
petitioned_pairs.append( pair )
|
|
|
|
elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
if not add_only:
|
|
|
|
current_pairs.append( pair )
|
|
|
|
|
|
elif self._CanAdd( pair ):
|
|
|
|
new_pairs.append( pair )
|
|
|
|
|
|
|
|
suggestions = []
|
|
|
|
suggestions.append( 'obvious by definition (a sword is a weapon)' )
|
|
suggestions.append( 'character/series/studio/etc... belonging (character x belongs to series y)' )
|
|
suggestions.append( 'character/person/etc... properties (character x is a female)' )
|
|
|
|
affected_pairs = []
|
|
|
|
if len( new_pairs ) > 0:
|
|
|
|
do_it = True
|
|
|
|
if not self._i_am_local_tag_service:
|
|
|
|
if self._service.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
reason = 'admin'
|
|
|
|
else:
|
|
|
|
if len( new_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in new_pairs ) )
|
|
|
|
|
|
message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your petition.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
do_it = False
|
|
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
for pair in new_pairs: self._pairs_to_reasons[ pair ] = reason
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].update( new_pairs )
|
|
|
|
affected_pairs.extend( new_pairs )
|
|
|
|
|
|
else:
|
|
|
|
if len( current_pairs ) > 0:
|
|
|
|
do_it = True
|
|
|
|
if not self._i_am_local_tag_service:
|
|
|
|
if len( current_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in current_pairs ) )
|
|
|
|
|
|
if len( current_pairs ) > 1:
|
|
|
|
message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Already exist.'
|
|
|
|
else:
|
|
|
|
message = 'The pair ' + pair_strings + ' already exists.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'petition it', no_label = 'do nothing' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
if self._service.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
reason = 'admin'
|
|
|
|
else:
|
|
|
|
message = 'Enter a reason for this pair to be removed. A janitor will review your petition.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
do_it = False
|
|
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
for pair in current_pairs: self._pairs_to_reasons[ pair ] = reason
|
|
|
|
|
|
|
|
else:
|
|
|
|
do_it = False
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
|
|
|
|
affected_pairs.extend( current_pairs )
|
|
|
|
|
|
|
|
if len( pending_pairs ) > 0:
|
|
|
|
if len( pending_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in pending_pairs ) )
|
|
|
|
|
|
if len( pending_pairs ) > 1:
|
|
|
|
message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are pending.'
|
|
|
|
else:
|
|
|
|
message = 'The pair ' + pair_strings + ' is pending.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
|
|
|
|
affected_pairs.extend( pending_pairs )
|
|
|
|
|
|
|
|
if len( petitioned_pairs ) > 0:
|
|
|
|
if len( petitioned_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in petitioned_pairs ) )
|
|
|
|
|
|
if len( petitioned_pairs ) > 1:
|
|
|
|
message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are petitioned.'
|
|
|
|
else:
|
|
|
|
message = 'The pair ' + pair_strings + ' is petitioned.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
|
|
|
|
affected_pairs.extend( petitioned_pairs )
|
|
|
|
|
|
|
|
|
|
if len( affected_pairs ) > 0:
|
|
|
|
def in_current( pair ):
|
|
|
|
for status in ( HC.CONTENT_STATUS_CURRENT, HC.CONTENT_STATUS_PENDING, HC.CONTENT_STATUS_PETITIONED ):
|
|
|
|
if pair in self._current_statuses_to_pairs[ status ]:
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
affected_pairs = [ ( self._tag_parents.HasData( pair ), in_current( pair ), pair ) for pair in affected_pairs ]
|
|
|
|
to_add = [ pair for ( exists, current, pair ) in affected_pairs if not exists ]
|
|
to_update = [ pair for ( exists, current, pair ) in affected_pairs if exists and current ]
|
|
to_delete = [ pair for ( exists, current, pair ) in affected_pairs if exists and not current ]
|
|
|
|
self._tag_parents.AddDatas( to_add )
|
|
self._tag_parents.UpdateDatas( to_update )
|
|
self._tag_parents.DeleteDatas( to_delete )
|
|
|
|
self._tag_parents.Sort()
|
|
|
|
|
|
|
|
def _CanAdd( self, potential_pair ):
|
|
|
|
( potential_child, potential_parent ) = potential_pair
|
|
|
|
if potential_child == potential_parent: return False
|
|
|
|
current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
|
|
|
|
current_children = { child for ( child, parent ) in current_pairs }
|
|
|
|
# test for loops
|
|
|
|
if potential_parent in current_children:
|
|
|
|
simple_children_to_parents = ClientManagers.BuildSimpleChildrenToParents( current_pairs )
|
|
|
|
if ClientManagers.LoopInSimpleChildrenToParents( simple_children_to_parents, potential_child, potential_parent ):
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Adding '+potential_child+'->'+potential_parent+' would create a loop!' )
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
def _ConvertPairToListCtrlTuples( self, pair ):
|
|
|
|
( child, parent ) = pair
|
|
|
|
if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
|
|
|
|
status = HC.CONTENT_STATUS_PENDING
|
|
|
|
elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
|
|
|
|
status = HC.CONTENT_STATUS_PETITIONED
|
|
|
|
elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
status = HC.CONTENT_STATUS_CURRENT
|
|
|
|
|
|
sign = HydrusData.ConvertStatusToPrefix( status )
|
|
|
|
pretty_status = sign
|
|
|
|
display_tuple = ( pretty_status, child, parent )
|
|
sort_tuple = ( status, child, parent )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _DeserialiseImportString( self, import_string ):
|
|
|
|
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
|
|
|
|
if len( tags ) % 2 == 1:
|
|
|
|
raise Exception( 'Uneven number of tags found!' )
|
|
|
|
|
|
pairs = []
|
|
|
|
for i in range( len( tags ) // 2 ):
|
|
|
|
pair = ( tags[ 2 * i ], tags[ ( 2 * i ) + 1 ] )
|
|
|
|
pairs.append( pair )
|
|
|
|
|
|
return pairs
|
|
|
|
|
|
def _ExportToClipboard( self ):
|
|
|
|
export_string = self._GetExportString()
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', export_string )
|
|
|
|
|
|
def _ExportToTXT( self ):
|
|
|
|
export_string = self._GetExportString()
|
|
|
|
with QP.FileDialog( self, 'Set the export path.', defaultFile = 'parents.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
path = dlg.GetPath()
|
|
|
|
with open( path, 'w', encoding = 'utf-8' ) as f:
|
|
|
|
f.write( export_string )
|
|
|
|
|
|
|
|
|
|
|
|
def _GetExportString( self ):
|
|
|
|
tags = []
|
|
|
|
for ( a, b ) in self._tag_parents.GetData( only_selected = True ):
|
|
|
|
tags.append( a )
|
|
tags.append( b )
|
|
|
|
|
|
export_string = os.linesep.join( tags )
|
|
|
|
return export_string
|
|
|
|
|
|
def _ImportFromClipboard( self, add_only = False ):
|
|
|
|
try:
|
|
|
|
import_string = HG.client_controller.GetClipboardText()
|
|
|
|
except HydrusExceptions.DataMissing as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', str(e) )
|
|
|
|
return
|
|
|
|
|
|
pairs = self._DeserialiseImportString( import_string )
|
|
|
|
self._AddPairs( pairs, add_only = add_only )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
|
|
def _ImportFromTXT( self, add_only = False ):
|
|
|
|
with QP.FileDialog( self, 'Select the file to import.', acceptMode = QW.QFileDialog.AcceptOpen ) as dlg:
|
|
|
|
if dlg.exec() != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
path = dlg.GetPath()
|
|
|
|
|
|
|
|
with open( path, 'r', encoding = 'utf-8' ) as f:
|
|
|
|
import_string = f.read()
|
|
|
|
|
|
pairs = self._DeserialiseImportString( import_string )
|
|
|
|
self._AddPairs( pairs, add_only = add_only )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
|
|
def _ListCtrlActivated( self ):
|
|
|
|
parents_to_children = collections.defaultdict( set )
|
|
|
|
pairs = self._tag_parents.GetData( only_selected = True )
|
|
|
|
if len( pairs ) > 0:
|
|
|
|
self._AddPairs( pairs )
|
|
|
|
|
|
|
|
def _SetButtonStatus( self ):
|
|
|
|
if len( self._children.GetTags() ) == 0 or len( self._parents.GetTags() ) == 0:
|
|
|
|
self._add.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._add.setEnabled( True )
|
|
|
|
|
|
|
|
def _UpdateListCtrlData( self ):
|
|
|
|
children = self._children.GetTags()
|
|
parents = self._parents.GetTags()
|
|
|
|
pertinent_tags = children.union( parents )
|
|
|
|
self._tag_parents.DeleteDatas( self._tag_parents.GetData() )
|
|
|
|
all_pairs = set()
|
|
|
|
show_all = self._show_all.isChecked()
|
|
|
|
for ( status, pairs ) in list(self._current_statuses_to_pairs.items()):
|
|
|
|
if status == HC.CONTENT_STATUS_DELETED:
|
|
|
|
continue
|
|
|
|
|
|
if len( pertinent_tags ) == 0:
|
|
|
|
if status == HC.CONTENT_STATUS_CURRENT and not show_all:
|
|
|
|
continue
|
|
|
|
|
|
# show all pending/petitioned
|
|
|
|
all_pairs.update( pairs )
|
|
|
|
else:
|
|
|
|
# show all appropriate
|
|
|
|
for pair in pairs:
|
|
|
|
( a, b ) = pair
|
|
|
|
if a in pertinent_tags or b in pertinent_tags or show_all:
|
|
|
|
all_pairs.add( pair )
|
|
|
|
|
|
|
|
|
|
|
|
self._tag_parents.AddDatas( all_pairs )
|
|
|
|
self._tag_parents.Sort()
|
|
|
|
|
|
def EnterChildren( self, tags ):
|
|
|
|
if len( tags ) > 0:
|
|
|
|
self._parents.RemoveTags( tags )
|
|
|
|
self._children.EnterTags( tags )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
|
|
def EnterParents( self, tags ):
|
|
|
|
if len( tags ) > 0:
|
|
|
|
self._children.RemoveTags( tags )
|
|
|
|
self._parents.EnterTags( tags )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
|
|
def EventAddButton( self ):
|
|
|
|
children = self._children.GetTags()
|
|
parents = self._parents.GetTags()
|
|
|
|
pairs = list( itertools.product( children, parents ) )
|
|
|
|
self._AddPairs( pairs )
|
|
|
|
self._children.SetTags( [] )
|
|
self._parents.SetTags( [] )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
def GetContentUpdates( self ):
|
|
|
|
# we make it manually here because of the mass pending tags done (but not undone on a rescind) on a pending pair!
|
|
# we don't want to send a pend and then rescind it, cause that will spam a thousand bad tags and not undo it
|
|
|
|
content_updates = []
|
|
|
|
if self._i_am_local_tag_service:
|
|
|
|
for pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]: content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_ADD, pair ) )
|
|
for pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]: content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE, pair ) )
|
|
|
|
else:
|
|
|
|
current_pending = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]
|
|
original_pending = self._original_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
current_petitioned = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]
|
|
original_petitioned = self._original_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]
|
|
|
|
new_pends = current_pending.difference( original_pending )
|
|
rescinded_pends = original_pending.difference( current_pending )
|
|
|
|
new_petitions = current_petitioned.difference( original_petitioned )
|
|
rescinded_petitions = original_petitioned.difference( current_petitioned )
|
|
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_PEND, pair, reason = self._pairs_to_reasons[ pair ] ) for pair in new_pends ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_RESCIND_PEND, pair ) for pair in rescinded_pends ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_PETITION, pair, reason = self._pairs_to_reasons[ pair ] ) for pair in new_petitions ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_RESCIND_PETITION, pair ) for pair in rescinded_petitions ) )
|
|
|
|
|
|
return ( self._service_key, content_updates )
|
|
|
|
|
|
def HasUncommittedPair( self ):
|
|
|
|
return len( self._children.GetTags() ) > 0 and len( self._parents.GetTags() ) > 0
|
|
|
|
|
|
def SetTagBoxFocus( self ):
|
|
|
|
if len( self._children.GetTags() ) == 0: self._child_input.setFocus( QC.Qt.OtherFocusReason )
|
|
else: self._parent_input.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
|
|
def THREADInitialise( self, tags, service_key ):
|
|
|
|
def qt_code( original_statuses_to_pairs, current_statuses_to_pairs ):
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
self._original_statuses_to_pairs = original_statuses_to_pairs
|
|
self._current_statuses_to_pairs = current_statuses_to_pairs
|
|
|
|
self._status_st.setText( 'Files with a tag on the left will also be given the tag on the right.'+os.linesep+'As an experiment, this panel will only display the \'current\' pairs for those tags entered below.' )
|
|
self._count_st.setText( 'Starting with '+HydrusData.ToHumanInt(len(original_statuses_to_pairs[HC.CONTENT_STATUS_CURRENT]))+' pairs.' )
|
|
|
|
self._child_input.setEnabled( True )
|
|
self._parent_input.setEnabled( True )
|
|
|
|
if tags is None:
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
else:
|
|
|
|
self.EnterChildren( tags )
|
|
|
|
|
|
|
|
original_statuses_to_pairs = HG.client_controller.Read( 'tag_parents', service_key )
|
|
|
|
current_statuses_to_pairs = collections.defaultdict( set )
|
|
|
|
current_statuses_to_pairs.update( { key : set( value ) for ( key, value ) in list(original_statuses_to_pairs.items()) } )
|
|
|
|
QP.CallAfter( qt_code, original_statuses_to_pairs, current_statuses_to_pairs )
|
|
|
|
|
|
|
|
class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|
|
|
def __init__( self, parent, tags = None ):
|
|
|
|
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
|
|
|
|
self._tag_repositories = ClientGUICommon.BetterNotebook( self )
|
|
|
|
#
|
|
|
|
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
|
|
|
|
services = list( HG.client_controller.services_manager.GetServices( ( HC.LOCAL_TAG, ) ) )
|
|
|
|
services.extend( [ service for service in HG.client_controller.services_manager.GetServices( ( HC.TAG_REPOSITORY, ) ) if service.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_PETITION ) ] )
|
|
|
|
for service in services:
|
|
|
|
name = service.GetName()
|
|
service_key = service.GetServiceKey()
|
|
|
|
page = self._Panel( self._tag_repositories, service_key, tags )
|
|
|
|
select = service_key == default_tag_repository_key
|
|
|
|
self._tag_repositories.addTab( page, name )
|
|
if select: self._tag_repositories.setCurrentIndex( self._tag_repositories.indexOf( page ) )
|
|
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._tag_repositories, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
|
|
def _SetSearchFocus( self ):
|
|
|
|
page = self._tag_repositories.currentWidget()
|
|
|
|
if page is not None:
|
|
|
|
page.SetTagBoxFocus()
|
|
|
|
|
|
|
|
def CommitChanges( self ):
|
|
|
|
if self._tag_repositories.currentWidget().HasUncommittedPair():
|
|
|
|
message = 'Are you sure you want to OK? You have an uncommitted pair.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
raise HydrusExceptions.VetoException( 'Cancelled OK due to uncommitted pair.' )
|
|
|
|
|
|
|
|
service_keys_to_content_updates = {}
|
|
|
|
for page in self._tag_repositories.GetPages():
|
|
|
|
( service_key, content_updates ) = page.GetContentUpdates()
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
service_keys_to_content_updates[ service_key ] = content_updates
|
|
|
|
|
|
|
|
if len( service_keys_to_content_updates ) > 0:
|
|
|
|
HG.client_controller.Write( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
|
|
def EventServiceChanged( self, event ):
|
|
|
|
page = self._tag_repositories.currentWidget()
|
|
|
|
if page is not None:
|
|
|
|
HG.client_controller.CallAfterQtSafe( page, page.SetTagBoxFocus )
|
|
|
|
|
|
|
|
class _Panel( QW.QWidget ):
|
|
|
|
def __init__( self, parent, service_key, tags = None ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._service_key = service_key
|
|
|
|
self._service = HG.client_controller.services_manager.GetService( self._service_key )
|
|
|
|
self._i_am_local_tag_service = self._service.GetServiceType() == HC.LOCAL_TAG
|
|
|
|
self._original_statuses_to_pairs = collections.defaultdict( set )
|
|
self._current_statuses_to_pairs = collections.defaultdict( set )
|
|
|
|
self._pairs_to_reasons = {}
|
|
|
|
self._current_new = None
|
|
|
|
self._show_all = QW.QCheckBox( self )
|
|
|
|
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
|
|
|
columns = [ ( '', 6 ), ( 'old', 25 ), ( 'new', 25 ), ( 'note', -1 ) ]
|
|
|
|
self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'tag_siblings', 8, 40, columns, self._ConvertPairToListCtrlTuples, delete_key_callback = self._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
|
|
|
|
listctrl_panel.SetListCtrl( self._tag_siblings )
|
|
|
|
self._tag_siblings.Sort( 2 )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'from clipboard', 'Load siblings from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, False ) ) )
|
|
menu_items.append( ( 'normal', 'from clipboard (only add pairs--no deletions)', 'Load siblings from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, True ) ) )
|
|
menu_items.append( ( 'normal', 'from .txt file', 'Load siblings from a .txt file.', HydrusData.Call( self._ImportFromTXT, False ) ) )
|
|
menu_items.append( ( 'normal', 'from .txt file (only add pairs--no deletions)', 'Load siblings from a .txt file.', HydrusData.Call( self._ImportFromTXT, True ) ) )
|
|
|
|
listctrl_panel.AddMenuButton( 'import', menu_items )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'to clipboard', 'Save selected siblings to your clipboard.', self._ExportToClipboard ) )
|
|
menu_items.append( ( 'normal', 'to .txt file', 'Save selected siblings to a .txt file.', self._ExportToTXT ) )
|
|
|
|
listctrl_panel.AddMenuButton( 'export', menu_items, enabled_only_on_selection = True )
|
|
|
|
self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, show_sibling_text = False )
|
|
self._new_sibling = ClientGUICommon.BetterStaticText( self )
|
|
|
|
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._old_siblings, ( 12, 6 ) )
|
|
|
|
self._old_siblings.setMinimumHeight( preview_height )
|
|
|
|
expand_parents = False
|
|
|
|
self._old_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterOlds, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key, show_paste_button = True )
|
|
self._old_input.setEnabled( False )
|
|
|
|
self._new_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.SetNew, expand_parents, CC.LOCAL_FILE_SERVICE_KEY, service_key )
|
|
self._new_input.setEnabled( False )
|
|
|
|
self._add = QW.QPushButton( 'add', self )
|
|
self._add.clicked.connect( self.EventAddButton )
|
|
self._add.setEnabled( False )
|
|
|
|
#
|
|
|
|
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' )
|
|
self._count_st = ClientGUICommon.BetterStaticText( self, '' )
|
|
|
|
new_sibling_box = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( new_sibling_box, (10,10), CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( new_sibling_box, self._new_sibling, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( new_sibling_box, (10,10), CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
text_box = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( text_box, self._old_siblings, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( text_box, new_sibling_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
input_box = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( input_box, self._old_input )
|
|
QP.AddToLayout( input_box, self._new_input )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._status_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._add, CC.FLAGS_LONE_BUTTON )
|
|
QP.AddToLayout( vbox, text_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, input_box )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._tag_siblings.itemSelectionChanged.connect( self._SetButtonStatus )
|
|
|
|
self._show_all.clicked.connect( self._UpdateListCtrlData )
|
|
self._old_siblings.listBoxChanged.connect( self._UpdateListCtrlData )
|
|
|
|
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
|
|
|
|
|
|
def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reason = None ):
|
|
|
|
pairs = list( pairs )
|
|
|
|
pairs.sort( key = lambda c_p1: HydrusTags.ConvertTagToSortable( c_p1[1] ) )
|
|
|
|
new_pairs = []
|
|
current_pairs = []
|
|
petitioned_pairs = []
|
|
pending_pairs = []
|
|
|
|
for pair in pairs:
|
|
|
|
if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
|
|
|
|
if not add_only:
|
|
|
|
pending_pairs.append( pair )
|
|
|
|
|
|
elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
|
|
|
|
if not remove_only:
|
|
|
|
petitioned_pairs.append( pair )
|
|
|
|
|
|
elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
if not add_only:
|
|
|
|
current_pairs.append( pair )
|
|
|
|
|
|
elif not remove_only and self._CanAdd( pair ):
|
|
|
|
new_pairs.append( pair )
|
|
|
|
|
|
|
|
suggestions = []
|
|
|
|
suggestions.append( 'merging underscores/typos/phrasing/unnamespaced to a single uncontroversial good tag' )
|
|
suggestions.append( 'rewording/namespacing based on preference' )
|
|
|
|
if len( new_pairs ) > 0:
|
|
|
|
do_it = True
|
|
|
|
if not self._i_am_local_tag_service:
|
|
|
|
if default_reason is not None:
|
|
|
|
reason = default_reason
|
|
|
|
elif self._service.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
reason = 'admin'
|
|
|
|
else:
|
|
|
|
if len( new_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in new_pairs ) )
|
|
|
|
|
|
message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your petition.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
do_it = False
|
|
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
for pair in new_pairs: self._pairs_to_reasons[ pair ] = reason
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].update( new_pairs )
|
|
|
|
|
|
else:
|
|
|
|
if len( current_pairs ) > 0:
|
|
|
|
do_it = True
|
|
|
|
if not self._i_am_local_tag_service:
|
|
|
|
if default_reason is not None:
|
|
|
|
reason = default_reason
|
|
|
|
elif self._service.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_OVERRULE ):
|
|
|
|
reason = 'admin'
|
|
|
|
else:
|
|
|
|
if len( pending_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in pending_pairs ) )
|
|
|
|
|
|
message = 'Enter a reason for:'
|
|
message += os.linesep * 2
|
|
message += pair_strings
|
|
message += os.linesep * 2
|
|
message += 'to be removed. You will see the delete as soon as you upload, but a janitor will review your petition to decide if all users should receive it as well.'
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
reason = dlg.GetValue()
|
|
|
|
else:
|
|
|
|
do_it = False
|
|
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
for pair in current_pairs:
|
|
|
|
self._pairs_to_reasons[ pair ] = reason
|
|
|
|
|
|
|
|
|
|
if do_it:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
|
|
|
|
|
|
|
|
if len( pending_pairs ) > 0:
|
|
|
|
if len( pending_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in pending_pairs ) )
|
|
|
|
|
|
if len( pending_pairs ) > 1:
|
|
|
|
message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are pending.'
|
|
|
|
else:
|
|
|
|
message = 'The pair ' + pair_strings + ' is pending.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
|
|
|
|
|
|
|
|
if len( petitioned_pairs ) > 0:
|
|
|
|
if len( petitioned_pairs ) > 10:
|
|
|
|
pair_strings = 'The many pairs you entered.'
|
|
|
|
else:
|
|
|
|
pair_strings = ', '.join( ( old + '->' + new for ( old, new ) in petitioned_pairs ) )
|
|
|
|
|
|
if len( petitioned_pairs ) > 1:
|
|
|
|
message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are petitioned.'
|
|
|
|
else:
|
|
|
|
message = 'The pair ' + pair_strings + ' is petitioned.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
|
|
|
|
|
|
|
|
|
|
|
|
def _AutoPetitionConflicts( self, pairs ):
|
|
|
|
current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
|
|
|
|
current_olds_to_news = dict( current_pairs )
|
|
|
|
current_olds = { current_old for ( current_old, current_new ) in current_pairs }
|
|
|
|
pairs_to_auto_petition = set()
|
|
|
|
for ( old, new ) in pairs:
|
|
|
|
if old in current_olds:
|
|
|
|
conflicting_new = current_olds_to_news[ old ]
|
|
|
|
if conflicting_new != new:
|
|
|
|
conflicting_pair = ( old, conflicting_new )
|
|
|
|
pairs_to_auto_petition.add( conflicting_pair )
|
|
|
|
|
|
|
|
|
|
if len( pairs_to_auto_petition ) > 0:
|
|
|
|
pairs_to_auto_petition = list( pairs_to_auto_petition )
|
|
|
|
self._AddPairs( pairs_to_auto_petition, remove_only = True, default_reason = 'AUTO-PETITION TO REASSIGN TO: ' + new )
|
|
|
|
|
|
|
|
def _CanAdd( self, potential_pair ):
|
|
|
|
( potential_old, potential_new ) = potential_pair
|
|
|
|
current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
|
|
|
|
current_olds = { old for ( old, new ) in current_pairs }
|
|
|
|
# test for ambiguity
|
|
|
|
if potential_old in current_olds:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'There already is a relationship set for the tag '+potential_old+'.' )
|
|
|
|
return False
|
|
|
|
|
|
# test for loops
|
|
|
|
if potential_new in current_olds:
|
|
|
|
seen_tags = set()
|
|
|
|
d = dict( current_pairs )
|
|
|
|
next_new = potential_new
|
|
|
|
while next_new in d:
|
|
|
|
next_new = d[ next_new ]
|
|
|
|
if next_new == potential_old:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Adding '+potential_old+'->'+potential_new+' would create a loop!' )
|
|
|
|
return False
|
|
|
|
|
|
if next_new in seen_tags:
|
|
|
|
message = 'The pair you mean to add seems to connect to a sibling loop already in your database! Please undo this loop first. The tags involved in the loop are:'
|
|
message += os.linesep * 2
|
|
message += ', '.join( seen_tags )
|
|
|
|
QW.QMessageBox.critical( self, 'Error', message )
|
|
|
|
return False
|
|
|
|
|
|
seen_tags.add( next_new )
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
def _ConvertPairToListCtrlTuples( self, pair ):
|
|
|
|
( old, new ) = pair
|
|
|
|
if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
|
|
|
|
status = HC.CONTENT_STATUS_PENDING
|
|
|
|
elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
|
|
|
|
status = HC.CONTENT_STATUS_PETITIONED
|
|
|
|
elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
|
|
|
|
status = HC.CONTENT_STATUS_CURRENT
|
|
|
|
|
|
sign = HydrusData.ConvertStatusToPrefix( status )
|
|
|
|
pretty_status = sign
|
|
|
|
existing_olds = self._old_siblings.GetTags()
|
|
|
|
note = ''
|
|
|
|
if old in existing_olds:
|
|
|
|
if status == HC.CONTENT_STATUS_PENDING:
|
|
|
|
note = 'CONFLICT: Will be rescinded on add.'
|
|
|
|
elif status == HC.CONTENT_STATUS_CURRENT:
|
|
|
|
note = 'CONFLICT: Will be petitioned/deleted on add.'
|
|
|
|
|
|
|
|
display_tuple = ( pretty_status, old, new, note )
|
|
sort_tuple = ( status, old, new, note )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _DeserialiseImportString( self, import_string ):
|
|
|
|
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
|
|
|
|
if len( tags ) % 2 == 1:
|
|
|
|
raise Exception( 'Uneven number of tags found!' )
|
|
|
|
|
|
pairs = []
|
|
|
|
for i in range( len( tags ) // 2 ):
|
|
|
|
pair = ( tags[ 2 * i ], tags[ ( 2 * i ) + 1 ] )
|
|
|
|
pairs.append( pair )
|
|
|
|
|
|
return pairs
|
|
|
|
|
|
def _ExportToClipboard( self ):
|
|
|
|
export_string = self._GetExportString()
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', export_string )
|
|
|
|
|
|
def _ExportToTXT( self ):
|
|
|
|
export_string = self._GetExportString()
|
|
|
|
with QP.FileDialog( self, 'Set the export path.', defaultFile = 'siblings.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
path = dlg.GetPath()
|
|
|
|
with open( path, 'w', encoding = 'utf-8' ) as f:
|
|
|
|
f.write( export_string )
|
|
|
|
|
|
|
|
|
|
|
|
def _GetExportString( self ):
|
|
|
|
tags = []
|
|
|
|
for ( a, b ) in self._tag_siblings.GetData( only_selected = True ):
|
|
|
|
tags.append( a )
|
|
tags.append( b )
|
|
|
|
|
|
export_string = os.linesep.join( tags )
|
|
|
|
return export_string
|
|
|
|
|
|
def _ImportFromClipboard( self, add_only = False ):
|
|
|
|
try:
|
|
|
|
import_string = HG.client_controller.GetClipboardText()
|
|
|
|
except HydrusExceptions.DataMissing as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', str(e) )
|
|
|
|
return
|
|
|
|
|
|
pairs = self._DeserialiseImportString( import_string )
|
|
|
|
self._AutoPetitionConflicts( pairs )
|
|
|
|
self._AddPairs( pairs, add_only = add_only )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
|
|
def _ImportFromTXT( self, add_only = False ):
|
|
|
|
with QP.FileDialog( self, 'Select the file to import.', acceptMode = QW.QFileDialog.AcceptOpen ) as dlg:
|
|
|
|
if dlg.exec() != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
path = dlg.GetPath()
|
|
|
|
|
|
|
|
with open( path, 'r', encoding = 'utf-8' ) as f:
|
|
|
|
import_string = f.read()
|
|
|
|
|
|
pairs = self._DeserialiseImportString( import_string )
|
|
|
|
self._AutoPetitionConflicts( pairs )
|
|
|
|
self._AddPairs( pairs, add_only = add_only )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
|
|
def _ListCtrlActivated( self ):
|
|
|
|
pairs = self._tag_siblings.GetData( only_selected = True )
|
|
|
|
if len( pairs ) > 0:
|
|
|
|
self._AddPairs( pairs )
|
|
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
|
|
def _SetButtonStatus( self ):
|
|
|
|
if self._current_new is None or len( self._old_siblings.GetTags() ) == 0:
|
|
|
|
self._add.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._add.setEnabled( True )
|
|
|
|
|
|
|
|
def _UpdateListCtrlData( self ):
|
|
|
|
olds = self._old_siblings.GetTags()
|
|
|
|
pertinent_tags = set( olds )
|
|
|
|
if self._current_new is not None:
|
|
|
|
pertinent_tags.add( self._current_new )
|
|
|
|
|
|
self._tag_siblings.DeleteDatas( self._tag_siblings.GetData() )
|
|
|
|
all_pairs = set()
|
|
|
|
show_all = self._show_all.isChecked()
|
|
|
|
for ( status, pairs ) in self._current_statuses_to_pairs.items():
|
|
|
|
if status == HC.CONTENT_STATUS_DELETED:
|
|
|
|
continue
|
|
|
|
|
|
if len( pertinent_tags ) == 0:
|
|
|
|
if status == HC.CONTENT_STATUS_CURRENT and not show_all:
|
|
|
|
continue
|
|
|
|
|
|
# show all pending/petitioned
|
|
|
|
all_pairs.update( pairs )
|
|
|
|
else:
|
|
|
|
# show all appropriate
|
|
|
|
for pair in pairs:
|
|
|
|
( a, b ) = pair
|
|
|
|
if a in pertinent_tags or b in pertinent_tags or show_all:
|
|
|
|
all_pairs.add( pair )
|
|
|
|
|
|
|
|
|
|
|
|
self._tag_siblings.AddDatas( all_pairs )
|
|
|
|
self._tag_siblings.Sort()
|
|
|
|
|
|
def EnterOlds( self, olds ):
|
|
|
|
if self._current_new in olds:
|
|
|
|
self.SetNew( set() )
|
|
|
|
|
|
self._old_siblings.EnterTags( olds )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
def EventAddButton( self ):
|
|
|
|
if self._current_new is not None and len( self._old_siblings.GetTags() ) > 0:
|
|
|
|
olds = self._old_siblings.GetTags()
|
|
|
|
pairs = [ ( old, self._current_new ) for old in olds ]
|
|
|
|
self._AutoPetitionConflicts( pairs )
|
|
|
|
self._AddPairs( pairs )
|
|
|
|
self._old_siblings.SetTags( set() )
|
|
self.SetNew( set() )
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
|
|
def GetContentUpdates( self ):
|
|
|
|
# we make it manually here because of the mass pending tags done (but not undone on a rescind) on a pending pair!
|
|
# we don't want to send a pend and then rescind it, cause that will spam a thousand bad tags and not undo it
|
|
|
|
# actually, we don't do this for siblings, but we do for parents, and let's have them be the same
|
|
|
|
content_updates = []
|
|
|
|
if self._i_am_local_tag_service:
|
|
|
|
for pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
|
|
|
|
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_ADD, pair ) )
|
|
|
|
|
|
for pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
|
|
|
|
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE, pair ) )
|
|
|
|
|
|
else:
|
|
|
|
current_pending = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]
|
|
original_pending = self._original_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]
|
|
|
|
current_petitioned = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]
|
|
original_petitioned = self._original_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]
|
|
|
|
new_pends = current_pending.difference( original_pending )
|
|
rescinded_pends = original_pending.difference( current_pending )
|
|
|
|
new_petitions = current_petitioned.difference( original_petitioned )
|
|
rescinded_petitions = original_petitioned.difference( current_petitioned )
|
|
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_PEND, pair, reason = self._pairs_to_reasons[ pair ] ) for pair in new_pends ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_RESCIND_PEND, pair ) for pair in rescinded_pends ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_PETITION, pair, reason = self._pairs_to_reasons[ pair ] ) for pair in new_petitions ) )
|
|
content_updates.extend( ( HydrusData.ContentUpdate( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_RESCIND_PETITION, pair ) for pair in rescinded_petitions ) )
|
|
|
|
|
|
return ( self._service_key, content_updates )
|
|
|
|
|
|
def HasUncommittedPair( self ):
|
|
|
|
return len( self._old_siblings.GetTags() ) > 0 and self._current_new is not None
|
|
|
|
|
|
def SetNew( self, new_tags ):
|
|
|
|
if len( new_tags ) == 0:
|
|
|
|
self._new_sibling.setText( '' )
|
|
|
|
self._current_new = None
|
|
|
|
else:
|
|
|
|
new = list( new_tags )[0]
|
|
|
|
self._old_siblings.RemoveTags( { new } )
|
|
|
|
self._new_sibling.setText( new )
|
|
|
|
self._current_new = new
|
|
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
self._SetButtonStatus()
|
|
|
|
|
|
def SetTagBoxFocus( self ):
|
|
|
|
if len( self._old_siblings.GetTags() ) == 0:
|
|
|
|
self._old_input.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
else:
|
|
|
|
self._new_input.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
|
|
|
|
def THREADInitialise( self, tags, service_key ):
|
|
|
|
def qt_code( original_statuses_to_pairs, current_statuses_to_pairs ):
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
self._original_statuses_to_pairs = original_statuses_to_pairs
|
|
self._current_statuses_to_pairs = current_statuses_to_pairs
|
|
|
|
self._status_st.setText( 'Tags on the left will be replaced by those on the right.' )
|
|
self._count_st.setText( 'Starting with '+HydrusData.ToHumanInt(len(original_statuses_to_pairs[HC.CONTENT_STATUS_CURRENT]))+' pairs.' )
|
|
|
|
self._old_input.setEnabled( True )
|
|
self._new_input.setEnabled( True )
|
|
|
|
if tags is None:
|
|
|
|
self._UpdateListCtrlData()
|
|
|
|
else:
|
|
|
|
self.EnterOlds( tags )
|
|
|
|
|
|
|
|
original_statuses_to_pairs = HG.client_controller.Read( 'tag_siblings', service_key )
|
|
|
|
current_statuses_to_pairs = collections.defaultdict( set )
|
|
|
|
current_statuses_to_pairs.update( { key : set( value ) for ( key, value ) in original_statuses_to_pairs.items() } )
|
|
|
|
QP.CallAfter( qt_code, original_statuses_to_pairs, current_statuses_to_pairs )
|
|
|
|
|
|
|
|
class TagFilterButton( ClientGUICommon.BetterButton ):
|
|
|
|
def __init__( self, parent, message, tag_filter, is_blacklist = False, label_prefix = None ):
|
|
|
|
ClientGUICommon.BetterButton.__init__( self, parent, 'tag filter', self._EditTagFilter )
|
|
|
|
self._message = message
|
|
self._tag_filter = tag_filter
|
|
self._is_blacklist = is_blacklist
|
|
self._label_prefix = label_prefix
|
|
|
|
self._UpdateLabel()
|
|
|
|
|
|
def _EditTagFilter( self ):
|
|
|
|
with ClientGUITopLevelWindows.DialogEdit( self, 'edit tag filter' ) as dlg:
|
|
|
|
namespaces = HG.client_controller.network_engine.domain_manager.GetParserNamespaces()
|
|
|
|
panel = EditTagFilterPanel( dlg, self._tag_filter, prefer_blacklist = self._is_blacklist, namespaces = namespaces, message = self._message )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
self._tag_filter = panel.GetValue()
|
|
|
|
self._UpdateLabel()
|
|
|
|
|
|
|
|
|
|
def _UpdateLabel( self ):
|
|
|
|
if self._is_blacklist:
|
|
|
|
tt = self._tag_filter.ToBlacklistString()
|
|
|
|
else:
|
|
|
|
tt = self._tag_filter.ToPermittedString()
|
|
|
|
|
|
if self._label_prefix is not None:
|
|
|
|
tt = self._label_prefix + tt
|
|
|
|
|
|
button_text = HydrusText.ElideText( tt, 45 )
|
|
|
|
self.setText( button_text )
|
|
|
|
self.setToolTip( tt )
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
return self._tag_filter
|
|
|
|
|
|
def SetValue( self, tag_filter ):
|
|
|
|
self._tag_filter = tag_filter
|
|
|
|
self._UpdateLabel()
|
|
|
|
|
|
class TagSummaryGenerator( HydrusSerialisable.SerialisableBase ):
|
|
|
|
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR
|
|
SERIALISABLE_NAME = 'Tag Summary Generator'
|
|
SERIALISABLE_VERSION = 2
|
|
|
|
def __init__( self, background_colour = None, text_colour = None, namespace_info = None, separator = None, example_tags = None, show = True ):
|
|
|
|
if background_colour is None:
|
|
|
|
background_colour = QG.QColor( 223, 227, 230, 255 )
|
|
|
|
|
|
if text_colour is None:
|
|
|
|
text_colour = QG.QColor( 1, 17, 26, 255 )
|
|
|
|
|
|
if namespace_info is None:
|
|
|
|
namespace_info = []
|
|
|
|
namespace_info.append( ( 'creator', '', ', ' ) )
|
|
namespace_info.append( ( 'series', '', ', ' ) )
|
|
namespace_info.append( ( 'title', '', ', ' ) )
|
|
|
|
|
|
if separator is None:
|
|
|
|
separator = ' - '
|
|
|
|
|
|
if example_tags is None:
|
|
|
|
example_tags = []
|
|
|
|
|
|
self._background_colour = background_colour
|
|
self._text_colour = text_colour
|
|
self._namespace_info = namespace_info
|
|
self._separator = separator
|
|
self._example_tags = list( example_tags )
|
|
self._show = show
|
|
|
|
self._UpdateNamespaceLookup()
|
|
|
|
|
|
def _GetSerialisableInfo( self ):
|
|
|
|
bc = self._background_colour
|
|
|
|
background_colour_rgba = [ bc.red(), bc.green(), bc.blue(), bc.alpha() ]
|
|
|
|
tc = self._text_colour
|
|
|
|
text_colour_rgba = [ tc.red(), tc.green(), tc.blue(), tc.alpha() ]
|
|
|
|
return ( background_colour_rgba, text_colour_rgba, self._namespace_info, self._separator, self._example_tags, self._show )
|
|
|
|
|
|
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
|
|
|
( background_rgba, text_rgba, self._namespace_info, self._separator, self._example_tags, self._show ) = serialisable_info
|
|
|
|
( r, g, b, a ) = background_rgba
|
|
|
|
self._background_colour = QG.QColor( r, g, b, a )
|
|
|
|
( r, g, b, a ) = text_rgba
|
|
|
|
self._text_colour = QG.QColor( r, g, b, a )
|
|
|
|
self._namespace_info = [ tuple( row ) for row in self._namespace_info ]
|
|
|
|
self._UpdateNamespaceLookup()
|
|
|
|
|
|
def _UpdateNamespaceLookup( self ):
|
|
|
|
self._interesting_namespaces = { namespace for ( namespace, prefix, separator ) in self._namespace_info }
|
|
|
|
|
|
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
|
|
|
|
if version == 1:
|
|
|
|
( namespace_info, separator, example_tags ) = old_serialisable_info
|
|
|
|
background_rgba = ( 223, 227, 230, 255 )
|
|
text_rgba = ( 1, 17, 26, 255 )
|
|
show = True
|
|
|
|
new_serialisable_info = ( background_rgba, text_rgba, namespace_info, separator, example_tags, show )
|
|
|
|
return ( 2, new_serialisable_info )
|
|
|
|
|
|
|
|
def GenerateExampleSummary( self ):
|
|
|
|
if not self._show:
|
|
|
|
return 'not showing'
|
|
|
|
else:
|
|
|
|
return self.GenerateSummary( self._example_tags )
|
|
|
|
|
|
|
|
def GenerateSummary( self, tags, max_length = None ):
|
|
|
|
if not self._show:
|
|
|
|
return ''
|
|
|
|
|
|
namespaces_to_subtags = collections.defaultdict( list )
|
|
|
|
for tag in tags:
|
|
|
|
( namespace, subtag ) = HydrusTags.SplitTag( tag )
|
|
|
|
if namespace in self._interesting_namespaces:
|
|
|
|
namespaces_to_subtags[ namespace ].append( subtag )
|
|
|
|
|
|
|
|
for ( namespace, unsorted_l ) in list( namespaces_to_subtags.items() ):
|
|
|
|
sorted_l = HydrusTags.SortNumericTags( unsorted_l )
|
|
|
|
sorted_l = HydrusTags.CollapseMultipleSortedNumericTagsToMinMax( sorted_l )
|
|
|
|
namespaces_to_subtags[ namespace ] = sorted_l
|
|
|
|
|
|
namespace_texts = []
|
|
|
|
for ( namespace, prefix, separator ) in self._namespace_info:
|
|
|
|
subtags = namespaces_to_subtags[ namespace ]
|
|
|
|
if len( subtags ) > 0:
|
|
|
|
namespace_text = prefix + separator.join( namespaces_to_subtags[ namespace ] )
|
|
|
|
namespace_texts.append( namespace_text )
|
|
|
|
|
|
|
|
summary = self._separator.join( namespace_texts )
|
|
|
|
if max_length is not None:
|
|
|
|
summary = summary[:max_length]
|
|
|
|
|
|
return summary
|
|
|
|
|
|
def GetBackgroundColour( self ):
|
|
|
|
return self._background_colour
|
|
|
|
|
|
def GetTextColour( self ):
|
|
|
|
return self._text_colour
|
|
|
|
|
|
def ToTuple( self ):
|
|
|
|
return ( self._background_colour, self._text_colour, self._namespace_info, self._separator, self._example_tags, self._show )
|
|
|
|
|
|
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR ] = TagSummaryGenerator
|