3845 lines
129 KiB
Python
3845 lines
129 KiB
Python
import collections
|
|
import itertools
|
|
import os
|
|
import threading
|
|
import typing
|
|
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
from qtpy import QtGui as QG
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusGlobals as HG
|
|
from hydrus.core import HydrusSerialisable
|
|
from hydrus.core import HydrusTags
|
|
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientSearch
|
|
from hydrus.client import ClientSerialisable
|
|
from hydrus.client.gui import ClientGUIAsync
|
|
from hydrus.client.gui import ClientGUICore as CGC
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIMenus
|
|
from hydrus.client.gui import ClientGUIShortcuts
|
|
from hydrus.client.gui import ClientGUITagSorting
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.lists import ClientGUIListBoxesData
|
|
from hydrus.client.gui.search import ClientGUISearch
|
|
from hydrus.client.gui.widgets import ClientGUICommon
|
|
from hydrus.client.gui.widgets import ClientGUIMenuButton
|
|
from hydrus.client.media import ClientMedia
|
|
from hydrus.client.metadata import ClientTags
|
|
from hydrus.client.metadata import ClientTagSorting
|
|
|
|
class BetterQListWidget( QW.QListWidget ):
|
|
|
|
def _DeleteIndices( self, indices: typing.Iterable[ int ] ):
|
|
|
|
indices = sorted( indices, reverse = True )
|
|
|
|
for index in indices:
|
|
|
|
item = self.takeItem( index )
|
|
|
|
del item
|
|
|
|
|
|
|
|
def _GetDataIndices( self, datas: typing.Collection[ object ] ) -> typing.List[ int ]:
|
|
|
|
indices = []
|
|
|
|
for index in range( self.count() ):
|
|
|
|
list_widget_item = self.item( index )
|
|
|
|
data = self._GetRowData( list_widget_item )
|
|
|
|
if data in datas:
|
|
|
|
indices.append( index )
|
|
|
|
|
|
|
|
return indices
|
|
|
|
|
|
def _GetListWidgetItems( self, only_selected = False ):
|
|
|
|
# not sure if selectedItems is always sorted, so just do it manually
|
|
|
|
list_widget_items = []
|
|
|
|
for index in range( self.count() ):
|
|
|
|
list_widget_item = self.item( index )
|
|
|
|
if only_selected and not list_widget_item.isSelected():
|
|
|
|
continue
|
|
|
|
|
|
list_widget_items.append( list_widget_item )
|
|
|
|
|
|
return list_widget_items
|
|
|
|
|
|
def _GetRowData( self, list_widget_item: QW.QListWidgetItem ):
|
|
|
|
return list_widget_item.data( QC.Qt.UserRole )
|
|
|
|
|
|
def _GetSelectedIndices( self ):
|
|
|
|
return [ model_index.row() for model_index in self.selectedIndexes() ]
|
|
|
|
|
|
def _MoveRow( self, index: int, distance: int ):
|
|
|
|
new_index = index + distance
|
|
|
|
new_index = max( 0, new_index )
|
|
new_index = min( new_index, self.count() - 1 )
|
|
|
|
if index == new_index:
|
|
|
|
return
|
|
|
|
|
|
was_selected = self.item( index ).isSelected()
|
|
|
|
list_widget_item = self.takeItem( index )
|
|
|
|
self.insertItem( new_index, list_widget_item )
|
|
|
|
list_widget_item.setSelected( was_selected )
|
|
|
|
|
|
def Append( self, text: str, data: object ):
|
|
|
|
item = QW.QListWidgetItem()
|
|
|
|
item.setText( text )
|
|
item.setData( QC.Qt.UserRole, data )
|
|
|
|
self.addItem( item )
|
|
|
|
|
|
def DeleteData( self, datas: typing.Collection[ object ] ):
|
|
|
|
indices = self._GetDataIndices( datas )
|
|
|
|
self._DeleteIndices( indices )
|
|
|
|
|
|
def DeleteSelected( self ):
|
|
|
|
indices = self._GetSelectedIndices()
|
|
|
|
self._DeleteIndices( indices )
|
|
|
|
|
|
def GetData( self, only_selected: bool = False ) -> typing.List[ object ]:
|
|
|
|
datas = []
|
|
|
|
list_widget_items = self._GetListWidgetItems( only_selected = only_selected )
|
|
|
|
for list_widget_item in list_widget_items:
|
|
|
|
data = self._GetRowData( list_widget_item )
|
|
|
|
datas.append( data )
|
|
|
|
|
|
return datas
|
|
|
|
|
|
def GetNumSelected( self ) -> int:
|
|
|
|
indices = self._GetSelectedIndices()
|
|
|
|
return len( indices )
|
|
|
|
|
|
def MoveSelected( self, distance: int ):
|
|
|
|
if distance == 0:
|
|
|
|
return
|
|
|
|
|
|
# if going up, -1, then do them in ascending order
|
|
# if going down, +1, then do them in descending order
|
|
|
|
indices = sorted( self._GetSelectedIndices(), reverse = distance > 0 )
|
|
|
|
for index in indices:
|
|
|
|
self._MoveRow( index, distance )
|
|
|
|
|
|
|
|
def PopData( self, index: int ):
|
|
|
|
if index < 0 or index > self.count() - 1:
|
|
|
|
return None
|
|
|
|
|
|
list_widget_item = self.item( index )
|
|
|
|
data = self._GetRowData( list_widget_item )
|
|
|
|
self._DeleteIndices( [ index ] )
|
|
|
|
return data
|
|
|
|
|
|
def SelectData( self, datas: typing.Collection[ object ] ):
|
|
|
|
list_widget_items = self._GetListWidgetItems()
|
|
|
|
for list_widget_item in list_widget_items:
|
|
|
|
data = self._GetRowData( list_widget_item )
|
|
|
|
list_widget_item.setSelected( data in datas )
|
|
|
|
|
|
|
|
class AddEditDeleteListBox( QW.QWidget ):
|
|
|
|
listBoxChanged = QC.Signal()
|
|
|
|
def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_callable, edit_callable ):
|
|
|
|
self._data_to_pretty_callable = data_to_pretty_callable
|
|
self._add_callable = add_callable
|
|
self._edit_callable = edit_callable
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._listbox = BetterQListWidget( self )
|
|
self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection )
|
|
|
|
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
|
|
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
|
|
self._delete_button = ClientGUICommon.BetterButton( self, 'delete', self._Delete )
|
|
|
|
self._enabled_only_on_selection_buttons = []
|
|
|
|
self._permitted_object_types = []
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
self._buttons_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( self._buttons_hbox, self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( self._buttons_hbox, self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( self._buttons_hbox, self._delete_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
QP.AddToLayout( vbox, self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
#
|
|
|
|
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._listbox, ( 20, height_num_chars ) )
|
|
|
|
self._listbox.setMinimumWidth( width )
|
|
self._listbox.setMinimumHeight( height )
|
|
|
|
#
|
|
|
|
self._ShowHideButtons()
|
|
|
|
self._listbox.itemSelectionChanged.connect( self._ShowHideButtons )
|
|
|
|
if self._edit_callable is not None:
|
|
|
|
self._listbox.itemDoubleClicked.connect( self._Edit )
|
|
|
|
else:
|
|
|
|
self._listbox.itemDoubleClicked.connect( self._Delete )
|
|
|
|
|
|
|
|
def _Add( self ):
|
|
|
|
try:
|
|
|
|
data = self._add_callable()
|
|
|
|
except HydrusExceptions.VetoException:
|
|
|
|
return
|
|
|
|
|
|
self._AddData( data )
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _AddAllDefaults( self, defaults_callable ):
|
|
|
|
defaults = defaults_callable()
|
|
|
|
for default in defaults:
|
|
|
|
self._AddData( default )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _AddData( self, data ):
|
|
|
|
self._SetNonDupeName( data )
|
|
|
|
pretty_data = self._data_to_pretty_callable( data )
|
|
|
|
self._listbox.Append( pretty_data, data )
|
|
|
|
|
|
def _AddSomeDefaults( self, defaults_callable ):
|
|
|
|
defaults = defaults_callable()
|
|
|
|
selected = False
|
|
|
|
choice_tuples = [ ( self._data_to_pretty_callable( default ), default, selected ) for default in defaults ]
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
try:
|
|
|
|
defaults_to_add = ClientGUIDialogsQuick.SelectMultipleFromList( self, 'select the defaults to add', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
for default in defaults_to_add:
|
|
|
|
self._AddData( default )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Delete( self ):
|
|
|
|
num_selected = self._listbox.GetNumSelected()
|
|
|
|
if num_selected == 0:
|
|
|
|
return
|
|
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove {} selected?'.format( HydrusData.ToHumanInt( num_selected ) ) )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
self._listbox.DeleteSelected()
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Edit( self ):
|
|
|
|
for list_widget_item in self._listbox.selectedItems():
|
|
|
|
data = list_widget_item.data( QC.Qt.UserRole )
|
|
|
|
try:
|
|
|
|
new_data = self._edit_callable( data )
|
|
|
|
except HydrusExceptions.VetoException:
|
|
|
|
break
|
|
|
|
|
|
self._SetNonDupeName( new_data )
|
|
|
|
pretty_new_data = self._data_to_pretty_callable( new_data )
|
|
|
|
list_widget_item.setText( pretty_new_data )
|
|
list_widget_item.setData( QC.Qt.UserRole, new_data )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Duplicate( self ):
|
|
|
|
dupe_data = self._GetExportObject()
|
|
|
|
if dupe_data is not None:
|
|
|
|
dupe_data = dupe_data.Duplicate()
|
|
|
|
self._ImportObject( dupe_data )
|
|
|
|
|
|
|
|
def _ExportToClipboard( self ):
|
|
|
|
export_object = self._GetExportObject()
|
|
|
|
if export_object is not None:
|
|
|
|
json = export_object.DumpToString()
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', json )
|
|
|
|
|
|
|
|
def _ExportToPNG( self ):
|
|
|
|
export_object = self._GetExportObject()
|
|
|
|
if export_object is not None:
|
|
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import ClientGUISerialisable
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
|
|
|
|
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
|
|
def _ExportToPNGs( self ):
|
|
|
|
export_object = self._GetExportObject()
|
|
|
|
if export_object is None:
|
|
|
|
return
|
|
|
|
|
|
if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
|
|
|
|
self._ExportToPNG()
|
|
|
|
return
|
|
|
|
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import ClientGUISerialisable
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to pngs' ) as dlg:
|
|
|
|
panel = ClientGUISerialisable.PNGsExportPanel( dlg, export_object )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
|
|
def _GetExportObject( self ):
|
|
|
|
to_export = HydrusSerialisable.SerialisableList()
|
|
|
|
for obj in self.GetData( only_selected = True ):
|
|
|
|
to_export.append( obj )
|
|
|
|
|
|
if len( to_export ) == 0:
|
|
|
|
return None
|
|
|
|
elif len( to_export ) == 1:
|
|
|
|
return to_export[0]
|
|
|
|
else:
|
|
|
|
return to_export
|
|
|
|
|
|
|
|
def _ImportFromClipboard( 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 )
|
|
|
|
self._ImportObject( obj )
|
|
|
|
except Exception as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard' )
|
|
|
|
|
|
|
|
def _ImportFromPNG( self ):
|
|
|
|
with QP.FileDialog( self, 'select the png or pngs with the encoded data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'PNG (*.png)|*.png' ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
for path in dlg.GetPaths():
|
|
|
|
try:
|
|
|
|
payload = ClientSerialisable.LoadFromPNG( path )
|
|
|
|
except Exception as e:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', str(e) )
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
obj = HydrusSerialisable.CreateFromNetworkBytes( payload )
|
|
|
|
self._ImportObject( obj )
|
|
|
|
except:
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in the png!' )
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ImportObject( self, obj ):
|
|
|
|
bad_object_type_names = set()
|
|
|
|
if isinstance( obj, HydrusSerialisable.SerialisableList ):
|
|
|
|
for sub_obj in obj:
|
|
|
|
self._ImportObject( sub_obj )
|
|
|
|
|
|
else:
|
|
|
|
if isinstance( obj, self._permitted_object_types ):
|
|
|
|
self._AddData( obj )
|
|
|
|
else:
|
|
|
|
bad_object_type_names.add( HydrusData.GetTypeName( type( obj ) ) )
|
|
|
|
|
|
|
|
if len( bad_object_type_names ) > 0:
|
|
|
|
message = 'The imported objects included these types:'
|
|
message += os.linesep * 2
|
|
message += os.linesep.join( bad_object_type_names )
|
|
message += os.linesep * 2
|
|
message += 'Whereas this control only allows:'
|
|
message += os.linesep * 2
|
|
message += os.linesep.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
|
|
|
|
QW.QMessageBox.critical( self, 'Error', message )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _SetNonDupeName( self, obj ):
|
|
|
|
pass
|
|
|
|
|
|
def _ShowHideButtons( self ):
|
|
|
|
if self._listbox.GetNumSelected() == 0:
|
|
|
|
self._edit_button.setEnabled( False )
|
|
self._delete_button.setEnabled( False )
|
|
|
|
for button in self._enabled_only_on_selection_buttons:
|
|
|
|
button.setEnabled( False )
|
|
|
|
|
|
else:
|
|
|
|
self._edit_button.setEnabled( True )
|
|
self._delete_button.setEnabled( True )
|
|
|
|
for button in self._enabled_only_on_selection_buttons:
|
|
|
|
button.setEnabled( True )
|
|
|
|
|
|
|
|
|
|
def AddDatas( self, datas ):
|
|
|
|
for data in datas:
|
|
|
|
self._AddData( data )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def AddDefaultsButton( self, defaults_callable ):
|
|
|
|
import_menu_items = []
|
|
|
|
all_call = HydrusData.Call( self._AddAllDefaults, defaults_callable )
|
|
some_call = HydrusData.Call( self._AddSomeDefaults, defaults_callable )
|
|
|
|
import_menu_items.append( ( 'normal', 'add them all', 'Load all the defaults.', all_call ) )
|
|
import_menu_items.append( ( 'normal', 'select from a list', 'Load some of the defaults.', some_call ) )
|
|
|
|
button = ClientGUIMenuButton.MenuButton( self, 'add defaults', import_menu_items )
|
|
|
|
QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
|
|
def AddImportExportButtons( self, permitted_object_types ):
|
|
|
|
self._permitted_object_types = permitted_object_types
|
|
|
|
export_menu_items = []
|
|
|
|
export_menu_items.append( ( 'normal', 'to clipboard', 'Serialise the selected data and put it on your clipboard.', self._ExportToClipboard ) )
|
|
export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPNG ) )
|
|
|
|
all_objs_are_named = False not in ( issubclass( o, HydrusSerialisable.SerialisableBaseNamed ) for o in self._permitted_object_types )
|
|
|
|
if all_objs_are_named:
|
|
|
|
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPNGs ) )
|
|
|
|
|
|
import_menu_items = []
|
|
|
|
import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
|
|
import_menu_items.append( ( 'normal', 'from pngs', 'Load a data from an encoded png.', self._ImportFromPNG ) )
|
|
|
|
button = ClientGUIMenuButton.MenuButton( self, 'export', export_menu_items )
|
|
QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
self._enabled_only_on_selection_buttons.append( button )
|
|
|
|
button = ClientGUIMenuButton.MenuButton( self, 'import', import_menu_items )
|
|
QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
button = ClientGUICommon.BetterButton( self, 'duplicate', self._Duplicate )
|
|
QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
self._enabled_only_on_selection_buttons.append( button )
|
|
|
|
self._ShowHideButtons()
|
|
|
|
|
|
def AddSeparator( self ):
|
|
|
|
QP.AddToLayout( self._buttons_hbox, (20,20), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
def GetCount( self ):
|
|
|
|
return self._listbox.count()
|
|
|
|
|
|
def GetData( self, only_selected = False ):
|
|
|
|
return self._listbox.GetData( only_selected = only_selected )
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
return self.GetData()
|
|
|
|
|
|
class AddEditDeleteListBoxUniqueNamedObjects( AddEditDeleteListBox ):
|
|
|
|
def _SetNonDupeName( self, obj ):
|
|
|
|
disallowed_names = { o.GetName() for o in self.GetData() }
|
|
|
|
HydrusSerialisable.SetNonDupeName( obj, disallowed_names )
|
|
|
|
|
|
class QueueListBox( QW.QWidget ):
|
|
|
|
listBoxChanged = QC.Signal()
|
|
|
|
def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_callable = None, edit_callable = None ):
|
|
|
|
self._data_to_pretty_callable = data_to_pretty_callable
|
|
self._add_callable = add_callable
|
|
self._edit_callable = edit_callable
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._listbox = BetterQListWidget( self )
|
|
self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection )
|
|
|
|
self._up_button = ClientGUICommon.BetterButton( self, '\u2191', self._Up )
|
|
|
|
self._delete_button = ClientGUICommon.BetterButton( self, 'X', self._Delete )
|
|
|
|
self._down_button = ClientGUICommon.BetterButton( self, '\u2193', self._Down )
|
|
|
|
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
|
|
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
|
|
|
|
if self._add_callable is None:
|
|
|
|
self._add_button.hide()
|
|
|
|
|
|
if self._edit_callable is None:
|
|
|
|
self._edit_button.hide()
|
|
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
buttons_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( buttons_vbox, self._up_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( buttons_vbox, self._delete_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( buttons_vbox, self._down_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( hbox, buttons_vbox, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
buttons_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( buttons_hbox, self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( buttons_hbox, self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
#
|
|
|
|
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._listbox, ( 20, height_num_chars ) )
|
|
|
|
self._listbox.setMinimumWidth( width )
|
|
self._listbox.setMinimumHeight( height )
|
|
|
|
#
|
|
|
|
self._listbox.itemSelectionChanged.connect( self._UpdateButtons )
|
|
|
|
if self._edit_callable is not None:
|
|
|
|
self._listbox.itemDoubleClicked.connect( self._Edit )
|
|
|
|
else:
|
|
|
|
self._listbox.itemDoubleClicked.connect( self._Delete )
|
|
|
|
|
|
self._UpdateButtons()
|
|
|
|
|
|
def _Add( self ):
|
|
|
|
try:
|
|
|
|
data = self._add_callable()
|
|
|
|
except HydrusExceptions.VetoException:
|
|
|
|
return
|
|
|
|
|
|
self._AddData( data )
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _AddData( self, data ):
|
|
|
|
pretty_data = self._data_to_pretty_callable( data )
|
|
|
|
self._listbox.Append( pretty_data, data )
|
|
|
|
|
|
def _Delete( self ):
|
|
|
|
num_selected = self._listbox.GetNumSelected()
|
|
|
|
if num_selected == 0:
|
|
|
|
return
|
|
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove {} selected?'.format( HydrusData.ToHumanInt( num_selected ) ) )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._listbox.DeleteSelected()
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
|
|
def _Down( self ):
|
|
|
|
self._listbox.MoveSelected( 1 )
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Edit( self ):
|
|
|
|
for list_widget_item in self._listbox.selectedItems():
|
|
|
|
data = list_widget_item.data( QC.Qt.UserRole )
|
|
|
|
try:
|
|
|
|
new_data = self._edit_callable( data )
|
|
|
|
except HydrusExceptions.VetoException:
|
|
|
|
break
|
|
|
|
|
|
pretty_new_data = self._data_to_pretty_callable( new_data )
|
|
|
|
list_widget_item.setText( pretty_new_data )
|
|
list_widget_item.setData( QC.Qt.UserRole, new_data )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Up( self ):
|
|
|
|
self._listbox.MoveSelected( -1 )
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _UpdateButtons( self ):
|
|
|
|
if self._listbox.GetNumSelected() == 0:
|
|
|
|
self._up_button.setEnabled( False )
|
|
self._delete_button.setEnabled( False )
|
|
self._down_button.setEnabled( False )
|
|
|
|
self._edit_button.setEnabled( False )
|
|
|
|
else:
|
|
|
|
self._up_button.setEnabled( True )
|
|
self._delete_button.setEnabled( True )
|
|
self._down_button.setEnabled( True )
|
|
|
|
self._edit_button.setEnabled( True )
|
|
|
|
|
|
|
|
def AddDatas( self, datas ):
|
|
|
|
for data in datas:
|
|
|
|
self._AddData( data )
|
|
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def GetCount( self ):
|
|
|
|
return self._listbox.count()
|
|
|
|
|
|
def GetData( self, only_selected = False ):
|
|
|
|
return self._listbox.GetData( only_selected = only_selected )
|
|
|
|
|
|
def Pop( self ):
|
|
|
|
if self._listbox.count() == 0:
|
|
|
|
return None
|
|
|
|
|
|
return self._listbox.PopData( 0 )
|
|
|
|
|
|
class ListBox( QW.QScrollArea ):
|
|
|
|
listBoxChanged = QC.Signal()
|
|
mouseActivationOccurred = QC.Signal()
|
|
|
|
TEXT_X_PADDING = 3
|
|
|
|
def __init__( self, parent: QW.QWidget, child_rows_allowed: bool, terms_may_have_child_rows: bool, height_num_chars = 10, has_async_text_info = False ):
|
|
|
|
QW.QScrollArea.__init__( self, parent )
|
|
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
|
|
self.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff )
|
|
self.setVerticalScrollBarPolicy( QC.Qt.ScrollBarAsNeeded )
|
|
self.setWidget( ListBox._InnerWidget( self ) )
|
|
self.setWidgetResizable( True )
|
|
|
|
self._background_colour = QG.QColor( 255, 255, 255 )
|
|
|
|
self._ordered_terms = []
|
|
self._terms_to_logical_indices = {}
|
|
self._terms_to_positional_indices = {}
|
|
self._positional_indices_to_terms = {}
|
|
self._selected_terms = set()
|
|
self._total_positional_rows = 0
|
|
|
|
self._last_hit_logical_index = None
|
|
self._last_drag_start_logical_index = None
|
|
self._drag_started = False
|
|
|
|
self._last_view_start = None
|
|
|
|
self._height_num_chars = height_num_chars
|
|
self._minimum_height_num_chars = 8
|
|
|
|
self._num_rows_per_page = 0
|
|
|
|
self._child_rows_allowed = child_rows_allowed
|
|
self._terms_may_have_child_rows = terms_may_have_child_rows
|
|
|
|
#
|
|
|
|
self._has_async_text_info = has_async_text_info
|
|
self._terms_to_async_text_info = {}
|
|
self._pending_async_text_info_terms = set()
|
|
self._currently_fetching_async_text_info_terms = set()
|
|
self._async_text_info_lock = threading.Lock()
|
|
self._async_text_info_shared_data = dict()
|
|
self._async_text_info_updater = self._InitialiseAsyncTextInfoUpdater()
|
|
|
|
#
|
|
|
|
self.setFont( QW.QApplication.font() )
|
|
|
|
self._widget_event_filter = QP.WidgetEventFilter( self.widget() )
|
|
|
|
self._widget_event_filter.EVT_LEFT_DOWN( self.EventMouseSelect )
|
|
self._widget_event_filter.EVT_RIGHT_DOWN( self.EventMouseSelect )
|
|
|
|
|
|
def __len__( self ):
|
|
|
|
return len( self._ordered_terms )
|
|
|
|
|
|
def __bool__( self ):
|
|
|
|
return QP.isValid( self )
|
|
|
|
|
|
def _Activate( self, shift_down ) -> bool:
|
|
|
|
return False
|
|
|
|
|
|
def _ActivateFromKeyboard( self, shift_down ):
|
|
|
|
selected_indices = []
|
|
|
|
for term in self._selected_terms:
|
|
|
|
try:
|
|
|
|
logical_index = self._GetLogicalIndexFromTerm( term )
|
|
|
|
selected_indices.append( logical_index )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
pass
|
|
|
|
|
|
|
|
action_occurred = self._Activate( shift_down )
|
|
|
|
if action_occurred and len( self._selected_terms ) == 0 and len( selected_indices ) > 0:
|
|
|
|
ideal_index = min( selected_indices )
|
|
|
|
ideal_indices = [ ideal_index, ideal_index - 1, 0 ]
|
|
|
|
for ideal_index in ideal_indices:
|
|
|
|
if ideal_index <= len( self._ordered_terms ) - 1:
|
|
|
|
self._Hit( False, False, ideal_index )
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
return action_occurred
|
|
|
|
|
|
def _AddEditMenu( self, menu: QW.QMenu ):
|
|
|
|
pass
|
|
|
|
|
|
def _ApplyAsyncInfoToTerm( self, term, info ) -> typing.Tuple[ bool, bool ]:
|
|
|
|
# this guy comes with the lock
|
|
|
|
return ( False, False )
|
|
|
|
|
|
def _DeleteActivate( self ):
|
|
|
|
pass
|
|
|
|
|
|
def _AppendTerms( self, terms ):
|
|
|
|
previously_selected_terms = { term for term in terms if term in self._selected_terms }
|
|
|
|
clear_terms = [ term for term in terms if term in self._terms_to_logical_indices ]
|
|
|
|
if len( clear_terms ) > 0:
|
|
|
|
self._RemoveTerms( clear_terms )
|
|
|
|
|
|
for term in terms:
|
|
|
|
self._ordered_terms.append( term )
|
|
|
|
self._terms_to_logical_indices[ term ] = len( self._ordered_terms ) - 1
|
|
self._terms_to_positional_indices[ term ] = self._total_positional_rows
|
|
self._positional_indices_to_terms[ self._total_positional_rows ] = term
|
|
|
|
if self._has_async_text_info:
|
|
|
|
# goes before getrowcount so we can populate if needed
|
|
|
|
self._StartAsyncTextInfoLookup( term )
|
|
|
|
|
|
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
|
|
|
|
|
|
if len( previously_selected_terms ) > 0:
|
|
|
|
self._selected_terms.update( previously_selected_terms )
|
|
|
|
|
|
|
|
def _Clear( self ):
|
|
|
|
self._ordered_terms = []
|
|
self._selected_terms = set()
|
|
self._terms_to_logical_indices = {}
|
|
self._terms_to_positional_indices = {}
|
|
self._positional_indices_to_terms = {}
|
|
self._total_positional_rows = 0
|
|
|
|
self._last_hit_logical_index = None
|
|
|
|
self._last_view_start = None
|
|
|
|
|
|
def _DataHasChanged( self ):
|
|
|
|
self._SetVirtualSize()
|
|
|
|
self.widget().update()
|
|
|
|
self.listBoxChanged.emit()
|
|
|
|
|
|
def _Deselect( self, index ):
|
|
|
|
term = self._GetTermFromLogicalIndex( index )
|
|
|
|
self._selected_terms.discard( term )
|
|
|
|
|
|
def _DeselectAll( self ):
|
|
|
|
self._selected_terms = set()
|
|
|
|
|
|
def _GetLogicalIndexFromTerm( self, term ):
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
return self._terms_to_logical_indices[ term ]
|
|
|
|
|
|
raise HydrusExceptions.DataMissing()
|
|
|
|
|
|
def _GetLogicalIndexUnderMouse( self, mouse_event ):
|
|
|
|
y = mouse_event.pos().y()
|
|
|
|
if mouse_event.type() == QC.QEvent.MouseMove:
|
|
|
|
visible_rect = QP.ScrollAreaVisibleRect( self )
|
|
|
|
visible_rect_y = visible_rect.y()
|
|
|
|
y += visible_rect_y
|
|
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
positional_index = y // text_height
|
|
|
|
if positional_index >= self._total_positional_rows:
|
|
|
|
return None
|
|
|
|
|
|
( logical_index, positional_index ) = self._GetLogicalIndicesFromPositionalIndex( positional_index )
|
|
|
|
return logical_index
|
|
|
|
|
|
def _GetLogicalIndicesFromPositionalIndex( self, positional_index: int ):
|
|
|
|
if positional_index < 0 or positional_index >= self._total_positional_rows:
|
|
|
|
return ( None, positional_index )
|
|
|
|
|
|
while positional_index not in self._positional_indices_to_terms and positional_index >= 0:
|
|
|
|
positional_index -= 1
|
|
|
|
|
|
if positional_index < 0:
|
|
|
|
return ( None, 0 )
|
|
|
|
|
|
return ( self._terms_to_logical_indices[ self._positional_indices_to_terms[ positional_index ] ], positional_index )
|
|
|
|
|
|
def _GetPositionalIndexFromLogicalIndex( self, logical_index: int ):
|
|
|
|
try:
|
|
|
|
term = self._GetTermFromLogicalIndex( logical_index )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
return 0
|
|
|
|
|
|
return self._terms_to_positional_indices[ term ]
|
|
|
|
|
|
def _GetPredicatesFromTerms( self, terms: typing.Collection[ ClientGUIListBoxesData.ListBoxItem ] ):
|
|
|
|
return list( itertools.chain.from_iterable( ( term.GetSearchPredicates() for term in terms ) ) )
|
|
|
|
|
|
def _GetRowsOfTextsAndColours( self, term: ClientGUIListBoxesData.ListBoxItem ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def _GetSelectedPredicatesAndInverseCopies( self ):
|
|
|
|
predicates = self._GetPredicatesFromTerms( self._selected_terms )
|
|
inverse_predicates = [ predicate.GetInverseCopy() for predicate in predicates if predicate.IsInvertible() ]
|
|
|
|
if len( predicates ) > 1 and ClientSearch.PREDICATE_TYPE_OR_CONTAINER not in ( p.GetType() for p in predicates ):
|
|
|
|
or_predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, value = list( predicates ) )
|
|
|
|
else:
|
|
|
|
or_predicate = None
|
|
|
|
|
|
return ( predicates, or_predicate, inverse_predicates )
|
|
|
|
|
|
def _GetSafeHitIndex( self, logical_index, direction = None ):
|
|
|
|
if direction is None:
|
|
|
|
if logical_index == 0:
|
|
|
|
direction = 1
|
|
|
|
else:
|
|
|
|
direction = -1
|
|
|
|
|
|
|
|
num_terms = len( self._ordered_terms )
|
|
|
|
if num_terms == 0:
|
|
|
|
return None
|
|
|
|
|
|
original_logical_index = logical_index
|
|
|
|
if logical_index is not None:
|
|
|
|
# if click/selection is out of bounds, fix it
|
|
if logical_index == -1 or logical_index > num_terms:
|
|
|
|
logical_index = num_terms - 1
|
|
|
|
elif logical_index == num_terms or logical_index < -1:
|
|
|
|
logical_index = 0
|
|
|
|
|
|
|
|
return logical_index
|
|
|
|
|
|
def _GetTagsFromTerms( self, terms: typing.Collection[ ClientGUIListBoxesData.ListBoxItem ] ):
|
|
|
|
return list( itertools.chain.from_iterable( ( term.GetTags() for term in terms ) ) )
|
|
|
|
|
|
def _GetTermFromLogicalIndex( self, logical_index ) -> ClientGUIListBoxesData.ListBoxItem:
|
|
|
|
if logical_index < 0 or logical_index > len( self._ordered_terms ) - 1:
|
|
|
|
raise HydrusExceptions.DataMissing( 'No term for index ' + str( logical_index ) )
|
|
|
|
|
|
return self._ordered_terms[ logical_index ]
|
|
|
|
|
|
def _HandleClick( self, event ):
|
|
|
|
logical_index = self._GetLogicalIndexUnderMouse( event )
|
|
|
|
shift = event.modifiers() & QC.Qt.ShiftModifier
|
|
ctrl = event.modifiers() & QC.Qt.ControlModifier
|
|
|
|
self._Hit( shift, ctrl, logical_index )
|
|
|
|
|
|
def _Hit( self, shift, ctrl, logical_index, only_add = False ):
|
|
|
|
if logical_index is not None and ( logical_index < 0 or logical_index >= len( self._ordered_terms ) ):
|
|
|
|
logical_index = None
|
|
|
|
|
|
to_select = set()
|
|
to_deselect = set()
|
|
|
|
deselect_all = False
|
|
|
|
if shift:
|
|
|
|
if logical_index is not None:
|
|
|
|
if ctrl:
|
|
|
|
if self._LogicalIndexIsSelected( logical_index ):
|
|
|
|
if self._last_hit_logical_index is not None:
|
|
|
|
lower = min( logical_index, self._last_hit_logical_index )
|
|
upper = max( logical_index, self._last_hit_logical_index )
|
|
|
|
to_deselect = list( range( lower, upper + 1 ) )
|
|
|
|
else:
|
|
|
|
to_deselect.add( logical_index )
|
|
|
|
|
|
|
|
else:
|
|
|
|
# we are now saying if you shift-click on something already selected, we'll make no changes, but we'll move focus ghost
|
|
if not self._LogicalIndexIsSelected( logical_index ):
|
|
|
|
if self._last_hit_logical_index is not None:
|
|
|
|
lower = min( logical_index, self._last_hit_logical_index )
|
|
upper = max( logical_index, self._last_hit_logical_index )
|
|
|
|
to_select = list( range( lower, upper + 1 ) )
|
|
|
|
else:
|
|
|
|
to_select.add( logical_index )
|
|
|
|
|
|
|
|
|
|
|
|
elif ctrl:
|
|
|
|
if logical_index is not None:
|
|
|
|
if self._LogicalIndexIsSelected( logical_index ):
|
|
|
|
to_deselect.add( logical_index )
|
|
|
|
else:
|
|
|
|
to_select.add( logical_index )
|
|
|
|
|
|
|
|
else:
|
|
|
|
if logical_index is None:
|
|
|
|
deselect_all = True
|
|
|
|
else:
|
|
|
|
if not self._LogicalIndexIsSelected( logical_index ):
|
|
|
|
deselect_all = True
|
|
to_select.add( logical_index )
|
|
|
|
|
|
|
|
|
|
if deselect_all:
|
|
|
|
self._DeselectAll()
|
|
|
|
|
|
for index in to_select:
|
|
|
|
self._Select( index )
|
|
|
|
|
|
for index in to_deselect:
|
|
|
|
self._Deselect( index )
|
|
|
|
|
|
self._last_hit_logical_index = logical_index
|
|
|
|
if self._last_hit_logical_index is not None:
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
last_hit_positional_index = self._GetPositionalIndexFromLogicalIndex( self._last_hit_logical_index )
|
|
|
|
y = text_height * last_hit_positional_index
|
|
|
|
visible_rect = QP.ScrollAreaVisibleRect( self )
|
|
|
|
visible_rect_y = visible_rect.y()
|
|
|
|
visible_rect_height = visible_rect.height()
|
|
|
|
if y < visible_rect_y:
|
|
|
|
self.ensureVisible( 0, y, 0, 0 )
|
|
|
|
elif y > visible_rect_y + visible_rect_height - text_height:
|
|
|
|
self.ensureVisible( 0, y + text_height , 0, 0 )
|
|
|
|
|
|
|
|
self.widget().update()
|
|
|
|
|
|
def _HitFirstSelectedItem( self ):
|
|
|
|
selected_indices = []
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
for term in self._selected_terms:
|
|
|
|
try:
|
|
|
|
logical_index = self._GetLogicalIndexFromTerm( term )
|
|
|
|
selected_indices.append( logical_index )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if len( selected_indices ) > 0:
|
|
|
|
first_logical_index = min( selected_indices )
|
|
|
|
self._Hit( False, False, first_logical_index )
|
|
|
|
|
|
|
|
|
|
def _InitialiseAsyncTextInfoUpdater( self ):
|
|
|
|
def loading_callable():
|
|
|
|
pass
|
|
|
|
|
|
work_callable = self._InitialiseAsyncTextInfoUpdaterWorkCallable()
|
|
|
|
def publish_callable( terms_to_info ):
|
|
|
|
any_sort_info_changed = False
|
|
any_num_rows_changed = False
|
|
|
|
with self._async_text_info_lock:
|
|
|
|
self._currently_fetching_async_text_info_terms.difference_update( terms_to_info.keys() )
|
|
|
|
self._terms_to_async_text_info.update( terms_to_info )
|
|
|
|
for ( term, info ) in terms_to_info.items():
|
|
|
|
# ok in the time since this happened, we may have actually changed the term object, so let's cycle to the actual object in use atm!
|
|
if term in self._terms_to_positional_indices:
|
|
|
|
term = self._positional_indices_to_terms[ self._terms_to_positional_indices[ term ] ]
|
|
|
|
|
|
( sort_info_changed, num_rows_changed ) = self._ApplyAsyncInfoToTerm( term, info )
|
|
|
|
if sort_info_changed:
|
|
|
|
any_sort_info_changed = True
|
|
|
|
|
|
if num_rows_changed:
|
|
|
|
any_num_rows_changed = True
|
|
|
|
|
|
|
|
|
|
if any_sort_info_changed:
|
|
|
|
self._Sort()
|
|
# this does regentermstoindices
|
|
|
|
elif any_num_rows_changed:
|
|
|
|
self._RegenTermsToIndices()
|
|
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
return ClientGUIAsync.AsyncQtUpdater( self, loading_callable, work_callable, publish_callable )
|
|
|
|
|
|
def _InitialiseAsyncTextInfoUpdaterWorkCallable( self ):
|
|
|
|
async_lock = self._async_text_info_lock
|
|
currently_fetching = self._currently_fetching_async_text_info_terms
|
|
pending = self._pending_async_text_info_terms
|
|
|
|
def work_callable():
|
|
|
|
with async_lock:
|
|
|
|
to_lookup = set( pending )
|
|
|
|
pending.clear()
|
|
|
|
currently_fetching.update( to_lookup )
|
|
|
|
|
|
terms_to_info = { term : None for term in to_lookup }
|
|
|
|
return terms_to_info
|
|
|
|
|
|
return work_callable
|
|
|
|
|
|
def _LogicalIndexIsSelected( self, logical_index ):
|
|
|
|
try:
|
|
|
|
term = self._GetTermFromLogicalIndex( logical_index )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
return False
|
|
|
|
|
|
return term in self._selected_terms
|
|
|
|
|
|
def _Redraw( self, painter ):
|
|
|
|
painter.setBackground( QG.QBrush( self._background_colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
if len( self._ordered_terms ) == 0:
|
|
|
|
return
|
|
|
|
|
|
#
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
visible_rect = QP.ScrollAreaVisibleRect( self )
|
|
|
|
visible_rect_y = visible_rect.y()
|
|
|
|
visible_rect_width = visible_rect.width()
|
|
visible_rect_height = visible_rect.height()
|
|
|
|
first_visible_positional_index = max( 0, visible_rect_y // text_height )
|
|
|
|
last_visible_positional_index = ( visible_rect_y + visible_rect_height ) // text_height
|
|
|
|
if ( visible_rect_y + visible_rect_height ) % text_height != 0:
|
|
|
|
last_visible_positional_index += 1
|
|
|
|
|
|
last_visible_positional_index = max( 0, min( last_visible_positional_index, self._total_positional_rows - 1 ) )
|
|
|
|
#
|
|
|
|
( first_visible_logical_index, first_visible_positional_index ) = self._GetLogicalIndicesFromPositionalIndex( first_visible_positional_index )
|
|
( last_visible_logical_index, last_visible_positional_index ) = self._GetLogicalIndicesFromPositionalIndex( last_visible_positional_index )
|
|
|
|
# some crazy situation with ultra laggy sessions where we are rendering a zero or negative size list or something
|
|
if first_visible_logical_index is None or last_visible_logical_index is None:
|
|
|
|
return
|
|
|
|
|
|
current_visible_index = first_visible_positional_index
|
|
|
|
for logical_index in range( first_visible_logical_index, last_visible_logical_index + 1 ):
|
|
|
|
term = self._GetTermFromLogicalIndex( logical_index )
|
|
|
|
rows_of_texts_and_colours = self._GetRowsOfTextsAndColours( term )
|
|
|
|
for texts_and_colours in rows_of_texts_and_colours:
|
|
|
|
x_start = self.TEXT_X_PADDING
|
|
y_top = current_visible_index * text_height
|
|
|
|
for ( text, ( r, g, b ) ) in texts_and_colours:
|
|
|
|
text_colour = QG.QColor( r, g, b )
|
|
|
|
if term in self._selected_terms:
|
|
|
|
painter.setBrush( QG.QBrush( text_colour ) )
|
|
|
|
painter.setPen( QC.Qt.NoPen )
|
|
|
|
if x_start == self.TEXT_X_PADDING:
|
|
|
|
background_colour_x = 0
|
|
|
|
else:
|
|
|
|
background_colour_x = x_start
|
|
|
|
|
|
painter.drawRect( background_colour_x, y_top, visible_rect_width, text_height )
|
|
|
|
text_colour = self._background_colour
|
|
|
|
|
|
painter.setPen( QG.QPen( text_colour ) )
|
|
|
|
( this_text_size, text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, text )
|
|
|
|
this_text_width = this_text_size.width()
|
|
this_text_height = this_text_size.height()
|
|
|
|
painter.drawText( QC.QRectF( x_start, y_top, this_text_width, this_text_height ), text )
|
|
|
|
x_start += this_text_width
|
|
|
|
|
|
current_visible_index += 1
|
|
|
|
|
|
|
|
|
|
def _RegenTermsToIndices( self ):
|
|
|
|
self._terms_to_logical_indices = {}
|
|
self._terms_to_positional_indices = {}
|
|
self._positional_indices_to_terms = {}
|
|
self._total_positional_rows = 0
|
|
|
|
for ( logical_index, term ) in enumerate( self._ordered_terms ):
|
|
|
|
self._terms_to_logical_indices[ term ] = logical_index
|
|
self._terms_to_positional_indices[ term ] = self._total_positional_rows
|
|
self._positional_indices_to_terms[ self._total_positional_rows ] = term
|
|
|
|
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
|
|
|
|
|
|
|
|
def _RemoveSelectedTerms( self ):
|
|
|
|
self._RemoveTerms( list( self._selected_terms ) )
|
|
|
|
|
|
def _RemoveTerms( self, terms ):
|
|
|
|
removable_terms = { term for term in terms if term in self._terms_to_logical_indices }
|
|
|
|
if len( removable_terms ) == 0:
|
|
|
|
return
|
|
|
|
|
|
for term in removable_terms:
|
|
|
|
self._ordered_terms.remove( term )
|
|
|
|
|
|
self._selected_terms.difference_update( removable_terms )
|
|
|
|
self._RegenTermsToIndices()
|
|
|
|
self._last_hit_logical_index = None
|
|
|
|
|
|
def _Select( self, index ):
|
|
|
|
try:
|
|
|
|
term = self._GetTermFromLogicalIndex( index )
|
|
|
|
self._selected_terms.add( term )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _SelectAll( self ):
|
|
|
|
self._selected_terms = set( self._terms_to_logical_indices.keys() )
|
|
|
|
|
|
def _SetVirtualSize( self ):
|
|
|
|
self.setWidgetResizable( True )
|
|
|
|
my_size = self.widget().size()
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
ideal_virtual_size = QC.QSize( my_size.width(), text_height * self._total_positional_rows )
|
|
|
|
if ideal_virtual_size != my_size:
|
|
|
|
self.widget().setMinimumSize( ideal_virtual_size )
|
|
|
|
|
|
|
|
def _Sort( self ):
|
|
|
|
self._ordered_terms.sort()
|
|
|
|
self._RegenTermsToIndices()
|
|
|
|
|
|
def _StartAsyncTextInfoLookup( self, term ):
|
|
|
|
with self._async_text_info_lock:
|
|
|
|
if term in self._terms_to_async_text_info:
|
|
|
|
info = self._terms_to_async_text_info[ term ]
|
|
|
|
self._ApplyAsyncInfoToTerm( term, info )
|
|
|
|
elif term not in self._currently_fetching_async_text_info_terms:
|
|
|
|
self._pending_async_text_info_terms.add( term )
|
|
|
|
self._async_text_info_updater.update()
|
|
|
|
|
|
|
|
|
|
def _GetAsyncTextInfoLookupCallable( self ):
|
|
|
|
return lambda terms: {}
|
|
|
|
|
|
def keyPressEvent( self, event ):
|
|
|
|
shift = event.modifiers() & QC.Qt.ShiftModifier
|
|
ctrl = event.modifiers() & QC.Qt.ControlModifier
|
|
|
|
key_code = event.key()
|
|
|
|
if self.hasFocus() and key_code in ClientGUIShortcuts.DELETE_KEYS_QT:
|
|
|
|
self._DeleteActivate()
|
|
|
|
elif key_code in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ):
|
|
|
|
self._ActivateFromKeyboard( shift )
|
|
|
|
else:
|
|
|
|
if ctrl and key_code in ( ord( 'A' ), ord( 'a' ) ):
|
|
|
|
self._SelectAll()
|
|
|
|
self.widget().update()
|
|
|
|
else:
|
|
|
|
hit_logical_index = None
|
|
|
|
if len( self._ordered_terms ) > 1:
|
|
|
|
roll_up = False
|
|
roll_down = False
|
|
|
|
if key_code in ( QC.Qt.Key_Home, ):
|
|
|
|
hit_logical_index = 0
|
|
|
|
elif key_code in ( QC.Qt.Key_End, ):
|
|
|
|
hit_logical_index = len( self._ordered_terms ) - 1
|
|
|
|
roll_up = True
|
|
|
|
elif self._last_hit_logical_index is not None:
|
|
|
|
if key_code in ( QC.Qt.Key_Up, ):
|
|
|
|
hit_logical_index = ( self._last_hit_logical_index - 1 ) % len( self._ordered_terms )
|
|
|
|
elif key_code in ( QC.Qt.Key_Down, ):
|
|
|
|
hit_logical_index = ( self._last_hit_logical_index + 1 ) % len( self._ordered_terms )
|
|
|
|
elif key_code in ( QC.Qt.Key_PageUp, QC.Qt.Key_PageDown ):
|
|
|
|
last_hit_positional_index = self._GetPositionalIndexFromLogicalIndex( self._last_hit_logical_index )
|
|
|
|
if key_code == QC.Qt.Key_PageUp:
|
|
|
|
hit_positional_index = max( 0, last_hit_positional_index - self._num_rows_per_page )
|
|
|
|
else:
|
|
|
|
hit_positional_index = min( self._total_positional_rows - 1, last_hit_positional_index + self._num_rows_per_page )
|
|
|
|
|
|
( hit_logical_index, hit_positional_index ) = self._GetLogicalIndicesFromPositionalIndex( hit_positional_index )
|
|
|
|
|
|
|
|
|
|
if hit_logical_index is None:
|
|
|
|
# don't send to parent, which will do silly scroll window business with arrow key presses
|
|
event.ignore()
|
|
|
|
else:
|
|
|
|
self._Hit( shift, ctrl, hit_logical_index )
|
|
|
|
|
|
|
|
|
|
|
|
def mouseDoubleClickEvent( self, event ):
|
|
|
|
if event.button() == QC.Qt.LeftButton:
|
|
|
|
shift_down = event.modifiers() & QC.Qt.ShiftModifier
|
|
|
|
action_occurred = self._Activate( shift_down )
|
|
|
|
if action_occurred:
|
|
|
|
self.mouseActivationOccurred.emit()
|
|
|
|
|
|
else:
|
|
|
|
QW.QScrollArea.mouseDoubleClickEvent( self, event )
|
|
|
|
|
|
|
|
def mouseMoveEvent( self, event ):
|
|
|
|
is_dragging = event.buttons() & QC.Qt.LeftButton
|
|
|
|
if is_dragging:
|
|
|
|
logical_index = self._GetLogicalIndexUnderMouse( event )
|
|
|
|
if self._last_drag_start_logical_index is None:
|
|
|
|
self._last_drag_start_logical_index = logical_index
|
|
|
|
elif logical_index != self._last_drag_start_logical_index:
|
|
|
|
ctrl = event.modifiers() & QC.Qt.ControlModifier
|
|
|
|
if not self._drag_started:
|
|
|
|
self._Hit( True, ctrl, self._last_drag_start_logical_index )
|
|
|
|
self._drag_started = True
|
|
|
|
|
|
self._Hit( True, ctrl, logical_index )
|
|
|
|
|
|
else:
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
def mouseReleaseEvent( self, event ):
|
|
|
|
self._last_drag_start_logical_index = None
|
|
self._drag_started = False
|
|
|
|
event.ignore()
|
|
|
|
|
|
def EventMouseSelect( self, event ):
|
|
|
|
self._HandleClick( event )
|
|
|
|
return True # was: event.ignore()
|
|
|
|
|
|
class _InnerWidget( QW.QWidget ):
|
|
|
|
def __init__( self, parent ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._parent = parent
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
self._parent._Redraw( painter )
|
|
|
|
|
|
|
|
def resizeEvent( self, event ):
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
visible_rect = QP.ScrollAreaVisibleRect( self )
|
|
|
|
self.verticalScrollBar().setSingleStep( text_height )
|
|
|
|
visible_rect_height = visible_rect.height()
|
|
|
|
self._num_rows_per_page = visible_rect_height // text_height
|
|
|
|
self._SetVirtualSize()
|
|
|
|
self.widget().update()
|
|
|
|
|
|
def GetIdealHeight( self ):
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
return text_height * self._total_positional_rows + 20
|
|
|
|
|
|
def HasValues( self ):
|
|
|
|
return len( self._ordered_terms ) > 0
|
|
|
|
|
|
def minimumSizeHint( self ):
|
|
|
|
size_hint = QW.QScrollArea.minimumSizeHint( self )
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
minimum_height = self._minimum_height_num_chars * text_height + ( self.frameWidth() * 2 )
|
|
|
|
size_hint.setHeight( minimum_height )
|
|
|
|
return size_hint
|
|
|
|
|
|
def MoveSelectionDown( self ):
|
|
|
|
if len( self._ordered_terms ) > 1 and self._last_hit_logical_index is not None:
|
|
|
|
logical_index = ( self._last_hit_logical_index + 1 ) % len( self._ordered_terms )
|
|
|
|
self._Hit( False, False, logical_index )
|
|
|
|
|
|
|
|
def MoveSelectionUp( self ):
|
|
|
|
if len( self._ordered_terms ) > 1 and self._last_hit_logical_index is not None:
|
|
|
|
logical_index = ( self._last_hit_logical_index - 1 ) % len( self._ordered_terms )
|
|
|
|
self._Hit( False, False, logical_index )
|
|
|
|
|
|
|
|
def SelectTopItem( self ):
|
|
|
|
if len( self._ordered_terms ) > 0:
|
|
|
|
if len( self._selected_terms ) == 1 and self._LogicalIndexIsSelected( 0 ):
|
|
|
|
return
|
|
|
|
|
|
self._DeselectAll()
|
|
|
|
self._Hit( False, False, 0 )
|
|
|
|
self.widget().update()
|
|
|
|
|
|
|
|
def SetChildRowsAllowed( self, value: bool ):
|
|
|
|
if self._terms_may_have_child_rows and self._child_rows_allowed != value:
|
|
|
|
self._child_rows_allowed = value
|
|
|
|
self._RegenTermsToIndices()
|
|
|
|
self._SetVirtualSize()
|
|
|
|
self.widget().update()
|
|
|
|
|
|
|
|
def SetMinimumHeightNumChars( self, minimum_height_num_chars ):
|
|
|
|
self._minimum_height_num_chars = minimum_height_num_chars
|
|
|
|
|
|
def sizeHint( self ):
|
|
|
|
size_hint = QW.QScrollArea.sizeHint( self )
|
|
|
|
text_height = self.fontMetrics().height()
|
|
|
|
ideal_height = self._height_num_chars * text_height + ( self.frameWidth() * 2 )
|
|
|
|
size_hint.setHeight( ideal_height )
|
|
|
|
return size_hint
|
|
|
|
|
|
COPY_ALL_TAGS = 0
|
|
COPY_ALL_TAGS_WITH_COUNTS = 1
|
|
COPY_SELECTED_TAGS = 2
|
|
COPY_SELECTED_TAGS_WITH_COUNTS = 3
|
|
COPY_SELECTED_SUBTAGS = 4
|
|
COPY_SELECTED_SUBTAGS_WITH_COUNTS = 5
|
|
COPY_ALL_SUBTAGS = 6
|
|
COPY_ALL_SUBTAGS_WITH_COUNTS = 7
|
|
|
|
class ListBoxTags( ListBox ):
|
|
|
|
can_spawn_new_windows = True
|
|
|
|
def __init__( self, parent, *args, tag_display_type: int = ClientTags.TAG_DISPLAY_STORAGE, **kwargs ):
|
|
|
|
self._tag_display_type = tag_display_type
|
|
|
|
child_rows_allowed = HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_taglists' )
|
|
terms_may_have_child_rows = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
ListBox.__init__( self, parent, child_rows_allowed, terms_may_have_child_rows, *args, **kwargs )
|
|
|
|
self._render_for_user = not self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
self._sibling_decoration_allowed = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
self._page_key = None # placeholder. if a subclass sets this, it changes menu behaviour to allow 'select this tag' menu pubsubs
|
|
|
|
self._UpdateBackgroundColour()
|
|
|
|
self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventMouseMiddleClick )
|
|
|
|
HG.client_controller.sub( self, 'ForceTagRecalc', 'refresh_all_tag_presentation_gui' )
|
|
HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
|
|
|
|
|
|
def _GetCopyableTagStrings( self, command ):
|
|
|
|
only_selected = command in ( COPY_SELECTED_TAGS, COPY_SELECTED_TAGS_WITH_COUNTS, COPY_SELECTED_SUBTAGS, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
|
|
with_counts = command in ( COPY_ALL_TAGS_WITH_COUNTS, COPY_ALL_SUBTAGS_WITH_COUNTS, COPY_SELECTED_TAGS_WITH_COUNTS, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
|
|
only_subtags = command in ( COPY_ALL_SUBTAGS, COPY_ALL_SUBTAGS_WITH_COUNTS, COPY_SELECTED_SUBTAGS, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
|
|
|
|
if only_selected:
|
|
|
|
if len( self._selected_terms ) > 1:
|
|
|
|
# keep order
|
|
terms = [ term for term in self._ordered_terms if term in self._selected_terms ]
|
|
|
|
else:
|
|
|
|
# nice and fast
|
|
terms = self._selected_terms
|
|
|
|
|
|
else:
|
|
|
|
terms = self._ordered_terms
|
|
|
|
|
|
copyable_tag_strings = [ term.GetCopyableText( with_counts = with_counts ) for term in terms ]
|
|
|
|
if only_subtags:
|
|
|
|
copyable_tag_strings = [ HydrusTags.SplitTag( tag_string )[1] for tag_string in copyable_tag_strings ]
|
|
|
|
if not with_counts:
|
|
|
|
copyable_tag_strings = HydrusData.DedupeList( copyable_tag_strings )
|
|
|
|
|
|
|
|
return copyable_tag_strings
|
|
|
|
|
|
def _GetCurrentFileServiceKey( self ):
|
|
|
|
return CC.LOCAL_FILE_SERVICE_KEY
|
|
|
|
|
|
def _GetCurrentPagePredicates( self ) -> typing.Set[ ClientSearch.Predicate ]:
|
|
|
|
return set()
|
|
|
|
|
|
def _GetNamespaceColours( self ):
|
|
|
|
return HC.options[ 'namespace_colours' ]
|
|
|
|
|
|
def _CanProvideCurrentPagePredicates( self ):
|
|
|
|
return False
|
|
|
|
|
|
def _GetRowsOfTextsAndColours( self, term: ClientGUIListBoxesData.ListBoxItem ):
|
|
|
|
namespace_colours = self._GetNamespaceColours()
|
|
|
|
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._sibling_decoration_allowed, self._child_rows_allowed )
|
|
|
|
rows_of_texts_and_colours = []
|
|
|
|
for texts_and_namespaces in rows_of_texts_and_namespaces:
|
|
|
|
texts_and_colours = []
|
|
|
|
for ( text, namespace ) in texts_and_namespaces:
|
|
|
|
if namespace in namespace_colours:
|
|
|
|
rgb = namespace_colours[ namespace ]
|
|
|
|
else:
|
|
|
|
rgb = namespace_colours[ None ]
|
|
|
|
|
|
texts_and_colours.append( ( text, rgb ) )
|
|
|
|
|
|
rows_of_texts_and_colours.append( texts_and_colours )
|
|
|
|
|
|
return rows_of_texts_and_colours
|
|
|
|
|
|
def _HasCounts( self ):
|
|
|
|
return False
|
|
|
|
|
|
def _NewSearchPages( self, pages_of_predicates ):
|
|
|
|
activate_window = HG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
|
|
|
|
for predicates in pages_of_predicates:
|
|
|
|
predicates = ClientGUISearch.FleshOutPredicates( self, predicates )
|
|
|
|
if len( predicates ) == 0:
|
|
|
|
break
|
|
|
|
|
|
s = sorted( ( predicate.ToString() for predicate in predicates ) )
|
|
|
|
page_name = ', '.join( s )
|
|
|
|
file_service_key = self._GetCurrentFileServiceKey()
|
|
|
|
HG.client_controller.pub( 'new_page_query', file_service_key, initial_predicates = predicates, page_name = page_name, activate_window = activate_window )
|
|
|
|
activate_window = False
|
|
|
|
|
|
|
|
def _ProcessMenuCopyEvent( self, command ):
|
|
|
|
texts = self._GetCopyableTagStrings( command )
|
|
|
|
if len( texts ) > 0:
|
|
|
|
text = os.linesep.join( texts )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _ProcessMenuPredicateEvent( self, command ):
|
|
|
|
pass
|
|
|
|
|
|
def _ProcessMenuTagEvent( self, command ):
|
|
|
|
tags = self._GetTagsFromTerms( self._selected_terms )
|
|
|
|
tags = [ tag for tag in tags if tag is not None ]
|
|
|
|
if command in ( 'hide', 'hide_namespace' ):
|
|
|
|
if len( tags ) == 1:
|
|
|
|
( tag, ) = tags
|
|
|
|
if command == 'hide':
|
|
|
|
message = 'Hide "{}" from here?'.format( tag )
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
HG.client_controller.tag_display_manager.HideTag( self._tag_display_type, CC.COMBINED_TAG_SERVICE_KEY, tag )
|
|
|
|
elif command == 'hide_namespace':
|
|
|
|
( namespace, subtag ) = HydrusTags.SplitTag( tag )
|
|
|
|
if namespace == '':
|
|
|
|
insert = 'unnamespaced'
|
|
|
|
else:
|
|
|
|
insert = '"{}"'.format( namespace )
|
|
|
|
|
|
message = 'Hide {} tags from here?'.format( insert )
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
if namespace != '':
|
|
|
|
namespace += ':'
|
|
|
|
|
|
HG.client_controller.tag_display_manager.HideTag( self._tag_display_type, CC.COMBINED_TAG_SERVICE_KEY, namespace )
|
|
|
|
|
|
HG.client_controller.pub( 'notify_new_tag_display_rules' )
|
|
|
|
|
|
else:
|
|
|
|
from hydrus.client.gui import ClientGUITags
|
|
|
|
if command == 'parent':
|
|
|
|
title = 'manage tag parents'
|
|
|
|
elif command == 'sibling':
|
|
|
|
title = 'manage tag siblings'
|
|
|
|
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogManage( self, title ) as dlg:
|
|
|
|
if command == 'parent':
|
|
|
|
panel = ClientGUITags.ManageTagParents( dlg, tags )
|
|
|
|
elif command == 'sibling':
|
|
|
|
panel = ClientGUITags.ManageTagSiblings( dlg, tags )
|
|
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
|
|
def _UpdateBackgroundColour( self ):
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
self._background_colour = new_options.GetColour( CC.COLOUR_TAGS_BOX )
|
|
|
|
self.widget().update()
|
|
|
|
|
|
def EventMouseMiddleClick( self, event ):
|
|
|
|
self._HandleClick( event )
|
|
|
|
if self.can_spawn_new_windows:
|
|
|
|
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
|
|
|
|
if len( predicates ) > 0:
|
|
|
|
shift_down = event.modifiers() & QC.Qt.ShiftModifier
|
|
|
|
if shift_down and or_predicate is not None:
|
|
|
|
predicates = ( or_predicate, )
|
|
|
|
|
|
self._NewSearchPages( [ predicates ] )
|
|
|
|
|
|
|
|
|
|
def contextMenuEvent( self, event ):
|
|
|
|
if event.reason() == QG.QContextMenuEvent.Keyboard:
|
|
|
|
self.ShowMenu()
|
|
|
|
|
|
|
|
def mouseReleaseEvent( self, event ):
|
|
|
|
if event.button() != QC.Qt.RightButton:
|
|
|
|
ListBox.mouseReleaseEvent( self, event )
|
|
|
|
return
|
|
|
|
|
|
self.ShowMenu()
|
|
|
|
|
|
def ShowMenu( self ):
|
|
|
|
sub_selection_string = None
|
|
|
|
if len( self._ordered_terms ) > 0:
|
|
|
|
selected_actual_tags = self._GetTagsFromTerms( self._selected_terms )
|
|
|
|
menu = QW.QMenu()
|
|
|
|
if self._terms_may_have_child_rows:
|
|
|
|
add_it = True
|
|
|
|
if self._child_rows_allowed:
|
|
|
|
if len( self._ordered_terms ) == self._total_positional_rows:
|
|
|
|
# no parents to hide!
|
|
|
|
add_it = False
|
|
|
|
|
|
message = 'hide parent rows'
|
|
|
|
else:
|
|
|
|
message = 'show parent rows'
|
|
|
|
|
|
if add_it:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, message, 'Show/hide parents.', self.SetChildRowsAllowed, not self._child_rows_allowed )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
|
|
|
|
copy_menu = QW.QMenu( menu )
|
|
|
|
selected_copyable_tag_strings = self._GetCopyableTagStrings( COPY_SELECTED_TAGS )
|
|
selected_copyable_subtag_strings = self._GetCopyableTagStrings( COPY_SELECTED_SUBTAGS )
|
|
|
|
if len( selected_copyable_tag_strings ) == 1:
|
|
|
|
( selection_string, ) = selected_copyable_tag_strings
|
|
|
|
else:
|
|
|
|
selection_string = '{} selected'.format( HydrusData.ToHumanInt( len( selected_copyable_tag_strings ) ) )
|
|
|
|
|
|
if len( selected_copyable_tag_strings ) > 0:
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, selection_string, 'Copy the selected tags to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_TAGS )
|
|
|
|
if len( selected_copyable_subtag_strings ) == 1:
|
|
|
|
# this does a quick test for 'are we selecting a namespaced tags' that also allows for having both 'samus aran' and 'character:samus aran'
|
|
if set( selected_copyable_subtag_strings ) != set( selected_copyable_tag_strings ):
|
|
|
|
( sub_selection_string, ) = selected_copyable_subtag_strings
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, sub_selection_string, 'Copy the selected subtag to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_SUBTAGS )
|
|
|
|
|
|
else:
|
|
|
|
sub_selection_string = '{} selected subtags'.format( HydrusData.ToHumanInt( len( selected_copyable_subtag_strings ) ) )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, sub_selection_string, 'Copy the selected subtags to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_SUBTAGS )
|
|
|
|
|
|
if self._HasCounts():
|
|
|
|
ClientGUIMenus.AppendSeparator( copy_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, '{} with counts'.format( selection_string ), 'Copy the selected tags, with their counts, to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_TAGS_WITH_COUNTS )
|
|
|
|
if sub_selection_string is not None:
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, '{} with counts'.format( sub_selection_string ), 'Copy the selected subtags, with their counts, to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
|
|
|
|
|
|
|
|
|
|
copy_all_is_appropriate = len( self._ordered_terms ) > len( self._selected_terms )
|
|
|
|
if copy_all_is_appropriate:
|
|
|
|
ClientGUIMenus.AppendSeparator( copy_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'all tags', 'Copy all the tags in this list to your clipboard.', self._ProcessMenuCopyEvent, COPY_ALL_TAGS )
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'all subtags', 'Copy all the subtags in this list to your clipboard.', self._ProcessMenuCopyEvent, COPY_ALL_SUBTAGS )
|
|
|
|
if self._HasCounts():
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'all tags with counts', 'Copy all the tags in this list, with their counts, to your clipboard.', self._ProcessMenuCopyEvent, COPY_ALL_TAGS_WITH_COUNTS )
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'all subtags with counts', 'Copy all the subtags in this list, with their counts, to your clipboard.', self._ProcessMenuCopyEvent, COPY_ALL_SUBTAGS_WITH_COUNTS )
|
|
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, copy_menu, 'copy' )
|
|
|
|
#
|
|
|
|
can_launch_sibling_and_parent_dialogs = len( selected_actual_tags ) > 0 and self.can_spawn_new_windows
|
|
can_show_siblings_and_parents = len( selected_actual_tags ) == 1
|
|
|
|
if can_show_siblings_and_parents or can_launch_sibling_and_parent_dialogs:
|
|
|
|
siblings_menu = QW.QMenu( menu )
|
|
parents_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, siblings_menu, 'siblings' )
|
|
ClientGUIMenus.AppendMenu( menu, parents_menu, 'parents' )
|
|
|
|
if can_launch_sibling_and_parent_dialogs:
|
|
|
|
if len( selected_actual_tags ) == 1:
|
|
|
|
( tag, ) = selected_actual_tags
|
|
|
|
text = tag
|
|
|
|
else:
|
|
|
|
text = 'selection'
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( siblings_menu, 'add siblings to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )
|
|
ClientGUIMenus.AppendMenuItem( parents_menu, 'add parents to ' + text, 'Add a parent to this tag.', self._ProcessMenuTagEvent, 'parent' )
|
|
|
|
|
|
if can_show_siblings_and_parents:
|
|
|
|
( selected_tag, ) = selected_actual_tags
|
|
|
|
def sp_work_callable():
|
|
|
|
selected_tag_to_service_keys_to_siblings_and_parents = HG.client_controller.Read( 'tag_siblings_and_parents_lookup', ( selected_tag, ) )
|
|
|
|
service_keys_to_siblings_and_parents = selected_tag_to_service_keys_to_siblings_and_parents[ selected_tag ]
|
|
|
|
return service_keys_to_siblings_and_parents
|
|
|
|
|
|
def sp_publish_callable( service_keys_to_siblings_and_parents ):
|
|
|
|
service_keys_in_order = HG.client_controller.services_manager.GetServiceKeys( HC.REAL_TAG_SERVICES )
|
|
|
|
all_siblings = set()
|
|
|
|
siblings_to_service_keys = collections.defaultdict( set )
|
|
parents_to_service_keys = collections.defaultdict( set )
|
|
children_to_service_keys = collections.defaultdict( set )
|
|
|
|
ideals_to_service_keys = collections.defaultdict( set )
|
|
|
|
for ( service_key, ( sibling_chain_members, ideal_tag, descendants, ancestors ) ) in service_keys_to_siblings_and_parents.items():
|
|
|
|
all_siblings.update( sibling_chain_members )
|
|
|
|
for sibling in sibling_chain_members:
|
|
|
|
if sibling == ideal_tag:
|
|
|
|
ideals_to_service_keys[ ideal_tag ].add( service_key )
|
|
|
|
continue
|
|
|
|
|
|
if sibling == selected_tag: # don't care about the selected tag unless it is ideal
|
|
|
|
continue
|
|
|
|
|
|
siblings_to_service_keys[ sibling ].add( service_key )
|
|
|
|
|
|
for ancestor in ancestors:
|
|
|
|
parents_to_service_keys[ ancestor ].add( service_key )
|
|
|
|
|
|
for descendant in descendants:
|
|
|
|
children_to_service_keys[ descendant ].add( service_key )
|
|
|
|
|
|
|
|
all_siblings.discard( selected_tag )
|
|
|
|
num_siblings = len( all_siblings )
|
|
num_parents = len( parents_to_service_keys )
|
|
num_children = len( children_to_service_keys )
|
|
|
|
service_keys_to_service_names = { service_key : HG.client_controller.services_manager.GetName( service_key ) for service_key in service_keys_in_order }
|
|
|
|
ALL_SERVICES_LABEL = 'all services'
|
|
|
|
def convert_service_keys_to_name_string( s_ks ):
|
|
|
|
if len( s_ks ) == len( service_keys_in_order ):
|
|
|
|
return ALL_SERVICES_LABEL
|
|
|
|
|
|
return ', '.join( ( service_keys_to_service_names[ service_key ] for service_key in service_keys_in_order if service_key in s_ks ) )
|
|
|
|
|
|
def group_and_sort_siblings_to_service_keys( t_to_s_ks ):
|
|
|
|
# convert "tag -> everywhere I am" to "sorted groups of locations -> what we have in common, also sorted"
|
|
|
|
service_key_groups_to_tags = collections.defaultdict( list )
|
|
|
|
for ( t, s_ks ) in t_to_s_ks.items():
|
|
|
|
service_key_groups_to_tags[ tuple( s_ks ) ].append( t )
|
|
|
|
|
|
tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
|
|
|
for t_list in service_key_groups_to_tags.values():
|
|
|
|
ClientTagSorting.SortTags( tag_sort, t_list )
|
|
|
|
|
|
service_key_groups = sorted( service_key_groups_to_tags.keys(), key = lambda s_k_g: ( -len( s_k_g ), convert_service_keys_to_name_string( s_k_g ) ) )
|
|
|
|
service_key_group_names_and_tags = [ ( convert_service_keys_to_name_string( s_k_g ), service_key_groups_to_tags[ s_k_g ] ) for s_k_g in service_key_groups ]
|
|
|
|
return service_key_group_names_and_tags
|
|
|
|
|
|
def group_and_sort_parents_to_service_keys( p_to_s_ks, c_to_s_ks ):
|
|
|
|
# convert two lots of "tag -> everywhere I am" to "sorted groups of locations -> what we have in common, also sorted"
|
|
|
|
service_key_groups_to_tags = collections.defaultdict( lambda: ( [], [] ) )
|
|
|
|
for ( p, s_ks ) in p_to_s_ks.items():
|
|
|
|
service_key_groups_to_tags[ tuple( s_ks ) ][0].append( p )
|
|
|
|
|
|
for ( c, s_ks ) in c_to_s_ks.items():
|
|
|
|
service_key_groups_to_tags[ tuple( s_ks ) ][1].append( c )
|
|
|
|
|
|
tag_sort = ClientTagSorting.TagSort.STATICGetTextASCDefault()
|
|
|
|
for ( t_list_1, t_list_2 ) in service_key_groups_to_tags.values():
|
|
|
|
ClientTagSorting.SortTags( tag_sort, t_list_1 )
|
|
ClientTagSorting.SortTags( tag_sort, t_list_2 )
|
|
|
|
|
|
service_key_groups = sorted( service_key_groups_to_tags.keys(), key = lambda s_k_g: ( -len( s_k_g ), convert_service_keys_to_name_string( s_k_g ) ) )
|
|
|
|
service_key_group_names_and_tags = [ ( convert_service_keys_to_name_string( s_k_g ), service_key_groups_to_tags[ s_k_g ] ) for s_k_g in service_key_groups ]
|
|
|
|
return service_key_group_names_and_tags
|
|
|
|
|
|
if num_siblings == 0:
|
|
|
|
siblings_menu.setTitle( 'no siblings' )
|
|
|
|
else:
|
|
|
|
siblings_menu.setTitle( '{} siblings'.format( HydrusData.ToHumanInt( num_siblings ) ) )
|
|
|
|
#
|
|
|
|
ClientGUIMenus.AppendSeparator( siblings_menu )
|
|
|
|
ideals = sorted( ideals_to_service_keys.keys(), key = HydrusTags.ConvertTagToSortable )
|
|
|
|
for ideal in ideals:
|
|
|
|
if ideal == selected_tag:
|
|
|
|
continue
|
|
|
|
|
|
ideal_label = 'ideal is "{}" on: {}'.format( ideal, convert_service_keys_to_name_string( ideals_to_service_keys[ ideal ] ) )
|
|
|
|
ClientGUIMenus.AppendMenuItem( siblings_menu, ideal_label, ideal_label, HG.client_controller.pub, 'clipboard', 'text', ideal_tag )
|
|
|
|
|
|
#
|
|
|
|
for ( s_k_name, tags ) in group_and_sort_siblings_to_service_keys( siblings_to_service_keys ):
|
|
|
|
ClientGUIMenus.AppendSeparator( siblings_menu )
|
|
|
|
if s_k_name != ALL_SERVICES_LABEL:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( siblings_menu, '--{}--'.format( s_k_name ) )
|
|
|
|
|
|
for tag in tags:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( siblings_menu, tag )
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
if num_parents + num_children == 0:
|
|
|
|
parents_menu.setTitle( 'no parents' )
|
|
|
|
else:
|
|
|
|
parents_menu.setTitle( '{} parents, {} children'.format( HydrusData.ToHumanInt( num_parents ), HydrusData.ToHumanInt( num_children ) ) )
|
|
|
|
ClientGUIMenus.AppendSeparator( parents_menu )
|
|
|
|
for ( s_k_name, ( parents, children ) ) in group_and_sort_parents_to_service_keys( parents_to_service_keys, children_to_service_keys ):
|
|
|
|
ClientGUIMenus.AppendSeparator( parents_menu )
|
|
|
|
if s_k_name != ALL_SERVICES_LABEL:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( parents_menu, '--{}--'.format( s_k_name ) )
|
|
|
|
|
|
for parent in parents:
|
|
|
|
parent_label = 'parent: {}'.format( parent )
|
|
|
|
ClientGUIMenus.AppendMenuItem( parents_menu, parent_label, parent_label, HG.client_controller.pub, 'clipboard', 'text', parent )
|
|
|
|
|
|
for child in children:
|
|
|
|
child_label = 'child: {}'.format( child )
|
|
|
|
ClientGUIMenus.AppendMenuItem( parents_menu, child_label, child_label, HG.client_controller.pub, 'clipboard', 'text', child )
|
|
|
|
|
|
|
|
|
|
|
|
async_job = ClientGUIAsync.AsyncQtJob( menu, sp_work_callable, sp_publish_callable )
|
|
|
|
async_job.start()
|
|
|
|
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
|
|
|
|
if len( predicates ) > 0:
|
|
|
|
if self.can_spawn_new_windows or self._CanProvideCurrentPagePredicates():
|
|
|
|
search_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, search_menu, 'search' )
|
|
|
|
|
|
if self.can_spawn_new_windows:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'open a new search page for ' + selection_string, 'Open a new search page starting with the selected predicates.', self._NewSearchPages, [ predicates ] )
|
|
|
|
if or_predicate is not None:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'open a new OR search page for ' + selection_string, 'Open a new search page starting with the selected merged as an OR search predicate.', self._NewSearchPages, [ ( or_predicate, ) ] )
|
|
|
|
|
|
if len( predicates ) > 1:
|
|
|
|
for_each_predicates = [ ( predicate, ) for predicate in predicates ]
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'open new search pages for each in selection', 'Open one new search page for each selected predicate.', self._NewSearchPages, for_each_predicates )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( search_menu )
|
|
|
|
|
|
if self._CanProvideCurrentPagePredicates():
|
|
|
|
current_predicates = self._GetCurrentPagePredicates()
|
|
|
|
predicates = set( predicates )
|
|
inverse_predicates = set( inverse_predicates )
|
|
|
|
if len( predicates ) == 1:
|
|
|
|
( pred, ) = predicates
|
|
|
|
predicates_selection_string = pred.ToString( with_count = False )
|
|
|
|
else:
|
|
|
|
predicates_selection_string = 'selected'
|
|
|
|
|
|
some_selected_in_current = HydrusData.SetsIntersect( predicates, current_predicates )
|
|
|
|
if some_selected_in_current:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'remove {} from current search'.format( predicates_selection_string ), 'Remove the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_predicates' )
|
|
|
|
|
|
some_selected_not_in_current = len( predicates.intersection( current_predicates ) ) < len( predicates )
|
|
|
|
if some_selected_not_in_current:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'add {} to current search'.format( predicates_selection_string ), 'Add the selected predicates to the current search.', self._ProcessMenuPredicateEvent, 'add_predicates' )
|
|
|
|
|
|
if or_predicate is not None:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'add an OR of {} to current search'.format( predicates_selection_string ), 'Add the selected predicates as an OR predicate to the current search.', self._ProcessMenuPredicateEvent, 'add_or_predicate' )
|
|
|
|
|
|
some_selected_are_excluded_explicitly = HydrusData.SetsIntersect( inverse_predicates, current_predicates )
|
|
|
|
if some_selected_are_excluded_explicitly:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'permit {} for current search'.format( predicates_selection_string ), 'Stop disallowing the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_inverse_predicates' )
|
|
|
|
|
|
some_selected_are_not_excluded_explicitly = len( inverse_predicates.intersection( current_predicates ) ) < len( inverse_predicates )
|
|
|
|
if some_selected_are_not_excluded_explicitly:
|
|
|
|
ClientGUIMenus.AppendMenuItem( search_menu, 'exclude {} from current search'.format( predicates_selection_string ), 'Disallow the selected predicates for the current search.', self._ProcessMenuPredicateEvent, 'add_inverse_predicates' )
|
|
|
|
|
|
|
|
self._AddEditMenu( menu )
|
|
|
|
|
|
if len( selected_actual_tags ) > 0 and self._page_key is not None:
|
|
|
|
select_menu = QW.QMenu( menu )
|
|
|
|
tags_sorted_to_show_on_menu = HydrusTags.SortNumericTags( selected_actual_tags )
|
|
|
|
tags_sorted_to_show_on_menu_string = ', '.join( tags_sorted_to_show_on_menu )
|
|
|
|
while len( tags_sorted_to_show_on_menu_string ) > 64:
|
|
|
|
if len( tags_sorted_to_show_on_menu ) == 1:
|
|
|
|
tags_sorted_to_show_on_menu_string = '(many/long tags)'
|
|
|
|
else:
|
|
|
|
tags_sorted_to_show_on_menu.pop( -1 )
|
|
|
|
tags_sorted_to_show_on_menu_string = ', '.join( tags_sorted_to_show_on_menu + [ '\u2026' ] )
|
|
|
|
|
|
|
|
if len( selected_actual_tags ) == 1:
|
|
|
|
label = 'files with "{}"'.format( tags_sorted_to_show_on_menu_string )
|
|
|
|
else:
|
|
|
|
label = 'files with all of "{}"'.format( tags_sorted_to_show_on_menu_string )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( select_menu, label, 'Select the files with these tags.', HG.client_controller.pub, 'select_files_with_tags', self._page_key, 'AND', set( selected_actual_tags ) )
|
|
|
|
if len( selected_actual_tags ) > 1:
|
|
|
|
label = 'files with any of "{}"'.format( tags_sorted_to_show_on_menu_string )
|
|
|
|
ClientGUIMenus.AppendMenuItem( select_menu, label, 'Select the files with any of these tags.', HG.client_controller.pub, 'select_files_with_tags', self._page_key, 'OR', set( selected_actual_tags ) )
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, select_menu, 'select' )
|
|
|
|
|
|
|
|
if len( selected_actual_tags ) == 1:
|
|
|
|
( selected_tag, ) = selected_actual_tags
|
|
|
|
if self._tag_display_type in ( ClientTags.TAG_DISPLAY_SINGLE_MEDIA, ClientTags.TAG_DISPLAY_SELECTION_LIST ):
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
( namespace, subtag ) = HydrusTags.SplitTag( selected_tag )
|
|
|
|
hide_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( hide_menu, '"{}" tags from here'.format( ClientTags.RenderNamespaceForUser( namespace ) ), 'Hide this namespace from view in future.', self._ProcessMenuTagEvent, 'hide_namespace' )
|
|
ClientGUIMenus.AppendMenuItem( hide_menu, '"{}" from here'.format( selected_tag ), 'Hide this tag from view in future.', self._ProcessMenuTagEvent, 'hide' )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, hide_menu, 'hide' )
|
|
|
|
|
|
def set_favourite_tags( tag ):
|
|
|
|
favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )
|
|
|
|
if selected_tag in favourite_tags:
|
|
|
|
favourite_tags.remove( tag )
|
|
|
|
else:
|
|
|
|
favourite_tags.append( tag )
|
|
|
|
|
|
HG.client_controller.new_options.SetStringList( 'favourite_tags', favourite_tags )
|
|
|
|
HG.client_controller.pub( 'notify_new_favourite_tags' )
|
|
|
|
|
|
favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) )
|
|
|
|
if selected_tag in favourite_tags:
|
|
|
|
label = 'remove "{}" from favourites'.format( selected_tag )
|
|
description = 'Remove this tag from your favourites'
|
|
|
|
else:
|
|
|
|
label = 'add "{}" to favourites'.format( selected_tag )
|
|
description = 'Add this tag from your favourites'
|
|
|
|
|
|
favourites_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( favourites_menu, label, description, set_favourite_tags, selected_tag )
|
|
|
|
m = ClientGUIMenus.AppendMenu( menu, favourites_menu, 'favourites' )
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
|
|
def ForceTagRecalc( self ):
|
|
|
|
pass
|
|
|
|
|
|
class ListBoxTagsPredicates( ListBoxTags ):
|
|
|
|
def __init__( self, *args, tag_display_type = ClientTags.TAG_DISPLAY_ACTUAL, **kwargs ):
|
|
|
|
ListBoxTags.__init__( self, *args, tag_display_type = tag_display_type, **kwargs )
|
|
|
|
|
|
def _GenerateTermFromPredicate( self, predicate: ClientSearch.Predicate ) -> ClientGUIListBoxesData.ListBoxItemPredicate:
|
|
|
|
return ClientGUIListBoxesData.ListBoxItemPredicate( predicate )
|
|
|
|
|
|
def _GetMutuallyExclusivePredicates( self, predicate ):
|
|
|
|
all_predicates = self._GetPredicatesFromTerms( self._ordered_terms )
|
|
|
|
m_e_predicates = { existing_predicate for existing_predicate in all_predicates if existing_predicate.IsMutuallyExclusive( predicate ) }
|
|
|
|
return m_e_predicates
|
|
|
|
|
|
def _HasCounts( self ):
|
|
|
|
return True
|
|
|
|
|
|
def GetPredicates( self ) -> typing.Set[ ClientSearch.Predicate ]:
|
|
|
|
return set( self._GetPredicatesFromTerms( self._ordered_terms ) )
|
|
|
|
|
|
def SetPredicates( self, predicates ):
|
|
|
|
selected_terms = set( self._selected_terms )
|
|
|
|
self._Clear()
|
|
|
|
terms = [ self._GenerateTermFromPredicate( predicate ) for predicate in predicates ]
|
|
|
|
self._AppendTerms( terms )
|
|
|
|
for term in selected_terms:
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
self._selected_terms.add( term )
|
|
|
|
|
|
|
|
self._HitFirstSelectedItem()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
class ListBoxTagsColourOptions( ListBoxTags ):
|
|
|
|
PROTECTED_TERMS = ( None, '' )
|
|
can_spawn_new_windows = False
|
|
|
|
def __init__( self, parent, initial_namespace_colours ):
|
|
|
|
ListBoxTags.__init__( self, parent )
|
|
|
|
terms = []
|
|
|
|
for ( namespace, colour ) in initial_namespace_colours.items():
|
|
|
|
colour = tuple( colour ) # tuple to convert from list, for oooold users who have list colours
|
|
|
|
term = self._GenerateTermFromNamespaceAndColour( namespace, colour )
|
|
|
|
terms.append( term )
|
|
|
|
|
|
self._AppendTerms( terms )
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def _Activate( self, shift_down ):
|
|
|
|
deletable_terms = [ term for term in self._selected_terms if term.GetNamespace() not in self.PROTECTED_TERMS ]
|
|
|
|
if len( deletable_terms ) > 0:
|
|
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, 'Delete all selected colours?' )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._RemoveTerms( deletable_terms )
|
|
|
|
self._DataHasChanged()
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _DeleteActivate( self ):
|
|
|
|
shift_down = False
|
|
|
|
self._Activate( shift_down )
|
|
|
|
|
|
def _GenerateTermFromNamespaceAndColour( self, namespace, colour ) -> ClientGUIListBoxesData.ListBoxItemNamespaceColour:
|
|
|
|
return ClientGUIListBoxesData.ListBoxItemNamespaceColour( namespace, colour )
|
|
|
|
|
|
def _GetNamespaceColours( self ):
|
|
|
|
return dict( ( term.GetNamespaceAndColour() for term in self._ordered_terms ) )
|
|
|
|
|
|
def SetNamespaceColour( self, namespace, colour: QG.QColor ):
|
|
|
|
colour_tuple = ( colour.red(), colour.green(), colour.blue() )
|
|
|
|
for term in self._ordered_terms:
|
|
|
|
if term.GetNamespace() == namespace:
|
|
|
|
self._RemoveTerms( ( term, ) )
|
|
|
|
break
|
|
|
|
|
|
|
|
term = self._GenerateTermFromNamespaceAndColour( namespace, colour_tuple )
|
|
|
|
self._AppendTerms( ( term, ) )
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def GetNamespaceColours( self ):
|
|
|
|
return self._GetNamespaceColours()
|
|
|
|
|
|
def GetSelectedNamespaceColours( self ):
|
|
|
|
namespace_colours = dict( ( term.GetNamespaceAndColour() for term in self._selected_terms ) )
|
|
|
|
return namespace_colours
|
|
|
|
|
|
class ListBoxTagsFilter( ListBoxTags ):
|
|
|
|
tagsRemoved = QC.Signal( list )
|
|
|
|
def __init__( self, parent ):
|
|
|
|
ListBoxTags.__init__( self, parent )
|
|
|
|
|
|
def _Activate( self, shift_down ) -> bool:
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
tag_slices = [ term.GetTagSlice() for term in self._selected_terms ]
|
|
|
|
self._RemoveSelectedTerms()
|
|
|
|
self.tagsRemoved.emit( tag_slices )
|
|
|
|
self._DataHasChanged()
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _GenerateTermFromTagSlice( self, tag_slice ) -> ClientGUIListBoxesData.ListBoxItemTagSlice:
|
|
|
|
return ClientGUIListBoxesData.ListBoxItemTagSlice( tag_slice )
|
|
|
|
|
|
def AddTagSlices( self, tag_slices ):
|
|
|
|
terms = [ self._GenerateTermFromTagSlice( tag_slice ) for tag_slice in tag_slices ]
|
|
|
|
self._AppendTerms( terms )
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def EnterTagSlices( self, tag_slices ):
|
|
|
|
for tag_slice in tag_slices:
|
|
|
|
term = self._GenerateTermFromTagSlice( tag_slice )
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
self._RemoveTerms( ( term, ) )
|
|
|
|
else:
|
|
|
|
self._AppendTerms( ( term, ) )
|
|
|
|
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def GetSelectedTagSlices( self ):
|
|
|
|
return [ term.GetTagSlice() for term in self._selected_terms ]
|
|
|
|
|
|
def GetTagSlices( self ):
|
|
|
|
return [ term.GetTagSlice() for term in self._ordered_terms ]
|
|
|
|
|
|
def RemoveTagSlices( self, tag_slices ):
|
|
|
|
removee_terms = [ self._GenerateTermFromTagSlice( tag_slice ) for tag_slice in tag_slices ]
|
|
|
|
self._RemoveTerms( removee_terms )
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def SetTagSlices( self, tag_slices ):
|
|
|
|
self._Clear()
|
|
|
|
self.AddTagSlices( tag_slices )
|
|
|
|
|
|
class ListBoxTagsDisplayCapable( ListBoxTags ):
|
|
|
|
def __init__( self, parent, service_key = None, tag_display_type = ClientTags.TAG_DISPLAY_ACTUAL, **kwargs ):
|
|
|
|
if service_key is None:
|
|
|
|
service_key = CC.COMBINED_TAG_SERVICE_KEY
|
|
|
|
|
|
self._service_key = service_key
|
|
|
|
has_async_text_info = tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
ListBoxTags.__init__( self, parent, has_async_text_info = has_async_text_info, tag_display_type = tag_display_type, **kwargs )
|
|
|
|
|
|
def _ApplyAsyncInfoToTerm( self, term, info ) -> typing.Tuple[ bool, bool ]:
|
|
|
|
# this guy comes with the lock
|
|
|
|
if info is None:
|
|
|
|
return ( False, False )
|
|
|
|
|
|
sort_info_changed = False
|
|
num_rows_changed = False
|
|
|
|
( ideal, parents ) = info
|
|
|
|
if ideal is not None and ideal != term.GetTag():
|
|
|
|
term.SetIdealTag( ideal )
|
|
|
|
sort_info_changed = True
|
|
|
|
|
|
if parents is not None:
|
|
|
|
term.SetParents( parents )
|
|
|
|
num_rows_changed = True
|
|
|
|
|
|
return ( sort_info_changed, num_rows_changed )
|
|
|
|
|
|
def _InitialiseAsyncTextInfoUpdaterWorkCallable( self ):
|
|
|
|
if not self._has_async_text_info:
|
|
|
|
return ListBoxTags._InitialiseAsyncTextInfoUpdaterWorkCallable( self )
|
|
|
|
|
|
self._async_text_info_shared_data[ 'service_key' ] = self._service_key
|
|
|
|
async_text_info_shared_data = self._async_text_info_shared_data
|
|
async_lock = self._async_text_info_lock
|
|
currently_fetching = self._currently_fetching_async_text_info_terms
|
|
pending = self._pending_async_text_info_terms
|
|
|
|
def work_callable():
|
|
|
|
with async_lock:
|
|
|
|
to_lookup = list( pending )
|
|
|
|
pending.clear()
|
|
|
|
currently_fetching.update( to_lookup )
|
|
|
|
service_key = async_text_info_shared_data[ 'service_key' ]
|
|
|
|
|
|
terms_to_info = { term : None for term in to_lookup }
|
|
|
|
for batch_to_lookup in HydrusData.SplitListIntoChunks( to_lookup, 500 ):
|
|
|
|
tags_to_terms = { term.GetTag() : term for term in batch_to_lookup }
|
|
|
|
tags_to_lookup = set( tags_to_terms.keys() )
|
|
|
|
db_tags_to_ideals_and_parents = HG.client_controller.Read( 'tag_display_decorators', service_key, tags_to_lookup )
|
|
|
|
terms_to_info.update( { tags_to_terms[ tag ] : info for ( tag, info ) in db_tags_to_ideals_and_parents.items() } )
|
|
|
|
|
|
return terms_to_info
|
|
|
|
|
|
return work_callable
|
|
|
|
|
|
def GetSelectedTags( self ):
|
|
|
|
return set( self._GetTagsFromTerms( self._selected_terms ) )
|
|
|
|
|
|
def SetTagServiceKey( self, service_key ):
|
|
|
|
self._service_key = service_key
|
|
|
|
with self._async_text_info_lock:
|
|
|
|
self._async_text_info_shared_data[ 'service_key' ] = self._service_key
|
|
|
|
self._pending_async_text_info_terms.clear()
|
|
self._currently_fetching_async_text_info_terms.clear()
|
|
self._terms_to_async_text_info = {}
|
|
|
|
|
|
|
|
class ListBoxTagsStrings( ListBoxTagsDisplayCapable ):
|
|
|
|
def __init__( self, parent, service_key = None, sort_tags = True, **kwargs ):
|
|
|
|
self._sort_tags = sort_tags
|
|
|
|
ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, **kwargs )
|
|
|
|
|
|
def _GenerateTermFromTag( self, tag: str ) -> ClientGUIListBoxesData.ListBoxItemTextTag:
|
|
|
|
return ClientGUIListBoxesData.ListBoxItemTextTag( tag )
|
|
|
|
|
|
def GetTags( self ):
|
|
|
|
return set( self._GetTagsFromTerms( self._ordered_terms ) )
|
|
|
|
|
|
def SetTags( self, tags ):
|
|
|
|
previously_selected_terms = set( self._selected_terms )
|
|
|
|
self._Clear()
|
|
|
|
terms_to_add = [ self._GenerateTermFromTag( tag ) for tag in tags ]
|
|
|
|
self._AppendTerms( terms_to_add )
|
|
|
|
for term in previously_selected_terms:
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
self._selected_terms.add( term )
|
|
|
|
|
|
|
|
self._HitFirstSelectedItem()
|
|
|
|
if self._sort_tags:
|
|
|
|
self._Sort()
|
|
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
class ListBoxTagsStringsAddRemove( ListBoxTagsStrings ):
|
|
|
|
tagsAdded = QC.Signal()
|
|
tagsRemoved = QC.Signal()
|
|
|
|
def _Activate( self, shift_down ) -> bool:
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
tags = self._GetTagsFromTerms( self._selected_terms )
|
|
|
|
self._RemoveSelectedTerms()
|
|
|
|
self._DataHasChanged()
|
|
|
|
self.tagsRemoved.emit()
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _RemoveTags( self, tags ):
|
|
|
|
terms = [ self._GenerateTermFromTag( tag ) for tag in tags ]
|
|
|
|
self._RemoveTerms( terms )
|
|
|
|
self._DataHasChanged()
|
|
|
|
self.tagsRemoved.emit()
|
|
|
|
|
|
def AddTags( self, tags ):
|
|
|
|
terms = [ self._GenerateTermFromTag( tag ) for tag in tags ]
|
|
|
|
self._AppendTerms( terms )
|
|
|
|
if self._sort_tags:
|
|
|
|
self._Sort()
|
|
|
|
|
|
self._DataHasChanged()
|
|
|
|
self.tagsAdded.emit()
|
|
|
|
|
|
def Clear( self ):
|
|
|
|
self._Clear()
|
|
|
|
self._DataHasChanged()
|
|
|
|
# doesn't do a removed tags call, this is a different lad
|
|
|
|
|
|
def EnterTags( self, tags ):
|
|
|
|
tags_removed = False
|
|
tags_added = False
|
|
|
|
for tag in tags:
|
|
|
|
term = self._GenerateTermFromTag( tag )
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
self._RemoveTerms( ( term, ) )
|
|
|
|
tags_removed = True
|
|
|
|
else:
|
|
|
|
self._AppendTerms( ( term, ) )
|
|
|
|
tags_added = True
|
|
|
|
|
|
|
|
if self._sort_tags:
|
|
|
|
self._Sort()
|
|
|
|
|
|
self._DataHasChanged()
|
|
|
|
if tags_added:
|
|
|
|
self.tagsAdded.emit()
|
|
|
|
|
|
if tags_removed:
|
|
|
|
self.tagsRemoved.emit()
|
|
|
|
|
|
|
|
def keyPressEvent( self, event ):
|
|
|
|
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
|
|
|
|
if key in ClientGUIShortcuts.DELETE_KEYS_QT:
|
|
|
|
shift_down = modifier == ClientGUIShortcuts.SHORTCUT_MODIFIER_SHIFT
|
|
|
|
action_occurred = self._Activate( shift_down )
|
|
|
|
else:
|
|
|
|
ListBoxTagsStrings.keyPressEvent( self, event )
|
|
|
|
|
|
|
|
def RemoveTags( self, tags ):
|
|
|
|
self._RemoveTags( tags )
|
|
|
|
|
|
class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|
|
|
def __init__( self, parent, tag_display_type, service_key = None, include_counts = True ):
|
|
|
|
if service_key is None:
|
|
|
|
service_key = CC.COMBINED_TAG_SERVICE_KEY
|
|
|
|
|
|
ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, tag_display_type = tag_display_type, height_num_chars = 24 )
|
|
|
|
self._tag_sort = HG.client_controller.new_options.GetDefaultTagSort()
|
|
|
|
self._last_media_results = set()
|
|
|
|
self._include_counts = include_counts
|
|
|
|
self._current_tags_to_count = collections.Counter()
|
|
self._deleted_tags_to_count = collections.Counter()
|
|
self._pending_tags_to_count = collections.Counter()
|
|
self._petitioned_tags_to_count = collections.Counter()
|
|
|
|
self._show_current = True
|
|
self._show_deleted = False
|
|
self._show_pending = True
|
|
self._show_petitioned = True
|
|
|
|
|
|
def _GenerateTermFromTag( self, tag: str ) -> ClientGUIListBoxesData.ListBoxItemTextTag:
|
|
|
|
current_count = self._current_tags_to_count[ tag ] if self._show_current and tag in self._current_tags_to_count else 0
|
|
deleted_count = self._deleted_tags_to_count[ tag ] if self._show_deleted and tag in self._deleted_tags_to_count else 0
|
|
pending_count = self._pending_tags_to_count[ tag ] if self._show_pending and tag in self._pending_tags_to_count else 0
|
|
petitioned_count = self._petitioned_tags_to_count[ tag ] if self._show_petitioned and tag in self._petitioned_tags_to_count else 0
|
|
|
|
return ClientGUIListBoxesData.ListBoxItemTextTagWithCounts(
|
|
tag,
|
|
current_count,
|
|
deleted_count,
|
|
pending_count,
|
|
petitioned_count,
|
|
self._include_counts
|
|
)
|
|
|
|
|
|
def _HasCounts( self ):
|
|
|
|
return self._include_counts
|
|
|
|
|
|
def _UpdateTerms( self, limit_to_these_tags = None ):
|
|
|
|
previous_selected_terms = set( self._selected_terms )
|
|
|
|
if limit_to_these_tags is None:
|
|
|
|
self._Clear()
|
|
|
|
nonzero_tags = set()
|
|
|
|
if self._show_current: nonzero_tags.update( ( tag for ( tag, count ) in self._current_tags_to_count.items() if count > 0 ) )
|
|
if self._show_deleted: nonzero_tags.update( ( tag for ( tag, count ) in self._deleted_tags_to_count.items() if count > 0 ) )
|
|
if self._show_pending: nonzero_tags.update( ( tag for ( tag, count ) in self._pending_tags_to_count.items() if count > 0 ) )
|
|
if self._show_petitioned: nonzero_tags.update( ( tag for ( tag, count ) in self._petitioned_tags_to_count.items() if count > 0 ) )
|
|
|
|
else:
|
|
|
|
if not isinstance( limit_to_these_tags, set ):
|
|
|
|
limit_to_these_tags = set( limit_to_these_tags )
|
|
|
|
|
|
clear_terms = [ self._GenerateTermFromTag( tag ) for tag in limit_to_these_tags ]
|
|
|
|
self._RemoveTerms( clear_terms )
|
|
|
|
nonzero_tags = set()
|
|
|
|
if self._show_current: nonzero_tags.update( ( tag for ( tag, count ) in self._current_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
|
|
if self._show_deleted: nonzero_tags.update( ( tag for ( tag, count ) in self._deleted_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
|
|
if self._show_pending: nonzero_tags.update( ( tag for ( tag, count ) in self._pending_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
|
|
if self._show_petitioned: nonzero_tags.update( ( tag for ( tag, count ) in self._petitioned_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
|
|
|
|
|
|
nonzero_terms = [ self._GenerateTermFromTag( tag ) for tag in nonzero_tags ]
|
|
|
|
self._AppendTerms( nonzero_terms )
|
|
|
|
for term in previous_selected_terms:
|
|
|
|
if term in self._terms_to_logical_indices:
|
|
|
|
self._selected_terms.add( term )
|
|
|
|
|
|
|
|
self._Sort()
|
|
|
|
|
|
def _Sort( self ):
|
|
|
|
# I do this weird terms to count instead of tags to count because of tag vs ideal tag gubbins later on in sort
|
|
|
|
terms_to_count = collections.Counter()
|
|
|
|
jobs = [
|
|
( self._show_current, self._current_tags_to_count ),
|
|
( self._show_deleted, self._deleted_tags_to_count ),
|
|
( self._show_pending, self._pending_tags_to_count ),
|
|
( self._show_petitioned, self._petitioned_tags_to_count )
|
|
]
|
|
|
|
counts_to_include = [ c for ( show, c ) in jobs if show ]
|
|
|
|
for term in self._ordered_terms:
|
|
|
|
tag = term.GetTag()
|
|
|
|
count = sum( ( c[ tag ] for c in counts_to_include if tag in c ) )
|
|
|
|
terms_to_count[ term ] = count
|
|
|
|
|
|
item_to_tag_key_wrapper = lambda term: term.GetTag()
|
|
item_to_sibling_key_wrapper = item_to_tag_key_wrapper
|
|
|
|
if self._sibling_decoration_allowed:
|
|
|
|
item_to_sibling_key_wrapper = lambda term: term.GetBestTag()
|
|
|
|
|
|
ClientTagSorting.SortTags( self._tag_sort, self._ordered_terms, tag_items_to_count = terms_to_count, item_to_tag_key_wrapper = item_to_tag_key_wrapper, item_to_sibling_key_wrapper = item_to_sibling_key_wrapper )
|
|
|
|
self._RegenTermsToIndices()
|
|
|
|
|
|
def IncrementTagsByMedia( self, media ):
|
|
|
|
flat_media = ClientMedia.FlattenMedia( media )
|
|
|
|
media_results = [ m.GetMediaResult() for m in flat_media ]
|
|
|
|
self.IncrementTagsByMediaResults( media_results )
|
|
|
|
|
|
def IncrementTagsByMediaResults( self, media_results ):
|
|
|
|
if not isinstance( media_results, set ):
|
|
|
|
media_results = set( media_results )
|
|
|
|
|
|
media_results = media_results.difference( self._last_media_results )
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediaResultsTagCount( media_results, self._service_key, self._tag_display_type )
|
|
|
|
tags_changed = set()
|
|
|
|
if self._show_current: tags_changed.update( current_tags_to_count.keys() )
|
|
if self._show_deleted: tags_changed.update( deleted_tags_to_count.keys() )
|
|
if self._show_pending: tags_changed.update( pending_tags_to_count.keys() )
|
|
if self._show_petitioned: tags_changed.update( petitioned_tags_to_count.keys() )
|
|
|
|
self._current_tags_to_count.update( current_tags_to_count )
|
|
self._deleted_tags_to_count.update( deleted_tags_to_count )
|
|
self._pending_tags_to_count.update( pending_tags_to_count )
|
|
self._petitioned_tags_to_count.update( petitioned_tags_to_count )
|
|
|
|
if len( tags_changed ) > 0:
|
|
|
|
self._UpdateTerms( tags_changed )
|
|
|
|
|
|
self._last_media_results.update( media_results )
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def SetTagsByMedia( self, media ):
|
|
|
|
flat_media = ClientMedia.FlattenMedia( media )
|
|
|
|
media_results = [ m.GetMediaResult() for m in flat_media ]
|
|
|
|
self.SetTagsByMediaResults( media_results )
|
|
|
|
|
|
def SetTagsByMediaResults( self, media_results ):
|
|
|
|
if not isinstance( media_results, set ):
|
|
|
|
media_results = set( media_results )
|
|
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediaResultsTagCount( media_results, self._service_key, self._tag_display_type )
|
|
|
|
self._current_tags_to_count = current_tags_to_count
|
|
self._deleted_tags_to_count = deleted_tags_to_count
|
|
self._pending_tags_to_count = pending_tags_to_count
|
|
self._petitioned_tags_to_count = petitioned_tags_to_count
|
|
|
|
self._UpdateTerms()
|
|
|
|
self._last_media_results = media_results
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def SetTagsByMediaFromMediaPanel( self, media, tags_changed ):
|
|
|
|
flat_media = ClientMedia.FlattenMedia( media )
|
|
|
|
media_results = [ m.GetMediaResult() for m in flat_media ]
|
|
|
|
self.SetTagsByMediaResultsFromMediaPanel( media_results, tags_changed )
|
|
|
|
|
|
def SetTagsByMediaResultsFromMediaPanel( self, media_results, tags_changed ):
|
|
|
|
if not isinstance( media_results, set ):
|
|
|
|
media_results = set( media_results )
|
|
|
|
|
|
# this uses the last-set media and count cache to generate new numbers and is faster than re-counting from scratch when the tags have not changed
|
|
|
|
selection_shrank_a_lot = len( media_results ) < len( self._last_media_results ) // 10 # if we are dropping to a much smaller selection (e.g. 5000 -> 1), we should just recalculate from scratch
|
|
|
|
if tags_changed or selection_shrank_a_lot:
|
|
|
|
self.SetTagsByMediaResults( media_results )
|
|
|
|
return
|
|
|
|
|
|
removees = self._last_media_results.difference( media_results )
|
|
|
|
if len( removees ) == 0:
|
|
|
|
self.IncrementTagsByMediaResults( media_results )
|
|
|
|
return
|
|
|
|
|
|
adds = media_results.difference( self._last_media_results )
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediaResultsTagCount( removees, self._service_key, self._tag_display_type )
|
|
|
|
self._current_tags_to_count.subtract( current_tags_to_count )
|
|
self._deleted_tags_to_count.subtract( deleted_tags_to_count )
|
|
self._pending_tags_to_count.subtract( pending_tags_to_count )
|
|
self._petitioned_tags_to_count.subtract( petitioned_tags_to_count )
|
|
|
|
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientMedia.GetMediaResultsTagCount( adds, self._service_key, self._tag_display_type )
|
|
|
|
self._current_tags_to_count.update( current_tags_to_count )
|
|
self._deleted_tags_to_count.update( deleted_tags_to_count )
|
|
self._pending_tags_to_count.update( pending_tags_to_count )
|
|
self._petitioned_tags_to_count.update( petitioned_tags_to_count )
|
|
|
|
for counter in ( self._current_tags_to_count, self._deleted_tags_to_count, self._pending_tags_to_count, self._petitioned_tags_to_count ):
|
|
|
|
tags = list( counter.keys() )
|
|
|
|
for tag in tags:
|
|
|
|
if counter[ tag ] == 0:
|
|
|
|
del counter[ tag ]
|
|
|
|
|
|
|
|
|
|
self._UpdateTerms()
|
|
|
|
self._last_media_results = media_results
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def SetTagServiceKey( self, service_key ):
|
|
|
|
ListBoxTagsDisplayCapable.SetTagServiceKey( self, service_key )
|
|
|
|
self.SetTagsByMediaResults( self._last_media_results )
|
|
|
|
|
|
def SetSort( self, tag_sort: ClientTagSorting.TagSort ):
|
|
|
|
self._tag_sort = tag_sort
|
|
|
|
self._Sort()
|
|
|
|
self._DataHasChanged()
|
|
|
|
|
|
def SetShow( self, show_type, value ):
|
|
|
|
if show_type == 'current': self._show_current = value
|
|
elif show_type == 'deleted': self._show_deleted = value
|
|
elif show_type == 'pending': self._show_pending = value
|
|
elif show_type == 'petitioned': self._show_petitioned = value
|
|
|
|
self._UpdateTerms()
|
|
|
|
|
|
def ForceTagRecalc( self ):
|
|
|
|
self.SetTagsByMediaResults( self._last_media_results )
|
|
|
|
|
|
class StaticBoxSorterForListBoxTags( ClientGUICommon.StaticBox ):
|
|
|
|
def __init__( self, parent, title, show_siblings_sort = False ):
|
|
|
|
ClientGUICommon.StaticBox.__init__( self, parent, title )
|
|
|
|
self._original_title = title
|
|
|
|
self._tags_box = None
|
|
|
|
# make this its own panel
|
|
self._tag_sort = ClientGUITagSorting.TagSortControl( self, HG.client_controller.new_options.GetDefaultTagSort(), show_siblings = show_siblings_sort )
|
|
|
|
self._tag_sort.valueChanged.connect( self.EventSort )
|
|
|
|
self.Add( self._tag_sort, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
def SetTagServiceKey( self, service_key ):
|
|
|
|
if self._tags_box is None:
|
|
|
|
return
|
|
|
|
|
|
self._tags_box.SetTagServiceKey( service_key )
|
|
|
|
title = self._original_title
|
|
|
|
if service_key != CC.COMBINED_TAG_SERVICE_KEY:
|
|
|
|
title = '{} for {}'.format( title, HG.client_controller.services_manager.GetName( service_key ) )
|
|
|
|
|
|
self.SetTitle( title )
|
|
|
|
|
|
def EventSort( self ):
|
|
|
|
if self._tags_box is None:
|
|
|
|
return
|
|
|
|
|
|
sort = self._tag_sort.GetValue()
|
|
|
|
self._tags_box.SetSort( sort )
|
|
|
|
|
|
def SetTagsBox( self, tags_box: ListBoxTagsMedia ):
|
|
|
|
self._tags_box = tags_box
|
|
|
|
self.Add( self._tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
def SetTagsByMedia( self, media ):
|
|
|
|
if self._tags_box is None:
|
|
|
|
return
|
|
|
|
|
|
self._tags_box.SetTagsByMedia( media )
|
|
|
|
|
|
class ListBoxTagsMediaHoverFrame( ListBoxTagsMedia ):
|
|
|
|
def __init__( self, parent, canvas_key ):
|
|
|
|
ListBoxTagsMedia.__init__( self, parent, ClientTags.TAG_DISPLAY_SINGLE_MEDIA, include_counts = False )
|
|
|
|
self._canvas_key = canvas_key
|
|
|
|
|
|
def _Activate( self, shift_down ) -> bool:
|
|
|
|
HG.client_controller.pub( 'canvas_manage_tags', self._canvas_key )
|
|
|
|
return True
|
|
|
|
|
|
class ListBoxTagsMediaTagsDialog( ListBoxTagsMedia ):
|
|
|
|
def __init__( self, parent, enter_func, delete_func ):
|
|
|
|
ListBoxTagsMedia.__init__( self, parent, ClientTags.TAG_DISPLAY_STORAGE, include_counts = True )
|
|
|
|
self._enter_func = enter_func
|
|
self._delete_func = delete_func
|
|
|
|
|
|
def _Activate( self, shift_down ) -> bool:
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
tags = set( self._GetTagsFromTerms( self._selected_terms ) )
|
|
|
|
self._enter_func( tags )
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _DeleteActivate( self ):
|
|
|
|
if len( self._selected_terms ) > 0:
|
|
|
|
tags = set( self._GetTagsFromTerms( self._selected_terms ) )
|
|
|
|
self._delete_func( tags )
|
|
|
|
|
|
|