hydrus/hydrus/client/ClientManagers.py

621 lines
21 KiB
Python

import collections
import threading
import typing
from qtpy import QtGui as QG
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
# now let's fill out grandparents
def BuildServiceKeysToChildrenToParents( service_keys_to_simple_children_to_parents ):
# important thing here, and reason why it is recursive, is because we want to preserve the parent-grandparent interleaving in list order
def AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, parents ):
for parent in parents:
if parent not in this_childs_parents:
this_childs_parents.append( parent )
# this parent has its own parents, so the child should get those as well
if parent in simple_children_to_parents:
grandparents = simple_children_to_parents[ parent ]
AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, grandparents )
service_keys_to_children_to_parents = collections.defaultdict( HydrusData.default_dict_list )
for ( service_key, simple_children_to_parents ) in service_keys_to_simple_children_to_parents.items():
children_to_parents = service_keys_to_children_to_parents[ service_key ]
for ( child, parents ) in list( simple_children_to_parents.items() ):
this_childs_parents = children_to_parents[ child ]
AddParentsAndGrandparents( simple_children_to_parents, this_childs_parents, parents )
return service_keys_to_children_to_parents
def BuildServiceKeysToSimpleChildrenToParents( service_keys_to_pairs_flat ):
service_keys_to_simple_children_to_parents = collections.defaultdict( HydrusData.default_dict_set )
for ( service_key, pairs ) in service_keys_to_pairs_flat.items():
service_keys_to_simple_children_to_parents[ service_key ] = BuildSimpleChildrenToParents( pairs )
return service_keys_to_simple_children_to_parents
# take pairs, make dict of child -> parents while excluding loops
# no grandparents here
def BuildSimpleChildrenToParents( pairs ):
simple_children_to_parents = HydrusData.default_dict_set()
for ( child, parent ) in pairs:
if child == parent:
continue
if parent in simple_children_to_parents and LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):
continue
simple_children_to_parents[ child ].add( parent )
return simple_children_to_parents
def LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ):
potential_loop_paths = { parent }
while True:
new_potential_loop_paths = set()
for potential_loop_path in potential_loop_paths:
if potential_loop_path in simple_children_to_parents:
new_potential_loop_paths.update( simple_children_to_parents[ potential_loop_path ] )
potential_loop_paths = new_potential_loop_paths
if child in potential_loop_paths:
return True
elif len( potential_loop_paths ) == 0:
return False
class BitmapManager( object ):
MAX_MEMORY_ALLOWANCE = 512 * 1024 * 1024
def __init__( self, controller ):
self._controller = controller
self._media_background_pixmap_path = None
self._media_background_pixmap = None
def _GetQtImageFormat( self, depth ):
if depth == 24:
return QG.QImage.Format_RGB888
elif depth == 32:
return QG.QImage.Format_RGBA8888
def GetQtImage( self, width, height, depth = 24 ):
if width < 0:
width = 20
if height < 0:
height = 20
qt_image_format = self._GetQtImageFormat( depth )
return QG.QImage( width, height, qt_image_format )
def GetQtPixmap( self, width, height ):
if width < 0:
width = 20
if height < 0:
height = 20
return QG.QPixmap( width, height )
def GetQtImageFromBuffer( self, width, height, depth, data ):
if isinstance( data, memoryview ) and not data.c_contiguous:
data = data.copy()
qt_image_format = self._GetQtImageFormat( depth )
bytes_per_line = ( depth / 8 ) * width
# no copy here
qt_image = QG.QImage( data, width, height, bytes_per_line, qt_image_format )
# cheeky solution here
# the QImage init does not take python ownership of the data, so if it gets garbage collected, we crash
# so, add a beardy python ref to it, no problem :^)
# other anwser here is to do a .copy, but this can be a _little_ expensive and eats memory
qt_image.python_data_reference = data
return qt_image
def GetQtPixmapFromBuffer( self, width, height, depth, data ):
if isinstance( data, memoryview ) and not data.c_contiguous:
data = data.copy()
qt_image_format = self._GetQtImageFormat( depth )
bytes_per_line = ( depth / 8 ) * width
# no copy, no new data allocated
qt_image = QG.QImage( data, width, height, bytes_per_line, qt_image_format )
# _should_ be a safe copy of the hot data
pixmap = QG.QPixmap.fromImage( qt_image )
return pixmap
def GetMediaBackgroundPixmap( self ):
pixmap_path = self._controller.new_options.GetNoneableString( 'media_background_bmp_path' )
if pixmap_path != self._media_background_pixmap_path:
self._media_background_pixmap_path = pixmap_path
try:
self._media_background_pixmap = QG.QPixmap( self._media_background_pixmap_path )
except Exception as e:
self._media_background_pixmap = None
HydrusData.ShowText( 'Loading a bmp caused an error!' )
HydrusData.ShowException( e )
return None
return self._media_background_pixmap
class FileViewingStatsManager( object ):
def __init__( self, controller ):
self._controller = controller
self._lock = threading.Lock()
self._pending_updates = {}
self._last_update = HydrusData.GetNow()
self._my_flush_job = self._controller.CallRepeating( 5, 60, self.REPEATINGFlush )
def _GenerateViewsRow( self, viewtype, viewtime_delta ):
new_options = HG.client_controller.new_options
preview_views_delta = 0
preview_viewtime_delta = 0
media_views_delta = 0
media_viewtime_delta = 0
if viewtype == 'preview':
preview_min = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_min_time' )
preview_max = new_options.GetNoneableInteger( 'file_viewing_statistics_preview_max_time' )
if preview_max is not None:
viewtime_delta = min( viewtime_delta, preview_max )
if preview_min is None or viewtime_delta >= preview_min:
preview_views_delta = 1
preview_viewtime_delta = viewtime_delta
elif viewtype in ( 'media', 'media_duplicates_filter' ):
do_it = True
if viewtype == 'media_duplicates_filter' and not new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ):
do_it = False
if do_it:
media_min = new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' )
media_max = new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' )
if media_max is not None:
viewtime_delta = min( viewtime_delta, media_max )
if media_min is None or viewtime_delta >= media_min:
media_views_delta = 1
media_viewtime_delta = viewtime_delta
return ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
def _RowMakesChanges( self, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
preview_change = preview_views_delta != 0 or preview_viewtime_delta != 0
media_change = media_views_delta != 0 or media_viewtime_delta != 0
return preview_change or media_change
def _PubSubRow( self, hash, row ):
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
pubsub_row = ( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, pubsub_row )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
HG.client_controller.pub( 'content_updates_data', service_keys_to_content_updates )
HG.client_controller.pub( 'content_updates_gui', service_keys_to_content_updates )
def REPEATINGFlush( self ):
self.Flush()
def Flush( self ):
with self._lock:
if len( self._pending_updates ) > 0:
content_updates = []
for ( hash, ( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) ) in self._pending_updates.items():
row = ( hash, preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_ADD, row )
content_updates.append( content_update )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : content_updates }
# non-synchronous, non-publishing
self._controller.Write( 'content_updates', service_keys_to_content_updates, publish_content_updates = False )
self._pending_updates = {}
def FinishViewing( self, viewtype, hash, viewtime_delta ):
if not HG.client_controller.new_options.GetBoolean( 'file_viewing_statistics_active' ):
return
with self._lock:
row = self._GenerateViewsRow( viewtype, viewtime_delta )
if not self._RowMakesChanges( row ):
return
if hash not in self._pending_updates:
self._pending_updates[ hash ] = row
else:
( preview_views_delta, preview_viewtime_delta, media_views_delta, media_viewtime_delta ) = row
( existing_preview_views_delta, existing_preview_viewtime_delta, existing_media_views_delta, existing_media_viewtime_delta ) = self._pending_updates[ hash ]
self._pending_updates[ hash ] = ( existing_preview_views_delta + preview_views_delta, existing_preview_viewtime_delta + preview_viewtime_delta, existing_media_views_delta + media_views_delta, existing_media_viewtime_delta + media_viewtime_delta )
self._PubSubRow( hash, row )
class UndoManager( object ):
def __init__( self, controller ):
self._controller = controller
self._commands = []
self._inverted_commands = []
self._current_index = 0
self._lock = threading.Lock()
self._controller.sub( self, 'Undo', 'undo' )
self._controller.sub( self, 'Redo', 'redo' )
def _FilterServiceKeysToContentUpdates( self, service_keys_to_content_updates ):
filtered_service_keys_to_content_updates = {}
for ( service_key, content_updates ) in service_keys_to_content_updates.items():
filtered_content_updates = []
for content_update in content_updates:
( data_type, action, row ) = content_update.ToTuple()
if data_type == HC.CONTENT_TYPE_FILES:
if action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE, HC.CONTENT_UPDATE_UNDELETE, HC.CONTENT_UPDATE_RESCIND_PETITION, HC.CONTENT_UPDATE_ADVANCED ):
continue
elif data_type == HC.CONTENT_TYPE_MAPPINGS:
if action in ( HC.CONTENT_UPDATE_RESCIND_PETITION, HC.CONTENT_UPDATE_ADVANCED ):
continue
else:
continue
filtered_content_update = HydrusData.ContentUpdate( data_type, action, row )
filtered_content_updates.append( filtered_content_update )
if len( filtered_content_updates ) > 0:
filtered_service_keys_to_content_updates[ service_key ] = filtered_content_updates
return filtered_service_keys_to_content_updates
def _InvertServiceKeysToContentUpdates( self, service_keys_to_content_updates ):
inverted_service_keys_to_content_updates = {}
for ( service_key, content_updates ) in service_keys_to_content_updates.items():
inverted_content_updates = []
for content_update in content_updates:
( data_type, action, row ) = content_update.ToTuple()
inverted_row = row
if data_type == HC.CONTENT_TYPE_FILES:
if action == HC.CONTENT_UPDATE_ARCHIVE: inverted_action = HC.CONTENT_UPDATE_INBOX
elif action == HC.CONTENT_UPDATE_INBOX: inverted_action = HC.CONTENT_UPDATE_ARCHIVE
elif action == HC.CONTENT_UPDATE_PEND: inverted_action = HC.CONTENT_UPDATE_RESCIND_PEND
elif action == HC.CONTENT_UPDATE_RESCIND_PEND: inverted_action = HC.CONTENT_UPDATE_PEND
elif action == HC.CONTENT_UPDATE_PETITION: inverted_action = HC.CONTENT_UPDATE_RESCIND_PETITION
elif data_type == HC.CONTENT_TYPE_MAPPINGS:
if action == HC.CONTENT_UPDATE_ADD: inverted_action = HC.CONTENT_UPDATE_DELETE
elif action == HC.CONTENT_UPDATE_DELETE: inverted_action = HC.CONTENT_UPDATE_ADD
elif action == HC.CONTENT_UPDATE_PEND: inverted_action = HC.CONTENT_UPDATE_RESCIND_PEND
elif action == HC.CONTENT_UPDATE_RESCIND_PEND: inverted_action = HC.CONTENT_UPDATE_PEND
elif action == HC.CONTENT_UPDATE_PETITION: inverted_action = HC.CONTENT_UPDATE_RESCIND_PETITION
inverted_content_update = HydrusData.ContentUpdate( data_type, inverted_action, inverted_row )
inverted_content_updates.append( inverted_content_update )
inverted_service_keys_to_content_updates[ service_key ] = inverted_content_updates
return inverted_service_keys_to_content_updates
def AddCommand( self, action, *args, **kwargs ):
with self._lock:
inverted_action = action
inverted_args = args
inverted_kwargs = kwargs
if action == 'content_updates':
( service_keys_to_content_updates, ) = args
service_keys_to_content_updates = self._FilterServiceKeysToContentUpdates( service_keys_to_content_updates )
if len( service_keys_to_content_updates ) == 0: return
inverted_service_keys_to_content_updates = self._InvertServiceKeysToContentUpdates( service_keys_to_content_updates )
if len( inverted_service_keys_to_content_updates ) == 0: return
inverted_args = ( inverted_service_keys_to_content_updates, )
else: return
self._commands = self._commands[ : self._current_index ]
self._inverted_commands = self._inverted_commands[ : self._current_index ]
self._commands.append( ( action, args, kwargs ) )
self._inverted_commands.append( ( inverted_action, inverted_args, inverted_kwargs ) )
self._current_index += 1
self._controller.pub( 'notify_new_undo' )
def GetUndoRedoStrings( self ):
with self._lock:
( undo_string, redo_string ) = ( None, None )
if self._current_index > 0:
undo_index = self._current_index - 1
( action, args, kwargs ) = self._commands[ undo_index ]
if action == 'content_updates':
( service_keys_to_content_updates, ) = args
undo_string = 'undo ' + ClientData.ConvertServiceKeysToContentUpdatesToPrettyString( service_keys_to_content_updates )
if len( self._commands ) > 0 and self._current_index < len( self._commands ):
redo_index = self._current_index
( action, args, kwargs ) = self._commands[ redo_index ]
if action == 'content_updates':
( service_keys_to_content_updates, ) = args
redo_string = 'redo ' + ClientData.ConvertServiceKeysToContentUpdatesToPrettyString( service_keys_to_content_updates )
return ( undo_string, redo_string )
def Undo( self ):
action = None
with self._lock:
if self._current_index > 0:
self._current_index -= 1
( action, args, kwargs ) = self._inverted_commands[ self._current_index ]
if action is not None:
self._controller.WriteSynchronous( action, *args, **kwargs )
self._controller.pub( 'notify_new_undo' )
def Redo( self ):
action = None
with self._lock:
if len( self._commands ) > 0 and self._current_index < len( self._commands ):
( action, args, kwargs ) = self._commands[ self._current_index ]
self._current_index += 1
if action is not None:
self._controller.WriteSynchronous( action, *args, **kwargs )
self._controller.pub( 'notify_new_undo' )