4943 lines
163 KiB
Python
4943 lines
163 KiB
Python
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 HydrusImageHandling
|
|
from hydrus.core import HydrusPaths
|
|
from hydrus.core import HydrusTags
|
|
|
|
from hydrus.client import ClientApplicationCommand as CAC
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientData
|
|
from hydrus.client import ClientDuplicates
|
|
from hydrus.client import ClientLocation
|
|
from hydrus.client import ClientPaths
|
|
from hydrus.client import ClientSearch
|
|
from hydrus.client.gui import ClientGUICore as CGC
|
|
from hydrus.client.gui import ClientGUIDialogs
|
|
from hydrus.client.gui import ClientGUIDialogsManage
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIMedia
|
|
from hydrus.client.gui import ClientGUIMediaActions
|
|
from hydrus.client.gui import ClientGUIMediaControls
|
|
from hydrus.client.gui import ClientGUIMenus
|
|
from hydrus.client.gui import ClientGUIRatings
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsManagement
|
|
from hydrus.client.gui import ClientGUIShortcuts
|
|
from hydrus.client.gui import ClientGUITags
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.canvas import ClientGUICanvasHoverFrames
|
|
from hydrus.client.gui.canvas import ClientGUICanvasMedia
|
|
from hydrus.client.media import ClientMedia
|
|
from hydrus.client.metadata import ClientRatings
|
|
from hydrus.client.metadata import ClientTags
|
|
from hydrus.client.metadata import ClientTagSorting
|
|
|
|
ZOOM_CENTERPOINT_MEDIA_CENTER = 0
|
|
ZOOM_CENTERPOINT_VIEWER_CENTER = 1
|
|
ZOOM_CENTERPOINT_MOUSE = 2
|
|
ZOOM_CENTERPOINT_MEDIA_TOP_LEFT = 3
|
|
|
|
ZOOM_CENTERPOINT_TYPES = ( ZOOM_CENTERPOINT_VIEWER_CENTER, ZOOM_CENTERPOINT_MOUSE, ZOOM_CENTERPOINT_MEDIA_CENTER, ZOOM_CENTERPOINT_MEDIA_TOP_LEFT )
|
|
|
|
zoom_centerpoints_str_lookup = {}
|
|
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MEDIA_CENTER ] = 'media center'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_VIEWER_CENTER ] = 'viewer center'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MOUSE ] = 'mouse (or viewer center if mouse outside)'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MEDIA_TOP_LEFT ] = 'media top-left'
|
|
|
|
OPEN_EXTERNALLY_BUTTON_SIZE = ( 200, 45 )
|
|
|
|
def AddAudioVolumeMenu( menu, canvas_type ):
|
|
|
|
mute_volume_type = None
|
|
volume_volume_type = ClientGUIMediaControls.AUDIO_GLOBAL
|
|
|
|
if canvas_type == CC.CANVAS_MEDIA_VIEWER:
|
|
|
|
mute_volume_type = ClientGUIMediaControls.AUDIO_MEDIA_VIEWER
|
|
|
|
if HG.client_controller.new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ):
|
|
|
|
volume_volume_type = ClientGUIMediaControls.AUDIO_MEDIA_VIEWER
|
|
|
|
|
|
elif canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
mute_volume_type = ClientGUIMediaControls.AUDIO_PREVIEW
|
|
|
|
if HG.client_controller.new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ):
|
|
|
|
volume_volume_type = ClientGUIMediaControls.AUDIO_PREVIEW
|
|
|
|
|
|
|
|
volume_menu = QW.QMenu( menu )
|
|
|
|
( global_mute_option_name, global_volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ ClientGUIMediaControls.AUDIO_GLOBAL ]
|
|
|
|
if HG.client_controller.new_options.GetBoolean( global_mute_option_name ):
|
|
|
|
label = 'unmute global'
|
|
|
|
else:
|
|
|
|
label = 'mute global'
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( volume_menu, label, 'Mute/unmute audio.', ClientGUIMediaControls.FlipMute, ClientGUIMediaControls.AUDIO_GLOBAL )
|
|
|
|
#
|
|
|
|
if mute_volume_type is not None:
|
|
|
|
ClientGUIMenus.AppendSeparator( volume_menu )
|
|
|
|
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ mute_volume_type ]
|
|
|
|
if HG.client_controller.new_options.GetBoolean( mute_option_name ):
|
|
|
|
label = 'unmute {}'.format( ClientGUIMediaControls.volume_types_str_lookup[ mute_volume_type ] )
|
|
|
|
else:
|
|
|
|
label = 'mute {}'.format( ClientGUIMediaControls.volume_types_str_lookup[ mute_volume_type ] )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( volume_menu, label, 'Mute/unmute audio.', ClientGUIMediaControls.FlipMute, mute_volume_type )
|
|
|
|
|
|
#
|
|
|
|
ClientGUIMenus.AppendSeparator( volume_menu )
|
|
|
|
( mute_option_name, volume_option_name ) = ClientGUIMediaControls.volume_types_to_option_names[ volume_volume_type ]
|
|
|
|
# 0-100 inclusive
|
|
volumes = list( range( 0, 110, 10 ) )
|
|
|
|
current_volume = HG.client_controller.new_options.GetInteger( volume_option_name )
|
|
|
|
if current_volume not in volumes:
|
|
|
|
volumes.append( current_volume )
|
|
|
|
volumes.sort()
|
|
|
|
|
|
for volume in volumes:
|
|
|
|
label = 'volume: {}'.format( volume )
|
|
|
|
if volume == current_volume:
|
|
|
|
ClientGUIMenus.AppendMenuCheckItem( volume_menu, label, 'Set the volume.', True, ClientGUIMediaControls.ChangeVolume, volume_volume_type, volume )
|
|
|
|
else:
|
|
|
|
ClientGUIMenus.AppendMenuItem( volume_menu, label, 'Set the volume.', ClientGUIMediaControls.ChangeVolume, volume_volume_type, volume )
|
|
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, volume_menu, 'volume' )
|
|
|
|
def CalculateCanvasMediaSize( media, canvas_size: QC.QSize, show_action ):
|
|
|
|
canvas_width = canvas_size.width()
|
|
canvas_height = canvas_size.height()
|
|
|
|
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
|
|
|
|
canvas_height -= animated_scanbar_height
|
|
'''
|
|
|
|
canvas_width = max( canvas_width, 80 )
|
|
canvas_height = max( canvas_height, 60 )
|
|
|
|
return ( canvas_width, canvas_height )
|
|
|
|
def CalculateCanvasZooms( canvas, media, show_action ):
|
|
|
|
if media is None:
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
if show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
( media_width, media_height ) = CalculateMediaSize( media, 1.0 )
|
|
|
|
if media_width == 0 or media_height == 0:
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
( canvas_width, canvas_height ) = CalculateCanvasMediaSize( media, canvas.size(), show_action )
|
|
|
|
width_zoom = canvas_width / media_width
|
|
|
|
height_zoom = canvas_height / media_height
|
|
|
|
canvas_zoom = min( ( width_zoom, height_zoom ) )
|
|
|
|
#
|
|
|
|
mime = media.GetMime()
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = new_options.GetMediaZoomOptions( mime )
|
|
|
|
if exact_zooms_only:
|
|
|
|
max_regular_zoom = 1.0
|
|
|
|
if canvas_zoom > 1.0:
|
|
|
|
while max_regular_zoom * 2 < canvas_zoom:
|
|
|
|
max_regular_zoom *= 2
|
|
|
|
|
|
elif canvas_zoom < 1.0:
|
|
|
|
while max_regular_zoom > canvas_zoom:
|
|
|
|
max_regular_zoom /= 2
|
|
|
|
|
|
|
|
else:
|
|
|
|
regular_zooms = new_options.GetMediaZooms()
|
|
|
|
valid_regular_zooms = [ zoom for zoom in regular_zooms if zoom < canvas_zoom ]
|
|
|
|
if len( valid_regular_zooms ) > 0:
|
|
|
|
max_regular_zoom = max( valid_regular_zooms )
|
|
|
|
else:
|
|
|
|
max_regular_zoom = canvas_zoom
|
|
|
|
|
|
|
|
if media.GetMime() in HC.AUDIO:
|
|
|
|
scale_up_action = CC.MEDIA_VIEWER_SCALE_100
|
|
scale_down_action = CC.MEDIA_VIEWER_SCALE_TO_CANVAS
|
|
|
|
elif canvas.PREVIEW_WINDOW:
|
|
|
|
scale_up_action = preview_scale_up
|
|
scale_down_action = preview_scale_down
|
|
|
|
else:
|
|
|
|
scale_up_action = media_scale_up
|
|
scale_down_action = media_scale_down
|
|
|
|
|
|
can_be_scaled_down = media_width > canvas_width or media_height > canvas_height
|
|
can_be_scaled_up = media_width < canvas_width and media_height < canvas_height
|
|
|
|
#
|
|
|
|
if can_be_scaled_up:
|
|
|
|
scale_action = scale_up_action
|
|
|
|
elif can_be_scaled_down:
|
|
|
|
scale_action = scale_down_action
|
|
|
|
else:
|
|
|
|
scale_action = CC.MEDIA_VIEWER_SCALE_100
|
|
|
|
|
|
if scale_action == CC.MEDIA_VIEWER_SCALE_100:
|
|
|
|
default_zoom = 1.0
|
|
|
|
elif scale_action == CC.MEDIA_VIEWER_SCALE_MAX_REGULAR:
|
|
|
|
default_zoom = max_regular_zoom
|
|
|
|
else:
|
|
|
|
default_zoom = canvas_zoom
|
|
|
|
|
|
return ( default_zoom, canvas_zoom )
|
|
|
|
def CalculateMediaContainerSize( media, zoom, show_action ):
|
|
|
|
if show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
raise Exception( 'This media should not be shown in the media viewer!' )
|
|
|
|
elif show_action == CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON:
|
|
|
|
( width, height ) = OPEN_EXTERNALLY_BUTTON_SIZE
|
|
|
|
if media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
bounding_dimensions = HG.client_controller.options[ 'thumbnail_dimensions' ]
|
|
thumbnail_scale_type = HG.client_controller.new_options.GetInteger( 'thumbnail_scale_type' )
|
|
|
|
( clip_rect, ( thumb_width, thumb_height ) ) = HydrusImageHandling.GetThumbnailResolutionAndClipRegion( media.GetResolution(), bounding_dimensions, thumbnail_scale_type )
|
|
|
|
height = height + thumb_height
|
|
|
|
|
|
return QC.QSize( width, height )
|
|
|
|
else:
|
|
|
|
( media_width, media_height ) = CalculateMediaSize( media, zoom )
|
|
|
|
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
|
|
|
|
media_height += animated_scanbar_height
|
|
'''
|
|
|
|
return QC.QSize( media_width, media_height )
|
|
|
|
|
|
def CalculateMediaSize( media, zoom ):
|
|
|
|
if media.GetMime() in HC.AUDIO:
|
|
|
|
( original_width, original_height ) = ( 360, 240 )
|
|
|
|
else:
|
|
|
|
( original_width, original_height ) = media.GetResolution()
|
|
|
|
|
|
media_width = int( round( zoom * original_width ) )
|
|
media_height = int( round( zoom * original_height ) )
|
|
|
|
media_width = max( 1, media_width )
|
|
media_height = max( 1, media_height )
|
|
|
|
return ( media_width, media_height )
|
|
|
|
class Canvas( QW.QWidget, CAC.ApplicationCommandProcessorMixin ):
|
|
|
|
PREVIEW_WINDOW = False
|
|
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER
|
|
|
|
def __init__( self, parent, location_context: ClientLocation.LocationContext ):
|
|
|
|
CAC.ApplicationCommandProcessorMixin.__init__( self )
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding )
|
|
|
|
self._location_context = location_context
|
|
|
|
self._current_media_start_time = HydrusData.GetNow()
|
|
|
|
self._new_options = HG.client_controller.new_options
|
|
|
|
self._canvas_key = HydrusData.GenerateKey()
|
|
|
|
self._maintain_pan_and_zoom = False
|
|
|
|
self._service_keys_to_services = {}
|
|
|
|
self._current_media = None
|
|
|
|
catch_mouse = True
|
|
|
|
# once we have catch_mouse full shortcut support for canvases, swap out this out for an option to swallow activating clicks
|
|
ignore_activating_mouse_click = catch_mouse and not self.PREVIEW_WINDOW
|
|
|
|
self._my_shortcuts_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'media', 'media_viewer' ], catch_mouse = catch_mouse, ignore_activating_mouse_click = ignore_activating_mouse_click )
|
|
|
|
self._click_drag_reporting_filter = MediaContainerDragClickReportingFilter( self )
|
|
|
|
self.installEventFilter( self._click_drag_reporting_filter )
|
|
|
|
self._media_container = ClientGUICanvasMedia.MediaContainer( self, self.CANVAS_TYPE, self._click_drag_reporting_filter )
|
|
|
|
self._current_zoom = 1.0
|
|
self._canvas_zoom = 1.0
|
|
|
|
self._last_drag_pos = None
|
|
self._current_drag_is_touch = False
|
|
self._last_motion_pos = QC.QPoint( 0, 0 )
|
|
self._media_window_pos = QC.QPoint( 0, 0 )
|
|
|
|
self._widget_event_filter = QP.WidgetEventFilter( self )
|
|
|
|
self._media_container.readyForNeighbourPrefetch.connect( self._PrefetchNeighbours )
|
|
|
|
HG.client_controller.sub( self, 'ZoomIn', 'canvas_zoom_in' )
|
|
HG.client_controller.sub( self, 'ZoomOut', 'canvas_zoom_out' )
|
|
HG.client_controller.sub( self, 'ZoomSwitch', 'canvas_zoom_switch' )
|
|
HG.client_controller.sub( self, 'OpenExternally', 'canvas_open_externally' )
|
|
HG.client_controller.sub( self, 'ManageTags', 'canvas_manage_tags' )
|
|
HG.client_controller.sub( self, 'update', 'notify_new_colourset' )
|
|
|
|
|
|
def _Archive( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
HG.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, ( self._current_media.GetHash(), ) ) ] } )
|
|
|
|
|
|
|
|
def _CanDisplayMedia( self, media ):
|
|
|
|
if media is None:
|
|
|
|
return True
|
|
|
|
|
|
media = media.GetDisplayMedia()
|
|
|
|
if media is None:
|
|
|
|
return True
|
|
|
|
|
|
locations_manager = media.GetLocationsManager()
|
|
|
|
if not locations_manager.IsLocal():
|
|
|
|
return False
|
|
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( media )
|
|
|
|
if media_show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
def _CopyBMPToClipboard( self ):
|
|
|
|
copied = False
|
|
|
|
if self._current_media is not None:
|
|
|
|
if self._current_media.GetMime() in HC.IMAGES:
|
|
|
|
HG.client_controller.pub( 'clipboard', 'bmp', self._current_media )
|
|
|
|
copied = True
|
|
|
|
|
|
|
|
return copied
|
|
|
|
|
|
def _CopyHashToClipboard( self, hash_type ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
ClientGUIMedia.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
|
|
|
|
|
|
def _CopyFileToClipboard( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
paths = [ client_files_manager.GetFilePath( self._current_media.GetHash(), self._current_media.GetMime() ) ]
|
|
|
|
HG.client_controller.pub( 'clipboard', 'paths', paths )
|
|
|
|
|
|
|
|
def _CopyPathToClipboard( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
path = client_files_manager.GetFilePath( self._current_media.GetHash(), self._current_media.GetMime() )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', path )
|
|
|
|
|
|
|
|
def _Delete( self, media = None, default_reason = None, file_service_key = None, just_get_jobs = False ):
|
|
|
|
if media is None:
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
|
|
media = [ self._current_media ]
|
|
|
|
|
|
if default_reason is None:
|
|
|
|
default_reason = 'Deleted from Preview or Media Viewer.'
|
|
|
|
|
|
if file_service_key is None:
|
|
|
|
if len( self._location_context.current_service_keys ) == 1:
|
|
|
|
( possible_suggested_file_service_key, ) = self._location_context.current_service_keys
|
|
|
|
if HG.client_controller.services_manager.GetServiceType( possible_suggested_file_service_key ) in HC.SPECIFIC_LOCAL_FILE_SERVICES + ( HC.FILE_REPOSITORY, ):
|
|
|
|
file_service_key = possible_suggested_file_service_key
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
( involves_physical_delete, jobs ) = ClientGUIDialogsQuick.GetDeleteFilesJobs( self, media, default_reason, suggested_file_service_key = file_service_key )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return False
|
|
|
|
|
|
def do_it( jobs ):
|
|
|
|
for service_keys_to_content_updates in jobs:
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
|
|
if just_get_jobs:
|
|
|
|
return jobs
|
|
|
|
else:
|
|
|
|
HG.client_controller.CallToThread( do_it, jobs )
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _DoEdgePan( self, pan_type ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
media_size = self._media_container.size()
|
|
|
|
delta_x = 0
|
|
delta_y = 0
|
|
|
|
if pan_type == CAC.SIMPLE_PAN_TOP_EDGE:
|
|
|
|
delta_y = - self._media_window_pos.y()
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_LEFT_EDGE:
|
|
|
|
delta_x = - self._media_window_pos.x()
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_BOTTOM_EDGE:
|
|
|
|
delta_y = my_size.height() - ( self._media_window_pos.y() + media_size.height() )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_RIGHT_EDGE:
|
|
|
|
delta_x = my_size.width() - ( self._media_window_pos.x() + media_size.width() )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_VERTICAL_CENTER:
|
|
|
|
delta_y = ( my_size.height() / 2 ) - ( self._media_window_pos.y() + ( media_size.height() / 2 ) )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_HORIZONTAL_CENTER:
|
|
|
|
delta_x = ( my_size.width() / 2 ) - ( self._media_window_pos.x() + ( media_size.width() / 2 ) )
|
|
|
|
|
|
delta = QC.QPoint( delta_x, delta_y )
|
|
|
|
self._media_window_pos += delta
|
|
|
|
self._DrawCurrentMedia()
|
|
|
|
|
|
def _DoManualPan( self, delta_x_step, delta_y_step ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
media_size = self._media_container.size()
|
|
|
|
x_pan_distance = min( my_size.width(), media_size.width() ) // 12
|
|
y_pan_distance = min( my_size.height(), media_size.height() ) // 12
|
|
|
|
delta_x = delta_x_step * x_pan_distance
|
|
delta_y = delta_y_step * y_pan_distance
|
|
|
|
delta = QC.QPoint( delta_x, delta_y )
|
|
|
|
self._media_window_pos += delta
|
|
|
|
self._DrawCurrentMedia()
|
|
|
|
|
|
def _DrawBackgroundBitmap( self, painter: QG.QPainter ):
|
|
|
|
background_colour = self._GetBackgroundColour()
|
|
|
|
painter.setBackground( QG.QBrush( background_colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
self._DrawBackgroundDetails( painter )
|
|
|
|
|
|
def _DrawBackgroundDetails( self, painter ):
|
|
|
|
pass
|
|
|
|
|
|
def _DrawCurrentMedia( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
size = self.size()
|
|
|
|
if size.width() > 0 and size.height() > 0:
|
|
|
|
self._SizeAndPositionMediaContainer()
|
|
|
|
|
|
|
|
def _GetBackgroundColour( self ):
|
|
|
|
return self._new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND )
|
|
|
|
|
|
def _GetShowAction( self, media ):
|
|
|
|
start_paused = False
|
|
start_with_embed = False
|
|
|
|
bad_result = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, start_paused, start_with_embed )
|
|
|
|
if media is None:
|
|
|
|
return bad_result
|
|
|
|
|
|
mime = media.GetMime()
|
|
|
|
if mime not in HC.ALLOWED_MIMES: # stopgap to catch a collection or application_unknown due to unusual import order/media moving
|
|
|
|
return bad_result
|
|
|
|
|
|
if self.PREVIEW_WINDOW:
|
|
|
|
return self._new_options.GetPreviewShowAction( mime )
|
|
|
|
else:
|
|
|
|
return self._new_options.GetMediaShowAction( mime )
|
|
|
|
|
|
|
|
def _GetIndexString( self ):
|
|
|
|
return ''
|
|
|
|
|
|
def _GetMediaContainerSize( self ):
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
new_size = CalculateMediaContainerSize( self._current_media, self._current_zoom, media_show_action )
|
|
|
|
return new_size
|
|
|
|
|
|
def _Inbox( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
HG.client_controller.Write( 'content_updates', { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_INBOX, ( self._current_media.GetHash(), ) ) ] } )
|
|
|
|
|
|
def _IsZoomable( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
return media_show_action not in ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW )
|
|
|
|
|
|
def _MaintainZoom( self, previous_media ):
|
|
|
|
if previous_media is None:
|
|
|
|
self._ReinitZoom()
|
|
|
|
else:
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
if previous_media.GetMime() not in HC.MIMES_WITH_THUMBNAILS or self._current_media.GetMime() not in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
return
|
|
|
|
|
|
# set up canvas zoom
|
|
|
|
previous_current_zoom = self._current_zoom
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( previous_media )
|
|
|
|
( previous_default_zoom, previous_canvas_zoom ) = CalculateCanvasZooms( self, previous_media, media_show_action )
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
( gumpf_current_zoom, self._canvas_zoom ) = CalculateCanvasZooms( self, self._current_media, media_show_action )
|
|
|
|
# previously, we always matched width, but this causes a problem in dupe viewer when B has a little watermark on the bottom, spilling below bottom of screen
|
|
# I think in future we will have more options regarding all this, and this method will change significantly
|
|
# however for now we really just want a hardcoded ok solution for all situations, so let's just hook on default canvas zoom situation
|
|
|
|
( previous_width, previous_height ) = CalculateMediaSize( previous_media, self._current_zoom )
|
|
|
|
( previous_media_100_width, previous_media_100_height ) = previous_media.GetResolution()
|
|
( current_media_100_width, current_media_100_height ) = self._current_media.GetResolution()
|
|
|
|
width_locked_zoom = previous_width / current_media_100_width
|
|
height_locked_zoom = previous_height / current_media_100_height
|
|
|
|
width_locked_size = CalculateMediaContainerSize( self._current_media, width_locked_zoom, media_show_action )
|
|
height_locked_size = CalculateMediaContainerSize( self._current_media, height_locked_zoom, media_show_action )
|
|
|
|
# if landscape, go height, portrait, go width
|
|
if previous_media_100_width > previous_media_100_height and current_media_100_width > current_media_100_height:
|
|
|
|
lock_height = True
|
|
|
|
elif previous_media_100_width < previous_media_100_height and current_media_100_width < current_media_100_height:
|
|
|
|
lock_height = False
|
|
|
|
else:
|
|
|
|
# for weird stuff, we'll choose the smaller of the two ratios
|
|
|
|
width_difference = max( previous_media_100_width, current_media_100_width ) / min( previous_media_100_width, current_media_100_width )
|
|
height_difference = max( previous_media_100_height, current_media_100_height ) / min( previous_media_100_height, current_media_100_height )
|
|
|
|
lock_height = height_difference <= width_difference
|
|
|
|
|
|
# however we don't want to accidentally zoom in if the media we are switching to is larger. it'll spill over the bottom of the canvas
|
|
# therefore let's have a little safety check
|
|
|
|
if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.05:
|
|
|
|
# we were looking at the default zoom, near or at canvas edge(s), probably hadn't zoomed before switching comparison
|
|
# we want to make sure our comparison does not spill over the canvas edge
|
|
|
|
close_to_vertical_edge = self.height() * 0.95 <= self._media_container.height() <= self.height() * 1.05
|
|
vertical_spillover_could_be_deceptive = self._media_container.height() < width_locked_size.height() < self._media_container.height() * 1.1
|
|
|
|
# locking by width will spill over bottom of screen
|
|
if close_to_vertical_edge and vertical_spillover_could_be_deceptive:
|
|
|
|
lock_height = True
|
|
|
|
|
|
close_to_horizontal_edge = self.width() * 0.95 <= self._media_container.width() <= self.width() * 1.05
|
|
horizontal_spillover_could_be_deceptive = self._media_container.width() < height_locked_size.width() < self._media_container.width() * 1.1
|
|
|
|
# locking by height will spill over right of screen
|
|
if close_to_horizontal_edge and horizontal_spillover_could_be_deceptive:
|
|
|
|
lock_height = False
|
|
|
|
|
|
|
|
if lock_height:
|
|
|
|
self._current_zoom = height_locked_zoom
|
|
|
|
else:
|
|
|
|
self._current_zoom = width_locked_zoom
|
|
|
|
|
|
HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom )
|
|
|
|
# and fix drag delta, or rewangle this so drag delta is offset to start with anyway m8, yeah
|
|
|
|
|
|
|
|
def _ManageNotes( self, name_to_start_on = None ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
ClientGUIMediaActions.EditFileNotes( self, self._current_media, name_to_start_on = name_to_start_on )
|
|
|
|
|
|
def _ManageRatings( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
if len( HG.client_controller.services_manager.GetServices( HC.RATINGS_SERVICES ) ) > 0:
|
|
|
|
with ClientGUIDialogsManage.DialogManageRatings( self, ( self._current_media, ) ) as dlg:
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
|
|
def _ManageTags( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
for child in self.children():
|
|
|
|
if isinstance( child, ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel ):
|
|
|
|
panel = child.GetPanel()
|
|
|
|
if isinstance( panel, ClientGUITags.ManageTagsPanel ):
|
|
|
|
child.activateWindow()
|
|
|
|
command = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SET_SEARCH_FOCUS )
|
|
|
|
panel.ProcessApplicationCommand( command )
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# take any focus away from hover window, which will mess up window order when it hides due to the new frame
|
|
self.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
title = 'manage tags'
|
|
frame_key = 'manage_tags_frame'
|
|
|
|
manage_tags = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = ClientGUITags.ManageTagsPanel( manage_tags, self._location_context, ( self._current_media, ), immediate_commit = True, canvas_key = self._canvas_key )
|
|
|
|
manage_tags.SetPanel( panel )
|
|
|
|
|
|
def _ManageURLs( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
title = 'manage known urls'
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogManage( self, title ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsManagement.ManageURLsPanel( dlg, ( self._current_media, ) )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.exec()
|
|
|
|
|
|
|
|
def _MediaFocusWentToExternalProgram( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
mime = self._current_media.GetMime()
|
|
|
|
if self._current_media.HasDuration():
|
|
|
|
self._media_container.Pause()
|
|
|
|
|
|
|
|
def _OpenExternally( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
hash = self._current_media.GetHash()
|
|
mime = self._current_media.GetMime()
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
path = client_files_manager.GetFilePath( hash, mime )
|
|
|
|
launch_path = self._new_options.GetMimeLaunch( mime )
|
|
|
|
HydrusPaths.LaunchFile( path, launch_path )
|
|
|
|
self._MediaFocusWentToExternalProgram()
|
|
|
|
|
|
def _OpenFileInWebBrowser( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
hash = self._current_media.GetHash()
|
|
mime = self._current_media.GetMime()
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
path = client_files_manager.GetFilePath( hash, mime )
|
|
|
|
ClientPaths.LaunchPathInWebBrowser( path )
|
|
|
|
self._MediaFocusWentToExternalProgram()
|
|
|
|
|
|
|
|
def _OpenFileLocation( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
hash = self._current_media.GetHash()
|
|
mime = self._current_media.GetMime()
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
path = client_files_manager.GetFilePath( hash, mime )
|
|
|
|
HydrusPaths.OpenFileLocation( path )
|
|
|
|
self._MediaFocusWentToExternalProgram()
|
|
|
|
|
|
|
|
def _OpenKnownURL( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
ClientGUIMedia.DoOpenKnownURLFromShortcut( self, self._current_media )
|
|
|
|
|
|
|
|
def _PauseCurrentMedia( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
self._media_container.Pause()
|
|
|
|
|
|
def _PausePlayCurrentMedia( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
self._media_container.PausePlay()
|
|
|
|
|
|
def _PrefetchNeighbours( self ):
|
|
|
|
pass
|
|
|
|
|
|
def _ReinitZoom( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
( self._current_zoom, self._canvas_zoom ) = CalculateCanvasZooms( self, self._current_media, media_show_action )
|
|
|
|
HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom )
|
|
|
|
|
|
def _RescueOffScreenMediaWindow( self ):
|
|
|
|
size = self._GetMediaContainerSize()
|
|
|
|
my_rect = self.rect()
|
|
media_rect = QC.QRect( self._media_window_pos, size )
|
|
|
|
if not my_rect.intersects( media_rect ):
|
|
|
|
# up/down
|
|
|
|
height_buffer = min( media_rect.height(), self.height() // 5 )
|
|
|
|
if media_rect.bottom() < my_rect.top():
|
|
|
|
media_rect.moveBottom( my_rect.top() + height_buffer )
|
|
|
|
elif media_rect.top() > my_rect.bottom():
|
|
|
|
media_rect.moveTop( my_rect.bottom() - height_buffer )
|
|
|
|
|
|
# left/right
|
|
|
|
width_buffer = min( media_rect.width(), self.width() // 5 )
|
|
|
|
if media_rect.right() < my_rect.left():
|
|
|
|
media_rect.moveRight( my_rect.left() + width_buffer )
|
|
|
|
elif media_rect.left() > my_rect.right():
|
|
|
|
media_rect.moveLeft( my_rect.right() - width_buffer )
|
|
|
|
|
|
self._media_window_pos = media_rect.topLeft()
|
|
|
|
|
|
|
|
def _ResetMediaWindowCenterPosition( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
media_size = CalculateMediaContainerSize( self._current_media, self._current_zoom, media_show_action )
|
|
|
|
x = ( my_size.width() - media_size.width() ) // 2
|
|
y = ( my_size.height() - media_size.height() ) // 2
|
|
|
|
self._media_window_pos = QC.QPoint( x, y )
|
|
|
|
self._last_drag_pos = None
|
|
|
|
# this forces a resize if needed, which may have been missed in natural event handling if the window is bigger than its parent and counts as out of view in a weird way
|
|
# this fixes the 'going from borderless to regular window on a 16:9 video on a perfect 16:9 screen doesn't scale it down until I pan it' bug
|
|
self._DrawCurrentMedia()
|
|
|
|
|
|
def _SaveCurrentMediaViewTime( self ):
|
|
|
|
now = HydrusData.GetNow()
|
|
|
|
view_timestamp = self._current_media_start_time
|
|
|
|
viewtime_delta = now - self._current_media_start_time
|
|
|
|
self._current_media_start_time = now
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
hash = self._current_media.GetHash()
|
|
|
|
HG.client_controller.file_viewing_stats_manager.FinishViewing( hash, self.CANVAS_TYPE, view_timestamp, viewtime_delta )
|
|
|
|
|
|
def _SeekDeltaCurrentMedia( self, direction, duration_ms ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
self._media_container.SeekDelta( direction, duration_ms )
|
|
|
|
|
|
def _ShowMediaInNewPage( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
hash = self._current_media.GetHash()
|
|
|
|
hashes = { hash }
|
|
|
|
HG.client_controller.pub( 'new_page_query', self._location_context, initial_hashes = hashes )
|
|
|
|
|
|
def _SizeAndPositionMediaContainer( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
new_size = self._GetMediaContainerSize()
|
|
|
|
if new_size != self._media_container.size():
|
|
|
|
self._media_container.setFixedSize( new_size )
|
|
|
|
|
|
if self._media_window_pos == self._media_container.pos():
|
|
|
|
if HC.PLATFORM_MACOS:
|
|
|
|
self._media_container.update()
|
|
|
|
|
|
else:
|
|
|
|
self._media_container.move( self._media_window_pos )
|
|
|
|
|
|
|
|
def _TryToChangeZoom( self, new_zoom, zoom_center_type_override = None ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
media_window_size = self._media_container.size()
|
|
|
|
media_window_width = media_window_size.width()
|
|
media_window_height = media_window_size.height()
|
|
|
|
new_media_window_size = CalculateMediaContainerSize( self._current_media, new_zoom, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV )
|
|
|
|
new_media_window_width = new_media_window_size.width()
|
|
new_media_window_height = new_media_window_size.height()
|
|
|
|
if new_media_window_width > 32000 or new_media_window_height > 32000:
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
|
|
old_size_bigger = my_size.width() < media_window_width or my_size.height() < media_window_height
|
|
new_size_fits = my_size.width() >= new_media_window_width and my_size.height() >= new_media_window_height
|
|
|
|
#
|
|
|
|
if media_window_width > 0 and media_window_height > 0:
|
|
|
|
if zoom_center_type_override is None:
|
|
|
|
zoom_center_type = HG.client_controller.new_options.GetInteger( 'media_viewer_zoom_center' )
|
|
|
|
else:
|
|
|
|
zoom_center_type = zoom_center_type_override
|
|
|
|
|
|
# viewer center is the default
|
|
zoom_centerpoint = QC.QPoint( my_size.width() // 2, my_size.height() // 2 )
|
|
|
|
if zoom_center_type == ZOOM_CENTERPOINT_MEDIA_CENTER:
|
|
|
|
zoom_centerpoint = self._media_window_pos + QC.QPoint( media_window_width // 2, media_window_height // 2 )
|
|
|
|
elif zoom_center_type == ZOOM_CENTERPOINT_MEDIA_TOP_LEFT:
|
|
|
|
zoom_centerpoint = self._media_window_pos
|
|
|
|
elif zoom_center_type == ZOOM_CENTERPOINT_MOUSE:
|
|
|
|
mouse_pos = self.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
if self.rect().contains( mouse_pos ):
|
|
|
|
zoom_centerpoint = mouse_pos
|
|
|
|
|
|
|
|
# probably a simpler way to calc this, but hey
|
|
widths_centerpoint_is_from_pos = ( zoom_centerpoint.x() - self._media_window_pos.x() ) / media_window_width
|
|
heights_centerpoint_is_from_pos = ( zoom_centerpoint.y() - self._media_window_pos.y() ) / media_window_height
|
|
|
|
zoom_width_delta = media_window_width - new_media_window_width
|
|
zoom_height_delta = media_window_height - new_media_window_height
|
|
|
|
centerpoint_adjusted_delta = QC.QPoint( int( zoom_width_delta * widths_centerpoint_is_from_pos ), int( zoom_height_delta * heights_centerpoint_is_from_pos ) )
|
|
|
|
self._media_window_pos += centerpoint_adjusted_delta
|
|
|
|
|
|
#
|
|
|
|
self._current_zoom = new_zoom
|
|
|
|
self._RescueOffScreenMediaWindow()
|
|
|
|
HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom )
|
|
'''
|
|
# rescue hack no longer needed as media center zoom is non-default
|
|
if old_size_bigger and new_size_fits:
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
'''
|
|
# due to the foolish 'giganto window' system for large zooms, some auto-update stuff doesn't work right if the convas rect is contained by the media rect, so do a refresh here
|
|
self._DrawCurrentMedia()
|
|
|
|
self.update()
|
|
|
|
|
|
def _Undelete( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
ClientGUIMediaActions.UndeleteMedia( self, ( self._current_media, ) )
|
|
|
|
|
|
def _ZoomIn( self, zoom_center_type_override = None ):
|
|
|
|
if self._current_media is not None and self._IsZoomable():
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = self._new_options.GetMediaZoomOptions( self._current_media.GetMime() )
|
|
|
|
possible_zooms = self._new_options.GetMediaZooms()
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
if exact_zoom <= self._current_zoom:
|
|
|
|
while exact_zoom <= self._current_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
else:
|
|
|
|
while exact_zoom / 2 > self._current_zoom:
|
|
|
|
exact_zoom /= 2
|
|
|
|
|
|
|
|
possible_zooms = self._new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
if exact_zoom > max_zoom:
|
|
|
|
return
|
|
|
|
|
|
possible_zooms = [ exact_zoom ]
|
|
|
|
|
|
possible_zooms.append( self._canvas_zoom )
|
|
|
|
bigger_zooms = [ zoom for zoom in possible_zooms if zoom > self._current_zoom ]
|
|
|
|
if len( bigger_zooms ) > 0:
|
|
|
|
new_zoom = min( bigger_zooms )
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
|
|
|
|
|
|
def _ZoomMax( self ):
|
|
|
|
if self._current_media is not None and self._IsZoomable():
|
|
|
|
possible_zooms = self._new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = self._new_options.GetMediaZoomOptions( self._current_media.GetMime() )
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
while exact_zoom * 2 <= max_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
max_zoom = exact_zoom
|
|
|
|
|
|
new_zoom = max_zoom
|
|
|
|
if self._current_zoom != new_zoom:
|
|
|
|
self._TryToChangeZoom( new_zoom )
|
|
|
|
|
|
|
|
|
|
def _ZoomOut( self, zoom_center_type_override = None ):
|
|
|
|
if self._current_media is not None and self._IsZoomable():
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = self._new_options.GetMediaZoomOptions( self._current_media.GetMime() )
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
if exact_zoom < self._current_zoom:
|
|
|
|
while exact_zoom * 2 < self._current_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
else:
|
|
|
|
while exact_zoom >= self._current_zoom:
|
|
|
|
exact_zoom /= 2
|
|
|
|
|
|
|
|
possible_zooms = [ exact_zoom ]
|
|
|
|
else:
|
|
|
|
possible_zooms = self._new_options.GetMediaZooms()
|
|
|
|
|
|
possible_zooms.append( self._canvas_zoom )
|
|
|
|
smaller_zooms = [ zoom for zoom in possible_zooms if zoom < self._current_zoom ]
|
|
|
|
if len( smaller_zooms ) > 0:
|
|
|
|
new_zoom = max( smaller_zooms )
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
|
|
|
|
|
|
def _ZoomSwitch( self, zoom_center_type_override = None ):
|
|
|
|
if self._current_media is not None and self._IsZoomable():
|
|
|
|
if self._canvas_zoom == 1.0 and self._current_zoom == 1.0:
|
|
|
|
return
|
|
|
|
|
|
if self._current_zoom == 1.0:
|
|
|
|
new_zoom = self._canvas_zoom
|
|
|
|
else:
|
|
|
|
new_zoom = 1.0
|
|
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
if new_zoom <= self._canvas_zoom:
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
|
|
|
|
|
|
def _ZoomSwitchMax( self, switch_base: float ):
|
|
|
|
if self._current_media is not None and self._IsZoomable():
|
|
|
|
if self._current_zoom == switch_base:
|
|
|
|
possible_zooms = self._new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = self._new_options.GetMediaZoomOptions( self._current_media.GetMime() )
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
while exact_zoom * 2 <= max_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
max_zoom = exact_zoom
|
|
|
|
|
|
new_zoom = max_zoom
|
|
|
|
else:
|
|
|
|
new_zoom = switch_base
|
|
|
|
|
|
self._TryToChangeZoom( new_zoom )
|
|
|
|
if new_zoom == switch_base:
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
|
|
|
|
|
|
def event( self, event ):
|
|
|
|
if event.type() == QC.QEvent.LayoutRequest:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return QW.QWidget.event( self, event )
|
|
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
self.ClearMedia()
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self.SetMedia( None )
|
|
|
|
|
|
def BeginDrag( self ):
|
|
|
|
point = self.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
self._last_drag_pos = point
|
|
self._current_drag_is_touch = False
|
|
|
|
|
|
def resizeEvent( self, event ):
|
|
|
|
my_size = self.size()
|
|
|
|
if self._current_media is not None:
|
|
|
|
media_container_size = self._media_container.size()
|
|
|
|
if my_size != media_container_size:
|
|
|
|
self._ReinitZoom()
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
def EndDrag( self ):
|
|
|
|
self._last_drag_pos = None
|
|
|
|
|
|
def FlipActiveCustomShortcutName( self, name ):
|
|
|
|
self._my_shortcuts_handler.FlipShortcuts( name )
|
|
|
|
|
|
def GetActiveCustomShortcutNames( self ):
|
|
|
|
return self._my_shortcuts_handler.GetCustomShortcutNames()
|
|
|
|
|
|
def ManageNotes( self, canvas_key, name_to_start_on = None ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ManageNotes( name_to_start_on = name_to_start_on )
|
|
|
|
|
|
|
|
def ManageTags( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ManageTags()
|
|
|
|
|
|
|
|
def MouseIsNearAnimationBar( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return self._media_container.MouseIsNearAnimationBar()
|
|
|
|
|
|
|
|
def MouseIsOverMedia( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
media_mouse_pos = self._media_container.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
media_rect = self._media_container.rect()
|
|
|
|
return media_rect.contains( media_mouse_pos )
|
|
|
|
|
|
|
|
def OpenExternally( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._OpenExternally()
|
|
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
self._DrawBackgroundBitmap( painter )
|
|
|
|
if self._current_media is not None:
|
|
|
|
self._DrawCurrentMedia()
|
|
|
|
|
|
|
|
def PauseMedia( self ):
|
|
|
|
self._PauseCurrentMedia()
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_MANAGE_FILE_RATINGS:
|
|
|
|
self._ManageRatings()
|
|
|
|
elif action == CAC.SIMPLE_MANAGE_FILE_TAGS:
|
|
|
|
self._ManageTags()
|
|
|
|
elif action == CAC.SIMPLE_MANAGE_FILE_URLS:
|
|
|
|
self._ManageURLs()
|
|
|
|
elif action == CAC.SIMPLE_MANAGE_FILE_NOTES:
|
|
|
|
self._ManageNotes()
|
|
|
|
elif action == CAC.SIMPLE_OPEN_KNOWN_URL:
|
|
|
|
self._OpenKnownURL()
|
|
|
|
elif action == CAC.SIMPLE_ARCHIVE_FILE:
|
|
|
|
self._Archive()
|
|
|
|
elif action == CAC.SIMPLE_COPY_BMP:
|
|
|
|
self._CopyBMPToClipboard()
|
|
|
|
elif action == CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE:
|
|
|
|
copied = self._CopyBMPToClipboard()
|
|
|
|
if not copied:
|
|
|
|
self._CopyFileToClipboard()
|
|
|
|
|
|
elif action == CAC.SIMPLE_COPY_FILE:
|
|
|
|
self._CopyFileToClipboard()
|
|
|
|
elif action == CAC.SIMPLE_COPY_PATH:
|
|
|
|
self._CopyPathToClipboard()
|
|
|
|
elif action == CAC.SIMPLE_COPY_SHA256_HASH:
|
|
|
|
self._CopyHashToClipboard( 'sha256' )
|
|
|
|
elif action == CAC.SIMPLE_COPY_MD5_HASH:
|
|
|
|
self._CopyHashToClipboard( 'md5' )
|
|
|
|
elif action == CAC.SIMPLE_COPY_SHA1_HASH:
|
|
|
|
self._CopyHashToClipboard( 'sha1' )
|
|
|
|
elif action == CAC.SIMPLE_COPY_SHA512_HASH:
|
|
|
|
self._CopyHashToClipboard( 'sha512' )
|
|
|
|
elif action == CAC.SIMPLE_DELETE_FILE:
|
|
|
|
self._Delete()
|
|
|
|
elif action == CAC.SIMPLE_UNDELETE_FILE:
|
|
|
|
self._Undelete()
|
|
|
|
elif action == CAC.SIMPLE_INBOX_FILE:
|
|
|
|
self._Inbox()
|
|
|
|
elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
|
|
|
|
self._OpenExternally()
|
|
|
|
elif action == CAC.SIMPLE_PAN_UP:
|
|
|
|
self._DoManualPan( 0, -1 )
|
|
|
|
elif action == CAC.SIMPLE_PAN_DOWN:
|
|
|
|
self._DoManualPan( 0, 1 )
|
|
|
|
elif action == CAC.SIMPLE_PAN_LEFT:
|
|
|
|
self._DoManualPan( -1, 0 )
|
|
|
|
elif action == CAC.SIMPLE_PAN_RIGHT:
|
|
|
|
self._DoManualPan( 1, 0 )
|
|
|
|
elif action in ( CAC.SIMPLE_PAN_TOP_EDGE, CAC.SIMPLE_PAN_BOTTOM_EDGE, CAC.SIMPLE_PAN_LEFT_EDGE, CAC.SIMPLE_PAN_RIGHT_EDGE, CAC.SIMPLE_PAN_VERTICAL_CENTER, CAC.SIMPLE_PAN_HORIZONTAL_CENTER ):
|
|
|
|
self._DoEdgePan( action )
|
|
|
|
elif action == CAC.SIMPLE_PAUSE_MEDIA:
|
|
|
|
self._PauseCurrentMedia()
|
|
|
|
elif action == CAC.SIMPLE_PAUSE_PLAY_MEDIA:
|
|
|
|
self._PausePlayCurrentMedia()
|
|
|
|
elif action == CAC.SIMPLE_MEDIA_SEEK_DELTA:
|
|
|
|
( direction, ms ) = command.GetSimpleData()
|
|
|
|
self._SeekDeltaCurrentMedia( direction, ms )
|
|
|
|
elif action == CAC.SIMPLE_MOVE_ANIMATION_TO_PREVIOUS_FRAME:
|
|
|
|
self._media_container.GotoPreviousOrNextFrame( -1 )
|
|
|
|
elif action == CAC.SIMPLE_MOVE_ANIMATION_TO_NEXT_FRAME:
|
|
|
|
self._media_container.GotoPreviousOrNextFrame( 1 )
|
|
|
|
elif action == CAC.SIMPLE_ZOOM_IN:
|
|
|
|
self._ZoomIn()
|
|
|
|
elif action == CAC.SIMPLE_ZOOM_IN_VIEWER_CENTER:
|
|
|
|
self._ZoomIn( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
|
|
|
|
elif action == CAC.SIMPLE_ZOOM_OUT:
|
|
|
|
self._ZoomOut()
|
|
|
|
elif action == CAC.SIMPLE_ZOOM_OUT_VIEWER_CENTER:
|
|
|
|
self._ZoomOut( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
|
|
|
|
elif action == CAC.SIMPLE_SWITCH_BETWEEN_100_PERCENT_AND_CANVAS_ZOOM:
|
|
|
|
self._ZoomSwitch()
|
|
|
|
elif action == CAC.SIMPLE_SWITCH_BETWEEN_100_PERCENT_AND_MAX_ZOOM:
|
|
|
|
self._ZoomSwitchMax( 1.0 )
|
|
|
|
elif action == CAC.SIMPLE_SWITCH_BETWEEN_CANVAS_AND_MAX_ZOOM:
|
|
|
|
self._ZoomSwitchMax( self._canvas_zoom )
|
|
|
|
elif action == CAC.SIMPLE_ZOOM_MAX:
|
|
|
|
self._ZoomMax()
|
|
|
|
elif action == CAC.SIMPLE_SWITCH_BETWEEN_100_PERCENT_AND_CANVAS_ZOOM_VIEWER_CENTER:
|
|
|
|
self._ZoomSwitch( zoom_center_type_override = ZOOM_CENTERPOINT_VIEWER_CENTER )
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
elif command.IsContentCommand():
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
command_processed = ClientGUIMediaActions.ApplyContentApplicationCommandToMedia( self, command, ( self._current_media, ) )
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def ResetMediaWindowCenterPosition( self ):
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
|
|
def SetLocationContext( self, location_context: ClientLocation.LocationContext ):
|
|
|
|
self._location_context = location_context
|
|
|
|
|
|
def SetMedia( self, media: typing.Optional[ ClientMedia.MediaSingleton ] ):
|
|
|
|
if media is not None and not self.isVisible():
|
|
|
|
return
|
|
|
|
|
|
if media is not None:
|
|
|
|
media = media.GetDisplayMedia()
|
|
|
|
if not self._CanDisplayMedia( media ):
|
|
|
|
media = None
|
|
|
|
|
|
|
|
if media != self._current_media:
|
|
|
|
HG.client_controller.ResetIdleTimer()
|
|
|
|
self._SaveCurrentMediaViewTime()
|
|
|
|
previous_media = self._current_media
|
|
|
|
self._current_media = media
|
|
|
|
if self._current_media is None:
|
|
|
|
self._media_container.ClearMedia()
|
|
|
|
else:
|
|
|
|
if previous_media is not None and self._maintain_pan_and_zoom:
|
|
|
|
self._MaintainZoom( previous_media )
|
|
|
|
else:
|
|
|
|
self._ReinitZoom()
|
|
|
|
|
|
if not self._maintain_pan_and_zoom:
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
|
|
initial_size = self._GetMediaContainerSize()
|
|
|
|
if self._current_media.GetLocationsManager().IsLocal() and initial_size.width() > 0 and initial_size.height() > 0:
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = self._GetShowAction( self._current_media )
|
|
|
|
self._media_container.SetMedia( self._current_media, initial_size, self._media_window_pos, media_show_action, media_start_paused, media_start_with_embed )
|
|
|
|
else:
|
|
|
|
self._current_media = None
|
|
|
|
|
|
|
|
HG.client_controller.pub( 'canvas_new_display_media', self._canvas_key, self._current_media )
|
|
|
|
HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() )
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def minimumSizeHint( self ):
|
|
|
|
return QC.QSize( 120, 120 )
|
|
|
|
|
|
def ZoomIn( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ZoomIn()
|
|
|
|
|
|
|
|
def ZoomOut( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ZoomOut()
|
|
|
|
|
|
|
|
def ZoomSwitch( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ZoomSwitch()
|
|
|
|
|
|
|
|
class MediaContainerDragClickReportingFilter( QC.QObject ):
|
|
|
|
def __init__( self, parent: Canvas ):
|
|
|
|
QC.QObject.__init__( self, parent )
|
|
|
|
self._canvas = parent
|
|
|
|
|
|
def eventFilter( self, watched, event ):
|
|
|
|
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.LeftButton:
|
|
|
|
self._canvas.BeginDrag()
|
|
|
|
elif event.type() == QC.QEvent.MouseButtonRelease and event.button() == QC.Qt.LeftButton:
|
|
|
|
self._canvas.EndDrag()
|
|
|
|
|
|
return False
|
|
|
|
|
|
class CanvasPanel( Canvas ):
|
|
|
|
PREVIEW_WINDOW = True
|
|
CANVAS_TYPE = CC.CANVAS_PREVIEW
|
|
|
|
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext ):
|
|
|
|
Canvas.__init__( self, parent, location_context )
|
|
|
|
self._page_key = page_key
|
|
|
|
self._hidden_page_current_media = None
|
|
self._hidden_page_paused_status = False
|
|
|
|
self._media_container.launchMediaViewer.connect( self.LaunchMediaViewer )
|
|
|
|
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
|
|
|
|
|
|
def mouseReleaseEvent( self, event ):
|
|
|
|
if event.button() != QC.Qt.RightButton:
|
|
|
|
Canvas.mouseReleaseEvent( self, event )
|
|
|
|
return
|
|
|
|
|
|
# contextmenu doesn't quite work here yet due to focus issues
|
|
|
|
self.ShowMenu()
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self._hidden_page_current_media = None
|
|
|
|
Canvas.ClearMedia( self )
|
|
|
|
|
|
def PageHidden( self ):
|
|
|
|
if self._hidden_page_current_media is not None:
|
|
|
|
return
|
|
|
|
|
|
# TODO: ultimately, make an object for media/paused/position and have any media player able to give that and take it instead of setmedia
|
|
# then we'll be able to 'continue' playing state from preview to full view and other stuff like this, and simply
|
|
# also use that for all setmedia, and then if we have %-in-start options and paused/play-start options, we can initialise this object for that
|
|
hidden_page_current_media = self._current_media
|
|
hidden_page_pause_status = self._media_container.IsPaused()
|
|
|
|
self.ClearMedia()
|
|
|
|
self._hidden_page_current_media = hidden_page_current_media
|
|
self._hidden_page_paused_status = hidden_page_pause_status
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
self.SetMedia( self._hidden_page_current_media )
|
|
|
|
self._hidden_page_current_media = None
|
|
|
|
if self._media_container.IsPaused() != self._hidden_page_paused_status:
|
|
|
|
self._media_container.PausePlay()
|
|
|
|
|
|
|
|
def ShowMenu( self ):
|
|
|
|
menu = QW.QMenu()
|
|
|
|
if self._current_media is not None:
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
advanced_mode = new_options.GetBoolean( 'advanced_mode' )
|
|
|
|
services = HG.client_controller.services_manager.GetServices()
|
|
|
|
locations_manager = self._current_media.GetLocationsManager()
|
|
|
|
local_ratings_services = [ service for service in services if service.GetServiceType() in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) ]
|
|
|
|
i_can_post_ratings = len( local_ratings_services ) > 0
|
|
|
|
#
|
|
|
|
info_lines = self._current_media.GetPrettyInfoLines()
|
|
|
|
top_line = info_lines.pop(0)
|
|
|
|
info_menu = QW.QMenu( menu )
|
|
|
|
for line in info_lines:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( info_menu, line, line )
|
|
|
|
|
|
ClientGUIMedia.AddFileViewingStatsMenu( info_menu, ( self._current_media, ) )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, info_menu, top_line )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
|
|
AddAudioVolumeMenu( menu, self.CANVAS_TYPE )
|
|
|
|
if self._current_media is not None:
|
|
|
|
#
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if self._current_media.HasInbox():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'archive', 'Archive this file.', self._Archive )
|
|
|
|
|
|
if self._current_media.HasArchive():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'inbox', 'Send this files back to the inbox.', self._Inbox )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
|
|
|
# brush this up to handle different service keys
|
|
# undelete do an optional service key too
|
|
|
|
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = HG.client_controller.services_manager.GetName )
|
|
|
|
for file_service_key in local_file_service_keys_we_are_in:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'delete from {}'.format( HG.client_controller.services_manager.GetName( file_service_key ) ), 'Delete this file.', self._Delete, file_service_key = file_service_key )
|
|
|
|
|
|
if locations_manager.IsTrashed():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'delete completely', 'Physically delete this file from disk.', self._Delete, file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'undelete', 'Take this file out of the trash.', self._Undelete )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
manage_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'tags', 'Manage this file\'s tags.', self._ManageTags )
|
|
|
|
if i_can_post_ratings:
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'ratings', 'Manage this file\'s ratings.', self._ManageRatings )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'urls', 'Manage this file\'s known URLs.', self._ManageURLs )
|
|
|
|
num_notes = self._current_media.GetNotesManager().GetNumNotes()
|
|
|
|
notes_str = 'notes'
|
|
|
|
if num_notes > 0:
|
|
|
|
notes_str = '{} ({})'.format( notes_str, HydrusData.ToHumanInt( num_notes ) )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
|
|
|
|
ClientGUIMedia.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
|
|
|
|
ClientGUIMedia.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
|
|
|
|
open_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in external program', 'Open this file in your OS\'s default program.', self._OpenExternally )
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Show your current media in a simple new page.', self._ShowMediaInNewPage )
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in web browser', 'Show this file in your OS\'s web browser.', self._OpenFileInWebBrowser )
|
|
|
|
show_open_in_explorer = advanced_mode and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
|
|
|
|
if show_open_in_explorer:
|
|
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in file browser', 'Show this file in your OS\'s file browser.', self._OpenFileLocation )
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
|
|
|
|
share_menu = QW.QMenu( menu )
|
|
|
|
copy_menu = QW.QMenu( share_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
|
|
|
|
copy_hash_menu = QW.QMenu( copy_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash.', self._CopyHashToClipboard, 'sha512' )
|
|
|
|
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
|
|
|
|
if advanced_mode:
|
|
|
|
hash_id_str = str( self._current_media.GetHashId() )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', HG.client_controller.pub, 'clipboard', 'text', hash_id_str )
|
|
|
|
|
|
if self._current_media.GetMime() in HC.IMAGES:
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'image (bitmap)', 'Copy this file to your clipboard as a bmp.', self._CopyBMPToClipboard )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy this file\'s path to your clipboard.', self._CopyPathToClipboard )
|
|
|
|
ClientGUIMenus.AppendMenu( share_menu, copy_menu, 'copy' )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
def LaunchMediaViewer( self ):
|
|
|
|
HG.client_controller.pub( 'launch_media_viewer', self._page_key )
|
|
|
|
|
|
def MediaFocusWentToExternalProgram( self, page_key ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
self._MediaFocusWentToExternalProgram()
|
|
|
|
|
|
|
|
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
my_hash = self._current_media.GetHash()
|
|
|
|
do_redraw = False
|
|
|
|
for ( service_key, content_updates ) in service_keys_to_content_updates.items():
|
|
|
|
if True in ( my_hash in content_update.GetHashes() for content_update in content_updates ):
|
|
|
|
do_redraw = True
|
|
|
|
break
|
|
|
|
|
|
|
|
if do_redraw:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
|
|
def SetMedia( self, media ):
|
|
|
|
if HC.options[ 'hide_preview' ]:
|
|
|
|
return
|
|
|
|
|
|
Canvas.SetMedia( self, media )
|
|
|
|
|
|
class CanvasWithDetails( Canvas ):
|
|
|
|
def __init__( self, parent, location_context ):
|
|
|
|
Canvas.__init__( self, parent, location_context )
|
|
|
|
HG.client_controller.sub( self, 'RedrawDetails', 'refresh_all_tag_presentation_gui' )
|
|
|
|
|
|
def _DrawAdditionalTopMiddleInfo( self, painter: QG.QPainter, current_y ):
|
|
|
|
pass
|
|
|
|
|
|
def _DrawBackgroundDetails( self, painter: QG.QPainter ):
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
if self._current_media is None:
|
|
|
|
text = self._GetNoMediaText()
|
|
|
|
( text_size, text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, text )
|
|
|
|
x = ( my_width - text_size.width() ) // 2
|
|
y = ( my_height - text_size.height() ) // 2
|
|
|
|
ClientGUIFunctions.DrawText( painter, x, y, text )
|
|
|
|
else:
|
|
|
|
self._DrawTags( painter )
|
|
|
|
self._DrawTopMiddle( painter )
|
|
|
|
current_y = self._DrawTopRight( painter )
|
|
|
|
self._DrawNotes( painter, current_y )
|
|
|
|
self._DrawIndexAndZoom( painter )
|
|
|
|
|
|
|
|
def _DrawIndexAndZoom( self, painter: QG.QPainter ):
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
# bottom-right index
|
|
|
|
bottom_right_string = ClientData.ConvertZoomToPercentage( self._current_zoom )
|
|
|
|
index_string = self._GetIndexString()
|
|
|
|
if len( index_string ) > 0:
|
|
|
|
bottom_right_string = '{} - {}'.format( bottom_right_string, index_string )
|
|
|
|
|
|
( text_size, bottom_right_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, bottom_right_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, my_width - text_size.width() - 3, my_height - text_size.height() - 3, bottom_right_string )
|
|
|
|
|
|
def _DrawNotes( self, painter: QG.QPainter, current_y: int ):
|
|
|
|
notes_manager = self._current_media.GetNotesManager()
|
|
|
|
names_to_notes = notes_manager.GetNamesToNotes()
|
|
|
|
if len( names_to_notes ) == 0:
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
max_notes_width_percentage = 20
|
|
|
|
PADDING = 4
|
|
|
|
max_notes_width = int( my_width * ( max_notes_width_percentage / 100 ) ) - ( PADDING * 2 )
|
|
|
|
notes_width = 0
|
|
|
|
original_font = painter.font()
|
|
|
|
name_font = QG.QFont( original_font )
|
|
name_font.setBold( True )
|
|
|
|
notes_font = QG.QFont( original_font )
|
|
notes_font.setBold( False )
|
|
|
|
for ( name, note ) in names_to_notes.items():
|
|
|
|
# without wrapping, let's see if we fit into a smaller box than the max possible
|
|
|
|
painter.setFont( name_font )
|
|
|
|
name_text_size = painter.fontMetrics().size( 0, name )
|
|
|
|
painter.setFont( notes_font )
|
|
|
|
note_text_size = painter.fontMetrics().size( 0, note )
|
|
|
|
notes_width = max( notes_width, name_text_size.width(), note_text_size.width() )
|
|
|
|
if notes_width > max_notes_width:
|
|
|
|
notes_width = max_notes_width
|
|
|
|
break
|
|
|
|
|
|
|
|
left_x = my_width - ( notes_width + PADDING )
|
|
|
|
current_y += PADDING * 2
|
|
|
|
draw_a_test_rect = False
|
|
|
|
if draw_a_test_rect:
|
|
|
|
painter.setPen( QG.QPen( QG.QColor( 20, 20, 20 ) ) )
|
|
painter.setBrush( QC.Qt.NoBrush )
|
|
|
|
painter.drawRect( left_x, current_y, notes_width, 100 )
|
|
|
|
|
|
for name in sorted( names_to_notes.keys() ):
|
|
|
|
painter.setFont( name_font )
|
|
|
|
text_rect = painter.fontMetrics().boundingRect( left_x, current_y, notes_width, 100, QC.Qt.AlignHCenter | QC.Qt.TextWordWrap, name )
|
|
|
|
painter.drawText( text_rect, QC.Qt.AlignHCenter | QC.Qt.TextWordWrap, name )
|
|
|
|
current_y += text_rect.height() + PADDING
|
|
|
|
#
|
|
|
|
painter.setFont( notes_font )
|
|
|
|
note = notes_manager.GetNote( name )
|
|
|
|
text_rect = painter.fontMetrics().boundingRect( left_x, current_y, notes_width, 100, QC.Qt.AlignJustify | QC.Qt.TextWordWrap, note )
|
|
|
|
painter.drawText( text_rect, QC.Qt.AlignJustify | QC.Qt.TextWordWrap, note )
|
|
|
|
current_y += text_rect.height() + PADDING
|
|
|
|
if current_y >= my_height:
|
|
|
|
break
|
|
|
|
|
|
# draw a horizontal line
|
|
|
|
|
|
|
|
painter.setFont( original_font )
|
|
|
|
|
|
def _DrawTags( self, painter: QG.QPainter ):
|
|
|
|
# tags on the top left
|
|
|
|
original_pen = painter.pen()
|
|
|
|
painter.setFont( QW.QApplication.font() )
|
|
|
|
tags_manager = self._current_media.GetTagsManager()
|
|
|
|
current = tags_manager.GetCurrent( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA )
|
|
pending = tags_manager.GetPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA )
|
|
petitioned = tags_manager.GetPetitioned( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA )
|
|
|
|
tags_i_want_to_display = set()
|
|
|
|
tags_i_want_to_display.update( current )
|
|
tags_i_want_to_display.update( pending )
|
|
tags_i_want_to_display.update( petitioned )
|
|
|
|
tags_i_want_to_display = list( tags_i_want_to_display )
|
|
|
|
tag_sort = HG.client_controller.new_options.GetDefaultTagSort()
|
|
|
|
ClientTagSorting.SortTags( tag_sort, tags_i_want_to_display )
|
|
|
|
current_y = 3
|
|
|
|
namespace_colours = HC.options[ 'namespace_colours' ]
|
|
|
|
for tag in tags_i_want_to_display:
|
|
|
|
display_string = ClientTags.RenderTag( tag, True )
|
|
|
|
if tag in pending:
|
|
|
|
display_string += ' (+)'
|
|
|
|
|
|
if tag in petitioned:
|
|
|
|
display_string += ' (-)'
|
|
|
|
|
|
( namespace, subtag ) = HydrusTags.SplitTag( tag )
|
|
|
|
if namespace in namespace_colours:
|
|
|
|
( r, g, b ) = namespace_colours[ namespace ]
|
|
|
|
else:
|
|
|
|
( r, g, b ) = namespace_colours[ None ]
|
|
|
|
|
|
painter.setPen( QG.QPen( QG.QColor( r, g, b ) ) )
|
|
|
|
( text_size, display_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, display_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, 5, current_y, display_string )
|
|
|
|
current_y += text_size.height()
|
|
|
|
|
|
painter.setPen( original_pen )
|
|
|
|
|
|
def _DrawTopMiddle( self, painter: QG.QPainter ):
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
# top-middle
|
|
|
|
painter.setPen( QG.QPen( self._new_options.GetColour( CC.COLOUR_MEDIA_TEXT ) ) )
|
|
|
|
current_y = 3
|
|
|
|
title_string = self._current_media.GetTitleString()
|
|
|
|
if len( title_string ) > 0:
|
|
|
|
( text_size, title_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, title_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, ( my_width - text_size.width() ) // 2, current_y, title_string )
|
|
|
|
current_y += text_size.height() + 3
|
|
|
|
|
|
info_string = self._GetInfoString()
|
|
|
|
( text_size, info_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, info_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, ( my_width - text_size.width() ) // 2, current_y, info_string )
|
|
|
|
current_y += text_size.height() + 3
|
|
|
|
self._DrawAdditionalTopMiddleInfo( painter, current_y )
|
|
|
|
|
|
def _DrawTopRight( self, painter: QG.QPainter ) -> int:
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
current_y = 2
|
|
|
|
# ratings
|
|
|
|
services_manager = HG.client_controller.services_manager
|
|
|
|
like_services = services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, ) )
|
|
|
|
like_services.reverse()
|
|
|
|
like_rating_current_x = my_width - 16 - 2 # -2 to line up exactly with the floating panel
|
|
|
|
for like_service in like_services:
|
|
|
|
service_key = like_service.GetServiceKey()
|
|
|
|
rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), service_key )
|
|
|
|
ClientGUIRatings.DrawLike( painter, like_rating_current_x, current_y, service_key, rating_state )
|
|
|
|
like_rating_current_x -= 16
|
|
|
|
|
|
if len( like_services ) > 0:
|
|
|
|
current_y += 18
|
|
|
|
|
|
numerical_services = services_manager.GetServices( ( HC.LOCAL_RATING_NUMERICAL, ) )
|
|
|
|
for numerical_service in numerical_services:
|
|
|
|
service_key = numerical_service.GetServiceKey()
|
|
|
|
( rating_state, rating ) = ClientRatings.GetNumericalStateFromMedia( ( self._current_media, ), service_key )
|
|
|
|
numerical_width = ClientGUIRatings.GetNumericalWidth( service_key )
|
|
|
|
ClientGUIRatings.DrawNumerical( painter, my_width - numerical_width - 2, current_y, service_key, rating_state, rating ) # -2 to line up exactly with the floating panel
|
|
|
|
current_y += 18
|
|
|
|
|
|
# icons
|
|
|
|
icons_to_show = []
|
|
|
|
if self._current_media.GetLocationsManager().IsTrashed():
|
|
|
|
icons_to_show.append( CC.global_pixmaps().trash )
|
|
|
|
|
|
if self._current_media.HasInbox():
|
|
|
|
icons_to_show.append( CC.global_pixmaps().inbox )
|
|
|
|
|
|
if len( icons_to_show ) > 0:
|
|
|
|
icon_x = 0
|
|
|
|
for icon in icons_to_show:
|
|
|
|
painter.drawPixmap( my_width + icon_x - 18, current_y, icon )
|
|
|
|
icon_x -= 18
|
|
|
|
|
|
current_y += 18
|
|
|
|
|
|
painter.setPen( QG.QPen( self._new_options.GetColour( CC.COLOUR_MEDIA_TEXT ) ) )
|
|
|
|
# repo strings
|
|
|
|
remote_strings = self._current_media.GetLocationsManager().GetRemoteLocationStrings()
|
|
|
|
for remote_string in remote_strings:
|
|
|
|
( text_size, remote_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, remote_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, my_width - text_size.width() - 3, current_y, remote_string )
|
|
|
|
current_y += text_size.height()
|
|
|
|
|
|
# urls
|
|
|
|
urls = self._current_media.GetLocationsManager().GetURLs()
|
|
|
|
url_tuples = HG.client_controller.network_engine.domain_manager.ConvertURLsToMediaViewerTuples( urls )
|
|
|
|
for ( display_string, url ) in url_tuples:
|
|
|
|
( text_size, display_string ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, display_string )
|
|
|
|
ClientGUIFunctions.DrawText( painter, my_width - text_size.width() - 3, current_y, display_string )
|
|
|
|
current_y += text_size.height() + 2
|
|
|
|
|
|
return current_y
|
|
|
|
|
|
def _GetInfoString( self ):
|
|
|
|
lines = self._current_media.GetPrettyInfoLines( only_interesting_lines = True )
|
|
|
|
lines.insert( 1, ClientData.ConvertZoomToPercentage( self._current_zoom ) )
|
|
|
|
info_string = ' | '.join( lines )
|
|
|
|
return info_string
|
|
|
|
|
|
def _GetNoMediaText( self ):
|
|
|
|
return 'No media to display'
|
|
|
|
|
|
def RedrawDetails( self ):
|
|
|
|
self.update()
|
|
|
|
|
|
def TryToDoPreClose( self ):
|
|
|
|
can_close = True
|
|
|
|
return can_close
|
|
|
|
|
|
class CanvasWithHovers( CanvasWithDetails ):
|
|
|
|
def __init__( self, parent, location_context ):
|
|
|
|
CanvasWithDetails.__init__( self, parent, location_context )
|
|
|
|
self._hovers = []
|
|
|
|
top_hover = self._GenerateHoverTopFrame()
|
|
|
|
top_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
|
|
|
|
self._hovers.append( top_hover )
|
|
|
|
self._my_shortcuts_handler.AddWindowToFilter( top_hover )
|
|
|
|
tags_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameTags( self, self, top_hover, self._canvas_key )
|
|
|
|
tags_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
|
|
|
|
self._hovers.append( tags_hover )
|
|
|
|
self._my_shortcuts_handler.AddWindowToFilter( tags_hover )
|
|
|
|
top_right_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameTopRight( self, self, top_hover, self._canvas_key )
|
|
|
|
top_right_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
|
|
|
|
self._hovers.append( top_right_hover )
|
|
|
|
self._my_shortcuts_handler.AddWindowToFilter( top_right_hover )
|
|
|
|
self._right_notes_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameRightNotes( self, self, top_right_hover, self._canvas_key )
|
|
|
|
self._right_notes_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
|
|
|
|
self._hovers.append( self._right_notes_hover )
|
|
|
|
self._my_shortcuts_handler.AddWindowToFilter( self._right_notes_hover )
|
|
|
|
for name in self._new_options.GetStringList( 'default_media_viewer_custom_shortcuts' ):
|
|
|
|
self._my_shortcuts_handler.AddShortcuts( name )
|
|
|
|
|
|
#
|
|
|
|
self._timer_cursor_hide_job = None
|
|
self._last_cursor_autohide_touch_time = HydrusData.GetNowFloat()
|
|
|
|
# need this as we need un-button-pressed move events for cursor hide
|
|
self.setMouseTracking( True )
|
|
|
|
self._RestartCursorHideWait()
|
|
|
|
HG.client_controller.sub( self, 'CloseFromHover', 'canvas_close' )
|
|
HG.client_controller.sub( self, 'FullscreenSwitch', 'canvas_fullscreen_switch' )
|
|
|
|
HG.client_controller.gui.RegisterUIUpdateWindow( self )
|
|
|
|
|
|
def _GenerateHoverTopFrame( self ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def _HideCursorCheck( self ):
|
|
|
|
hide_time_ms = HG.client_controller.new_options.GetNoneableInteger( 'media_viewer_cursor_autohide_time_ms' )
|
|
|
|
if hide_time_ms is None:
|
|
|
|
return
|
|
|
|
|
|
hide_time = hide_time_ms / 1000
|
|
|
|
can_hide = HydrusData.TimeHasPassedFloat( self._last_cursor_autohide_touch_time + hide_time )
|
|
|
|
can_check_again = ClientGUIFunctions.MouseIsOverWidget( self )
|
|
|
|
if not CC.CAN_HIDE_MOUSE:
|
|
|
|
can_hide = False
|
|
|
|
|
|
if CGC.core().MenuIsOpen():
|
|
|
|
can_hide = False
|
|
|
|
|
|
if ClientGUIFunctions.DialogIsOpen():
|
|
|
|
can_hide = False
|
|
|
|
can_check_again = False
|
|
|
|
|
|
if can_hide:
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.BlankCursor ) )
|
|
|
|
elif can_check_again:
|
|
|
|
self._RestartCursorHideCheckJob()
|
|
|
|
|
|
|
|
def _RestartCursorHideWait( self ):
|
|
|
|
self._last_cursor_autohide_touch_time = HydrusData.GetNowFloat()
|
|
|
|
self._RestartCursorHideCheckJob()
|
|
|
|
|
|
def _RestartCursorHideCheckJob( self ):
|
|
|
|
if self._timer_cursor_hide_job is not None:
|
|
|
|
timer_is_running_or_finished = self._timer_cursor_hide_job.CurrentlyWorking() or self._timer_cursor_hide_job.IsWorkComplete()
|
|
|
|
if not timer_is_running_or_finished:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._timer_cursor_hide_job = HG.client_controller.CallLaterQtSafe( self, 0.1, 'hide cursor check', self._HideCursorCheck )
|
|
|
|
|
|
def _TryToCloseWindow( self ):
|
|
|
|
self.window().close()
|
|
|
|
|
|
def CloseFromHover( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
|
|
|
|
def FullscreenSwitch( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self.parentWidget().FullscreenSwitch()
|
|
|
|
|
|
|
|
def mouseMoveEvent( self, event ):
|
|
|
|
current_focus_tlw = QW.QApplication.activeWindow()
|
|
|
|
my_tlw = self.window()
|
|
|
|
if isinstance( current_focus_tlw, ClientGUICanvasHoverFrames.CanvasHoverFrame ) and ClientGUIFunctions.IsQtAncestor( current_focus_tlw, my_tlw, through_tlws = True ):
|
|
|
|
my_tlw.activateWindow()
|
|
|
|
|
|
#
|
|
|
|
CC.CAN_HIDE_MOUSE = True
|
|
|
|
# due to the mouse setPos below, the event pos can get funky I think due to out of order coordinate setting events, so we'll poll current value directly
|
|
event_pos = self.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
mouse_currently_shown = self.cursor() == QG.QCursor( QC.Qt.ArrowCursor )
|
|
show_mouse = mouse_currently_shown
|
|
|
|
is_dragging = event.buttons() & QC.Qt.LeftButton and self._last_drag_pos is not None
|
|
has_moved = event_pos != self._last_motion_pos
|
|
|
|
if is_dragging:
|
|
|
|
delta = event_pos - self._last_drag_pos
|
|
|
|
approx_distance = delta.manhattanLength()
|
|
|
|
if approx_distance > 0:
|
|
|
|
touchscreen_canvas_drags_unanchor = HG.client_controller.new_options.GetBoolean( 'touchscreen_canvas_drags_unanchor' )
|
|
|
|
if not self._current_drag_is_touch and approx_distance > 50:
|
|
|
|
# if user is able to generate such a large distance, they are almost certainly touching
|
|
|
|
self._current_drag_is_touch = True
|
|
|
|
|
|
# touch events obviously don't mix with warping well. the touch just warps it back and again and we get a massive delta!
|
|
|
|
touch_anchor_override = touchscreen_canvas_drags_unanchor and self._current_drag_is_touch
|
|
anchor_and_hide_canvas_drags = HG.client_controller.new_options.GetBoolean( 'anchor_and_hide_canvas_drags' )
|
|
|
|
if anchor_and_hide_canvas_drags and not touch_anchor_override:
|
|
|
|
show_mouse = False
|
|
|
|
global_mouse_pos = self.mapToGlobal( self._last_drag_pos )
|
|
|
|
QG.QCursor.setPos( global_mouse_pos )
|
|
|
|
ClientGUIShortcuts.CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH += approx_distance
|
|
|
|
else:
|
|
|
|
show_mouse = True
|
|
|
|
self._last_drag_pos = QC.QPoint( event_pos )
|
|
|
|
|
|
self._media_window_pos += delta
|
|
|
|
self._DrawCurrentMedia()
|
|
|
|
|
|
else:
|
|
|
|
if has_moved:
|
|
|
|
self._last_motion_pos = QC.QPoint( event_pos )
|
|
|
|
show_mouse = True
|
|
|
|
|
|
|
|
if show_mouse:
|
|
|
|
if not mouse_currently_shown:
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.ArrowCursor ) )
|
|
|
|
|
|
self._RestartCursorHideWait()
|
|
|
|
else:
|
|
|
|
if mouse_currently_shown:
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.BlankCursor ) )
|
|
|
|
|
|
|
|
CanvasWithDetails.mouseMoveEvent( self, event )
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER:
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
if not command_processed:
|
|
|
|
command_processed = CanvasWithDetails.ProcessApplicationCommand( self, command )
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def TIMERUIUpdate( self ):
|
|
|
|
for hover in self._hovers:
|
|
|
|
hover.DoRegularHideShow()
|
|
|
|
|
|
class CanvasFilterDuplicates( CanvasWithHovers ):
|
|
|
|
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER_DUPLICATES
|
|
|
|
showPairInPage = QC.Signal( list )
|
|
|
|
def __init__( self, parent, file_search_context: ClientSearch.FileSearchContext, both_files_match, pixel_dupes_preference, max_hamming_distance ):
|
|
|
|
location_context = file_search_context.GetLocationContext()
|
|
|
|
CanvasWithHovers.__init__( self, parent, location_context )
|
|
|
|
hover = ClientGUICanvasHoverFrames.CanvasHoverFrameRightDuplicates( self, self, self._right_notes_hover, self._canvas_key )
|
|
|
|
hover.showPairInPage.connect( self._ShowPairInPage )
|
|
hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
|
|
|
|
self._hovers.append( hover )
|
|
|
|
self._my_shortcuts_handler.AddWindowToFilter( hover )
|
|
|
|
self._file_search_context = file_search_context
|
|
self._both_files_match = both_files_match
|
|
self._pixel_dupes_preference = pixel_dupes_preference
|
|
self._max_hamming_distance = max_hamming_distance
|
|
|
|
self._maintain_pan_and_zoom = True
|
|
|
|
self._currently_fetching_pairs = False
|
|
|
|
self._batch_of_pairs_to_process = []
|
|
self._current_pair_index = 0
|
|
self._processed_pairs = []
|
|
self._hashes_due_to_be_deleted_in_this_batch = set()
|
|
|
|
# ok we started excluding pairs if they had been deleted, now I am extending it to any files that have been processed.
|
|
# main thing is if you have AB, AC, that's neat and a bunch of people want it, but current processing system doesn't do B->A->C merge if it happens in a single batch
|
|
# I need to store dupe merge options rather than content updates apply them in db transaction or do the retroactive sync or similar to get this done properly
|
|
# so regrettably I turn it off for now
|
|
|
|
self._hashes_processed_in_this_batch = set()
|
|
|
|
self._media_list = ClientMedia.ListeningMediaList( location_context, [] )
|
|
|
|
self._my_shortcuts_handler.AddShortcuts( 'media_viewer_browser' )
|
|
self._my_shortcuts_handler.AddShortcuts( 'duplicate_filter' )
|
|
|
|
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
|
|
HG.client_controller.sub( self, 'Delete', 'canvas_delete' )
|
|
HG.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
|
|
HG.client_controller.sub( self, 'SwitchMedia', 'canvas_show_next' )
|
|
HG.client_controller.sub( self, 'SwitchMedia', 'canvas_show_previous' )
|
|
|
|
QP.CallAfter( self._LoadNextBatchOfPairs() )
|
|
|
|
|
|
def _CommitProcessed( self, blocking = True ):
|
|
|
|
pair_info = []
|
|
|
|
for ( media_result_pair, duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) in self._processed_pairs:
|
|
|
|
if duplicate_type is None:
|
|
|
|
if len( list_of_service_keys_to_content_updates ) > 0:
|
|
|
|
for service_keys_to_content_updates in list_of_service_keys_to_content_updates:
|
|
|
|
if blocking:
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
else:
|
|
|
|
HG.client_controller.Write( 'content_updates', service_keys_to_content_updates )
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
if was_auto_skipped:
|
|
|
|
continue # it was a 'skip' decision
|
|
|
|
|
|
first_hash = first_media.GetHash()
|
|
second_hash = second_media.GetHash()
|
|
|
|
pair_info.append( ( duplicate_type, first_hash, second_hash, list_of_service_keys_to_content_updates ) )
|
|
|
|
|
|
if len( pair_info ) > 0:
|
|
|
|
if blocking:
|
|
|
|
HG.client_controller.WriteSynchronous( 'duplicate_pair_status', pair_info )
|
|
|
|
else:
|
|
|
|
HG.client_controller.Write( 'duplicate_pair_status', pair_info )
|
|
|
|
|
|
|
|
self._processed_pairs = []
|
|
self._hashes_due_to_be_deleted_in_this_batch = set()
|
|
self._hashes_processed_in_this_batch = set()
|
|
|
|
|
|
def _CurrentMediaIsBetter( self, delete_second = True ):
|
|
|
|
self._ProcessPair( HC.DUPLICATE_BETTER, delete_second = delete_second )
|
|
|
|
|
|
def _Delete( self, media = None, reason = None, file_service_key = None ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
|
|
first_media = self._current_media
|
|
second_media = self._media_list.GetNext( self._current_media )
|
|
|
|
message = 'Delete just this file, or both?'
|
|
|
|
yes_tuples = []
|
|
|
|
yes_tuples.append( ( 'delete just this one', 'current' ) )
|
|
yes_tuples.append( ( 'delete both', 'both' ) )
|
|
|
|
try:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return False
|
|
|
|
|
|
if result == 'current':
|
|
|
|
media = [ first_media ]
|
|
|
|
default_reason = 'Deleted manually in Duplicate Filter.'
|
|
|
|
elif result == 'both':
|
|
|
|
media = [ first_media, second_media ]
|
|
|
|
default_reason = 'Deleted manually in Duplicate Filter, along with its potential duplicate.'
|
|
|
|
|
|
jobs = CanvasWithHovers._Delete( self, media = media, default_reason = default_reason, file_service_key = file_service_key, just_get_jobs = True )
|
|
|
|
deleted = isinstance( jobs, list ) and len( jobs ) > 0
|
|
|
|
if deleted:
|
|
|
|
for m in media:
|
|
|
|
self._hashes_due_to_be_deleted_in_this_batch.update( m.GetHashes() )
|
|
|
|
|
|
was_auto_skipped = False
|
|
|
|
self._processed_pairs.append( ( self._batch_of_pairs_to_process[ self._current_pair_index ], None, None, None, jobs, was_auto_skipped ) )
|
|
|
|
self._ShowNextPair()
|
|
|
|
|
|
return deleted
|
|
|
|
|
|
def _DoCustomAction( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
duplicate_types = [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE ]
|
|
|
|
choice_tuples = [ ( HC.duplicate_type_string_lookup[ duplicate_type ], duplicate_type ) for duplicate_type in duplicate_types ]
|
|
|
|
try:
|
|
|
|
duplicate_type = ClientGUIDialogsQuick.SelectFromList( self, 'select duplicate type', choice_tuples )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
if duplicate_type in [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] or ( new_options.GetBoolean( 'advanced_mode' ) and duplicate_type == HC.DUPLICATE_ALTERNATE ):
|
|
|
|
duplicate_action_options = new_options.GetDuplicateActionOptions( duplicate_type )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit duplicate merge options' ) as dlg_2:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditDuplicateActionOptionsPanel( dlg_2, duplicate_type, duplicate_action_options, for_custom_action = True )
|
|
|
|
dlg_2.SetPanel( panel )
|
|
|
|
if dlg_2.exec() == QW.QDialog.Accepted:
|
|
|
|
duplicate_action_options = panel.GetValue()
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
else:
|
|
|
|
duplicate_action_options = None
|
|
|
|
|
|
message = 'Delete any of the files?'
|
|
|
|
yes_tuples = []
|
|
|
|
yes_tuples.append( ( 'delete neither', 'delete_neither' ) )
|
|
yes_tuples.append( ( 'delete this one', 'delete_first' ) )
|
|
yes_tuples.append( ( 'delete the other', 'delete_second' ) )
|
|
yes_tuples.append( ( 'delete both', 'delete_both' ) )
|
|
|
|
try:
|
|
|
|
result = ClientGUIDialogsQuick.GetYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
delete_first = False
|
|
delete_second = False
|
|
delete_both = False
|
|
|
|
if result == 'delete_first':
|
|
|
|
delete_first = True
|
|
|
|
elif result == 'delete_second':
|
|
|
|
delete_second = True
|
|
|
|
elif result == 'delete_both':
|
|
|
|
delete_both = True
|
|
|
|
|
|
self._ProcessPair( duplicate_type, delete_first = delete_first, delete_second = delete_second, delete_both = delete_both, duplicate_action_options = duplicate_action_options )
|
|
|
|
|
|
def _DrawBackgroundDetails( self, painter ):
|
|
|
|
if self._currently_fetching_pairs:
|
|
|
|
text = 'Loading pairs\u2026'
|
|
|
|
( text_size, text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, text )
|
|
|
|
my_size = self.size()
|
|
|
|
x = ( my_size.width() - text_size.width() ) // 2
|
|
y = ( my_size.height() - text_size.height() ) // 2
|
|
|
|
ClientGUIFunctions.DrawText( painter, x, y, text )
|
|
|
|
else:
|
|
|
|
CanvasWithHovers._DrawBackgroundDetails( self, painter )
|
|
|
|
|
|
|
|
def _GenerateHoverTopFrame( self ):
|
|
|
|
return ClientGUICanvasHoverFrames.CanvasHoverFrameTopDuplicatesFilter( self, self, self._canvas_key )
|
|
|
|
|
|
def _GetBackgroundColour( self ):
|
|
|
|
normal_colour = self._new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND )
|
|
|
|
if self._current_media is None or len( self._media_list ) == 0:
|
|
|
|
return normal_colour
|
|
|
|
else:
|
|
|
|
if self._current_media == self._media_list.GetFirst():
|
|
|
|
return normal_colour
|
|
|
|
else:
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
duplicate_intensity = new_options.GetNoneableInteger( 'duplicate_background_switch_intensity' )
|
|
|
|
return ClientGUIFunctions.GetLighterDarkerColour( normal_colour, duplicate_intensity )
|
|
|
|
|
|
|
|
|
|
def _GetIndexString( self ):
|
|
|
|
if self._current_media is None or len( self._media_list ) == 0:
|
|
|
|
return '-'
|
|
|
|
else:
|
|
|
|
current_media_label = 'A' if self._current_media == self._media_list.GetFirst() else 'B'
|
|
|
|
progress = self._current_pair_index + 1
|
|
total = len( self._batch_of_pairs_to_process )
|
|
|
|
index_string = HydrusData.ConvertValueRangeToPrettyString( progress, total )
|
|
|
|
num_decisions_string = '{} decisions'.format( HydrusData.ToHumanInt( self._GetNumCommittableDecisions() ) )
|
|
|
|
return '{} - {} - {}'.format( current_media_label, index_string, num_decisions_string )
|
|
|
|
|
|
|
|
def _GetNoMediaText( self ):
|
|
|
|
return 'Looking for pairs to compare--please wait.'
|
|
|
|
|
|
def _GetNumCommittableDecisions( self ):
|
|
|
|
return len( [ 1 for ( media_result_pair, duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) in self._processed_pairs if len( list_of_service_keys_to_content_updates ) > 0 ] )
|
|
|
|
|
|
def _GetNumRemainingDecisions( self ):
|
|
|
|
# this looks a little weird, but I want to be clear that we make a decision on the final index
|
|
|
|
last_decision_index = len( self._batch_of_pairs_to_process ) - 1
|
|
|
|
number_of_decisions_after_the_current = last_decision_index - self._current_pair_index
|
|
|
|
return max( 0, 1 + number_of_decisions_after_the_current )
|
|
|
|
|
|
def _GoBack( self ):
|
|
|
|
if self._current_pair_index > 0:
|
|
|
|
it_went_ok = self._RewindProcessing()
|
|
|
|
if it_went_ok:
|
|
|
|
self._ShowCurrentPair()
|
|
|
|
|
|
|
|
|
|
def _LoadNextBatchOfPairs( self ):
|
|
|
|
self._hashes_due_to_be_deleted_in_this_batch = set()
|
|
self._hashes_processed_in_this_batch = set()
|
|
self._processed_pairs = [] # just in case someone 'skip'ed everything in the last batch, so this never got cleared above in the commit
|
|
|
|
self.ClearMedia()
|
|
|
|
self._media_list = ClientMedia.ListeningMediaList( self._location_context, [] )
|
|
|
|
self._currently_fetching_pairs = True
|
|
|
|
HG.client_controller.CallToThread( self.THREADFetchPairs, self._file_search_context, self._both_files_match, self._pixel_dupes_preference, self._max_hamming_distance )
|
|
|
|
self.update()
|
|
|
|
|
|
def _MediaAreAlternates( self ):
|
|
|
|
self._ProcessPair( HC.DUPLICATE_ALTERNATE )
|
|
|
|
|
|
def _MediaAreFalsePositive( self ):
|
|
|
|
self._ProcessPair( HC.DUPLICATE_FALSE_POSITIVE )
|
|
|
|
|
|
def _MediaAreTheSame( self ):
|
|
|
|
self._ProcessPair( HC.DUPLICATE_SAME_QUALITY )
|
|
|
|
|
|
def _PrefetchNeighbours( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
other_media = self._media_list.GetNext( self._current_media )
|
|
|
|
media_to_prefetch = [ other_media ]
|
|
|
|
# this doesn't handle big skip events, but that's a job for later
|
|
if self._GetNumRemainingDecisions() > 1: # i.e. more than the current one we are looking at
|
|
|
|
media_to_prefetch.extend( self._batch_of_pairs_to_process[ self._current_pair_index + 1 ] )
|
|
|
|
|
|
image_cache = HG.client_controller.GetCache( 'images' )
|
|
|
|
for media in media_to_prefetch:
|
|
|
|
hash = media.GetHash()
|
|
mime = media.GetMime()
|
|
|
|
if media.IsStaticImage():
|
|
|
|
if not image_cache.HasImageRenderer( hash ):
|
|
|
|
# we do qt safe to make sure the job is cancelled if we are destroyed
|
|
|
|
HG.client_controller.CallAfterQtSafe( self, 'image pre-fetch', image_cache.PrefetchImageRenderer, media )
|
|
|
|
|
|
|
|
|
|
|
|
def _ProcessPair( self, duplicate_type, delete_first = False, delete_second = False, delete_both = False, duplicate_action_options = None ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
if duplicate_action_options is None:
|
|
|
|
if duplicate_type in [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] or ( HG.client_controller.new_options.GetBoolean( 'advanced_mode' ) and duplicate_type == HC.DUPLICATE_ALTERNATE ):
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
duplicate_action_options = new_options.GetDuplicateActionOptions( duplicate_type )
|
|
|
|
else:
|
|
|
|
duplicate_action_options = ClientDuplicates.DuplicateActionOptions()
|
|
|
|
|
|
|
|
first_media = self._current_media
|
|
second_media = self._media_list.GetNext( first_media )
|
|
|
|
was_auto_skipped = False
|
|
|
|
self._hashes_processed_in_this_batch.update( first_media.GetHashes() )
|
|
self._hashes_processed_in_this_batch.update( second_media.GetHashes() )
|
|
|
|
if delete_first or delete_second or delete_both:
|
|
|
|
if delete_first or delete_both:
|
|
|
|
self._hashes_due_to_be_deleted_in_this_batch.update( first_media.GetHashes() )
|
|
|
|
|
|
if delete_second or delete_both:
|
|
|
|
self._hashes_due_to_be_deleted_in_this_batch.update( second_media.GetHashes() )
|
|
|
|
|
|
if duplicate_type in ( HC.DUPLICATE_BETTER, HC.DUPLICATE_WORSE ):
|
|
|
|
file_deletion_reason = 'better/worse'
|
|
|
|
if delete_second:
|
|
|
|
file_deletion_reason += ', worse file deleted'
|
|
|
|
|
|
else:
|
|
|
|
file_deletion_reason = HC.duplicate_type_string_lookup[ duplicate_type ]
|
|
|
|
|
|
if delete_both:
|
|
|
|
file_deletion_reason += ', both files deleted'
|
|
|
|
|
|
file_deletion_reason = 'Deleted in Duplicate Filter ({}).'.format( file_deletion_reason )
|
|
|
|
else:
|
|
|
|
file_deletion_reason = None
|
|
|
|
|
|
list_of_service_keys_to_content_updates = [ duplicate_action_options.ProcessPairIntoContentUpdates( first_media, second_media, delete_first = delete_first, delete_second = delete_second, delete_both = delete_both, file_deletion_reason = file_deletion_reason ) ]
|
|
|
|
self._processed_pairs.append( ( self._batch_of_pairs_to_process[ self._current_pair_index ], duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) )
|
|
|
|
self._ShowNextPair()
|
|
|
|
|
|
def _RewindProcessing( self ) -> bool:
|
|
|
|
if self._current_pair_index > 0:
|
|
|
|
( media_result_pair, duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
|
|
|
|
self._current_pair_index -= 1
|
|
|
|
while was_auto_skipped:
|
|
|
|
if len( self._processed_pairs ) == 0:
|
|
|
|
# the first one shouldn't be auto-skipped, so if it was and now we can't pop, something weird happened
|
|
|
|
HG.client_controller.pub( 'new_similar_files_potentials_search_numbers' )
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'Due to an unexpected series of events, the duplicate filter has no valid pair to back up to. It will now close.' )
|
|
|
|
self.window().deleteLater()
|
|
|
|
return False
|
|
|
|
|
|
( media_result_pair, duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
|
|
|
|
self._current_pair_index -= 1
|
|
|
|
|
|
# only want this for the one that wasn't auto-skipped
|
|
for media_result in media_result_pair:
|
|
|
|
hash = media_result.GetHash()
|
|
|
|
self._hashes_due_to_be_deleted_in_this_batch.discard( hash )
|
|
self._hashes_processed_in_this_batch.discard( hash )
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ShowCurrentPair( self ):
|
|
|
|
if self._currently_fetching_pairs:
|
|
|
|
return
|
|
|
|
|
|
( first_media_result, second_media_result ) = self._batch_of_pairs_to_process[ self._current_pair_index ]
|
|
|
|
first_media = ClientMedia.MediaSingleton( first_media_result )
|
|
second_media = ClientMedia.MediaSingleton( second_media_result )
|
|
|
|
score = ClientMedia.GetDuplicateComparisonScore( first_media, second_media )
|
|
|
|
if score > 0:
|
|
|
|
media_results_with_better_first = ( first_media_result, second_media_result )
|
|
|
|
else:
|
|
|
|
media_results_with_better_first = ( second_media_result, first_media_result )
|
|
|
|
|
|
self._media_list = ClientMedia.ListeningMediaList( self._location_context, media_results_with_better_first )
|
|
|
|
# reset zoom gubbins
|
|
self.SetMedia( None )
|
|
|
|
self.SetMedia( self._media_list.GetFirst() )
|
|
|
|
self._media_container.hide()
|
|
|
|
self._ReinitZoom()
|
|
|
|
self._ResetMediaWindowCenterPosition()
|
|
|
|
self._SizeAndPositionMediaContainer()
|
|
|
|
self._media_container.show()
|
|
|
|
|
|
def _ShowNextPair( self ):
|
|
|
|
if self._currently_fetching_pairs:
|
|
|
|
return
|
|
|
|
|
|
self._current_pair_index += 1
|
|
|
|
#
|
|
|
|
# hackery dackery doo to quick solve something that is calling this a bunch of times while the 'and continue?' dialog is open, making like 16 of them
|
|
# a full rewrite is needed on this awful workflow
|
|
|
|
tlws = QW.QApplication.topLevelWidgets()
|
|
|
|
for tlw in tlws:
|
|
|
|
if isinstance( tlw, ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion ) and tlw.isModal():
|
|
|
|
return
|
|
|
|
|
|
|
|
#
|
|
|
|
def pair_is_good( pair ):
|
|
|
|
( first_media_result, second_media_result ) = pair
|
|
|
|
first_hash = first_media_result.GetHash()
|
|
second_hash = second_media_result.GetHash()
|
|
|
|
if first_hash in self._hashes_processed_in_this_batch or second_hash in self._hashes_processed_in_this_batch:
|
|
|
|
return False
|
|
|
|
|
|
if first_hash in self._hashes_due_to_be_deleted_in_this_batch or second_hash in self._hashes_due_to_be_deleted_in_this_batch:
|
|
|
|
return False
|
|
|
|
|
|
first_media = ClientMedia.MediaSingleton( first_media_result )
|
|
second_media = ClientMedia.MediaSingleton( second_media_result )
|
|
|
|
if not self._CanDisplayMedia( first_media ) or not self._CanDisplayMedia( second_media ):
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
while True:
|
|
|
|
num_remaining = self._GetNumRemainingDecisions()
|
|
|
|
if num_remaining == 0:
|
|
|
|
num_committable = self._GetNumCommittableDecisions()
|
|
|
|
if num_committable > 0:
|
|
|
|
label = 'commit ' + HydrusData.ToHumanInt( num_committable ) + ' decisions and continue?'
|
|
|
|
result = ClientGUIDialogsQuick.GetInterstitialFilteringAnswer( self, label )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._CommitProcessed( blocking = True )
|
|
|
|
else:
|
|
|
|
it_went_ok = self._RewindProcessing()
|
|
|
|
if it_went_ok:
|
|
|
|
self._ShowCurrentPair()
|
|
|
|
|
|
return
|
|
|
|
|
|
else:
|
|
|
|
# nothing to commit, so let's see if we have a big problem here or if user just skipped all
|
|
|
|
we_saw_a_non_auto_skip = False
|
|
|
|
for ( media_result_pair, duplicate_type, first_media, second_media, list_of_service_keys_to_content_updates, was_auto_skipped ) in self._processed_pairs:
|
|
|
|
if not was_auto_skipped:
|
|
|
|
we_saw_a_non_auto_skip = True
|
|
|
|
break
|
|
|
|
|
|
|
|
if not we_saw_a_non_auto_skip:
|
|
|
|
HG.client_controller.pub( 'new_similar_files_potentials_search_numbers' )
|
|
|
|
QW.QMessageBox.critical( self, 'Error', 'It seems an entire batch of pairs were unable to be displayed. The duplicate filter will now close.' )
|
|
|
|
self.window().deleteLater()
|
|
|
|
return
|
|
|
|
|
|
|
|
self._LoadNextBatchOfPairs()
|
|
|
|
return
|
|
|
|
|
|
current_pair = self._batch_of_pairs_to_process[ self._current_pair_index ]
|
|
|
|
if pair_is_good( current_pair ):
|
|
|
|
self._ShowCurrentPair()
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
was_auto_skipped = True
|
|
|
|
self._processed_pairs.append( ( current_pair, None, None, None, [], was_auto_skipped ) )
|
|
|
|
self._current_pair_index += 1
|
|
|
|
|
|
|
|
|
|
def _ShowPairInPage( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
self.showPairInPage.emit( [ self._current_media, self._media_list.GetNext( self._current_media ) ] )
|
|
|
|
|
|
def _SkipPair( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return
|
|
|
|
|
|
was_auto_skipped = False
|
|
|
|
self._processed_pairs.append( ( self._batch_of_pairs_to_process[ self._current_pair_index ], None, None, None, [], was_auto_skipped ) )
|
|
|
|
self._ShowNextPair()
|
|
|
|
|
|
def _SwitchMedia( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
try:
|
|
|
|
other_media = self._media_list.GetNext( self._current_media )
|
|
|
|
self.SetMedia( other_media )
|
|
|
|
except HydrusExceptions.DataMissing:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def Archive( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Archive()
|
|
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
HG.client_controller.pub( 'new_similar_files_potentials_search_numbers' )
|
|
|
|
ClientMedia.hashes_to_jpeg_quality = {} # clear the cache
|
|
ClientMedia.hashes_to_pixel_hashes = {} # clear the cache
|
|
|
|
CanvasWithHovers.CleanBeforeDestroy( self )
|
|
|
|
|
|
def Delete( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Delete()
|
|
|
|
|
|
|
|
def Inbox( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Inbox()
|
|
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_DUPLICATE_FILTER_THIS_IS_BETTER_AND_DELETE_OTHER:
|
|
|
|
self._CurrentMediaIsBetter( delete_second = True )
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_THIS_IS_BETTER_BUT_KEEP_BOTH:
|
|
|
|
self._CurrentMediaIsBetter( delete_second = False )
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_EXACTLY_THE_SAME:
|
|
|
|
self._MediaAreTheSame()
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_ALTERNATES:
|
|
|
|
self._MediaAreAlternates()
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_FALSE_POSITIVE:
|
|
|
|
self._MediaAreFalsePositive()
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_CUSTOM_ACTION:
|
|
|
|
self._DoCustomAction()
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_SKIP:
|
|
|
|
self._SkipPair()
|
|
|
|
elif action == CAC.SIMPLE_DUPLICATE_FILTER_BACK:
|
|
|
|
self._GoBack()
|
|
|
|
elif action in ( CAC.SIMPLE_VIEW_FIRST, CAC.SIMPLE_VIEW_LAST, CAC.SIMPLE_VIEW_PREVIOUS, CAC.SIMPLE_VIEW_NEXT ):
|
|
|
|
self._SwitchMedia()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
if not command_processed:
|
|
|
|
command_processed = CanvasWithHovers.ProcessApplicationCommand( self, command )
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
|
|
|
def catch_up():
|
|
|
|
# ugly, but it will do for now
|
|
|
|
if len( self._media_list ) < 2:
|
|
|
|
self._ShowNextPair()
|
|
|
|
else:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
HG.client_controller.CallLaterQtSafe( self, 0.1, 'duplicates filter post-processing wait', catch_up )
|
|
|
|
|
|
def SetMedia( self, media ):
|
|
|
|
CanvasWithHovers.SetMedia( self, media )
|
|
|
|
if media is not None:
|
|
|
|
shown_media = self._current_media
|
|
comparison_media = self._media_list.GetNext( shown_media )
|
|
|
|
if shown_media != comparison_media:
|
|
|
|
HG.client_controller.pub( 'canvas_new_duplicate_pair', self._canvas_key, shown_media, comparison_media )
|
|
|
|
|
|
|
|
|
|
def SwitchMedia( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._SwitchMedia()
|
|
|
|
|
|
|
|
def TryToDoPreClose( self ):
|
|
|
|
num_committable = self._GetNumCommittableDecisions()
|
|
|
|
if num_committable > 0:
|
|
|
|
label = 'commit ' + HydrusData.ToHumanInt( num_committable ) + ' decisions?'
|
|
|
|
( result, cancelled ) = ClientGUIDialogsQuick.GetFinishFilteringAnswer( self, label )
|
|
|
|
if cancelled:
|
|
|
|
close_was_triggered_by_everything_being_processed = self._GetNumRemainingDecisions() == 0
|
|
|
|
if close_was_triggered_by_everything_being_processed:
|
|
|
|
self._GoBack()
|
|
|
|
|
|
return False
|
|
|
|
elif result == QW.QDialog.Accepted:
|
|
|
|
self._CommitProcessed( blocking = False )
|
|
|
|
|
|
|
|
return CanvasWithHovers.TryToDoPreClose( self )
|
|
|
|
|
|
def Undelete( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Undelete()
|
|
|
|
|
|
|
|
def THREADFetchPairs( self, file_search_context, both_files_match, pixel_dupes_preference, max_hamming_distance ):
|
|
|
|
def qt_close():
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
QW.QMessageBox.information( self, 'Information', 'All pairs have been filtered!' )
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
|
|
def qt_continue( unprocessed_pairs ):
|
|
|
|
if not self or not QP.isValid( self):
|
|
|
|
return
|
|
|
|
|
|
self._batch_of_pairs_to_process = unprocessed_pairs
|
|
self._current_pair_index = 0
|
|
|
|
self._currently_fetching_pairs = False
|
|
|
|
self._ShowCurrentPair()
|
|
|
|
|
|
result = HG.client_controller.Read( 'duplicate_pairs_for_filtering', file_search_context, both_files_match, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
if len( result ) == 0:
|
|
|
|
QP.CallAfter( qt_close )
|
|
|
|
else:
|
|
|
|
QP.CallAfter( qt_continue, result )
|
|
|
|
|
|
|
|
class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
|
|
|
|
exitFocusMedia = QC.Signal( ClientMedia.Media )
|
|
|
|
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ):
|
|
|
|
CanvasWithHovers.__init__( self, parent, location_context )
|
|
ClientMedia.ListeningMediaList.__init__( self, location_context, media_results )
|
|
|
|
self._page_key = page_key
|
|
|
|
self._just_started = True
|
|
|
|
|
|
def TryToDoPreClose( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
self.exitFocusMedia.emit( self._current_media )
|
|
|
|
|
|
return CanvasWithHovers.TryToDoPreClose( self )
|
|
|
|
|
|
def _GenerateHoverTopFrame( self ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def _GetIndexString( self ):
|
|
|
|
if self._current_media is None:
|
|
|
|
index_string = '-/' + HydrusData.ToHumanInt( len( self._sorted_media ) )
|
|
|
|
else:
|
|
|
|
index_string = HydrusData.ConvertValueRangeToPrettyString( self._sorted_media.index( self._current_media ) + 1, len( self._sorted_media ) )
|
|
|
|
|
|
return index_string
|
|
|
|
|
|
def _PrefetchNeighbours( self ):
|
|
|
|
media_looked_at = set()
|
|
|
|
to_render = []
|
|
|
|
previous = self._current_media
|
|
next = self._current_media
|
|
|
|
delay_base = HG.client_controller.new_options.GetInteger( 'media_viewer_prefetch_delay_base_ms' ) / 1000
|
|
|
|
num_to_go_back = HG.client_controller.new_options.GetInteger( 'media_viewer_prefetch_num_previous' )
|
|
num_to_go_forward = HG.client_controller.new_options.GetInteger( 'media_viewer_prefetch_num_next' )
|
|
|
|
# if media_looked_at nukes the list, we want shorter delays, so do next first
|
|
|
|
for i in range( num_to_go_forward ):
|
|
|
|
next = self._GetNext( next )
|
|
|
|
if next in media_looked_at:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
media_looked_at.add( next )
|
|
|
|
|
|
delay = delay_base * ( i + 1 )
|
|
|
|
to_render.append( ( next, delay ) )
|
|
|
|
|
|
for i in range( num_to_go_back ):
|
|
|
|
previous = self._GetPrevious( previous )
|
|
|
|
if previous in media_looked_at:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
media_looked_at.add( previous )
|
|
|
|
|
|
delay = delay_base * 2 * ( i + 1 )
|
|
|
|
to_render.append( ( previous, delay ) )
|
|
|
|
|
|
image_cache = HG.client_controller.GetCache( 'images' )
|
|
|
|
for ( media, delay ) in to_render:
|
|
|
|
hash = media.GetHash()
|
|
mime = media.GetMime()
|
|
|
|
if media.IsStaticImage():
|
|
|
|
if not image_cache.HasImageRenderer( hash ):
|
|
|
|
# we do qt safe to make sure the job is cancelled if we are destroyed
|
|
|
|
HG.client_controller.CallLaterQtSafe( self, delay, 'image pre-fetch', image_cache.PrefetchImageRenderer, media )
|
|
|
|
|
|
|
|
|
|
|
|
def _Remove( self ):
|
|
|
|
next_media = self._GetNext( self._current_media )
|
|
|
|
if next_media == self._current_media:
|
|
|
|
next_media = None
|
|
|
|
|
|
hashes = { self._current_media.GetHash() }
|
|
|
|
HG.client_controller.pub( 'remove_media', self._page_key, hashes )
|
|
|
|
singleton_media = { self._current_media }
|
|
|
|
ClientMedia.ListeningMediaList._RemoveMediaDirectly( self, singleton_media, {} )
|
|
|
|
if self.HasNoMedia():
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
elif self.HasMedia( self._current_media ):
|
|
|
|
HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() )
|
|
|
|
self.update()
|
|
|
|
else:
|
|
|
|
self.SetMedia( next_media )
|
|
|
|
|
|
|
|
def _ShowFirst( self ):
|
|
|
|
self.SetMedia( self._GetFirst() )
|
|
|
|
|
|
def _ShowLast( self ):
|
|
|
|
self.SetMedia( self._GetLast() )
|
|
|
|
|
|
def _ShowNext( self ):
|
|
|
|
self.SetMedia( self._GetNext( self._current_media ) )
|
|
|
|
|
|
def _ShowPrevious( self ):
|
|
|
|
self.SetMedia( self._GetPrevious( self._current_media ) )
|
|
|
|
|
|
def _StartSlideshow( self, interval: float ):
|
|
|
|
pass
|
|
|
|
|
|
def AddMediaResults( self, page_key, media_results ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
ClientMedia.ListeningMediaList.AddMediaResults( self, media_results )
|
|
|
|
HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() )
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def EventFullscreenSwitch( self, event ):
|
|
|
|
self.parentWidget().FullscreenSwitch()
|
|
|
|
|
|
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
|
|
|
if self._current_media is None:
|
|
|
|
# probably a file view stats update as we close down--ignore it
|
|
|
|
return
|
|
|
|
|
|
if self.HasMedia( self._current_media ):
|
|
|
|
next_media = self._GetNext( self._current_media )
|
|
|
|
if next_media == self._current_media:
|
|
|
|
next_media = None
|
|
|
|
|
|
else:
|
|
|
|
next_media = None
|
|
|
|
|
|
ClientMedia.ListeningMediaList.ProcessContentUpdates( self, service_keys_to_content_updates )
|
|
|
|
if self.HasNoMedia():
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
elif self.HasMedia( self._current_media ):
|
|
|
|
HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() )
|
|
|
|
self.update()
|
|
|
|
elif self.HasMedia( next_media ):
|
|
|
|
self.SetMedia( next_media )
|
|
|
|
else:
|
|
|
|
self.SetMedia( self._GetFirst() )
|
|
|
|
|
|
|
|
def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.LocationContext, kept: typing.Collection[ ClientMedia.MediaSingleton ], deleted: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
kept = list( kept )
|
|
deleted = list( deleted )
|
|
|
|
kept_hashes = [ m.GetHash() for m in kept ]
|
|
deleted_hashes = [ m.GetHash() for m in deleted ]
|
|
|
|
if HC.options[ 'remove_filtered_files' ]:
|
|
|
|
all_hashes = set()
|
|
|
|
all_hashes.update( kept_hashes )
|
|
all_hashes.update( deleted_hashes )
|
|
|
|
HG.client_controller.pub( 'remove_media', page_key, all_hashes )
|
|
|
|
|
|
location_context = location_context.Duplicate()
|
|
|
|
location_context.FixMissingServices( ClientLocation.ValidLocalDomainsFilter )
|
|
|
|
if location_context.IncludesCurrent():
|
|
|
|
deletee_file_service_keys = location_context.current_service_keys
|
|
|
|
else:
|
|
|
|
# if we are in a weird search domain, then just say 'delete from all local'
|
|
deletee_file_service_keys = [ CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ]
|
|
|
|
|
|
for block_of_deleted in HydrusData.SplitListIntoChunks( deleted, 64 ):
|
|
|
|
service_keys_to_content_updates = {}
|
|
|
|
reason = 'Deleted in Archive/Delete filter.'
|
|
|
|
for deletee_file_service_key in deletee_file_service_keys:
|
|
|
|
block_of_deleted_hashes = [ m.GetHash() for m in block_of_deleted if deletee_file_service_key in m.GetLocationsManager().GetCurrent() ]
|
|
|
|
service_keys_to_content_updates[ deletee_file_service_key ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, block_of_deleted_hashes, reason = reason ) ]
|
|
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
# we do a second set of removes to deal with late processing and a quick F5ing user
|
|
|
|
if HC.options[ 'remove_filtered_files' ]:
|
|
|
|
block_of_deleted_hashes = [ m.GetHash() for m in block_of_deleted ]
|
|
|
|
HG.client_controller.pub( 'remove_media', page_key, block_of_deleted_hashes )
|
|
|
|
|
|
HG.client_controller.WaitUntilViewFree()
|
|
|
|
|
|
for block_of_kept_hashes in HydrusData.SplitListIntoChunks( kept_hashes, 64 ):
|
|
|
|
service_keys_to_content_updates = {}
|
|
|
|
service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, block_of_kept_hashes ) ]
|
|
|
|
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
|
|
|
|
if HC.options[ 'remove_filtered_files' ]:
|
|
|
|
HG.client_controller.pub( 'remove_media', page_key, block_of_kept_hashes )
|
|
|
|
|
|
HG.client_controller.WaitUntilViewFree()
|
|
|
|
|
|
class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|
|
|
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ):
|
|
|
|
CanvasMediaList.__init__( self, parent, page_key, location_context, media_results )
|
|
|
|
self._my_shortcuts_handler.AddShortcuts( 'archive_delete_filter' )
|
|
|
|
self._kept = set()
|
|
self._deleted = set()
|
|
|
|
HG.client_controller.sub( self, 'Delete', 'canvas_delete' )
|
|
HG.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
|
|
|
|
first_media = self._GetFirst()
|
|
|
|
if first_media is not None:
|
|
|
|
QP.CallAfter( self.SetMedia, first_media ) # don't set this until we have a size > (20, 20)!
|
|
|
|
|
|
|
|
def _Back( self ):
|
|
|
|
if self._current_media == self._GetFirst():
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
self._ShowPrevious()
|
|
|
|
self._kept.discard( self._current_media )
|
|
self._deleted.discard( self._current_media )
|
|
|
|
|
|
|
|
def TryToDoPreClose( self ):
|
|
|
|
kept = list( self._kept )
|
|
|
|
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
|
|
|
|
if delete_lock_for_archived_files:
|
|
|
|
deleted = [ media.GetHash() for media in self._deleted if not media.HasArchive() ]
|
|
|
|
else:
|
|
|
|
deleted = list( self._deleted )
|
|
|
|
|
|
if len( kept ) > 0 or len( deleted ) > 0:
|
|
|
|
label_components = []
|
|
|
|
if len( kept ) > 0:
|
|
|
|
label_components.append( 'keep {}'.format( HydrusData.ToHumanInt( len( kept ) ) ) )
|
|
|
|
|
|
if len( deleted ) > 0:
|
|
|
|
if self._location_context.IncludesCurrent():
|
|
|
|
location_context_label = self._location_context.ToString( HG.client_controller.services_manager.GetName )
|
|
|
|
else:
|
|
|
|
location_context_label = 'all possible local file services'
|
|
|
|
|
|
label_components.append( 'delete (from {}) {}'.format( location_context_label, HydrusData.ToHumanInt( len( self._deleted ) ) ) )
|
|
|
|
|
|
label = '{} files?'.format( ' and '.join( label_components ) )
|
|
|
|
# TODO: ok so ideally we should total up the deleteds' actual file services and give users UI to select what they want to delete from
|
|
# so like '23 files in my files, 2 in favourites' and then a 'yes' button for all or just my files or favourites
|
|
|
|
( result, cancelled ) = ClientGUIDialogsQuick.GetFinishFilteringAnswer( self, label )
|
|
|
|
if cancelled:
|
|
|
|
if self._current_media in self._kept:
|
|
|
|
self._kept.remove( self._current_media )
|
|
|
|
|
|
if self._current_media in self._deleted:
|
|
|
|
self._deleted.remove( self._current_media )
|
|
|
|
|
|
return False
|
|
|
|
elif result == QW.QDialog.Accepted:
|
|
|
|
self._kept = set()
|
|
self._deleted = set()
|
|
|
|
self._current_media = self._GetFirst() # so the pubsub on close is better
|
|
|
|
HG.client_controller.CallToThread( CommitArchiveDelete, self._page_key, self._location_context, kept, deleted )
|
|
|
|
|
|
|
|
return CanvasMediaList.TryToDoPreClose( self )
|
|
|
|
|
|
def _Delete( self, media = None, reason = None, file_service_key = None ):
|
|
|
|
if self._current_media is None:
|
|
|
|
return False
|
|
|
|
|
|
self._deleted.add( self._current_media )
|
|
|
|
if self._current_media == self._GetLast():
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
else:
|
|
|
|
self._ShowNext()
|
|
|
|
|
|
return True
|
|
|
|
|
|
def _GenerateHoverTopFrame( self ):
|
|
|
|
return ClientGUICanvasHoverFrames.CanvasHoverFrameTopArchiveDeleteFilter( self, self, self._canvas_key )
|
|
|
|
|
|
def _Keep( self ):
|
|
|
|
self._kept.add( self._current_media )
|
|
|
|
if self._current_media == self._GetLast():
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
else:
|
|
|
|
self._ShowNext()
|
|
|
|
|
|
|
|
def _Skip( self ):
|
|
|
|
if self._current_media == self._GetLast():
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
else:
|
|
|
|
self._ShowNext()
|
|
|
|
|
|
|
|
def Keep( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Keep()
|
|
|
|
|
|
|
|
def Back( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Back()
|
|
|
|
|
|
|
|
def Delete( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Delete()
|
|
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action in ( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_KEEP, CAC.SIMPLE_ARCHIVE_FILE ):
|
|
|
|
self._Keep()
|
|
|
|
elif action in ( CAC.SIMPLE_ARCHIVE_DELETE_FILTER_DELETE, CAC.SIMPLE_DELETE_FILE ):
|
|
|
|
self._Delete()
|
|
|
|
elif action == CAC.SIMPLE_ARCHIVE_DELETE_FILTER_SKIP:
|
|
|
|
self._Skip()
|
|
|
|
elif action == CAC.SIMPLE_ARCHIVE_DELETE_FILTER_BACK:
|
|
|
|
self._Back()
|
|
|
|
elif action == CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER:
|
|
|
|
self._TryToCloseWindow()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
if not command_processed:
|
|
|
|
command_processed = CanvasMediaList.ProcessApplicationCommand( self, command )
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def Skip( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Skip()
|
|
|
|
|
|
|
|
def Undelete( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Undelete()
|
|
|
|
|
|
|
|
class CanvasMediaListNavigable( CanvasMediaList ):
|
|
|
|
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ):
|
|
|
|
CanvasMediaList.__init__( self, parent, page_key, location_context, media_results )
|
|
|
|
self._my_shortcuts_handler.AddShortcuts( 'media_viewer_browser' )
|
|
|
|
HG.client_controller.sub( self, 'Delete', 'canvas_delete' )
|
|
HG.client_controller.sub( self, 'ShowNext', 'canvas_show_next' )
|
|
HG.client_controller.sub( self, 'ShowPrevious', 'canvas_show_previous' )
|
|
HG.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
|
|
|
|
|
|
def _GenerateHoverTopFrame( self ):
|
|
|
|
return ClientGUICanvasHoverFrames.CanvasHoverFrameTopNavigableList( self, self, self._canvas_key )
|
|
|
|
|
|
def Archive( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Archive()
|
|
|
|
|
|
|
|
def Delete( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Delete()
|
|
|
|
|
|
|
|
def Inbox( self, canvas_key ):
|
|
|
|
if self._canvas_key == canvas_key:
|
|
|
|
self._Inbox()
|
|
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_REMOVE_FILE_FROM_VIEW:
|
|
|
|
self._Remove()
|
|
|
|
elif action == CAC.SIMPLE_VIEW_FIRST:
|
|
|
|
self._ShowFirst()
|
|
|
|
elif action == CAC.SIMPLE_VIEW_LAST:
|
|
|
|
self._ShowLast()
|
|
|
|
elif action == CAC.SIMPLE_VIEW_PREVIOUS:
|
|
|
|
self._ShowPrevious()
|
|
|
|
elif action == CAC.SIMPLE_VIEW_NEXT:
|
|
|
|
self._ShowNext()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
if not command_processed:
|
|
|
|
command_processed = CanvasMediaList.ProcessApplicationCommand( self, command )
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def ShowFirst( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ShowFirst()
|
|
|
|
|
|
|
|
def ShowLast( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ShowLast()
|
|
|
|
|
|
|
|
def ShowNext( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ShowNext()
|
|
|
|
|
|
|
|
def ShowPrevious( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._ShowPrevious()
|
|
|
|
|
|
|
|
def Undelete( self, canvas_key ):
|
|
|
|
if canvas_key == self._canvas_key:
|
|
|
|
self._Undelete()
|
|
|
|
|
|
|
|
class CanvasMediaListBrowser( CanvasMediaListNavigable ):
|
|
|
|
def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results, first_hash ):
|
|
|
|
CanvasMediaListNavigable.__init__( self, parent, page_key, location_context, media_results )
|
|
|
|
self._timer_slideshow_job = None
|
|
self._timer_slideshow_interval = 0.0
|
|
|
|
if first_hash is None:
|
|
|
|
first_media = self._GetFirst()
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
first_media = self._GetMedia( { first_hash } )[0]
|
|
|
|
except:
|
|
|
|
first_media = self._GetFirst()
|
|
|
|
|
|
|
|
if first_media is not None:
|
|
|
|
QP.CallAfter( self.SetMedia, first_media ) # don't set this until we have a size > (20, 20)!
|
|
|
|
|
|
HG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' )
|
|
|
|
|
|
def _PausePlaySlideshow( self ):
|
|
|
|
if self._RunningSlideshow():
|
|
|
|
self._StopSlideshow()
|
|
|
|
elif self._timer_slideshow_interval > 0:
|
|
|
|
self._StartSlideshow( interval = self._timer_slideshow_interval )
|
|
|
|
|
|
|
|
def _RunningSlideshow( self ):
|
|
|
|
return self._timer_slideshow_job is not None
|
|
|
|
|
|
def _StartSlideshow( self, interval: float ):
|
|
|
|
self._StopSlideshow()
|
|
|
|
if interval > 0:
|
|
|
|
self._timer_slideshow_interval = interval
|
|
|
|
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, 'slideshow', self.DoSlideshow )
|
|
|
|
|
|
|
|
def _StartSlideshowCustomInterval( self ):
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( self, 'Enter the interval, in seconds.', default = '15', min_char_width = 12 ) as dlg:
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
try:
|
|
|
|
interval = float( dlg.GetValue() )
|
|
|
|
self._StartSlideshow( interval )
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _StopSlideshow( self ):
|
|
|
|
if self._RunningSlideshow():
|
|
|
|
self._timer_slideshow_job.Cancel()
|
|
|
|
self._timer_slideshow_job = None
|
|
|
|
self._media_container.StopForSlideshow( False )
|
|
|
|
|
|
|
|
def DoSlideshow( self ):
|
|
|
|
try:
|
|
|
|
# we are due for a slideshow change, so tell movie to stop for it once it has played once through
|
|
# if short movie, it has prob played through a lot and will shift immediately
|
|
# if longer movie, it has prob not played through but will now stop when it has and wait for us to change it then
|
|
self._media_container.StopForSlideshow( True )
|
|
|
|
if self._current_media is not None and self._RunningSlideshow():
|
|
|
|
if self._media_container.ReadyToSlideshow() and not CGC.core().MenuIsOpen():
|
|
|
|
self._media_container.StopForSlideshow( False )
|
|
|
|
self._ShowNext()
|
|
|
|
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, 'slideshow', self.DoSlideshow )
|
|
|
|
else:
|
|
|
|
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, 0.1, 'slideshow', self.DoSlideshow )
|
|
|
|
|
|
|
|
except:
|
|
|
|
self._timer_slideshow_job = None
|
|
|
|
raise
|
|
|
|
|
|
|
|
def contextMenuEvent( self, event ):
|
|
|
|
if event.reason() == QG.QContextMenuEvent.Keyboard:
|
|
|
|
self.ShowMenu()
|
|
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_PAUSE_PLAY_SLIDESHOW:
|
|
|
|
self._PausePlaySlideshow()
|
|
|
|
elif action == CAC.SIMPLE_SHOW_MENU:
|
|
|
|
self.ShowMenu()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
if not command_processed:
|
|
|
|
command_processed = CanvasMediaListNavigable.ProcessApplicationCommand( self, command )
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def ShowMenu( self ):
|
|
|
|
if self._current_media is not None:
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
advanced_mode = new_options.GetBoolean( 'advanced_mode' )
|
|
|
|
services = HG.client_controller.services_manager.GetServices()
|
|
|
|
local_ratings_services = [ service for service in services if service.GetServiceType() in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) ]
|
|
|
|
i_can_post_ratings = len( local_ratings_services ) > 0
|
|
|
|
self._last_drag_pos = None # to stop successive right-click drag warp bug
|
|
|
|
locations_manager = self._current_media.GetLocationsManager()
|
|
|
|
menu = QW.QMenu()
|
|
|
|
#
|
|
|
|
info_lines = self._current_media.GetPrettyInfoLines()
|
|
|
|
top_line = info_lines.pop(0)
|
|
|
|
info_menu = QW.QMenu( menu )
|
|
|
|
for line in info_lines:
|
|
|
|
ClientGUIMenus.AppendMenuLabel( info_menu, line, line )
|
|
|
|
|
|
ClientGUIMedia.AddFileViewingStatsMenu( info_menu, ( self._current_media, ) )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, info_menu, top_line )
|
|
|
|
#
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if self._IsZoomable():
|
|
|
|
zoom_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( zoom_menu, 'zoom in', 'Zoom the media in.', self._ZoomIn )
|
|
ClientGUIMenus.AppendMenuItem( zoom_menu, 'zoom out', 'Zoom the media out.', self._ZoomOut )
|
|
|
|
if self._current_zoom != 1.0:
|
|
|
|
ClientGUIMenus.AppendMenuItem( zoom_menu, 'zoom to 100%', 'Set the zoom to 100%.', self._ZoomSwitch )
|
|
|
|
elif self._current_zoom != self._canvas_zoom:
|
|
|
|
ClientGUIMenus.AppendMenuItem( zoom_menu, 'zoom fit', 'Set the zoom so the media fits the canvas.', self._ZoomSwitch )
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, zoom_menu, 'current zoom: {}'.format( ClientData.ConvertZoomToPercentage( self._current_zoom ) ) )
|
|
|
|
|
|
AddAudioVolumeMenu( menu, self.CANVAS_TYPE )
|
|
|
|
if self.parentWidget().isFullScreen():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'exit fullscreen', 'Make this media viewer a regular window with borders.', self.parentWidget().FullscreenSwitch )
|
|
|
|
else:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'go fullscreen', 'Make this media viewer a fullscreen window without borders.', self.parentWidget().FullscreenSwitch )
|
|
|
|
|
|
slideshow = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( slideshow, '1 second', 'Start a slideshow with a one second interval.', self._StartSlideshow, 1.0 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, '5 second', 'Start a slideshow with a five second interval.', self._StartSlideshow, 5.0 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, '10 second', 'Start a slideshow with a ten second interval.', self._StartSlideshow, 10.0 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, '30 second', 'Start a slideshow with a thirty second interval.', self._StartSlideshow, 30.0 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, '60 second', 'Start a slideshow with a one minute interval.', self._StartSlideshow, 60.0 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, 'very fast', 'Start a very fast slideshow.', self._StartSlideshow, 0.08 )
|
|
ClientGUIMenus.AppendMenuItem( slideshow, 'custom interval', 'Start a slideshow with a custom interval.', self._StartSlideshowCustomInterval )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, slideshow, 'start slideshow' )
|
|
|
|
if self._RunningSlideshow():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'stop slideshow', 'Stop the current slideshow.', self._PausePlaySlideshow )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'remove from view', 'Remove this file from the list you are viewing.', self._Remove )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if self._current_media.HasInbox():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'archive', 'Archive this file, taking it out of the inbox.', self._Archive )
|
|
|
|
elif self._current_media.HasArchive():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'return to inbox', 'Put this file back in the inbox.', self._Inbox )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
#
|
|
|
|
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
|
|
|
# brush this up to handle different service keys
|
|
# undelete do an optional service key too
|
|
|
|
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = HG.client_controller.services_manager.GetName )
|
|
|
|
for file_service_key in local_file_service_keys_we_are_in:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'delete from {}'.format( HG.client_controller.services_manager.GetName( file_service_key ) ), 'Delete this file.', self._Delete, file_service_key = file_service_key )
|
|
|
|
|
|
#
|
|
|
|
if locations_manager.IsTrashed():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'delete physically now', 'Delete this file immediately. This cannot be undone.', self._Delete, file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'undelete', 'Take this file out of the trash, returning it to its original file service.', self._Undelete )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
manage_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'tags', 'Manage this file\'s tags.', self._ManageTags )
|
|
|
|
if i_can_post_ratings:
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'ratings', 'Manage this file\'s ratings.', self._ManageRatings )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, 'urls', 'Manage this file\'s known urls.', self._ManageURLs )
|
|
|
|
num_notes = self._current_media.GetNotesManager().GetNumNotes()
|
|
|
|
notes_str = 'notes'
|
|
|
|
if num_notes > 0:
|
|
|
|
notes_str = '{} ({})'.format( notes_str, HydrusData.ToHumanInt( num_notes ) )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
|
|
|
|
ClientGUIMedia.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
|
|
|
|
( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaActions.GetLocalFileActionServiceKeys( ( self._current_media, ) )
|
|
|
|
multiple_selected = False
|
|
|
|
ClientGUIMedia.AddLocalFilesMoveAddToMenu( self, menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
|
|
|
|
ClientGUIMedia.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
|
|
|
|
open_menu = QW.QMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in external program', 'Open this file in the default external program.', self._OpenExternally )
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Show your current media in a simple new page.', self._ShowMediaInNewPage )
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in web browser', 'Show this file in your OS\'s web browser.', self._OpenFileInWebBrowser )
|
|
|
|
show_open_in_explorer = advanced_mode and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
|
|
|
|
if show_open_in_explorer:
|
|
|
|
ClientGUIMenus.AppendMenuItem( open_menu, 'in file browser', 'Show this file in your OS\'s file browser.', self._OpenFileLocation )
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
|
|
|
|
share_menu = QW.QMenu( menu )
|
|
|
|
copy_menu = QW.QMenu( share_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
|
|
|
|
copy_hash_menu = QW.QMenu( copy_menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard.', self._CopyHashToClipboard, 'md5' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard.', self._CopyHashToClipboard, 'sha1' )
|
|
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash to your clipboard.', self._CopyHashToClipboard, 'sha512' )
|
|
|
|
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
|
|
|
|
if advanced_mode:
|
|
|
|
hash_id_str = str( self._current_media.GetHashId() )
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', HG.client_controller.pub, 'clipboard', 'text', hash_id_str )
|
|
|
|
|
|
if self._current_media.GetMime() in HC.IMAGES:
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'image (bitmap)', 'Copy this file to your clipboard as a BMP image.', self._CopyBMPToClipboard )
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy this file\'s path to your clipboard.', self._CopyPathToClipboard )
|
|
|
|
ClientGUIMenus.AppendMenu( share_menu, copy_menu, 'copy' )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
|