import HydrusConstants as HC import HydrusData import HydrusExceptions import HydrusGlobals as HG import ClientCaches import ClientConstants as CC import ClientData import ClientGUICommon import ClientGUIDialogs import ClientGUIDialogsManage import ClientGUIHoverFrames import ClientGUIMenus import ClientGUIScrolledPanelsEdit import ClientGUIScrolledPanelsManagement import ClientGUIShortcuts import ClientGUITopLevelWindows import ClientMedia import ClientRatings import ClientRendering import ClientTags import gc import HydrusImageHandling import HydrusPaths import HydrusSerialisable import HydrusTags import os import urlparse import webbrowser import wx if HC.PLATFORM_WINDOWS: import wx.lib.flashwin ID_TIMER_SLIDESHOW = wx.NewId() ID_TIMER_CURSOR_HIDE = wx.NewId() ID_TIMER_HOVER_SHOW = wx.NewId() ANIMATED_SCANBAR_HEIGHT = 20 ANIMATED_SCANBAR_CARET_WIDTH = 10 OPEN_EXTERNALLY_BUTTON_SIZE = ( 200, 45 ) def CalculateCanvasMediaSize( media, ( canvas_width, canvas_height ) ): if ShouldHaveAnimationBar( media ): canvas_height -= ANIMATED_SCANBAR_HEIGHT if media.GetMime() == HC.APPLICATION_FLASH: canvas_height -= 10 canvas_width -= 10 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 ) = media.GetResolution() if media_width == 0 or media_height == 0: return ( 1.0, 1.0 ) new_options = HG.client_controller.GetNewOptions() ( canvas_width, canvas_height ) = CalculateCanvasMediaSize( media, canvas.GetClientSize() ) width_zoom = canvas_width / float( media_width ) height_zoom = canvas_height / float( 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 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, action ): if 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 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(), HC.UNSCALED_THUMBNAIL_DIMENSIONS ) height = height + thumb_height return ( width, height ) else: ( media_width, media_height ) = CalculateMediaSize( media, zoom ) if ShouldHaveAnimationBar( media ): media_height += ANIMATED_SCANBAR_HEIGHT return ( media_width, media_height ) def CalculateMediaSize( media, zoom ): ( 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 ) def ShouldHaveAnimationBar( media ): is_animated_gif = media.GetMime() == HC.IMAGE_GIF and media.HasDuration() is_animated_flash = media.GetMime() == HC.APPLICATION_FLASH and media.HasDuration() is_native_video = media.GetMime() in HC.NATIVE_VIDEO has_more_than_one_frame = media.GetNumFrames() > 1 return is_animated_gif or is_animated_flash or is_native_video class Animation( wx.Window ): TIMER_MS = 5 def __init__( self, parent ): wx.Window.__init__( self, parent ) self._media = None self._animation_bar = None self._drag_happened = False self._left_down_event = None self._a_frame_has_been_drawn = False self._has_played_once_through = False self._num_frames = 1 self._current_frame_index = 0 self._current_frame_drawn = False self._next_frame_due_at = HydrusData.GetNowPrecise() self._slow_frame_score = 1.0 self._paused = True self._video_container = None self._canvas_bmp = None self._frame_bmp = None self._timer_video = wx.Timer( self ) self.Bind( wx.EVT_PAINT, self.EventPaint ) self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_TIMER, self.TIMEREventVideo ) self.Bind( wx.EVT_MOUSE_EVENTS, self.EventPropagateMouse ) self.Bind( wx.EVT_KEY_UP, self.EventPropagateKey ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) def __del__( self ): if self._video_container is not None: self._video_container.Stop() if self._frame_bmp is not None: self._frame_bmp.Destroy() if self._canvas_bmp is not None: self._canvas_bmp.Destroy() wx.CallLater( 500, gc.collect ) def _DrawFrame( self, dc ): current_frame = self._video_container.GetFrame( self._current_frame_index ) ( my_width, my_height ) = self._canvas_bmp.GetSize() ( frame_width, frame_height ) = current_frame.GetSize() if self._frame_bmp is None or self._frame_bmp.GetSize() != current_frame.GetSize(): self._frame_bmp = wx.EmptyBitmap( frame_width, frame_height, current_frame.GetDepth() * 8 ) current_frame.CopyToWxBitmap( self._frame_bmp ) # since stretchblit is unreliable, and since stretched drawing is so slow anyway, let's do it at the numpy_level # so this calls for 'copy this clipped region to this bmp' # the frame container clips the numpy_image, resizes up in cv, fills the bmp # then we blit in 0.001ms no prob if HC.PLATFORM_OSX or HC.PLATFORM_LINUX: # for some reason, stretchblit just draws white for os x # and for ubuntu 16.04, it only handles the first frame! # maybe a wx.copy problem? # or a mask? # os x double buffering something? # apparently some os x blit bindings might just be missing scale = float( my_width ) / frame_width dc.SetUserScale( scale, scale ) dc.DrawBitmap( self._frame_bmp, 0, 0 ) dc.SetUserScale( 1.0, 1.0 ) else: # next step here is to deal with superzoom cleverly, by having a clipped bmp # only blit from the clipped section of the src to our clipped bmp # on resize, get the parent canvas, get its clienttoscreen size/pos, compare that with our own, clip a bmp, something like that. # think we'll have to initialise the dc with that in mind, moving our smaller bmp to the correct virtual location on the window # I think this is dc.SetDeviceOrigin # and do something similar for staticimage # will need to setdirty on drag that reveals offscreen region # hence prob a good idea to give the bmp 100px or so spare offscreen buffer, to reduce redraw spam, if that can be neatly done mdc = wx.MemoryDC( self._frame_bmp ) dc.StretchBlit( 0, 0, my_width, my_height, mdc, 0, 0, frame_width, frame_height ) if self._animation_bar is not None: self._animation_bar.GotoFrame( self._current_frame_index ) self._current_frame_drawn = True next_frame_time_s = self._video_container.GetDuration( self._current_frame_index ) / 1000.0 next_frame_ideally_due = self._next_frame_due_at + next_frame_time_s if HydrusData.TimeHasPassedPrecise( next_frame_ideally_due ): self._next_frame_due_at = HydrusData.GetNowPrecise() + next_frame_time_s else: self._next_frame_due_at = next_frame_ideally_due self._a_frame_has_been_drawn = True def _DrawWhite( self, dc ): new_options = HG.client_controller.GetNewOptions() dc.SetBackground( wx.Brush( new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) ) ) dc.Clear() def _TellAnimationBarAboutPausedStatus( self ): if self._animation_bar is not None: self._animation_bar.SetPaused( self._paused ) def CurrentFrame( self ): return self._current_frame_index def EventEraseBackground( self, event ): pass def EventPaint( self, event ): if self._video_container is None: self._video_container = ClientRendering.RasterContainerVideo( self._media, self.GetClientSize(), init_position = self._current_frame_index ) dc = wx.BufferedPaintDC( self, self._canvas_bmp ) if not self._a_frame_has_been_drawn: self._DrawWhite( dc ) def EventPropagateKey( self, event ): event.ResumePropagation( 1 ) event.Skip() def EventPropagateMouse( self, event ): if self._animation_bar is not None: if not ( event.ShiftDown() or event.CmdDown() or event.AltDown() ): if event.LeftDClick(): hash = self._media.GetHash() mime = self._media.GetMime() client_files_manager = HG.client_controller.client_files_manager path = client_files_manager.GetFilePath( hash, mime ) new_options = HG.client_controller.GetNewOptions() launch_path = new_options.GetMimeLaunch( mime ) HydrusPaths.LaunchFile( path, launch_path ) self.Pause() return elif event.LeftDown(): self.PausePlay() self.GetParent().BeginDrag() return if self.IsShown(): # Can't ClientToScreen if not shown, like in init screen_position = self.ClientToScreen( event.GetPosition() ) ( x, y ) = self.GetParent().ScreenToClient( screen_position ) event.SetX( x ) event.SetY( y ) event.ResumePropagation( 1 ) event.Skip() def EventResize( self, event ): ( my_width, my_height ) = self.GetClientSize() if my_width > 0 and my_height > 0: if self._canvas_bmp is None: make_new_one = True else: ( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize() make_new_one = my_width != current_bmp_width or my_height != current_bmp_height if make_new_one: if self._canvas_bmp is not None: wx.CallAfter( self._canvas_bmp.Destroy ) self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 ) self._current_frame_drawn = False self._a_frame_has_been_drawn = False self.Refresh() if self._media is not None: ( media_width, media_height ) = self._media.GetResolution() if self._video_container is not None: ( renderer_width, renderer_height ) = self._video_container.GetSize() we_just_zoomed_in = my_width > renderer_width or my_height > renderer_height we_just_zoomed_out = my_width < renderer_width or my_height < renderer_height if we_just_zoomed_in: if self._video_container.IsScaled(): target_width = min( media_width, my_width ) target_height = min( media_height, my_height ) self._video_container.Stop() self._video_container = ClientRendering.RasterContainerVideo( self._media, ( target_width, target_height ), init_position = self._current_frame_index ) elif we_just_zoomed_out: if my_width < media_width or my_height < media_height: # i.e. new zoom is scaled self._video_container.Stop() self._video_container = ClientRendering.RasterContainerVideo( self._media, ( my_width, my_height ), init_position = self._current_frame_index ) def GotoFrame( self, frame_index ): if self._video_container is not None and self._video_container.IsInitialised(): if frame_index != self._current_frame_index: self._current_frame_index = frame_index self._video_container.GetReadyForFrame( self._current_frame_index ) self._current_frame_drawn = False self._paused = True self._TellAnimationBarAboutPausedStatus() def HasPlayedOnceThrough( self ): return self._has_played_once_through def IsPlaying( self ): return not self._paused def Play( self ): self._paused = False self._TellAnimationBarAboutPausedStatus() def Pause( self ): self._paused = True self._TellAnimationBarAboutPausedStatus() def PausePlay( self ): self._paused = not self._paused self._TellAnimationBarAboutPausedStatus() def SetAnimationBar( self, animation_bar ): self._animation_bar = animation_bar if self._animation_bar is not None: self._animation_bar.GotoFrame( self._current_frame_index ) self._TellAnimationBarAboutPausedStatus() def SetMedia( self, media, start_paused ): self._media = media self._drag_happened = False self._left_down_event = None self._a_frame_has_been_drawn = False self._has_played_once_through = False self._num_frames = self._media.GetNumFrames() self._current_frame_index = int( ( self._num_frames - 1 ) * HC.options[ 'animation_start_position' ] ) self._current_frame_drawn = False self._next_frame_due_at = HydrusData.GetNowPrecise() self._slow_frame_score = 1.0 self._paused = start_paused if self._video_container is not None: self._video_container.Stop() self._video_container = None self._frame_bmp = None self._timer_video.Start( self.TIMER_MS, wx.TIMER_CONTINUOUS ) self.Refresh() def TIMEREventVideo( self, event ): try: if self.IsShownOnScreen(): if self._current_frame_drawn: if not self._paused and HydrusData.TimeHasPassedPrecise( self._next_frame_due_at - self.TIMER_MS / 1000.0 ): num_frames = self._media.GetNumFrames() self._current_frame_index = ( self._current_frame_index + 1 ) % num_frames if self._current_frame_index == 0: self._has_played_once_through = True self._current_frame_drawn = False if self._video_container is not None: if not self._current_frame_drawn: if self._video_container.HasFrame( self._current_frame_index ): dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp ) self._DrawFrame( dc ) if self._animation_bar is not None: buffer_indices = self._video_container.GetBufferIndices() self._animation_bar.SetBufferIndices( buffer_indices ) except wx.PyDeadObjectError: self._timer_video.Stop() except: self._timer_video.Stop() raise class AnimationBar( wx.Window ): def __init__( self, parent ): wx.Window.__init__( self, parent ) self._dirty = False self._canvas_bmp = None self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) ) self._media_window = None self._paused = False self._num_frames = 1 self._current_frame_index = 0 self._buffer_indices = None self._has_experienced_mouse_down = False self._currently_in_a_drag = False self._it_was_playing = False self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse ) self.Bind( wx.EVT_TIMER, self.TIMERFlashIndexUpdate ) self.Bind( wx.EVT_PAINT, self.EventPaint ) self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) self._flash_index_update_timer = wx.Timer( self ) def _GetXFromFrameIndex( self, index, width_offset = 0 ): if self._num_frames < 2: return 0 ( my_width, my_height ) = self._canvas_bmp.GetSize() return int( float( my_width - width_offset ) * float( index ) / float( self._num_frames - 1 ) ) def _Redraw( self, dc ): ( my_width, my_height ) = self._canvas_bmp.GetSize() dc.SetPen( wx.TRANSPARENT_PEN ) background_colour = wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) if self._paused: background_colour = ClientData.GetLighterDarkerColour( background_colour ) dc.SetBackground( wx.Brush( background_colour ) ) dc.Clear() # if self._buffer_indices is not None: ( start_index, rendered_to_index, end_index ) = self._buffer_indices start_x = self._GetXFromFrameIndex( start_index ) rendered_to_x = self._GetXFromFrameIndex( rendered_to_index ) end_x = self._GetXFromFrameIndex( end_index ) if start_x != rendered_to_x: rendered_colour = ClientData.GetDifferentLighterDarkerColour( background_colour ) dc.SetBrush( wx.Brush( rendered_colour ) ) if rendered_to_x > start_x: dc.DrawRectangle( start_x, 0, rendered_to_x - start_x, ANIMATED_SCANBAR_HEIGHT ) else: dc.DrawRectangle( start_x, 0, my_width - start_x, ANIMATED_SCANBAR_HEIGHT ) dc.DrawRectangle( 0, 0, rendered_to_x, ANIMATED_SCANBAR_HEIGHT ) if rendered_to_x != end_x: to_be_rendered_colour = ClientData.GetDifferentLighterDarkerColour( background_colour, 1 ) dc.SetBrush( wx.Brush( to_be_rendered_colour ) ) if end_x > rendered_to_x: dc.DrawRectangle( rendered_to_x, 0, end_x - rendered_to_x, ANIMATED_SCANBAR_HEIGHT ) else: dc.DrawRectangle( rendered_to_x, 0, my_width - rendered_to_x, ANIMATED_SCANBAR_HEIGHT ) dc.DrawRectangle( 0, 0, end_x, ANIMATED_SCANBAR_HEIGHT ) dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNSHADOW ) ) ) caret_x = self._GetXFromFrameIndex( self._current_frame_index, width_offset = ANIMATED_SCANBAR_CARET_WIDTH ) dc.DrawRectangle( caret_x, 0, ANIMATED_SCANBAR_CARET_WIDTH, ANIMATED_SCANBAR_HEIGHT ) # dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) ) s = HydrusData.ConvertValueRangeToPrettyString( self._current_frame_index + 1, self._num_frames ) ( x, y ) = dc.GetTextExtent( s ) dc.DrawText( s, my_width - x - 3, 3 ) self._dirty = False def EventEraseBackground( self, event ): pass def EventMouse( self, event ): if self._media_window is not None: CC.CAN_HIDE_MOUSE = False if event.ButtonDown( wx.MOUSE_BTN_ANY ): self._has_experienced_mouse_down = True # sometimes, this can inherit mouse-down from previous filter or embed button reveal, resulting in undesired scan if not self._has_experienced_mouse_down: return ( my_width, my_height ) = self.GetClientSize() if event.Dragging(): self._currently_in_a_drag = True if event.ButtonIsDown( wx.MOUSE_BTN_ANY ): if not self._currently_in_a_drag: self._it_was_playing = self._media_window.IsPlaying() ( x, y ) = event.GetPosition() compensated_x_position = x - ( ANIMATED_SCANBAR_CARET_WIDTH / 2 ) proportion = float( compensated_x_position ) / float( my_width - ANIMATED_SCANBAR_CARET_WIDTH ) if proportion < 0: proportion = 0 if proportion > 1: proportion = 1 self._current_frame_index = int( proportion * ( self._num_frames - 1 ) + 0.5 ) self._dirty = True self.Refresh() self._media_window.GotoFrame( self._current_frame_index ) elif event.ButtonUp( wx.MOUSE_BTN_ANY ): if self._it_was_playing: self._media_window.Play() self._currently_in_a_drag = False def EventPaint( self, event ): if self._canvas_bmp is not None: dc = wx.BufferedPaintDC( self, self._canvas_bmp ) if self._dirty: self._Redraw( dc ) def EventResize( self, event ): ( my_width, my_height ) = self.GetClientSize() if my_width > 0 and my_height > 0: if self._canvas_bmp is None: make_new_one = True else: ( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize() make_new_one = my_width != current_bmp_width or my_height != current_bmp_height if make_new_one: if self._canvas_bmp is not None: wx.CallAfter( self._canvas_bmp.Destroy ) self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 ) self._dirty = True self.Refresh() def GotoFrame( self, frame_index ): self._current_frame_index = frame_index self._dirty = True self.Refresh() def SetBufferIndices( self, buffer_indices ): if buffer_indices != self._buffer_indices: self._buffer_indices = buffer_indices self._dirty = True self.Refresh() def SetMediaAndWindow( self, media, media_window ): self._media_window = media_window self._paused = False self._num_frames = max( media.GetNumFrames(), 1 ) self._current_frame_index = 0 self._buffer_indices = None self._has_experienced_mouse_down = False self._currently_in_a_drag = False self._it_was_playing = False if media.GetMime() == HC.APPLICATION_FLASH: self._flash_index_update_timer.Start( 100, wx.TIMER_CONTINUOUS ) else: self._flash_index_update_timer.Stop() self._dirty = True def SetNoneMedia( self ): self._media_window = None self._flash_index_update_timer.Stop() def SetPaused( self, paused ): self._paused = paused self._dirty = True self.Refresh() def TIMERFlashIndexUpdate( self, event ): try: if self.IsShownOnScreen(): try: frame_index = self._media_window.CurrentFrame() except AttributeError: text = 'The flash window produced an unusual error that probably means it never initialised properly. This is usually because Flash has not been installed for Internet Explorer. ' text += os.linesep * 2 text += 'Please close the client, open Internet Explorer, and install flash from Adobe\'s site and then try again. If that does not work, please tell the hydrus developer.' HydrusData.ShowText( text ) raise if frame_index != self._current_frame_index: self._current_frame_index = frame_index self._dirty = True self.Refresh() except wx.PyDeadObjectError: self._flash_index_update_timer.Stop() except: self._flash_index_update_timer.Stop() raise class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ): def __init__( self, parent ): if HC.PLATFORM_OSX: float_on_parent = True else: float_on_parent = False ClientGUITopLevelWindows.FrameThatResizes.__init__( self, parent, 'hydrus client media viewer', 'media_viewer', float_on_parent = float_on_parent ) self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook ) def _ProcessApplicationCommand( self, command ): command_processed = True command_type = command.GetCommandType() data = command.GetData() if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE: action = data if action == 'switch_between_fullscreen_borderless_and_regular_framed_window': self.FullscreenSwitch() elif action == 'flip_darkmode': HG.client_controller.gui.FlipDarkmode() else: command_processed = False else: command_processed = False return command_processed def _ProcessShortcut( self, shortcut ): shortcut_processed = False command = HG.client_controller.GetCommandFromShortcut( [ 'media_viewer' ], shortcut ) if command is not None: command_processed = self._ProcessApplicationCommand( command ) if command_processed: shortcut_processed = True return shortcut_processed def Close( self ): if HC.PLATFORM_OSX and self.IsFullScreen(): self.ShowFullScreen( False, wx.FULLSCREEN_ALL ) self.Destroy() def EventCharHook( self, event ): if ClientGUIShortcuts.IShouldCatchCharHook( self ): shortcut = ClientData.ConvertKeyEventToShortcut( event ) if shortcut is not None: shortcut_processed = self._ProcessShortcut( shortcut ) if shortcut_processed: return event.Skip() def FullscreenSwitch( self ): if self.IsFullScreen(): self.ShowFullScreen( False, wx.FULLSCREEN_ALL ) else: self.ShowFullScreen( True, wx.FULLSCREEN_ALL ) self._canvas_window.ResetDragDelta() def SetCanvas( self, canvas_window ): self._canvas_window = canvas_window vbox = wx.BoxSizer( wx.VERTICAL ) vbox.AddF( self._canvas_window, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) self.SetSizer( vbox ) ClientGUITopLevelWindows.SetTLWSizeAndPosition( self, self._frame_key ) self.Show( True ) wx.GetApp().SetTopWindow( self ) self.Bind( wx.EVT_CLOSE, self._canvas_window.EventClose ) class Canvas( wx.Window ): BORDER = wx.SIMPLE_BORDER PREVIEW_WINDOW = False def __init__( self, parent ): wx.Window.__init__( self, parent, style = self.BORDER ) self._file_service_key = CC.LOCAL_FILE_SERVICE_KEY self._reserved_shortcut_names = [] self._reserved_shortcut_names.append( 'media' ) self._reserved_shortcut_names.append( 'media_viewer' ) self._new_options = HG.client_controller.GetNewOptions() self._custom_shortcut_names = self._new_options.GetStringList( 'default_media_viewer_custom_shortcuts' ) self._canvas_key = HydrusData.GenerateKey() self._maintain_pan_and_zoom = False self._dirty = True self._closing = False self._manage_tags_panel = None self._service_keys_to_services = {} self._current_media = None self._media_container = MediaContainer( self ) self._current_zoom = 1.0 self._canvas_zoom = 1.0 self._last_drag_coordinates = None self._current_drag_is_touch = False self._last_motion_coordinates = ( 0, 0 ) self._total_drag_delta = ( 0, 0 ) self._UpdateBackgroundColour() self._canvas_bmp = wx.EmptyBitmap( 20, 20, 24 ) self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_PAINT, self.EventPaint ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook ) 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, 'EditMediaViewerCustomShortcuts', 'edit_media_viewer_custom_shortcuts' ) HG.client_controller.sub( self, 'ProcessApplicationCommand', 'canvas_application_command' ) HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' ) HG.client_controller.sub( self, '_SetDirty', '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 _CanProcessInput( self ): if HG.client_controller.MenuIsOpen(): return False if self._MouseIsOverFlash(): return False return True def _CopyBMPToClipboard( self ): if self._current_media is not None: HG.client_controller.pub( 'clipboard', 'bmp', self._current_media ) def _CopyHashToClipboard( self, hash_type ): sha256_hash = self._current_media.GetHash() if hash_type == 'sha256': hex_hash = sha256_hash.encode( '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.encode( 'hex' ) else: wx.MessageBox( '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, service_key = None ): if self._current_media is None: return do_it = False if service_key is None: locations_manager = self._current_media.GetLocationsManager() if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent(): service_key = CC.LOCAL_FILE_SERVICE_KEY elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent(): service_key = CC.TRASH_SERVICE_KEY else: return if service_key == CC.LOCAL_FILE_SERVICE_KEY: if not HC.options[ 'confirm_trash' ]: do_it = True text = 'Send this file to the trash?' elif service_key == CC.TRASH_SERVICE_KEY: text = 'Permanently delete this file?' if not do_it: with ClientGUIDialogs.DialogYesNo( self, text ) as dlg: if dlg.ShowModal() == wx.ID_YES: do_it = True self.SetFocus() # annoying bug because of the modal dialog if do_it: hashes = { self._current_media.GetHash() } HG.client_controller.Write( 'content_updates', { service_key : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes ) ] } ) def _DoManualPan( self, delta_x_step, delta_y_step ): if self._current_media is None: return ( my_x, my_y ) = self.GetClientSize() ( media_x, media_y ) = self._media_container.GetClientSize() x_pan_distance = min( my_x / 12, media_x / 12 ) y_pan_distance = min( my_y / 12, media_y / 12 ) delta_x = delta_x_step * x_pan_distance delta_y = delta_y_step * y_pan_distance ( old_delta_x, old_delta_y ) = self._total_drag_delta self._total_drag_delta = ( old_delta_x + delta_x, old_delta_y + delta_y ) self._DrawCurrentMedia() def _DrawBackgroundBitmap( self, dc ): background_colour = self._GetBackgroundColour() dc.SetBackground( wx.Brush( background_colour ) ) dc.Clear() self._DrawBackgroundDetails( dc ) self._dirty = False def _DrawBackgroundDetails( self, dc ): pass def _DrawCurrentMedia( self ): if self._current_media is None: return ( my_width, my_height ) = self.GetClientSize() if my_width > 0 and my_height > 0: self._SizeAndPositionMediaContainer() def _FocusIsElsewhere( self ): i_have_focus = ClientGUICommon.WindowOrSameTLPChildHasFocus( self ) my_hover_window_has_focus = ClientGUICommon.WindowOrAnyTLPChildHasFocus( self ) and isinstance( ClientGUICommon.GetFocusTLP(), ClientGUIHoverFrames.FullscreenHoverFrame ) focus_is_elsewhere = not ( i_have_focus or my_hover_window_has_focus ) if focus_is_elsewhere: return True return False def _GenerateOrderedShortcutNames( self ): # do custom first, then let the more specialised take priority shortcut_names = self._reserved_shortcut_names + self._custom_shortcut_names shortcut_names.reverse() return shortcut_names def _GetBackgroundColour( self ): return self._new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) def _GetShowAction( self, media ): if media is None: return CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW mime = media.GetMime() if mime == HC.APPLICATION_HYDRUS_CLIENT_COLLECTION: return CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW if self.PREVIEW_WINDOW: return self._new_options.GetPreviewShowAction( mime ) else: return self._new_options.GetMediaShowAction( mime ) def _GetIndexString( self ): return '' def _GetMediaContainerSizeAndPosition( self ): ( my_width, my_height ) = self.GetClientSize() action = self._GetShowAction( self._current_media ) ( media_width, media_height ) = CalculateMediaContainerSize( self._current_media, self._current_zoom, action ) ( drag_x, drag_y ) = self._total_drag_delta x_offset = ( my_width - media_width ) / 2 + drag_x y_offset = ( my_height - media_height ) / 2 + drag_y new_size = ( media_width, media_height ) new_position = ( x_offset, y_offset ) return ( new_size, new_position ) 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 return self._GetShowAction( self._current_media ) not in ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ) def _MaintainZoom( self, previous_media ): if previous_media is None: self._ReinitZoom() else: if self._current_media is None: return ( previous_width, previous_height ) = previous_media.GetResolution() ( current_width, current_height ) = self._current_media.GetResolution() previous_ratio = float( previous_width ) / float( previous_height ) current_ratio = float( current_width ) / float( current_height ) if previous_ratio == current_ratio: # if this new one is half the size, the new zoom needs to be twice as much to be the same size zoom_ratio = float( previous_width ) / float( current_width ) ultimate_canvas_zoom = self._current_zoom * zoom_ratio self._ReinitZoom() self._current_zoom = ultimate_canvas_zoom HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom ) else: self._ResetDragDelta() self._ReinitZoom() 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.ShowModal() def _ManageTags( self ): if self._current_media is None: return if self._manage_tags_panel: self._manage_tags_panel.SetFocus() else: # take any focus away from hover window, which will mess up window order when it hides due to the new frame self.SetFocus() title = 'manage tags' frame_key = 'manage_tags_frame' manage_tags = ClientGUITopLevelWindows.FrameThatTakesScrollablePanel( self, title, frame_key ) panel = ClientGUIScrolledPanelsManagement.ManageTagsPanel( manage_tags, self._file_service_key, ( self._current_media, ), immediate_commit = True, canvas_key = self._canvas_key ) manage_tags.SetPanel( panel ) self._manage_tags_panel = 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.ShowModal() def _MouseIsOverFlash( self ): if self._current_media is not None and self._current_media.GetMime() == HC.APPLICATION_FLASH: if self.MouseIsOverMedia(): return True return False 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 ) if self._current_media.HasDuration() and mime != HC.APPLICATION_FLASH: self._media_container.Pause() def _PrefetchNeighbours( self ): pass def _ProcessApplicationCommand( self, command ): 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 == '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 == '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 ( service_key, content_type, action, value ) = data try: service = HG.client_controller.services_manager.GetService( service_key ) except HydrusExceptions.DataMissing: command_processed = False return command_processed service_type = service.GetServiceType() hashes = ( self._current_media.GetHash(), ) if service_type in HC.TAG_SERVICES: tag = value tags_manager = self._current_media.GetTagsManager() current = tags_manager.GetCurrent( service_key ) pending = tags_manager.GetPending( service_key ) petitioned = tags_manager.GetPetitioned( service_key ) if service_type == HC.LOCAL_TAG: tags = [ tag ] if tag in current: content_update_action = HC.CONTENT_UPDATE_DELETE else: content_update_action = HC.CONTENT_UPDATE_ADD tag_parents_manager = HG.client_controller.GetManager( 'tag_parents' ) parents = tag_parents_manager.GetParents( service_key, tag ) tags.extend( parents ) rows = [ ( tag, hashes ) for tag in tags ] else: if tag in current: if tag in petitioned: content_update_action = HC.CONTENT_UPDATE_RESCIND_PETITION rows = [ ( tag, hashes ) ] else: message = 'Enter a reason for this tag to be removed. A janitor will review your petition.' with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg: if dlg.ShowModal() == wx.ID_OK: content_update_action = HC.CONTENT_UPDATE_PETITION rows = [ ( dlg.GetValue(), tag, hashes ) ] else: return else: tags = [ tag ] if tag in pending: content_update_action = HC.CONTENT_UPDATE_RESCIND_PEND else: content_update_action = HC.CONTENT_UPDATE_PEND tag_parents_manager = HG.client_controller.GetManager( 'tag_parents' ) parents = tag_parents_manager.GetParents( service_key, tag ) tags.extend( parents ) rows = [ ( tag, hashes ) for tag in tags ] content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_update_action, row ) for row in rows ] elif service_type in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ): ratings_manager = self._current_media.GetRatingsManager() current_rating = ratings_manager.GetRating( service_key ) rating = value if current_rating == rating: # to flip rating = None row = ( rating, hashes ) content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, row ) ] HG.client_controller.Write( 'content_updates', { service_key : content_updates } ) else: command_processed = False return command_processed def _ProcessShortcut( self, shortcut ): shortcut_processed = False shortcut_names = self._GenerateOrderedShortcutNames() command = HG.client_controller.GetCommandFromShortcut( shortcut_names, shortcut ) if command is not None: command_processed = self._ProcessApplicationCommand( command ) shortcut_processed = command_processed return shortcut_processed def _ReinitZoom( self ): if self._current_media is None: return show_action = self._GetShowAction( self._current_media ) ( self._current_zoom, self._canvas_zoom ) = CalculateCanvasZooms( self, self._current_media, show_action ) HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom ) def _ResetDragDelta( self ): self._total_drag_delta = ( 0, 0 ) self._last_drag_coordinates = None def _SetDirty( self ): self._dirty = True self.Refresh() def _SizeAndPositionMediaContainer( self ): if self._current_media is None: return ( new_size, new_position ) = self._GetMediaContainerSizeAndPosition() if new_size != self._media_container.GetSize(): self._media_container.SetSize( new_size ) if HC.PLATFORM_OSX and new_position == self._media_container.GetPosition(): self._media_container.Refresh() if new_position != self._media_container.GetPosition(): self._media_container.SetPosition( new_position ) def _TryToChangeZoom( self, new_zoom ): if self._current_media is None: return if self._current_media.GetMime() == HC.APPLICATION_FLASH: # we want to preserve whitespace around flash ( my_width, my_height ) = self.GetClientSize() action = self._GetShowAction( self._current_media ) ( new_media_width, new_media_height ) = CalculateMediaContainerSize( self._current_media, new_zoom, action ) if new_media_width >= my_width or new_media_height >= my_height: return ( drag_x, drag_y ) = self._total_drag_delta zoom_ratio = new_zoom / self._current_zoom self._total_drag_delta = ( int( drag_x * zoom_ratio ), int( drag_y * zoom_ratio ) ) self._current_zoom = new_zoom HG.client_controller.pub( 'canvas_new_zoom', self._canvas_key, self._current_zoom ) self._SetDirty() 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: with ClientGUIDialogs.DialogYesNo( self, 'Undelete this file?' ) as dlg: if dlg.ShowModal() == wx.ID_YES: 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(), ) ) ] } ) self.SetFocus() # annoying bug because of the modal dialog def _UpdateBackgroundColour( self ): colour = self._GetBackgroundColour() self.SetBackgroundColour( colour ) self.Refresh() 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() and self._canvas_zoom != 1.0: ( my_width, my_height ) = self.GetClientSize() ( media_width, media_height ) = self._current_media.GetResolution() if self._current_zoom == 1.0: new_zoom = self._canvas_zoom else: new_zoom = 1.0 if new_zoom <= self._canvas_zoom: self._ResetDragDelta() self._TryToChangeZoom( new_zoom ) def EditMediaViewerCustomShortcuts( self, canvas_key ): if canvas_key == self._canvas_key: all_shortcut_names = HG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS ) custom_shortcuts_names = [ name for name in all_shortcut_names if name not in CC.SHORTCUTS_RESERVED_NAMES ] if len( custom_shortcuts_names ) == 0: wx.MessageBox( 'You have no custom shortcuts set up, so you cannot choose any!' ) return with ClientGUITopLevelWindows.DialogEdit( self, 'manage shortcuts' ) as dlg: choice_tuples = [ ( name, name, name in self._custom_shortcut_names ) for name in custom_shortcuts_names ] panel = ClientGUIScrolledPanelsEdit.EditChooseMultiple( dlg, choice_tuples ) dlg.SetPanel( panel ) if dlg.ShowModal() == wx.ID_OK: self._custom_shortcut_names = panel.GetValue() def EventCharHook( self, event ): if self._CanProcessInput() and not self._FocusIsElsewhere(): # focus is likely on a tag manager frame in this case shortcut = ClientData.ConvertKeyEventToShortcut( event ) if shortcut is not None: shortcut_processed = self._ProcessShortcut( shortcut ) if shortcut_processed: return event.Skip() def BeginDrag( self, pos = None ): if pos is None: ( x, y ) = self.ScreenToClient( wx.GetMousePosition() ) else: ( x, y ) = pos self._last_drag_coordinates = ( x, y ) self._current_drag_is_touch = False def EventEraseBackground( self, event ): pass def EventPaint( self, event ): dc = wx.BufferedPaintDC( self, self._canvas_bmp ) if self._dirty: self._DrawBackgroundBitmap( dc ) if self._current_media is not None: self._DrawCurrentMedia() def EventResize( self, event ): if not self._closing: ( my_width, my_height ) = self.GetClientSize() self._canvas_bmp.Destroy() self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 ) if self._current_media is not None: ( media_width, media_height ) = self._media_container.GetClientSize() if my_width != media_width or my_height != media_height: self._ReinitZoom() self._SetDirty() event.Skip() 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: ( x, y ) = self._media_container.GetScreenPosition() ( width, height ) = self._media_container.GetSize() ( mouse_x, mouse_y ) = wx.GetMousePosition() if mouse_x >= x and mouse_x <= x + width and mouse_y >= y and mouse_y <= y + height: return True return False def OpenExternally( self, canvas_key ): if self._canvas_key == canvas_key: self._OpenExternally() def ProcessApplicationCommand( self, canvas_key, command ): if canvas_key == self._canvas_key: self._ProcessApplicationCommand( command ) def ResetDragDelta( self ): self._ResetDragDelta() def SetMedia( self, media ): if media is not None: media = media.GetDisplayMedia() locations_manager = media.GetLocationsManager() if not locations_manager.IsLocal(): media = None elif self._GetShowAction( media ) in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ): media = None if media != self._current_media: HG.client_controller.ResetIdleTimer() previous_media = self._current_media self._current_media = media if not self._maintain_pan_and_zoom: self._ResetDragDelta() if self._current_media is None: self._media_container.SetNoneMedia() else: if previous_media is not None and self._maintain_pan_and_zoom: self._MaintainZoom( previous_media ) else: self._ReinitZoom() ( initial_size, initial_position ) = self._GetMediaContainerSizeAndPosition() ( initial_width, initial_height ) = initial_size if self._current_media.GetLocationsManager().IsLocal() and initial_width > 0 and initial_height > 0: show_action = self._GetShowAction( self._current_media ) self._media_container.SetMedia( self._current_media, initial_size, initial_position, show_action ) 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._SetDirty() 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 HG.client_controller.sub( self, 'PreviewChanged', 'preview_changed' ) HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' ) self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu ) def EventShowMenu( self, event ): if self._current_media is not None: 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 menu = wx.Menu() for line in self._current_media.GetPrettyInfoLines(): ClientGUIMenus.AppendMenuLabel( menu, line, line ) # ClientGUIMenus.AppendSeparator( menu ) manage_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, manage_menu, 'tags', 'Manage this file\'s tags.', self._ManageTags ) if i_can_post_ratings: ClientGUIMenus.AppendMenuItem( self, manage_menu, 'ratings', 'Manage this file\'s ratings.', self._ManageRatings ) ClientGUIMenus.AppendMenuItem( self, manage_menu, 'known urls', 'Manage this file\'s known URLs.', self._ManageURLs ) ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' ) ClientGUIMenus.AppendSeparator( menu ) if self._current_media.HasInbox(): ClientGUIMenus.AppendMenuItem( self, menu, 'archive', 'Archive this file.', self._Archive ) if self._current_media.HasArchive(): ClientGUIMenus.AppendMenuItem( self, menu, 'inbox', 'Send this files back to the inbox.', self._Inbox ) if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent(): ClientGUIMenus.AppendMenuItem( self, menu, 'delete', 'Delete this file.', self._Delete, CC.LOCAL_FILE_SERVICE_KEY ) elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent(): ClientGUIMenus.AppendMenuItem( self, menu, 'delete completely', 'Physically delete this file from disk.', self._Delete, CC.TRASH_SERVICE_KEY ) ClientGUIMenus.AppendMenuItem( self, menu, 'undelete', 'Take this file out of the trash.', self._Undelete ) ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( self, menu, 'open externally', 'Open this file in your OS\'s default program.', self._OpenExternally ) urls = self._current_media.GetLocationsManager().GetURLs() if len( urls ) > 0: urls = list( urls ) urls.sort() urls_menu = wx.Menu() urls_visit_menu = wx.Menu() urls_copy_menu = wx.Menu() for url in urls: ClientGUIMenus.AppendMenuItem( self, urls_visit_menu, url, 'Open this url in your web browser.', webbrowser.open, url ) ClientGUIMenus.AppendMenuItem( self, urls_copy_menu, url, 'Copy this url to your clipboard.', HG.client_controller.pub, 'clipboard', 'text', url ) ClientGUIMenus.AppendMenu( urls_menu, urls_visit_menu, 'open' ) ClientGUIMenus.AppendMenu( urls_menu, urls_copy_menu, 'copy' ) ClientGUIMenus.AppendMenu( menu, urls_menu, 'known urls' ) share_menu = wx.Menu() copy_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard ) copy_hash_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'sha256 (hydrus default)', 'Open this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' ) ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'md5', 'Open this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' ) ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'sha1', 'Open this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' ) ClientGUIMenus.AppendMenuItem( self, 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 and self._current_media.GetDuration() is None: ClientGUIMenus.AppendMenuItem( self, copy_menu, 'image', 'Copy this file to your clipboard as a bmp.', self._CopyBMPToClipboard ) ClientGUIMenus.AppendMenuItem( self, 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' ) HG.client_controller.PopupMenu( self, menu ) event.Skip() def PreviewChanged( self, page_key, media ): if HC.options[ 'hide_preview' ]: return if page_key == self._page_key: self.SetMedia( media ) def ProcessContentUpdates( self, service_keys_to_content_updates ): if self._current_media is not None: my_hash = self._current_media.GetHash() do_redraw = False for ( service_key, content_updates ) in service_keys_to_content_updates.items(): if True in ( my_hash in content_update.GetHashes() for content_update in content_updates ): do_redraw = True break if do_redraw: self._SetDirty() class CanvasWithDetails( Canvas ): BORDER = wx.NO_BORDER def _DrawAdditionalTopMiddleInfo( self, dc, current_y ): pass def _DrawBackgroundDetails( self, dc ): if self._current_media is None: text = 'No media to display' ( width, height ) = dc.GetTextExtent( text ) ( my_width, my_height ) = self.GetClientSize() x = ( my_width - width ) // 2 y = ( my_height - height ) // 2 dc.DrawText( text, x, y ) else: ( client_width, client_height ) = self.GetClientSize() # tags on the top left dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) ) tags_manager = self._current_media.GetTagsManager() current = tags_manager.GetCurrent() pending = tags_manager.GetPending() petitioned = tags_manager.GetPetitioned() 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 ) ClientData.SortTagsList( tags_i_want_to_display, HC.options[ 'default_tag_sort' ] ) 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 ] dc.SetTextForeground( wx.Colour( r, g, b ) ) ( x, y ) = dc.GetTextExtent( display_string ) dc.DrawText( display_string, 5, current_y ) current_y += y dc.SetTextForeground( self._new_options.GetColour( CC.COLOUR_MEDIA_TEXT ) ) # top right current_y = 2 # ratings services_manager = HG.client_controller.services_manager like_services = services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, ), randomised = False ) like_services.reverse() like_rating_current_x = client_width - 16 for like_service in like_services: service_key = like_service.GetServiceKey() rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), service_key ) ClientRatings.DrawLike( dc, 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, ), randomised = False ) 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( dc, client_width - numerical_width, current_y, service_key, rating_state, rating ) current_y += 20 # icons icons_to_show = [] if CC.TRASH_SERVICE_KEY in self._current_media.GetLocationsManager().GetCurrent(): icons_to_show.append( CC.GlobalBMPs.trash ) if self._current_media.HasInbox(): icons_to_show.append( CC.GlobalBMPs.inbox ) if len( icons_to_show ) > 0: icon_x = 0 for icon in icons_to_show: dc.DrawBitmap( icon, client_width + icon_x - 18, current_y ) icon_x -= 18 current_y += 18 # repo strings remote_strings = self._current_media.GetLocationsManager().GetRemoteLocationStrings() for remote_string in remote_strings: ( text_width, text_height ) = dc.GetTextExtent( remote_string ) dc.DrawText( remote_string, client_width - text_width - 3, current_y ) current_y += text_height + 4 # urls urls = self._current_media.GetLocationsManager().GetURLs() urls = list( urls ) urls.sort() urls = urls[ : 10 ] for url in urls: parse = urlparse.urlparse( url ) url_string = parse.hostname ( text_width, text_height ) = dc.GetTextExtent( url_string ) dc.DrawText( url_string, client_width - text_width - 3, current_y ) current_y += text_height + 4 # top-middle current_y = 3 title_string = self._current_media.GetTitleString() if len( title_string ) > 0: ( x, y ) = dc.GetTextExtent( title_string ) dc.DrawText( title_string, ( client_width - x ) / 2, current_y ) current_y += y + 3 info_string = self._GetInfoString() ( x, y ) = dc.GetTextExtent( info_string ) dc.DrawText( info_string, ( client_width - x ) / 2, current_y ) current_y += y + 3 self._DrawAdditionalTopMiddleInfo( dc, current_y ) # bottom-right index index_string = self._GetIndexString() if len( index_string ) > 0: ( x, y ) = dc.GetTextExtent( index_string ) dc.DrawText( index_string, client_width - x - 3, client_height - y - 3 ) def _GetInfoString( self ): lines = self._current_media.GetPrettyInfoLines() lines.insert( 1, ClientData.ConvertZoomToPercentage( self._current_zoom ) ) info_string = ' | '.join( lines ) return info_string class CanvasWithHovers( CanvasWithDetails ): def __init__( self, parent ): CanvasWithDetails.__init__( self, parent ) self._hover_commands = self._GenerateHoverTopFrame() self._hover_tags = ClientGUIHoverFrames.FullscreenHoverFrameTags( self, self._canvas_key ) ratings_services = HG.client_controller.services_manager.GetServices( ( HC.RATINGS_SERVICES ) ) if len( ratings_services ) > 0: self._hover_ratings = ClientGUIHoverFrames.FullscreenHoverFrameTopRight( self, self._canvas_key ) # self._timer_cursor_hide = wx.Timer( self, id = ID_TIMER_CURSOR_HIDE ) self.Bind( wx.EVT_TIMER, self.TIMEREventCursorHide, id = ID_TIMER_CURSOR_HIDE ) self.Bind( wx.EVT_MOTION, self.EventDrag ) HG.client_controller.sub( self, 'Close', 'canvas_close' ) def _Close( self ): self._closing = True self.GetParent().Close() def _GenerateHoverTopFrame( self ): raise NotImplementedError() def Close( self, canvas_key ): if canvas_key == self._canvas_key: self._Close() def EventDragBegin( self, event ): ( x, y ) = event.GetPosition() self.BeginDrag( ( x, y ) ) event.Skip() def EventDragEnd( self, event ): self._last_drag_coordinates = None event.Skip() def EventDrag( self, event ): CC.CAN_HIDE_MOUSE = True ( x, y ) = event.GetPosition() show_mouse = self.GetCursor() == wx.StockCursor( wx.CURSOR_ARROW ) is_dragging = event.Dragging() and self._last_drag_coordinates is not None has_moved = ( x, y ) != self._last_motion_coordinates if is_dragging: ( old_x, old_y ) = self._last_drag_coordinates ( delta_x, delta_y ) = ( x - old_x, y - old_y ) delta_distance = ( float( delta_x ) ** 2 + float( delta_y ) ** 2 ) ** 0.5 if delta_distance > 0: if not self._current_drag_is_touch and delta_distance > 50: # if user is able to generate such a large distance, they are almost certainly touching self._current_drag_is_touch = True if HC.PLATFORM_WINDOWS and not self._current_drag_is_touch: # touch events obviously don't mix with warping well. the touch just warps it back and again and we get a massive delta! show_mouse = False self.WarpPointer( old_x, old_y ) else: show_mouse = True self._last_drag_coordinates = ( x, y ) ( old_delta_x, old_delta_y ) = self._total_drag_delta self._total_drag_delta = ( old_delta_x + delta_x, old_delta_y + delta_y ) self._DrawCurrentMedia() elif has_moved: self._last_motion_coordinates = ( x, y ) show_mouse = True if show_mouse: self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) ) self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT ) else: self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) ) def TIMEREventCursorHide( self, event ): try: if not CC.CAN_HIDE_MOUSE: return if HG.client_controller.MenuIsOpen(): self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT ) else: self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) ) except wx.PyDeadObjectError: self._timer_cursor_hide.Stop() except: self._timer_cursor_hide.Stop() raise class CanvasFilterDuplicates( CanvasWithHovers ): def __init__( self, parent, file_service_key ): CanvasWithHovers.__init__( self, parent ) self._file_service_key = file_service_key self._maintain_pan_and_zoom = True self._currently_fetching_pairs = False self._unprocessed_pairs = [] self._current_pair = None self._processed_pairs = [] self._batch_skip_hashes = set() self._media_list = ClientMedia.ListeningMediaList( self._file_service_key, [] ) self._reserved_shortcut_names.append( 'media_viewer_browser' ) self._reserved_shortcut_names.append( 'duplicate_filter' ) self.Bind( wx.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' ) wx.CallAfter( self._ShowNewPair ) def _Close( self ): num_committable = self._GetNumCommittableDecisions() if num_committable > 0: label = 'commit ' + HydrusData.ConvertIntToPrettyString( num_committable ) + ' decisions?' with ClientGUIDialogs.DialogFinishFiltering( self, label ) as dlg: modal = dlg.ShowModal() if modal == wx.ID_CANCEL: close_was_triggered_by_everything_being_processed = len( self._unprocessed_pairs ) == 0 if close_was_triggered_by_everything_being_processed: self._GoBack() return elif modal == wx.ID_YES: self._CommitProcessed() HG.client_controller.pub( 'refresh_dupe_numbers' ) CanvasWithHovers._Close( self ) def _CommitProcessed( self ): pair_info = [] for ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) in self._processed_pairs: if duplicate_type == HC.DUPLICATE_UNKNOWN: continue # it was a 'skip' decision first_hash = first_media.GetHash() second_hash = second_media.GetHash() list_of_service_keys_to_content_updates = duplicate_action_options.ProcessPairIntoContentUpdates( first_media, second_media ) pair_info.append( ( duplicate_type, first_hash, second_hash, list_of_service_keys_to_content_updates ) ) if len( pair_info ) > 0: HG.client_controller.WriteSynchronous( 'duplicate_pair_status', pair_info ) self._processed_pairs = [] self._batch_skip_hashes = set() def _CurrentMediaIsBetter( self ): self._ProcessPair( HC.DUPLICATE_BETTER ) def _Delete( self, service_key = None ): if self._current_media is None: return if service_key is None: locations_manager = self._current_media.GetLocationsManager() if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent(): service_key = CC.LOCAL_FILE_SERVICE_KEY elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent(): service_key = CC.TRASH_SERVICE_KEY else: return if service_key == CC.LOCAL_FILE_SERVICE_KEY: text = 'Send this just this file to the trash, or both?' elif service_key == CC.TRASH_SERVICE_KEY: text = 'Permanently 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.ShowModal() == wx.ID_YES: value = dlg.GetValue() if value == 'current': hashes = { self._current_media.GetHash() } elif value == 'both': hashes = { self._current_media.GetHash(), self._media_list.GetNext( self._current_media ).GetHash() } HG.client_controller.Write( 'content_updates', { service_key : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes ) ] } ) self.SetFocus() # annoying bug because of the modal dialog def _DoCustomAction( self ): if self._current_media is None: return duplicate_types = [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_NOT_DUPLICATE ] choice_tuples = [ ( HC.duplicate_type_string_lookup[ duplicate_type ], duplicate_type ) for duplicate_type in duplicate_types ] with ClientGUIDialogs.DialogSelectFromList( self, 'select duplicate type', choice_tuples ) as dlg_1: if dlg_1.ShowModal() == wx.ID_OK: duplicate_type = dlg_1.GetChoice() new_options = HG.client_controller.GetNewOptions() 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 ) dlg_2.SetPanel( panel ) if dlg_2.ShowModal() == wx.ID_OK: duplicate_action_options = panel.GetValue() self._ProcessPair( duplicate_type, duplicate_action_options ) def _DrawAdditionalTopMiddleInfo( self, dc, current_y ): if self._current_media is not None: shown_media = self._current_media comparison_media = self._media_list.GetNext( shown_media ) if shown_media != comparison_media: ( statements, score ) = ClientMedia.GetDuplicateComparisonStatements( shown_media, comparison_media ) ( client_width, client_height ) = self.GetClientSize() for statement in statements: ( width, height ) = dc.GetTextExtent( statement ) dc.DrawText( statement, ( client_width - width ) / 2, current_y ) current_y += height + 3 return current_y def _DrawBackgroundDetails( self, dc ): if self._currently_fetching_pairs: text = u'Loading pairs\u2026' ( width, height ) = dc.GetTextExtent( text ) ( my_width, my_height ) = self.GetClientSize() x = ( my_width - width ) // 2 y = ( my_height - height ) // 2 dc.DrawText( text, x, y ) else: CanvasWithHovers._DrawBackgroundDetails( self, dc ) def _GenerateHoverTopFrame( self ): return ClientGUIHoverFrames.FullscreenHoverFrameTopDuplicatesFilter( self, self._canvas_key ) def _GetBackgroundColour( self ): normal_colour = self._new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) if self._current_media is None: return normal_colour else: if self._current_media == self._media_list.GetFirst(): return normal_colour else: new_options = HG.client_controller.GetNewOptions() 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: 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 _GetNumCommittableDecisions( self ): return len( [ 1 for ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) in self._processed_pairs if duplicate_type != HC.DUPLICATE_UNKNOWN ] ) def _GoBack( self ): if len( self._processed_pairs ) > 0: self._unprocessed_pairs.append( self._current_pair ) ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) = self._processed_pairs.pop() self._unprocessed_pairs.append( hash_pair ) while was_auto_skipped: ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) = self._processed_pairs.pop() self._unprocessed_pairs.append( hash_pair ) self._batch_skip_hashes.difference_update( hash_pair ) self._ShowNewPair() def _MediaAreAlternates( self ): self._ProcessPair( HC.DUPLICATE_ALTERNATE ) def _MediaAreNotDupes( self ): self._ProcessPair( HC.DUPLICATE_NOT_DUPLICATE ) def _MediaAreTheSame( self ): self._ProcessPair( HC.DUPLICATE_SAME_QUALITY ) def _ProcessApplicationCommand( self, command ): 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': self._CurrentMediaIsBetter() elif action == 'duplicate_filter_exactly_the_same': self._MediaAreTheSame() elif action == 'duplicate_filter_alternates': self._MediaAreAlternates() elif action == 'duplicate_filter_not_dupes': self._MediaAreNotDupes() 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 _ProcessPair( self, duplicate_type, duplicate_action_options = None ): if self._current_media is None: return if duplicate_action_options is None: new_options = HG.client_controller.GetNewOptions() duplicate_action_options = new_options.GetDuplicateActionOptions( duplicate_type ) other_media = self._media_list.GetNext( self._current_media ) deleted_hashes = duplicate_action_options.GetDeletedHashes( self._current_media, other_media ) self._batch_skip_hashes.update( deleted_hashes ) was_auto_skipped = False self._processed_pairs.append( ( self._current_pair, duplicate_type, self._current_media, other_media, duplicate_action_options, was_auto_skipped ) ) self._ShowNewPair() def _ShowNewPair( self ): if self._currently_fetching_pairs: return num_committable = self._GetNumCommittableDecisions() if len( self._unprocessed_pairs ) == 0 and num_committable > 0: label = 'commit ' + HydrusData.ConvertIntToPrettyString( num_committable ) + ' decisions and continue?' with ClientGUIDialogs.DialogCommitInterstitialFiltering( self, label ) as dlg: modal = dlg.ShowModal() if modal == wx.ID_YES: self._CommitProcessed() else: ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) = self._processed_pairs.pop() self._unprocessed_pairs.append( hash_pair ) while was_auto_skipped: ( hash_pair, duplicate_type, first_media, second_media, duplicate_action_options, was_auto_skipped ) = self._processed_pairs.pop() self._unprocessed_pairs.append( hash_pair ) self._batch_skip_hashes.difference_update( hash_pair ) if len( self._unprocessed_pairs ) == 0: self._batch_skip_hashes = set() self._processed_pairs = [] # just in case someone 'skip'ed everything in the last batch, so this never got cleared above self.SetMedia( None ) self._media_list = ClientMedia.ListeningMediaList( self._file_service_key, [] ) self._currently_fetching_pairs = True HG.client_controller.CallToThread( self.THREADFetchPairs ) self._SetDirty() else: potential_pair = self._unprocessed_pairs.pop() ( first_hash, second_hash ) = potential_pair while first_hash in self._batch_skip_hashes or second_hash in self._batch_skip_hashes: was_auto_skipped = True self._processed_pairs.append( ( potential_pair, HC.DUPLICATE_UNKNOWN, None, None, None, was_auto_skipped ) ) if len( self._unprocessed_pairs ) == 0: self._ShowNewPair() # there are no useful decisions left in the queue, so let's reset return potential_pair = self._unprocessed_pairs.pop() ( first_hash, second_hash ) = potential_pair self._current_pair = potential_pair ( first_media_result, second_media_result ) = HG.client_controller.Read( 'media_results', self._current_pair ) first_media = ClientMedia.MediaSingleton( first_media_result ) second_media = ClientMedia.MediaSingleton( second_media_result ) ( statements, score ) = ClientMedia.GetDuplicateComparisonStatements( first_media, second_media ) if score > 0: media_results_with_better_first = ( first_media_result, second_media_result ) else: media_results_with_better_first = ( second_media_result, first_media_result ) self._media_list = ClientMedia.ListeningMediaList( self._file_service_key, media_results_with_better_first ) self.SetMedia( self._media_list.GetFirst() ) self._ResetDragDelta() self._ReinitZoom() def _SkipPair( self ): if self._current_media is None: return was_auto_skipped = False self._processed_pairs.append( ( self._current_pair, HC.DUPLICATE_UNKNOWN, None, None, None, was_auto_skipped ) ) self._ShowNewPair() def _SwitchMedia( self ): if self._current_media is not None: self.SetMedia( self._media_list.GetNext( self._current_media ) ) 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 EventCharHook( self, event ): if self._CanProcessInput() and not self._FocusIsElsewhere(): ( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event ) if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close() else: ( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event ) if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete() elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete() else: CanvasWithHovers.EventCharHook( self, event ) else: event.Skip() def EventClose( self, event ): self._Close() def EventMouse( self, event ): if self._CanProcessInput(): if event.ShiftDown(): caught = True if event.LeftDown(): self.EventDragBegin( event ) elif event.LeftUp(): self.EventDragEnd( event ) elif event.Dragging(): self.EventDrag( event ) else: caught = False if caught: return shortcut = ClientData.ConvertMouseEventToShortcut( event ) if shortcut is not None: shortcut_processed = self._ProcessShortcut( shortcut ) if shortcut_processed: return if event.GetWheelRotation() != 0: self._SwitchMedia() else: event.Skip() else: event.Skip() def Inbox( self, canvas_key ): if self._canvas_key == canvas_key: self._Inbox() def ProcessContentUpdates( self, service_keys_to_content_updates ): def catch_up(): # ugly, but it will do for now if self: if len( self._media_list ) < 2: self._ShowNewPair() else: self._SetDirty() wx.CallLater( 100, 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 Undelete( self, canvas_key ): if canvas_key == self._canvas_key: self._Undelete() def THREADFetchPairs( self ): def wx_close(): if self: wx.CallAfter( wx.MessageBox, 'All pairs have been filtered!' ) self._Close() def wx_continue( unprocessed_pairs ): if self: self._unprocessed_pairs = unprocessed_pairs self._currently_fetching_pairs = False self._ShowNewPair() result = HG.client_controller.Read( 'unique_duplicate_pairs', self._file_service_key, HC.DUPLICATE_UNKNOWN ) if len( result ) == 0: wx.CallAfter( wx_close ) else: wx.CallAfter( wx_continue, result ) class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ): 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.Bind( wx.EVT_LEFT_DOWN, self.EventDragBegin ) self.Bind( wx.EVT_LEFT_UP, self.EventDragEnd ) HG.client_controller.pub( 'set_focus', self._page_key, None ) HG.client_controller.sub( self, 'FullscreenSwitch', 'canvas_fullscreen_switch' ) def _Close( self ): HG.client_controller.pub( 'set_focus', self._page_key, self._current_media ) CanvasWithHovers._Close( self ) def _GetIndexString( self ): if self._current_media is None: index_string = '-/' + HydrusData.ConvertIntToPrettyString( 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 if self._just_started: delay_base = 800 num_to_go_back = 1 num_to_go_forward = 1 self._just_started = False else: delay_base = 400 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 ) ) ( my_width, my_height ) = self.GetClientSize() image_cache = HG.client_controller.GetCache( 'images' ) for ( media, delay ) in to_render: hash = media.GetHash() mime = media.GetMime() if mime in ( HC.IMAGE_JPEG, HC.IMAGE_PNG ): if not image_cache.HasImageRenderer( hash ): wx.CallLater( 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._Close() elif self.HasMedia( self._current_media ): HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() ) self._SetDirty() 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._SetDirty() def EventClose( self, event ): self._Close() def EventFullscreenSwitch( self, event ): self.GetParent().FullscreenSwitch() def FullscreenSwitch( self, canvas_key ): if canvas_key == self._canvas_key: self.GetParent().FullscreenSwitch() def KeepCursorAlive( self ): self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT ) def ProcessContentUpdates( self, service_keys_to_content_updates ): 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._Close() elif self.HasMedia( self._current_media ): HG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() ) self._SetDirty() 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._reserved_shortcut_names.append( 'archive_delete_filter' ) self._kept = set() self._deleted = set() self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse ) HG.client_controller.sub( self, 'Delete', 'canvas_delete' ) HG.client_controller.sub( self, 'Undelete', 'canvas_undelete' ) wx.CallAfter( self.SetMedia, self._GetFirst() ) # don't set this until we have a size > (20, 20)! def _Back( self ): if self._CanProcessInput(): if self._current_media == self._GetFirst(): return else: self._ShowPrevious() self._kept.discard( self._current_media ) self._deleted.discard( self._current_media ) def _Close( self ): if self._CanProcessInput(): if len( self._kept ) > 0 or len( self._deleted ) > 0: label = 'keep ' + HydrusData.ConvertIntToPrettyString( len( self._kept ) ) + ' and delete ' + HydrusData.ConvertIntToPrettyString( len( self._deleted ) ) + ' files?' with ClientGUIDialogs.DialogFinishFiltering( self, label ) as dlg: result = dlg.ShowModal() if result == wx.ID_CANCEL: 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 elif result == wx.ID_YES: 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 = [] 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 ) ) ) 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 ) CanvasMediaList._Close( self ) def _Delete( self ): self._deleted.add( self._current_media ) if self._current_media == self._GetLast(): self._Close() else: self._ShowNext() def _GenerateHoverTopFrame( self ): return ClientGUIHoverFrames.FullscreenHoverFrameTopArchiveDeleteFilter( self, self._canvas_key ) def _Keep( self ): self._kept.add( self._current_media ) if self._current_media == self._GetLast(): self._Close() else: self._ShowNext() def _ProcessApplicationCommand( self, command ): 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._Close() else: command_processed = False else: command_processed = False if not command_processed: command_processed = CanvasMediaList._ProcessApplicationCommand( self, command ) return command_processed def _Skip( self ): if self._current_media == self._GetLast(): self._Close() 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 EventBack( self, event ): self._Back() def EventCharHook( self, event ): if self._CanProcessInput() and not self._FocusIsElsewhere(): ( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event ) if modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close() else: CanvasMediaList.EventCharHook( self, event ) else: event.Skip() def EventDelete( self, event ): if self._CanProcessInput(): self._Delete() else: event.Skip() def EventMouse( self, event ): if self._CanProcessInput(): if event.ShiftDown(): caught = True if event.LeftDown(): self.EventDragBegin( event ) elif event.LeftUp(): self.EventDragEnd( event ) elif event.Dragging(): self.EventDrag( event ) else: caught = False if caught: return shortcut = ClientData.ConvertMouseEventToShortcut( event ) if shortcut is not None: shortcut_processed = self._ProcessShortcut( shortcut ) if shortcut_processed: return event.Skip() def EventSkip( self, event ): self._Skip() def EventUndelete( self, event ): if self._CanProcessInput(): self._Undelete() else: event.Skip() 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._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 ): return ClientGUIHoverFrames.FullscreenHoverFrameTopNavigableList( self, self._canvas_key ) def _ProcessApplicationCommand( self, command ): 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() elif action == 'remove_file_from_view': self._Remove() else: command_processed = False else: command_processed = False if not command_processed: command_processed = CanvasMediaList._ProcessApplicationCommand( self, command ) return command_processed 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 EventArchive( self, event ): self._Archive() def EventDelete( self, event ): self._Delete() def EventNext( self, event ): self._ShowNext() def EventPrevious( self, event ): self._ShowPrevious() def Inbox( self, canvas_key ): if self._canvas_key == canvas_key: self._Inbox() 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 = wx.Timer( self, id = ID_TIMER_SLIDESHOW ) self._timer_slideshow_interval = 0 self.Bind( wx.EVT_TIMER, self.TIMEREventSlideshow, id = ID_TIMER_SLIDESHOW ) self.Bind( wx.EVT_LEFT_DCLICK, self.EventClose ) self.Bind( wx.EVT_MIDDLE_DOWN, self.EventClose ) self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel ) self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu ) if first_hash is None: first_media = self._GetFirst() else: try: first_media = self._GetMedia( { first_hash } )[0] except: first_media = self._GetFirst() wx.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._timer_slideshow.IsRunning(): self._timer_slideshow.Stop() elif self._timer_slideshow.GetInterval() > 0: self._timer_slideshow.Start() def _StartSlideshow( self, interval = None ): self._timer_slideshow.Stop() if interval is None: with ClientGUIDialogs.DialogTextEntry( self, 'Enter the interval, in seconds.', default = '15' ) as dlg: if dlg.ShowModal() == wx.ID_OK: try: interval = int( float( dlg.GetValue() ) * 1000 ) except: return if interval > 0: self._timer_slideshow_interval = interval self._timer_slideshow.Start( self._timer_slideshow_interval, wx.TIMER_CONTINUOUS ) def EventCharHook( self, event ): if self._CanProcessInput() and not self._FocusIsElsewhere(): ( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event ) if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete() elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete() elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_SPACE, wx.WXK_NUMPAD_SPACE ): wx.CallAfter( self._PausePlaySlideshow ) elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close() else: CanvasMediaListNavigable.EventCharHook( self, event ) else: event.Skip() def EventMouseWheel( self, event ): if self._CanProcessInput(): if event.CmdDown(): if event.GetWheelRotation() > 0: self._ZoomIn() else: self._ZoomOut() else: if event.GetWheelRotation() > 0: self._ShowPrevious() else: self._ShowNext() else: event.Skip() def EventShowMenu( self, event ): if self._current_media is not None: 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_coordinates = None # to stop successive right-click drag warp bug locations_manager = self._current_media.GetLocationsManager() menu = wx.Menu() for line in self._current_media.GetPrettyInfoLines(): ClientGUIMenus.AppendMenuLabel( menu, line ) ClientGUIMenus.AppendSeparator( menu ) if self._IsZoomable(): ClientGUIMenus.AppendMenuLabel( menu, 'current zoom: ' + ClientData.ConvertZoomToPercentage( self._current_zoom ) ) ClientGUIMenus.AppendMenuItem( self, menu, 'zoom in', 'Zoom the media in.', self._ZoomIn ) ClientGUIMenus.AppendMenuItem( self, menu, 'zoom out', 'Zoom the media out.', self._ZoomOut ) if self._current_media.GetMime() != HC.APPLICATION_FLASH: if self._current_zoom != 1.0: ClientGUIMenus.AppendMenuItem( self, menu, 'zoom to 100%', 'Set the zoom to 100%.', self._ZoomSwitch ) elif self._current_zoom != self._canvas_zoom: ClientGUIMenus.AppendMenuItem( self, menu, 'zoom fit', 'Set the zoom so the media fits the canvas.', self._ZoomSwitch ) ClientGUIMenus.AppendSeparator( menu ) manage_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, manage_menu, 'tags', 'Manage this file\'s tags.', self._ManageTags ) if i_can_post_ratings: ClientGUIMenus.AppendMenuItem( self, manage_menu, 'ratings', 'Manage this file\'s ratings.', self._ManageRatings ) ClientGUIMenus.AppendMenuItem( self, manage_menu, 'known urls', 'Manage this file\'s known urls.', self._ManageURLs ) ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' ) ClientGUIMenus.AppendSeparator( menu ) if self._current_media.HasInbox(): ClientGUIMenus.AppendMenuItem( self, menu, 'archive', 'Archive this file, taking it out of the inbox.', self._Archive ) elif self._current_media.HasArchive(): ClientGUIMenus.AppendMenuItem( self, menu, 'return to inbox', 'Put this file back in the inbox.', self._Inbox ) ClientGUIMenus.AppendMenuItem( self, menu, 'remove', 'Remove this file from the list you are viewing.', self._Remove ) if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent(): ClientGUIMenus.AppendMenuItem( self, menu, 'delete', 'Send this file to the trash.', self._Delete, CC.LOCAL_FILE_SERVICE_KEY ) elif CC.TRASH_SERVICE_KEY in locations_manager.GetCurrent(): ClientGUIMenus.AppendMenuItem( self, menu, 'delete from trash now', 'Delete this file immediately. This cannot be undone.', self._Delete, CC.TRASH_SERVICE_KEY ) ClientGUIMenus.AppendMenuItem( self, menu, 'undelete', 'Take this file out of the trash, returning it to its original file service.', self._Undelete ) ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( self, menu, 'open externally', 'Open this file in the default external program.', self._OpenExternally ) urls = self._current_media.GetLocationsManager().GetURLs() if len( urls ) > 0: urls = list( urls ) urls.sort() urls_menu = wx.Menu() urls_visit_menu = wx.Menu() urls_copy_menu = wx.Menu() for url in urls: ClientGUIMenus.AppendMenuItem( self, urls_visit_menu, url, 'Open this url in your web browser.', webbrowser.open, url ) ClientGUIMenus.AppendMenuItem( self, urls_copy_menu, url, 'Copy this url to your clipboard.', HG.client_controller.pub, 'clipboard', 'text', url ) ClientGUIMenus.AppendMenu( urls_menu, urls_visit_menu, 'open' ) ClientGUIMenus.AppendMenu( urls_menu, urls_copy_menu, 'copy' ) ClientGUIMenus.AppendMenu( menu, urls_menu, 'known urls' ) share_menu = wx.Menu() copy_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard ) copy_hash_menu = wx.Menu() ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'sha256 (hydrus default)', 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' ) ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard.', self._CopyHashToClipboard, 'md5' ) ClientGUIMenus.AppendMenuItem( self, copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard.', self._CopyHashToClipboard, 'sha1' ) ClientGUIMenus.AppendMenuItem( self, 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 and self._current_media.GetDuration() is None: ClientGUIMenus.AppendMenuItem( self, copy_menu, 'image', 'Copy this file to your clipboard as a BMP image.', self._CopyBMPToClipboard ) ClientGUIMenus.AppendMenuItem( self, 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' ) ClientGUIMenus.AppendSeparator( menu ) slideshow = wx.Menu() ClientGUIMenus.AppendMenuItem( self, slideshow, '1 second', 'Start a slideshow with a one second interval.', self._StartSlideshow, 1000 ) ClientGUIMenus.AppendMenuItem( self, slideshow, '5 second', 'Start a slideshow with a five second interval.', self._StartSlideshow, 5000 ) ClientGUIMenus.AppendMenuItem( self, slideshow, '10 second', 'Start a slideshow with a ten second interval.', self._StartSlideshow, 10000 ) ClientGUIMenus.AppendMenuItem( self, slideshow, '30 second', 'Start a slideshow with a thirty second interval.', self._StartSlideshow, 30000 ) ClientGUIMenus.AppendMenuItem( self, slideshow, '60 second', 'Start a slideshow with a one minute interval.', self._StartSlideshow, 60000 ) ClientGUIMenus.AppendMenuItem( self, slideshow, 'very fast', 'Start a very fast slideshow.', self._StartSlideshow, 80 ) ClientGUIMenus.AppendMenuItem( self, slideshow, 'custom interval', 'Start a slideshow with a custom interval.', self._StartSlideshow ) ClientGUIMenus.AppendMenu( menu, slideshow, 'start slideshow' ) if self._timer_slideshow.IsRunning(): ClientGUIMenus.AppendMenuItem( self, menu, 'stop slideshow', 'Stop the current slideshow.', self._PausePlaySlideshow ) ClientGUIMenus.AppendSeparator( menu ) if self.GetParent().IsFullScreen(): ClientGUIMenus.AppendMenuItem( self, menu, 'exit fullscreen', 'Make this media viewer a regular window with borders.', self.GetParent().FullscreenSwitch ) else: ClientGUIMenus.AppendMenuItem( self, menu, 'go fullscreen', 'Make this media viewer a fullscreen window without borders.', self.GetParent().FullscreenSwitch ) HG.client_controller.PopupMenu( self, menu ) event.Skip() def TIMEREventSlideshow( self, event ): try: if self._current_media is not None: if self._media_container.ReadyToSlideshow() and not HG.client_controller.MenuIsOpen(): self._ShowNext() self._timer_slideshow.Start( self._timer_slideshow_interval, wx.TIMER_CONTINUOUS ) else: self._timer_slideshow.Start( 1000, wx.TIMER_CONTINUOUS ) except wx.PyDeadObjectError: self._timer_slideshow.Stop() except: self._timer_slideshow.Stop() raise class MediaContainer( wx.Window ): def __init__( self, parent ): wx.Window.__init__( self, parent ) self._media = None self._show_action = None self._media_window = None self._embed_button = EmbedButton( self ) self._embed_button.Bind( wx.EVT_LEFT_DOWN, self.EventEmbedButton ) self._animation_bar = AnimationBar( self ) self.Hide() self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_MOUSE_EVENTS, self.EventPropagateMouse ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) def _DestroyThisMediaWindow( self, media_window ): if media_window is not None: media_window.Hide() wx.CallLater( 50, media_window.Destroy ) def _HideAnimationBar( self ): self._animation_bar.SetNoneMedia() self._animation_bar.Hide() def _MakeMediaWindow( self ): old_media_window = self._media_window destroy_old_media_window = True ( media_initial_size, media_initial_position ) = ( self.GetClientSize(), ( 0, 0 ) ) if self._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 self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON: self._media_window = OpenExternallyPanel( self, self._media ) else: start_paused = self._show_action in ( CC.MEDIA_VIEWER_ACTION_SHOW_AS_NORMAL_PAUSED, CC.MEDIA_VIEWER_ACTION_SHOW_BEHIND_EMBED_PAUSED ) if ShouldHaveAnimationBar( self._media ) or self._media.GetMime() == HC.APPLICATION_FLASH: if ShouldHaveAnimationBar( self._media ): ( x, y ) = media_initial_size media_initial_size = ( x, y - ANIMATED_SCANBAR_HEIGHT ) if self._media.GetMime() == HC.APPLICATION_FLASH: self._media_window = wx.lib.flashwin.FlashWindow( self, size = media_initial_size, pos = media_initial_position ) if self._media_window is None: raise Exception( 'Failed to initialise the flash window' ) client_files_manager = HG.client_controller.client_files_manager self._media_window.movie = client_files_manager.GetFilePath( self._media.GetHash(), HC.APPLICATION_FLASH ) else: if isinstance( self._media_window, Animation ): destroy_old_media_window = False else: self._media_window = Animation( self ) self._media_window.SetAnimationBar( self._animation_bar ) self._media_window.SetMedia( self._media, start_paused ) if ShouldHaveAnimationBar( self._media ): self._animation_bar.Show() self._animation_bar.SetMediaAndWindow( self._media, self._media_window ) else: self._HideAnimationBar() else: if isinstance( self._media_window, StaticImage ): destroy_old_media_window = False else: self._media_window = StaticImage( self ) self._media_window.SetMedia( self._media ) self._HideAnimationBar() if old_media_window is not None and destroy_old_media_window: self._DestroyThisMediaWindow( old_media_window ) def _SizeAndPositionChildren( self ): if self._media is not None: ( my_width, my_height ) = self.GetClientSize() if self._media_window is None: self._embed_button.SetSize( ( my_width, my_height ) ) self._embed_button.SetPosition( ( 0, 0 ) ) else: ( media_width, media_height ) = ( my_width, my_height ) if ShouldHaveAnimationBar( self._media ): media_height -= ANIMATED_SCANBAR_HEIGHT self._animation_bar.SetSize( ( my_width, ANIMATED_SCANBAR_HEIGHT ) ) self._animation_bar.SetPosition( ( 0, my_height - ANIMATED_SCANBAR_HEIGHT ) ) self._media_window.SetSize( ( media_width, media_height ) ) self._media_window.SetPosition( ( 0, 0 ) ) def BeginDrag( self ): self.GetParent().BeginDrag() def EventEmbedButton( self, event ): self._embed_button.Hide() self._MakeMediaWindow() self._SizeAndPositionChildren() def EventEraseBackground( self, event ): pass def EventPropagateMouse( self, event ): if self._media is not None and self.IsShown(): # Can't ClientToScreen if not shown, like in init mime = self._media.GetMime() if mime in HC.IMAGES or mime in HC.VIDEO: screen_position = self.ClientToScreen( event.GetPosition() ) ( x, y ) = self.GetParent().ScreenToClient( screen_position ) event.SetX( x ) event.SetY( y ) event.ResumePropagation( 1 ) event.Skip() def EventResize( self, event ): if self._media is not None: self._SizeAndPositionChildren() def GotoPreviousOrNextFrame( self, direction ): if self._media is not None: if ShouldHaveAnimationBar( self._media ): current_frame_index = self._media_window.CurrentFrame() num_frames = self._media.GetNumFrames() if direction == 1: if current_frame_index == num_frames - 1: current_frame_index = 0 else: current_frame_index += 1 else: if current_frame_index == 0: current_frame_index = num_frames - 1 else: current_frame_index -= 1 self._media_window.GotoFrame( current_frame_index ) def MouseIsNearAnimationBar( self ): if self._media is None: return False else: if ShouldHaveAnimationBar( self._media ): ( x, y ) = self._animation_bar.GetScreenPosition() ( width, height ) = self._animation_bar.GetSize() ( mouse_x, mouse_y ) = wx.GetMousePosition() buffer_distance = 100 if mouse_x >= x - buffer_distance and mouse_x <= x + width + buffer_distance and mouse_y >= y - buffer_distance and mouse_y <= y + height + buffer_distance: return True return False def Pause( self ): if self._media is not None: if isinstance( self._media_window, Animation ): self._media_window.Pause() def ReadyToSlideshow( self ): if self._media is None: return False else: if isinstance( self._media_window, Animation ): if self._media_window.IsPlaying() and not self._media_window.HasPlayedOnceThrough(): return False if isinstance( self._media_window, StaticImage ): if not self._media_window.IsRendered(): return False return True def SetMedia( self, media, initial_size, initial_position, show_action ): self._media = media self.Show() self._show_action = show_action if self._show_action in ( CC.MEDIA_VIEWER_ACTION_SHOW_BEHIND_EMBED, CC.MEDIA_VIEWER_ACTION_SHOW_BEHIND_EMBED_PAUSED ): self._HideAnimationBar() self._DestroyThisMediaWindow( self._media_window ) self._media_window = None self._embed_button.SetMedia( self._media ) self._embed_button.Show() else: self._embed_button.Hide() self._MakeMediaWindow() self.SetSize( initial_size ) self.SetPosition( initial_position ) self._SizeAndPositionChildren() def SetNoneMedia( self ): self._media = None self._DestroyThisMediaWindow( self._media_window ) self._media_window = None self.Hide() class EmbedButton( wx.Window ): def __init__( self, parent ): wx.Window.__init__( self, parent ) self._media = None self._dirty = False self._canvas_bmp = None self._thumbnail_bmp = None self.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) ) self.Bind( wx.EVT_PAINT, self.EventPaint ) self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) HG.client_controller.sub( self, '_SetDirty', 'notify_new_colourset' ) def _Redraw( self, dc ): ( x, y ) = self.GetClientSize() center_x = x / 2 center_y = y / 2 radius = min( 50, center_x, center_y ) - 5 new_options = HG.client_controller.GetNewOptions() dc.SetBackground( wx.Brush( new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) ) ) dc.Clear() if self._thumbnail_bmp is not None: if ShouldHaveAnimationBar( self._media ): # animations will have the animation bar space underneath in this case, so colour it in dc.SetBackground( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) ) ) dc.DrawRectangle( 0, y - ANIMATED_SCANBAR_HEIGHT, x, ANIMATED_SCANBAR_HEIGHT ) ( thumb_width, thumb_height ) = self._thumbnail_bmp.GetSize() scale = x / float( thumb_width ) dc.SetUserScale( scale, scale ) dc.DrawBitmap( self._thumbnail_bmp, 0, 0 ) dc.SetUserScale( 1.0, 1.0 ) dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) ) ) dc.DrawCircle( center_x, center_y, radius ) dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) ) # play symbol is a an equilateral triangle triangle_side = radius * 0.8 half_triangle_side = int( triangle_side / 2 ) cos30 = 0.866 triangle_width = triangle_side * cos30 third_triangle_width = int( triangle_width / 3 ) points = [] points.append( ( center_x - third_triangle_width, center_y - half_triangle_side ) ) points.append( ( center_x + third_triangle_width * 2, center_y ) ) points.append( ( center_x - third_triangle_width, center_y + half_triangle_side ) ) dc.DrawPolygon( points ) # dc.SetPen( wx.Pen( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNSHADOW ) ) ) dc.SetBrush( wx.TRANSPARENT_BRUSH ) dc.DrawRectangle( 0, 0, x, y ) def _SetDirty( self ): self._dirty = True self.Refresh() def EventEraseBackground( self, event ): pass def EventPaint( self, event ): if self._canvas_bmp is not None: dc = wx.BufferedPaintDC( self, self._canvas_bmp ) if self._dirty: self._Redraw( dc ) def EventResize( self, event ): ( my_width, my_height ) = self.GetClientSize() if my_width > 0 and my_height > 0: if self._canvas_bmp is None: make_new_one = True else: ( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize() make_new_one = my_width != current_bmp_width or my_height != current_bmp_height if make_new_one: if self._canvas_bmp is not None: wx.CallAfter( self._canvas_bmp.Destroy ) self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 ) self._SetDirty() def SetMedia( self, media ): self._media = media if self._media is None: needs_thumb = False else: needs_thumb = self._media.GetLocationsManager().IsLocal() and self._media.GetMime() in HC.MIMES_WITH_THUMBNAILS if needs_thumb: hash = self._media.GetHash() mime = self._media.GetMime() thumbnail_path = HG.client_controller.client_files_manager.GetFullSizeThumbnailPath( hash ) self._thumbnail_bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetWxBitmap() self._SetDirty() else: self._thumbnail_bmp = None class OpenExternallyPanel( wx.Panel ): def __init__( self, parent, media ): wx.Panel.__init__( self, parent ) self._new_options = HG.client_controller.GetNewOptions() self.SetBackgroundColour( self._new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) ) self._media = media vbox = wx.BoxSizer( wx.VERTICAL ) if self._media.GetLocationsManager().IsLocal() and self._media.GetMime() in HC.MIMES_WITH_THUMBNAILS: hash = self._media.GetHash() mime = self._media.GetMime() thumbnail_path = HG.client_controller.client_files_manager.GetFullSizeThumbnailPath( hash ) bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetWxBitmap() thumbnail = ClientGUICommon.BufferedWindowIcon( self, bmp ) thumbnail.Bind( wx.EVT_LEFT_DOWN, self.EventButton ) vbox.AddF( thumbnail, CC.FLAGS_CENTER ) m_text = HC.mime_string_lookup[ media.GetMime() ] button = wx.Button( self, label = 'open ' + m_text + ' externally', size = OPEN_EXTERNALLY_BUTTON_SIZE ) vbox.AddF( button, CC.FLAGS_CENTER ) self.SetSizer( vbox ) self.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) ) self.Bind( wx.EVT_LEFT_DOWN, self.EventButton ) button.Bind( wx.EVT_BUTTON, self.EventButton ) def EventButton( self, event ): hash = self._media.GetHash() mime = self._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 ) class StaticImage( wx.Window ): def __init__( self, parent ): wx.Window.__init__( self, parent ) self._dirty = True self._media = None self._first_background_drawn = False self._image_renderer = None self._is_rendered = False self._canvas_bmp = None self._timer_render_wait = wx.Timer( self ) self.Bind( wx.EVT_PAINT, self.EventPaint ) self.Bind( wx.EVT_SIZE, self.EventResize ) self.Bind( wx.EVT_TIMER, self.TIMEREventRenderWait ) self.Bind( wx.EVT_MOUSE_EVENTS, self.EventPropagateMouse ) self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground ) def _DrawBackground( self, dc ): new_options = HG.client_controller.GetNewOptions() dc.SetBackground( wx.Brush( new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND ) ) ) dc.Clear() self._first_background_drawn = True def _Redraw( self, dc ): if self._image_renderer is not None and self._image_renderer.IsReady(): self._DrawBackground( dc ) wx_bitmap = self._image_renderer.GetWXBitmap( self._canvas_bmp.GetSize() ) dc.DrawBitmap( wx_bitmap, 0, 0 ) wx_bitmap.Destroy() self._is_rendered = True else: if not self._first_background_drawn: self._DrawBackground( dc ) self._dirty = False def _SetDirty( self ): self._dirty = True self.Refresh() def EventEraseBackground( self, event ): pass def EventPaint( self, event ): dc = wx.BufferedPaintDC( self, self._canvas_bmp ) if self._dirty: self._Redraw( dc ) def EventPropagateMouse( self, event ): if self.IsShown(): # Can't ClientToScreen if not shown, like in init screen_position = self.ClientToScreen( event.GetPosition() ) ( x, y ) = self.GetParent().ScreenToClient( screen_position ) event.SetX( x ) event.SetY( y ) event.ResumePropagation( 1 ) event.Skip() def EventResize( self, event ): ( my_width, my_height ) = self.GetClientSize() if my_width > 0 and my_height > 0: if self._canvas_bmp is None: make_new_one = True else: ( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize() make_new_one = my_width != current_bmp_width or my_height != current_bmp_height if make_new_one: if self._canvas_bmp is not None: wx.CallAfter( self._canvas_bmp.Destroy ) self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 ) self._first_background_drawn = False self._SetDirty() def IsRendered( self ): return self._is_rendered def SetMedia( self, media ): self._media = media image_cache = HG.client_controller.GetCache( 'images' ) self._image_renderer = image_cache.GetImageRenderer( self._media ) self._is_rendered = False if not self._image_renderer.IsReady(): self._timer_render_wait.Start( 16, wx.TIMER_CONTINUOUS ) self._dirty = True self.Refresh() def TIMEREventRenderWait( self, event ): try: if self._image_renderer.IsReady(): self._SetDirty() self._timer_render_wait.Stop() except wx.PyDeadObjectError: self._timer_render_wait.Stop() except: self._timer_render_wait.Stop() raise