hydrus/hydrus/ClientGUICanvas.py

4477 lines
141 KiB
Python

from . import HydrusConstants as HC
from . import HydrusData
from . import HydrusExceptions
from . import HydrusGlobals as HG
from . import ClientConstants as CC
from . import ClientData
from . import ClientDuplicates
from . import ClientGUICanvasMedia
from . import ClientGUICommon
from . import ClientGUICore as CGC
from . import ClientGUIDialogs
from . import ClientGUIDialogsManage
from . import ClientGUIDialogsQuick
from . import ClientGUIFunctions
from . import ClientGUICanvasHoverFrames
from . import ClientGUIMedia
from . import ClientGUIMediaControls
from . import ClientGUIMenus
from . import ClientGUIScrolledPanels
from . import ClientGUIScrolledPanelsEdit
from . import ClientGUIScrolledPanelsManagement
from . import ClientGUIShortcuts
from . import ClientGUITags
from . import ClientGUITopLevelWindows
from . import ClientMedia
from . import ClientPaths
from . import ClientRatings
from . import ClientTags
from . import HydrusImageHandling
from . import HydrusPaths
from . import HydrusTags
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from . import QtPorting as QP
import typing
OPEN_EXTERNALLY_BUTTON_SIZE = ( 200, 45 )
def AddAudioVolumeMenu( menu, canvas_type ):
mute_volume_type = None
volume_volume_type = ClientGUIMediaControls.AUDIO_GLOBAL
if canvas_type == ClientGUICommon.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 == ClientGUICommon.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:
( thumb_width, thumb_height ) = HydrusImageHandling.GetThumbnailResolution( media.GetResolution(), HG.client_controller.options[ 'thumbnail_dimensions' ] )
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 ) )
return ( media_width, media_height )
class Canvas( QW.QWidget ):
PREVIEW_WINDOW = False
def __init__( self, parent ):
QW.QWidget.__init__( self, parent )
self.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding )
self._file_service_key = CC.LOCAL_FILE_SERVICE_KEY
self._current_media_start_time = HydrusData.GetNow()
self._reserved_shortcut_names = []
self._reserved_shortcut_names.append( 'media' )
self._reserved_shortcut_names.append( 'media_viewer' )
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
if self.PREVIEW_WINDOW:
self._canvas_type = ClientGUICommon.CANVAS_PREVIEW
else:
self._canvas_type = ClientGUICommon.CANVAS_MEDIA_VIEWER
self._media_container = ClientGUICanvasMedia.MediaContainer( self, self._canvas_type )
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._UpdateBackgroundColour()
catch_mouse = False
# 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, initial_shortcuts_names = ( 'media', 'media_viewer' ), catch_mouse = catch_mouse, ignore_activating_mouse_click = ignore_activating_mouse_click )
self._widget_event_filter = QP.WidgetEventFilter( self )
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, 'ProcessApplicationCommand', 'canvas_application_command' )
HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
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 ):
if self._current_media is not None:
if self._current_media.GetMime() in HC.IMAGES:
HG.client_controller.pub( 'clipboard', 'bmp', self._current_media )
else:
QW.QMessageBox.critical( self, 'Error', 'Sorry, cannot take bmps of anything but static images right now!' )
def _CopyHashToClipboard( self, hash_type ):
sha256_hash = self._current_media.GetHash()
if hash_type == 'sha256':
hex_hash = sha256_hash.hex()
else:
if self._current_media.GetLocationsManager().IsLocal():
( other_hash, ) = HG.client_controller.Read( 'file_hashes', ( sha256_hash, ), 'sha256', hash_type )
hex_hash = other_hash.hex()
else:
QW.QMessageBox.warning( self, 'Warning', 'Unfortunately, you do not have that file in your database, so its non-sha256 hashes are unknown.' )
return
HG.client_controller.pub( 'clipboard', 'text', hex_hash )
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 ):
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.'
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 )
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 == 'pan_top_edge':
delta_y = - self._media_window_pos.y()
elif pan_type == 'pan_left_edge':
delta_x = - self._media_window_pos.x()
elif pan_type == 'pan_bottom_edge':
delta_y = my_size.height() - ( self._media_window_pos.y() + media_size.height() )
elif pan_type == 'pan_right_edge':
delta_x = my_size.width() - ( self._media_window_pos.x() + media_size.width() )
elif pan_type == 'pan_vertical_center':
delta_y = ( my_size.height() / 2 ) - ( self._media_window_pos.y() + ( media_size.height() / 2 ) )
elif pan_type == '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 ):
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 _IShouldCatchShortcutEvent( self, event = None ):
return ClientGUIShortcuts.IShouldCatchShortcutEvent( self, event = event, child_tlw_classes_who_can_pass_up = ( ClientGUICanvasHoverFrames.CanvasHoverFrame, ) )
def _MaintainZoom( self, previous_media ):
if previous_media is None:
self._ReinitZoom()
else:
if self._current_media is None:
return
# set up canvas zoom
( 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 )
# for init zoom, we want the _width_ to stay the same as previous
( previous_width, previous_height ) = CalculateMediaSize( previous_media, self._current_zoom )
( current_media_100_width, current_media_100_height ) = self._current_media.GetResolution()
self._current_zoom = previous_width / current_media_100_width
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 ):
def qt_do_it( media, notes ):
if not self or not QP.isValid( self ):
return
title = 'manage notes'
with ClientGUITopLevelWindows.DialogEdit( self, title ) as dlg:
panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg, [ 'manage_file_notes' ] )
control = QW.QPlainTextEdit( panel )
( min_width, min_height ) = ClientGUIFunctions.ConvertTextToPixels( control, ( 80, 14 ) )
control.setMinimumWidth( min_width )
control.setMinimumHeight( min_height )
control.setPlainText( notes )
panel.SetControl( control )
dlg.SetPanel( panel )
HG.client_controller.CallAfterQtSafe( control, control.setFocus, QC.Qt.OtherFocusReason )
HG.client_controller.CallAfterQtSafe( control, control.moveCursor, QG.QTextCursor.End )
if dlg.exec() == QW.QDialog.Accepted:
notes = control.toPlainText()
hash = media.GetHash()
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_NOTES, HC.CONTENT_UPDATE_SET, ( notes, hash ) ) ]
service_keys_to_content_updates = { CC.LOCAL_NOTES_SERVICE_KEY : content_updates }
HG.client_controller.Write( 'content_updates', service_keys_to_content_updates )
def thread_wait( media ):
# if it ultimately makes sense, I can load/cache notes in the media result
notes = HG.client_controller.Read( 'file_notes', media.GetHash() )
QP.CallAfter( qt_do_it, media, notes )
if self._current_media is None:
return
HG.client_controller.CallToThread( thread_wait, self._current_media )
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, ClientGUITopLevelWindows.FrameThatTakesScrollablePanel ):
panel = child.GetPanel()
if isinstance( panel, ClientGUITags.ManageTagsPanel ):
child.activateWindow()
command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_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 = ClientGUITopLevelWindows.FrameThatTakesScrollablePanel( self, title, frame_key )
panel = ClientGUITags.ManageTagsPanel( manage_tags, self._file_service_key, ( 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 ClientGUITopLevelWindows.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 _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
def _SaveCurrentMediaViewTime( self ):
now = HydrusData.GetNow()
viewtime_delta = now - self._current_media_start_time
self._current_media_start_time = now
if self._current_media is None:
return
if self.PREVIEW_WINDOW:
viewtype = 'preview'
else:
if isinstance( self, CanvasFilterDuplicates ):
viewtype = 'media_duplicates_filter'
else:
viewtype = 'media'
hash = self._current_media.GetHash()
HG.client_controller.file_viewing_stats_manager.FinishViewing( viewtype, hash, viewtime_delta )
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._file_service_key, 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 ):
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()
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
width_delta = media_window_width - new_media_window_width
height_delta = media_window_height - new_media_window_height
half_delta = QC.QPoint( width_delta // 2, height_delta // 2 )
self._media_window_pos += half_delta
self._current_zoom = new_zoom
HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom )
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 ):
locations_manager = self._current_media.GetLocationsManager()
if CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent():
do_it = False
if not HC.options[ 'confirm_trash' ]:
do_it = True
else:
result = ClientGUIDialogsQuick.GetYesNo( self, 'Undelete this file?' )
if result == QW.QDialog.Accepted:
do_it = True
if do_it:
HG.client_controller.Write( 'content_updates', { CC.TRASH_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, ( self._current_media.GetHash(), ) ) ] } )
def _UpdateBackgroundColour( self ):
colour = self._GetBackgroundColour()
QP.SetBackgroundColour( self, colour )
self.update()
def _ZoomIn( self ):
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 <= self._current_zoom:
exact_zoom *= 2
else:
while exact_zoom / 2 > self._current_zoom:
exact_zoom /= 2
possible_zooms = [ exact_zoom ]
else:
possible_zooms = self._new_options.GetMediaZooms()
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 )
def _ZoomOut( self ):
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 )
def _ZoomSwitch( self ):
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 )
if new_zoom <= self._canvas_zoom:
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 KeepCursorAlive( self ):
pass
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, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == 'manage_file_ratings':
self._ManageRatings()
elif action == 'manage_file_tags':
self._ManageTags()
elif action == 'manage_file_urls':
self._ManageURLs()
elif action == 'manage_file_notes':
self._ManageNotes()
elif action == 'open_known_url':
self._OpenKnownURL()
elif action == 'archive_file':
self._Archive()
elif action == 'copy_bmp':
self._CopyBMPToClipboard()
elif action == 'copy_file':
self._CopyFileToClipboard()
elif action == 'copy_path':
self._CopyPathToClipboard()
elif action == 'copy_sha256_hash':
self._CopyHashToClipboard( 'sha256' )
elif action == 'delete_file':
self._Delete()
elif action == 'inbox_file':
self._Inbox()
elif action == 'open_file_in_external_program':
self._OpenExternally()
elif action == 'pan_up':
self._DoManualPan( 0, -1 )
elif action == 'pan_down':
self._DoManualPan( 0, 1 )
elif action == 'pan_left':
self._DoManualPan( -1, 0 )
elif action == 'pan_right':
self._DoManualPan( 1, 0 )
elif action in ( 'pan_top_edge', 'pan_bottom_edge', 'pan_left_edge', 'pan_right_edge', 'pan_vertical_center', 'pan_horizontal_center' ):
self._DoEdgePan( action )
elif action == 'pause_media':
self._PauseCurrentMedia()
elif action == 'pause_play_media':
self._PausePlayCurrentMedia()
elif action == 'move_animation_to_previous_frame':
self._media_container.GotoPreviousOrNextFrame( -1 )
elif action == 'move_animation_to_next_frame':
self._media_container.GotoPreviousOrNextFrame( 1 )
elif action == 'zoom_in':
self._ZoomIn()
elif action == 'zoom_out':
self._ZoomOut()
elif action == 'switch_between_100_percent_and_canvas_zoom':
self._ZoomSwitch()
else:
command_processed = False
elif command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
if self._current_media is None:
return
command_processed = ClientGUIFunctions.ApplyContentApplicationCommandToMedia( self, command, ( self._current_media, ) )
else:
command_processed = False
return command_processed
def ResetMediaWindowCenterPosition( self ):
self._ResetMediaWindowCenterPosition()
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 )
self._PrefetchNeighbours()
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 CanvasPanel( Canvas ):
PREVIEW_WINDOW = True
def __init__( self, parent, page_key ):
Canvas.__init__( self, parent )
self._page_key = page_key
self._hidden_page_current_media = None
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 PageHidden( self ):
self._hidden_page_current_media = self._current_media
self.ClearMedia()
def PageShown( self ):
self.SetMedia( self._hidden_page_current_media )
self._hidden_page_current_media = None
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 )
if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent():
ClientGUIMenus.AppendMenuItem( menu, 'delete', 'Delete this file.', self._Delete, file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent():
ClientGUIMenus.AppendMenuItem( menu, 'delete completely', 'Physically delete this file from disk.', self._Delete, file_service_key = CC.TRASH_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, 'known urls', 'Manage this file\'s known URLs.', self._ManageURLs )
ClientGUIMenus.AppendMenuItem( manage_menu, 'notes', 'Manage this file\'s notes.', self._ManageNotes )
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 not HC.PLATFORM_LINUX
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 (hydrus default)', 'Open this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Open this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Open this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Open this file\'s SHA512 hash.', self._CopyHashToClipboard, 'sha512' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
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 list(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 ):
Canvas.__init__( self, parent )
HG.client_controller.sub( self, 'RedrawDetails', 'refresh_all_tag_presentation_gui' )
def _DrawAdditionalTopMiddleInfo( self, painter, current_y ):
pass
def _DrawBackgroundDetails( self, painter ):
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 = painter.fontMetrics().size( QC.Qt.TextSingleLine, text )
x = ( my_width - text_size.width() ) // 2
y = ( my_height - text_size.height() ) // 2
QP.DrawText( painter, x, y, text )
else:
# tags on the top left
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 )
ClientTags.SortTags( HC.options[ 'default_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 = painter.fontMetrics().size( QC.Qt.TextSingleLine, display_string )
QP.DrawText( painter, 5, current_y, display_string )
current_y += text_size.height()
# top right
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 )
ClientRatings.DrawLike( painter, like_rating_current_x, current_y, service_key, rating_state )
like_rating_current_x -= 16
if len( like_services ) > 0:
current_y += 20
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 = ClientRatings.GetNumericalWidth( service_key )
ClientRatings.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 += 20
# icons
icons_to_show = []
if CC.TRASH_SERVICE_KEY in self._current_media.GetLocationsManager().GetCurrent():
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 = painter.fontMetrics().size( QC.Qt.TextSingleLine, remote_string )
QP.DrawText( painter, my_width - text_size.width() - 3, current_y, remote_string )
current_y += text_size.height() + 4
# 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 = painter.fontMetrics().size( QC.Qt.TextSingleLine, display_string )
QP.DrawText( painter, my_width - text_size.width() - 3, current_y, display_string )
current_y += text_size.height() + 4
# top-middle
current_y = 3
title_string = self._current_media.GetTitleString()
if len( title_string ) > 0:
text_size = painter.fontMetrics().size( QC.Qt.TextSingleLine, title_string )
QP.DrawText( painter, ( my_width - text_size.width() ) // 2, current_y, title_string )
current_y += text_size.height() + 3
info_string = self._GetInfoString()
text_size = painter.fontMetrics().size( QC.Qt.TextSingleLine, info_string )
QP.DrawText( painter, ( my_width - text_size.width() ) // 2, current_y, info_string )
current_y += text_size.height() + 3
self._DrawAdditionalTopMiddleInfo( painter, current_y )
# bottom-right index
index_string = self._GetIndexString()
if len( index_string ) > 0:
text_size = painter.fontMetrics().size( QC.Qt.TextSingleLine, index_string )
QP.DrawText( painter, my_width - text_size.width() - 3, my_height - text_size.height() - 3, index_string )
def _GetInfoString( self ):
lines = self._current_media.GetPrettyInfoLines()
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 ):
CanvasWithDetails.__init__( self, parent )
self._GenerateHoverTopFrame()
ClientGUICanvasHoverFrames.CanvasHoverFrameTags( self, self, self._canvas_key )
ClientGUICanvasHoverFrames.CanvasHoverFrameTopRight( self, self, self._canvas_key )
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()
self._widget_event_filter.EVT_MOTION( self.EventMouseMove )
self._InitiateCursorHideWait()
HG.client_controller.sub( self, 'CloseFromHover', 'canvas_close' )
HG.client_controller.sub( self, 'FullscreenSwitch', 'canvas_fullscreen_switch' )
def _GenerateHoverTopFrame( self ):
raise NotImplementedError()
def _TryToCloseWindow( self ):
self.window().close()
def CloseFromHover( self, canvas_key ):
if canvas_key == self._canvas_key:
self._TryToCloseWindow()
def EventDragBegin( self, event ):
if event.button() != QC.Qt.LeftButton:
return True
self.BeginDrag()
return True # was: event.ignore()
def EventDragEnd( self, event ):
if event.button() != QC.Qt.LeftButton:
return True
self._last_drag_pos = None
return True # was: event.ignore()
def EventMouseMove( self, event ):
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() )
show_mouse = self.cursor() == QG.QCursor( QC.Qt.ArrowCursor )
is_dragging = ( event.type() == QC.QEvent.MouseMove and 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 )
else:
show_mouse = True
self._last_drag_pos = QC.QPoint( event_pos )
self._media_window_pos += delta
self._DrawCurrentMedia()
elif has_moved:
self._last_motion_pos = QC.QPoint( event_pos )
show_mouse = True
if show_mouse:
self.setCursor( QG.QCursor( QC.Qt.ArrowCursor ) )
self._InitiateCursorHideWait()
else:
self.setCursor( QG.QCursor( QC.Qt.BlankCursor ) )
return True
def FullscreenSwitch( self, canvas_key ):
if canvas_key == self._canvas_key:
self.parentWidget().FullscreenSwitch()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == '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 _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 = self.underMouse()
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 _InitiateCursorHideWait( 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, self._HideCursorCheck )
class CanvasFilterDuplicates( CanvasWithHovers ):
def __init__( self, parent, file_search_context, both_files_match ):
CanvasWithHovers.__init__( self, parent )
ClientGUICanvasHoverFrames.CanvasHoverFrameRightDuplicates( self, self, self._canvas_key )
self._file_search_context = file_search_context
self._both_files_match = both_files_match
self._maintain_pan_and_zoom = True
self._currently_fetching_pairs = False
self._unprocessed_pairs = []
self._current_pair = None
self._processed_pairs = []
self._hashes_due_to_be_deleted_in_this_batch = set()
file_service_key = self._file_search_context.GetFileServiceKey()
self._media_list = ClientMedia.ListeningMediaList( file_service_key, [] )
self._my_shortcuts_handler.AddShortcuts( 'media_viewer_browser' )
self._my_shortcuts_handler.AddShortcuts( 'duplicate_filter' )
self._reserved_shortcut_names.append( 'media_viewer_browser' )
self._reserved_shortcut_names.append( 'duplicate_filter' )
self._widget_event_filter.EVT_MOUSE_EVENTS( self.EventMouse )
# add support for 'f' to borderless
# add support for F4 and other general shortcuts so people can do edits before processing
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._ShowNewPair )
def _CommitProcessed( self, blocking = True ):
pair_info = []
for ( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) in self._processed_pairs:
if duplicate_type is None or 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, 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()
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
text = 'Delete just this file, or both?'
yes_tuples = []
yes_tuples.append( ( 'delete just this one', 'current' ) )
yes_tuples.append( ( 'delete both', 'both' ) )
with ClientGUIDialogs.DialogYesYesNo( self, text, yes_tuples = yes_tuples, no_label = 'forget it' ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
value = dlg.GetValue()
if value == 'current':
media = [ self._current_media ]
default_reason = 'Deleted manually in Duplicate Filter.'
elif value == 'both':
media = [ self._current_media, self._media_list.GetNext( self._current_media ) ]
default_reason = 'Deleted manually in Duplicate Filter, along with its potential duplicate.'
else:
return False
else:
return False
deleted = CanvasWithHovers._Delete( self, media = media, default_reason = default_reason, file_service_key = file_service_key )
if deleted:
self._SkipPair()
return True
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 ClientGUITopLevelWindows.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
text = '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' ) )
delete_first = False
delete_second = False
delete_both = False
with ClientGUIDialogs.DialogYesYesNo( self, text, yes_tuples = yes_tuples, no_label = 'forget it' ) as dlg:
result = dlg.exec()
if result == QW.QDialog.Accepted:
value = dlg.GetValue()
if value == 'delete_first':
delete_first = True
elif value == 'delete_second':
delete_second = True
elif value == 'delete_both':
delete_both = True
else:
return
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 = painter.fontMetrics().size( QC.Qt.TextSingleLine, text )
my_size = self.size()
x = ( my_size.width() - text_size.width() ) // 2
y = ( my_size.height() - text_size.height() ) // 2
QP.DrawText( painter, x, y, text )
else:
CanvasWithHovers._DrawBackgroundDetails( self, painter )
def _GenerateHoverTopFrame( self ):
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 ClientData.GetLighterDarkerColour( normal_colour, duplicate_intensity )
def _GetIndexString( self ):
if self._current_media is None or len( self._media_list ) == 0:
return '-'
else:
progress = len( self._processed_pairs ) + 1 # +1 here actually counts for the one currently displayed
total = progress + len( self._unprocessed_pairs )
index_string = HydrusData.ConvertValueRangeToPrettyString( progress, total )
if self._current_media == self._media_list.GetFirst():
return 'A - ' + index_string
else:
return 'B - ' + index_string
def _GetNoMediaText( self ):
return 'Looking for pairs to compare--please wait.'
def _GetNumCommittableDecisions( self ):
return len( [ 1 for ( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) in self._processed_pairs if duplicate_type is not None and not was_auto_skipped ] )
def _GoBack( self ):
if len( self._processed_pairs ) > 0 and self._GetNumCommittableDecisions() > 0:
self._unprocessed_pairs.append( self._current_pair )
( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
self._unprocessed_pairs.append( hash_pair )
while was_auto_skipped:
if len( self._processed_pairs ) == 0:
QW.QMessageBox.critical( self, 'Error', 'Due to an unexpected series of events (likely a series of file deletes), the duplicate filter has no valid pair to back up to. It will now close.' )
self.window().deleteLater()
return
( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
self._unprocessed_pairs.append( hash_pair )
self._hashes_due_to_be_deleted_in_this_batch.difference_update( hash_pair )
self._ShowNewPair()
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 _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
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
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._current_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) )
self._ShowNewPair()
def _ShowNewPair( self ):
if self._currently_fetching_pairs:
return
# 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, ClientGUITopLevelWindows.DialogCustomButtonQuestion ) and tlw.isModal():
return
#
num_committable = self._GetNumCommittableDecisions()
if len( self._unprocessed_pairs ) == 0 and 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:
( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
self._unprocessed_pairs.append( hash_pair )
while was_auto_skipped:
if len( self._processed_pairs ) == 0:
QW.QMessageBox.critical( self, 'Error', 'Due to an unexpected series of events (likely a series of file deletes), the duplicate filter has no valid pair to back up to. It will now close.' )
self.window().deleteLater()
return
( hash_pair, duplicate_type, first_media, second_media, service_keys_to_content_updates, was_auto_skipped ) = self._processed_pairs.pop()
self._unprocessed_pairs.append( hash_pair )
self._hashes_due_to_be_deleted_in_this_batch.difference_update( hash_pair )
file_service_key = self._file_search_context.GetFileServiceKey()
if len( self._unprocessed_pairs ) == 0:
self._hashes_due_to_be_deleted_in_this_batch = set()
self._processed_pairs = [] # just in case someone 'skip'ed everything in the last batch, so this never got cleared above
self.ClearMedia()
self._media_list = ClientMedia.ListeningMediaList( file_service_key, [] )
self._currently_fetching_pairs = True
HG.client_controller.CallToThread( self.THREADFetchPairs, self._file_search_context, self._both_files_match )
self.update()
else:
def pair_is_good( pair ):
( first_hash, second_hash ) = pair
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_result, second_media_result ) = HG.client_controller.Read( 'media_results', pair )
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
potential_pair = self._unprocessed_pairs.pop()
while not pair_is_good( potential_pair ):
was_auto_skipped = True
self._processed_pairs.append( ( potential_pair, None, None, None, {}, was_auto_skipped ) )
if len( self._unprocessed_pairs ) == 0:
if len( self._processed_pairs ) == 0:
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
else:
self._ShowNewPair() # there are no useful decisions left in the queue, so let's reset
return
potential_pair = self._unprocessed_pairs.pop()
self._current_pair = potential_pair
( first_media_result, second_media_result ) = HG.client_controller.Read( 'media_results', self._current_pair )
if not ( first_media_result.GetLocationsManager().IsLocal() and second_media_result.GetLocationsManager().IsLocal() ):
QW.QMessageBox.warning( self, 'Warning', 'At least one of the potential files in this pair was not in this client. Likely it was very recently deleted through a different process. Your decisions until now will be saved, and then the duplicate filter will close.' )
self._CommitProcessed( blocking = True )
self._TryToCloseWindow()
return
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( file_service_key, media_results_with_better_first )
self.SetMedia( self._media_list.GetFirst() )
self._media_container.hide()
self._ReinitZoom()
self._ResetMediaWindowCenterPosition()
self._SizeAndPositionMediaContainer()
self._media_container.show()
def _SkipPair( self ):
if self._current_media is None:
return
was_auto_skipped = False
self._processed_pairs.append( ( self._current_pair, None, None, None, {}, was_auto_skipped ) )
self._ShowNewPair()
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( 'refresh_dupe_page_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 EventMouse( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
if event.modifiers() & QC.Qt.ShiftModifier:
caught = True
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.LeftButton:
self.BeginDrag()
elif event.type() == QC.QEvent.MouseButtonRelease and event.button() == QC.Qt.LeftButton:
self.EndDrag()
elif event.type() == QC.QEvent.MouseMove and event.buttons() != QC.Qt.NoButton:
self.EventMouseMove( event )
else:
caught = False
if caught:
return
shortcut = ClientGUIShortcuts.ConvertMouseEventToShortcut( event )
if shortcut is not None:
shortcut_processed = self._my_shortcuts_handler.ProcessShortcut( shortcut )
if shortcut_processed:
return
if event.type() == QC.QEvent.Wheel and event.angleDelta().y() != 0:
self._SwitchMedia()
else:
return True # was: event.ignore()
else:
return True # was: event.ignore()
def Inbox( self, canvas_key ):
if self._canvas_key == canvas_key:
self._Inbox()
def keyPressEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if modifier == QC.Qt.NoModifier and key in ClientGUIShortcuts.DELETE_KEYS:
self._Delete()
elif modifier == QC.Qt.ShiftModifier and key in ClientGUIShortcuts.DELETE_KEYS:
self._Undelete()
else:
CanvasWithHovers.keyPressEvent( self, event )
else:
event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == 'duplicate_filter_this_is_better_and_delete_other':
self._CurrentMediaIsBetter( delete_second = True )
elif action == 'duplicate_filter_this_is_better_but_keep_both':
self._CurrentMediaIsBetter( delete_second = False )
elif action == 'duplicate_filter_exactly_the_same':
self._MediaAreTheSame()
elif action == 'duplicate_filter_alternates':
self._MediaAreAlternates()
elif action == 'duplicate_filter_false_positive':
self._MediaAreFalsePositive()
elif action == 'duplicate_filter_custom_action':
self._DoCustomAction()
elif action == 'duplicate_filter_skip':
self._SkipPair()
elif action == 'duplicate_filter_back':
self._GoBack()
elif action in ( 'view_first', 'view_last', 'view_previous', '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._ShowNewPair()
else:
self.update()
HG.client_controller.CallLaterQtSafe(self, 0.1, 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 = len( self._unprocessed_pairs ) == 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 ):
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._unprocessed_pairs = unprocessed_pairs
self._currently_fetching_pairs = False
self._ShowNewPair()
result = HG.client_controller.Read( 'duplicate_pairs_for_filtering', file_search_context, both_files_match )
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, media_results ):
CanvasWithHovers.__init__( self, parent )
ClientMedia.ListeningMediaList.__init__( self, CC.LOCAL_FILE_SERVICE_KEY, media_results )
self._page_key = page_key
self._just_started = True
self._widget_event_filter.EVT_LEFT_DOWN( self.EventDragBegin )
self._widget_event_filter.EVT_LEFT_UP( self.EventDragEnd )
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 = 0.1
num_to_go_back = 3
num_to_go_forward = 5
# 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 ):
HG.client_controller.CallLaterQtSafe(self, delay, image_cache.GetImageRenderer, 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 ):
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 KeepCursorAlive( self ):
self._InitiateCursorHideWait()
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() )
class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def __init__( self, parent, page_key, media_results ):
CanvasMediaList.__init__( self, parent, page_key, media_results )
self._my_shortcuts_handler.AddShortcuts( 'archive_delete_filter' )
self._reserved_shortcut_names.append( 'archive_delete_filter' )
self._kept = set()
self._deleted = set()
self._widget_event_filter.EVT_MOUSE_EVENTS( self.EventMouse )
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._IShouldCatchShortcutEvent():
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 ):
if len( self._kept ) > 0 or len( self._deleted ) > 0:
label = 'keep ' + HydrusData.ToHumanInt( len( self._kept ) ) + ' and delete ' + HydrusData.ToHumanInt( len( self._deleted ) ) + ' files?'
( 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:
def process_in_thread( service_keys_and_content_updates ):
for ( service_key, content_update ) in service_keys_and_content_updates:
HG.client_controller.WriteSynchronous( 'content_updates', { service_key : [ content_update ] } )
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
self._kept_hashes = [ media.GetHash() for media in self._kept ]
service_keys_and_content_updates = []
reason = 'Deleted in Archive/Delete filter.'
for chunk_of_hashes in HydrusData.SplitListIntoChunks( self._deleted_hashes, 64 ):
service_keys_and_content_updates.append( ( CC.LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, chunk_of_hashes, reason = reason ) ) )
service_keys_and_content_updates.append( ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) ) )
HG.client_controller.CallToThread( process_in_thread, service_keys_and_content_updates )
self._kept = set()
self._deleted = set()
self._current_media = self._GetFirst() # so the pubsub on close is better
if HC.options[ 'remove_filtered_files' ]:
all_hashes = set()
all_hashes.update( self._deleted_hashes )
all_hashes.update( self._kept_hashes )
HG.client_controller.pub( 'remove_media', self._page_key, all_hashes )
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 ):
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 EventDelete( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
self._Delete()
else:
return True # was: event.ignore()
def EventMouse( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
if event.modifiers() & QC.Qt.ShiftModifier:
caught = True
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.LeftButton:
self.BeginDrag()
elif event.type() == QC.QEvent.MouseButtonRelease and event.button() == QC.Qt.LeftButton:
self.EndDrag()
elif event.type() == QC.QEvent.MouseMove and event.buttons() != QC.Qt.NoButton:
self.EventMouseMove( event )
else:
caught = False
if caught:
return False
shortcut = ClientGUIShortcuts.ConvertMouseEventToShortcut( event )
if shortcut is not None:
shortcut_processed = self._my_shortcuts_handler.ProcessShortcut( shortcut )
if shortcut_processed:
return False
return True # was: event.ignore()
def EventUndelete( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
self._Undelete()
else:
return True # was: event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action in ( 'archive_delete_filter_keep', 'archive_file' ):
self._Keep()
elif action in ( 'archive_delete_filter_delete', 'delete_file' ):
self._Delete()
elif action == 'archive_delete_filter_skip':
self._Skip()
elif action == 'archive_delete_filter_back':
self._Back()
elif action == '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, media_results ):
CanvasMediaList.__init__( self, parent, page_key, media_results )
self._my_shortcuts_handler.AddShortcuts( 'media_viewer_browser' )
self._reserved_shortcut_names.append( '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 ):
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, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == 'remove_file_from_view':
self._Remove()
elif action == 'view_first':
self._ShowFirst()
elif action == 'view_last':
self._ShowLast()
elif action == 'view_previous':
self._ShowPrevious()
elif action == '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, media_results, first_hash ):
CanvasMediaListNavigable.__init__( self, parent, page_key, media_results )
self._timer_slideshow_job = None
self._timer_slideshow_interval = 0
self._widget_event_filter.EVT_MOUSE_EVENTS( self.EventMouse )
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( self._timer_slideshow_interval )
def _RunningSlideshow( self ):
return self._timer_slideshow_job is not None
def _StartSlideshow( self, interval = None ):
self._StopSlideshow()
if interval is None:
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() )
except:
return
if interval > 0:
self._timer_slideshow_interval = interval
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, self.DoSlideshow )
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, self.DoSlideshow )
else:
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, 0.1, self.DoSlideshow )
except:
self._timer_slideshow_job = None
raise
def contextMenuEvent( self, event ):
if event.reason() == QG.QContextMenuEvent.Keyboard:
self.ShowMenu()
def EventMouse( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
shortcut = ClientGUIShortcuts.ConvertMouseEventToShortcut( event )
if shortcut is not None:
shortcut_processed = self._my_shortcuts_handler.ProcessShortcut( shortcut )
if shortcut_processed:
return
else:
return True # was: event.ignore()
def keyPressEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if modifier == QC.Qt.NoModifier and key in ClientGUIShortcuts.DELETE_KEYS: self._Delete()
elif modifier == QC.Qt.ShiftModifier and key in ClientGUIShortcuts.DELETE_KEYS: self._Undelete()
else:
CanvasMediaListNavigable.keyPressEvent( self, event )
else:
event.ignore()
def ProcessApplicationCommand( self, command, canvas_key = None ):
if canvas_key is not None and canvas_key != self._canvas_key:
return False
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == 'pause_play_slideshow':
self._PausePlaySlideshow()
elif action == '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._StartSlideshow )
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 )
if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent():
ClientGUIMenus.AppendMenuItem( menu, 'delete', 'Send this file to the trash.', self._Delete, file_service_key=CC.LOCAL_FILE_SERVICE_KEY )
elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent():
ClientGUIMenus.AppendMenuItem( menu, 'delete from trash now', 'Delete this file immediately. This cannot be undone.', self._Delete, file_service_key=CC.TRASH_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, 'known urls', 'Manage this file\'s known urls.', self._ManageURLs )
ClientGUIMenus.AppendMenuItem( manage_menu, 'notes', 'Manage this file\'s notes.', self._ManageNotes )
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 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 not HC.PLATFORM_LINUX
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 (hydrus default)', '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 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 )
def wheelEvent( self, event ):
if self._IShouldCatchShortcutEvent( event = event ):
if event.modifiers() & QC.Qt.ControlModifier:
if event.angleDelta().y() > 0:
self._ZoomIn()
else:
self._ZoomOut()
else:
if event.angleDelta().y() > 0:
self._ShowPrevious()
else:
self._ShowNext()