hydrus/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py

960 lines
30 KiB
Python

import typing
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
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.client import ClientConstants as CC
from hydrus.client import ClientSearch
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 ClientGUITags
from hydrus.client.gui import QtPorting as QP
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
# wew lad
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
class CheckBoxDelegate( QW.QStyledItemDelegate ):
def __init__( self, parent = None ):
super( CheckBoxDelegate, self ).__init__( parent )
def createEditor( self, parent, op, idx ):
self.editor = QW.QCheckBox( parent )
class CollectComboCtrl( QW.QComboBox ):
itemChanged = QC.Signal()
def __init__( self, parent, media_collect ):
QW.QComboBox.__init__( self, parent )
self.view().pressed.connect( self._HandleItemPressed )
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
self.setItemDelegate( CheckBoxDelegate() )
self.setModel( QG.QStandardItemModel( self ) )
self._ReinitialiseChoices()
# Trick to display custom text
self._cached_text = ''
if media_collect.DoesACollect():
QP.CallAfter( self.SetCollectByValue, media_collect )
def _HandleItemPressed( self, index ):
item = self.model().itemFromIndex( index )
if item.checkState() == QC.Qt.Checked:
item.setCheckState( QC.Qt.Unchecked )
else:
item.setCheckState( QC.Qt.Checked )
self.SetValue( self._cached_text )
self.itemChanged.emit()
def _ReinitialiseChoices( self ):
text_and_data_tuples = set()
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
namespaces = media_sort.GetNamespaces()
try:
text_and_data_tuples.update( namespaces )
except:
HydrusData.DebugPrint( 'Bad namespaces: {}'.format( namespaces ) )
HydrusData.ShowText( 'Hey, your namespace-based sorts are likely damaged. Details have been written to the log, please let hydev know!' )
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
for ratings_service in ratings_services:
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
current_text_and_data_tuples = []
for i in range( self.count() ):
item = self.model().item( i, 0 )
t = item.text()
d = self.itemData( i, QC.Qt.UserRole )
current_text_and_data_tuples.append( ( t, d ) )
made_changes = False
if current_text_and_data_tuples != text_and_data_tuples:
if self.count() > 0:
# PRO TIP 4 U: if you say self.clear() here, the program has a ~15% chance to crash instantly if you have previously done a clear/add cycle!
# this affects PyQt and PySide, 5 and 6, running from source, so must be something in Qt core. some argument between the model and widget
self.model().clear()
for ( text, data ) in text_and_data_tuples:
self.addItem( text, userData = data )
item = self.model().item( self.count() - 1, 0 )
item.setCheckState( QC.Qt.Unchecked )
made_changes = True
return made_changes
def GetCheckedIndices( self ):
indices = []
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked:
indices.append( idx )
return indices
def GetCheckedStrings( self ):
strings = [ ]
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked:
strings.append( item.text() )
return strings
def GetValues( self ):
namespaces = []
rating_service_keys = []
for index in self.GetCheckedIndices():
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
if collect_type == 'namespace':
namespaces.append( collect_data )
elif collect_type == 'rating':
rating_service_keys.append( collect_data )
collect_strings = self.GetCheckedStrings()
if len( collect_strings ) > 0:
description = 'collect by ' + '-'.join( collect_strings )
else:
description = 'no collections'
return ( namespaces, rating_service_keys, description )
def hidePopup(self):
if not self.view().underMouse():
QW.QComboBox.hidePopup( self )
def paintEvent( self, e ):
painter = QW.QStylePainter( self )
painter.setPen( self.palette().color( QG.QPalette.Text ) )
opt = QW.QStyleOptionComboBox()
self.initStyleOption( opt )
opt.currentText = self._cached_text
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
def ReinitialiseChoices( self ):
return self._ReinitialiseChoices()
def SetValue( self, text ):
self._cached_text = text
self.setCurrentText( text )
def SetCollectByValue( self, media_collect ):
try:
indices_to_check = []
for index in range( self.count() ):
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
if p1 or p2:
indices_to_check.append( index )
self.SetCheckedIndices( indices_to_check )
self.itemChanged.emit()
except Exception as e:
HydrusData.ShowText( 'Failed to set a collect-by value!' )
HydrusData.ShowException( e )
def SetCheckedIndices( self, indices_to_check ):
for idx in range( self.count() ):
item = self.model().item( idx )
if idx in indices_to_check:
item.setCheckState( QC.Qt.Checked )
else:
item.setCheckState( QC.Qt.Unchecked )
class MediaCollectControl( QW.QWidget ):
def __init__( self, parent, management_controller = None, silent = False ):
QW.QWidget.__init__( self, parent )
# this is trash, rewrite it to deal with the media_collect object, not the management controller
self._management_controller = management_controller
if self._management_controller is not None and self._management_controller.HasVariable( 'media_collect' ):
self._media_collect = self._management_controller.GetVariable( 'media_collect' )
else:
self._media_collect = HG.client_controller.new_options.GetDefaultCollect()
self._silent = silent
self._collect_comboctrl = CollectComboCtrl( self, self._media_collect )
choice_tuples = [
( 'collect unmatched', True ),
( 'leave unmatched', False )
]
self._collect_unmatched = ClientGUIMenuButton.MenuChoiceButton( self, choice_tuples )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._collect_unmatched, 19 )
self._collect_unmatched.setMinimumWidth( width )
self._tag_context_button = ClientGUISearch.TagContextButton( self, self._media_collect.tag_context, use_short_label = True )
#
self._collect_unmatched.SetValue( self._media_collect.collect_unmatched )
#
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._collect_comboctrl, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._collect_unmatched, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._tag_context_button, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
#
self._UpdateButtonsVisible()
self._UpdateLabel()
self._collect_unmatched.valueChanged.connect( self.CollectValuesChanged )
self._collect_comboctrl.itemChanged.connect( self.CollectValuesChanged )
self._tag_context_button.valueChanged.connect( self.CollectValuesChanged )
self._collect_comboctrl.installEventFilter( self )
HG.client_controller.sub( self, 'NotifyAdvancedMode', 'notify_advanced_mode' )
HG.client_controller.sub( self, 'SetCollectFromPage', 'set_page_collect' )
def _BroadcastCollect( self ):
if not self._silent and self._management_controller is not None:
self._management_controller.SetVariable( 'media_collect', self._media_collect )
page_key = self._management_controller.GetVariable( 'page_key' )
HG.client_controller.pub( 'collect_media', page_key, self._media_collect )
HG.client_controller.pub( 'a_collect_happened', page_key )
def _UpdateButtonsVisible( self ):
self._tag_context_button.setVisible( HG.client_controller.new_options.GetBoolean( 'advanced_mode' ) )
def _UpdateLabel( self ):
( namespaces, rating_service_keys, description ) = self._collect_comboctrl.GetValues()
self._collect_comboctrl.SetValue( description )
def CollectValuesChanged( self ):
( namespaces, rating_service_keys, description ) = self._collect_comboctrl.GetValues()
self._UpdateLabel()
collect_unmatched = self._collect_unmatched.GetValue()
tag_context = self._tag_context_button.GetValue()
self._media_collect = ClientMedia.MediaCollect( namespaces = namespaces, rating_service_keys = rating_service_keys, collect_unmatched = collect_unmatched, tag_context = tag_context )
self._BroadcastCollect()
def eventFilter( self, watched, event ):
if watched == self._collect_comboctrl:
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.MiddleButton:
self.SetCollect( ClientMedia.MediaCollect( collect_unmatched = self._media_collect.collect_unmatched ) )
return True
return False
def GetValue( self ):
return self._media_collect
def ListenForNewOptions( self ):
HG.client_controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
def NotifyAdvancedMode( self ):
self._UpdateButtonsVisible()
def NotifyNewOptions( self ):
media_collect = self._media_collect.Duplicate()
made_changes = self._collect_comboctrl.ReinitialiseChoices()
if made_changes:
self.SetCollect( media_collect, do_broadcast = False )
def SetCollect( self, media_collect: ClientMedia.MediaCollect, do_broadcast = True ):
self._media_collect = media_collect
self._collect_comboctrl.blockSignals( True )
self._collect_unmatched.blockSignals( True )
self._collect_comboctrl.SetCollectByValue( self._media_collect )
self._collect_unmatched.SetValue( self._media_collect.collect_unmatched )
self._UpdateLabel()
self._collect_comboctrl.blockSignals( False )
self._collect_unmatched.blockSignals( False )
if do_broadcast:
self._BroadcastCollect()
def SetCollectFromPage( self, page_key, media_collect ):
if page_key == self._management_controller.GetVariable( 'page_key' ):
self.SetCollect( media_collect )
self._BroadcastCollect()
class MediaSortControl( QW.QWidget ):
sortChanged = QC.Signal( ClientMedia.MediaSort )
def __init__( self, parent, management_controller = None ):
QW.QWidget.__init__( self, parent )
self._management_controller = management_controller
self._sort_type = ( 'system', CC.SORT_FILES_BY_FILESIZE )
self._sort_type_button = ClientGUICommon.BetterButton( self, 'sort', self._SortTypeButtonClick )
self._sort_tag_display_type_button = ClientGUIMenuButton.MenuChoiceButton( self, [] )
self._sort_order_choice = ClientGUIMenuButton.MenuChoiceButton( self, [] )
tag_context = ClientSearch.TagContext( service_key = CC.COMBINED_TAG_SERVICE_KEY )
self._tag_context_button = ClientGUISearch.TagContextButton( self, tag_context, use_short_label = True )
type_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._sort_type_button, 14 )
self._sort_type_button.setMinimumWidth( type_width )
tdt_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._sort_tag_display_type_button, 8 )
self._sort_tag_display_type_button.setMinimumWidth( tdt_width )
asc_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._sort_order_choice, 14 )
self._sort_order_choice.setMinimumWidth( asc_width )
self._UpdateSortTypeLabel()
self._UpdateButtonsVisible()
self._UpdateAscDescLabelsAndDefault()
#
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._sort_type_button, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._sort_tag_display_type_button, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sort_order_choice, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._tag_context_button, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
HG.client_controller.sub( self, 'ACollectHappened', 'a_collect_happened' )
HG.client_controller.sub( self, 'BroadcastSort', 'do_page_sort' )
if self._management_controller is not None and self._management_controller.HasVariable( 'media_sort' ):
media_sort = self._management_controller.GetVariable( 'media_sort' )
try:
self.SetSort( media_sort )
except:
default_sort = ClientMedia.MediaSort( ( 'system', CC.SORT_FILES_BY_FILESIZE ), CC.SORT_ASC )
self.SetSort( default_sort )
self._sort_tag_display_type_button.valueChanged.connect( self.EventTagDisplayTypeChoice )
self._sort_order_choice.valueChanged.connect( self.EventSortAscChoice )
self._tag_context_button.valueChanged.connect( self.EventTagContextChanged )
def _BroadcastSort( self ):
media_sort = self._GetCurrentSort()
if self._management_controller is not None:
self._management_controller.SetVariable( 'media_sort', media_sort )
self.sortChanged.emit( media_sort )
def _GetCurrentSort( self ) -> ClientMedia.MediaSort:
sort_order = self._sort_order_choice.GetValue()
if sort_order is None:
sort_order = CC.SORT_ASC
tag_context = self._tag_context_button.GetValue()
media_sort = ClientMedia.MediaSort( sort_type = self._sort_type, sort_order = sort_order, tag_context = tag_context )
return media_sort
def _PopulateSortMenuOrList( self, menu = None ):
sort_types = []
menu_items_and_sort_types = []
submetatypes_to_menus = {}
for system_sort_type in CC.SYSTEM_SORT_TYPES_SORT_CONTROL_SORTED:
sort_type = ( 'system', system_sort_type )
sort_types.append( sort_type )
if menu is not None:
submetatype = CC.system_sort_type_submetatype_string_lookup[ system_sort_type ]
if submetatype is None:
menu_to_add_to = menu
else:
if submetatype not in submetatypes_to_menus:
submenu = QW.QMenu( menu )
submetatypes_to_menus[ submetatype ] = submenu
ClientGUIMenus.AppendMenu( menu, submenu, submetatype )
menu_to_add_to = submetatypes_to_menus[ submetatype ]
label = CC.sort_type_basic_string_lookup[ system_sort_type ]
menu_item = ClientGUIMenus.AppendMenuItem( menu_to_add_to, label, 'Select this sort type.', self._SetSortTypeFromUser, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
default_namespace_sorts = HG.client_controller.new_options.GetDefaultNamespaceSorts()
if menu is not None:
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenu( menu, submenu, 'namespaces' )
for namespace_sort in default_namespace_sorts:
sort_type = namespace_sort.sort_type
sort_types.append( sort_type )
if menu is not None:
example_sort = ClientMedia.MediaSort( sort_type, CC.SORT_ASC )
label = example_sort.GetSortTypeString()
menu_item = ClientGUIMenus.AppendMenuItem( submenu, label, 'Select this sort type.', self._SetSortTypeFromUser, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
if menu is not None:
ClientGUIMenus.AppendMenuItem( submenu, 'custom', 'Set a custom namespace sort', self._SetCustomNamespaceSortFromUser )
rating_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
if len( rating_service_keys ) > 0:
if menu is not None:
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenu( menu, submenu, 'ratings' )
for service_key in rating_service_keys:
sort_type = ( 'rating', service_key )
sort_types.append( sort_type )
if menu is not None:
example_sort = ClientMedia.MediaSort( sort_type, CC.SORT_ASC )
label = example_sort.GetSortTypeString()
menu_item = ClientGUIMenus.AppendMenuItem( submenu, label, 'Select this sort type.', self._SetSortTypeFromUser, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
if menu is not None:
for ( menu_item, sort_choice ) in menu_items_and_sort_types:
if sort_choice == self._sort_type:
menu_item.setCheckable( True )
menu_item.setChecked( True )
return sort_types
def _SetCustomNamespaceSortFromUser( self ):
if self._sort_type[0] == 'namespaces':
sort_data = self._sort_type[1]
else:
sort_data = ( [ 'series' ], ClientTags.TAG_DISPLAY_ACTUAL )
try:
sort_data = ClientGUITags.EditNamespaceSort( self, sort_data )
sort_type = ( 'namespaces', sort_data )
self._SetSortTypeFromUser( sort_type )
except HydrusExceptions.VetoException:
return
def _SortTypeButtonClick( self ):
menu = QW.QMenu()
self._PopulateSortMenuOrList( menu = menu )
CGC.core().PopupMenu( self, menu )
def _SetSortType( self, sort_type ):
self._sort_type = sort_type
self._UpdateSortTypeLabel()
self._UpdateButtonsVisible()
self._UpdateAscDescLabelsAndDefault()
def _SetSortTypeFromUser( self, sort_type ):
self._SetSortType( sort_type )
self._UserChoseASort()
self._BroadcastSort()
def _UpdateAscDescLabelsAndDefault( self ):
media_sort = self._GetCurrentSort()
self._sort_order_choice.blockSignals( True )
if media_sort.CanAsc():
( asc_str, desc_str, default_sort_order ) = media_sort.GetSortOrderStrings()
choice_tuples = [
( asc_str, CC.SORT_ASC ),
( desc_str, CC.SORT_DESC )
]
# if there are no changes to asc/desc texts, then we'll keep the previous value
if choice_tuples != self._sort_order_choice.GetChoiceTuples():
self._sort_order_choice.SetChoiceTuples( choice_tuples )
self._sort_order_choice.SetValue( default_sort_order )
self._sort_order_choice.setVisible( True )
else:
self._sort_order_choice.setVisible( False )
#self._sort_order_choice.SetChoiceTuples( [] )
self._sort_order_choice.blockSignals( False )
def _UpdateButtonsVisible( self ):
( sort_metatype, sort_data ) = self._sort_type
show_tag_button = sort_metatype == 'namespaces' and HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
self._tag_context_button.setVisible( show_tag_button )
self._sort_tag_display_type_button.setVisible( show_tag_button )
def _UpdateSortTypeLabel( self ):
example_sort = ClientMedia.MediaSort( self._sort_type, CC.SORT_ASC )
self._sort_type_button.setText( example_sort.GetSortTypeString() )
( sort_metatype, sort_data ) = self._sort_type
show_tdt = sort_metatype == 'namespaces' and HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
if show_tdt:
if sort_metatype == 'namespaces':
( namespaces, current_tag_display_type ) = sort_data
tag_display_types = [
ClientTags.TAG_DISPLAY_ACTUAL,
ClientTags.TAG_DISPLAY_SELECTION_LIST,
ClientTags.TAG_DISPLAY_SINGLE_MEDIA
]
choice_tuples = [ ( ClientTags.tag_display_str_lookup[ tag_display_type ], tag_display_type ) for tag_display_type in tag_display_types ]
self._sort_tag_display_type_button.blockSignals( True )
self._sort_tag_display_type_button.SetChoiceTuples( choice_tuples )
self._sort_tag_display_type_button.SetValue( current_tag_display_type )
self._sort_tag_display_type_button.blockSignals( False )
def _UserChoseASort( self ):
if HG.client_controller.new_options.GetBoolean( 'save_page_sort_on_change' ):
media_sort = self._GetCurrentSort()
HG.client_controller.new_options.SetDefaultSort( media_sort )
def ACollectHappened( self, page_key ):
if self._management_controller is not None:
my_page_key = self._management_controller.GetVariable( 'page_key' )
if page_key == my_page_key:
self._BroadcastSort()
def BroadcastSort( self, page_key = None ):
if page_key is not None and page_key != self._management_controller.GetVariable( 'page_key' ):
return
self._BroadcastSort()
def EventSortAscChoice( self ):
self._UserChoseASort()
self._BroadcastSort()
def EventTagContextChanged( self, tag_context: ClientSearch.TagContext ):
self._UserChoseASort()
self._BroadcastSort()
def EventTagDisplayTypeChoice( self ):
( sort_metatype, sort_data ) = self._sort_type
if sort_metatype == 'namespaces':
( namespaces, current_tag_display_type ) = sort_data
tag_display_type = self._sort_tag_display_type_button.GetValue()
sort_data = ( namespaces, tag_display_type )
self._SetSortType( ( sort_metatype, sort_data ) )
self._UserChoseASort()
self._BroadcastSort()
def GetSort( self ) -> ClientMedia.MediaSort:
return self._GetCurrentSort()
def NotifyAdvancedMode( self ):
self._UpdateButtonsVisible()
def SetSort( self, media_sort: ClientMedia.MediaSort ):
self._SetSortType( media_sort.sort_type )
# put this after 'asclabels', since we may transition from one-state to two-state
self._sort_order_choice.SetValue( media_sort.sort_order )
self._tag_context_button.SetValue( media_sort.tag_context )
def wheelEvent( self, event ):
if HG.client_controller.new_options.GetBoolean( 'menu_choice_buttons_can_mouse_scroll' ):
if self._sort_type_button.rect().contains( self._sort_type_button.mapFromGlobal( QG.QCursor.pos() ) ):
if event.angleDelta().y() > 0:
index_delta = -1
else:
index_delta = 1
sort_types = self._PopulateSortMenuOrList()
if self._sort_type in sort_types:
index = sort_types.index( self._sort_type )
new_index = ( index + index_delta ) % len( sort_types )
new_sort_type = sort_types[ new_index ]
self._SetSortTypeFromUser( new_sort_type )
event.accept()
else:
event.ignore()