5670 lines
176 KiB
Python
Executable File
5670 lines
176 KiB
Python
Executable File
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 ):
|
|
|
|
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ '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 )
|
|
|
|
HydrusPaths.LaunchFile( 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()
|
|
|
|
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.SetBackgroundColour( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) )
|
|
|
|
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' )
|
|
|
|
|
|
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 wx.Colour( *HC.options[ 'gui_colours' ][ '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 )
|
|
|
|
HydrusPaths.LaunchFile( 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 == '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()
|
|
pending = tags_manager.GetPending()
|
|
petitioned = tags_manager.GetPetitioned()
|
|
|
|
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 _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 )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
# is None bit means this is prob from a keydown->menu event
|
|
if event.GetEventObject() is None:
|
|
|
|
event.Skip()
|
|
|
|
else:
|
|
|
|
action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
( command, data ) = action
|
|
|
|
if command == 'archive_file': self._Archive()
|
|
elif command == 'copy_bmp': self._CopyBMPToClipboard()
|
|
elif command == 'copy_files': self._CopyFileToClipboard()
|
|
elif command == 'copy_hash': self._CopyHashToClipboard( data )
|
|
elif command == 'copy_path': self._CopyPathToClipboard()
|
|
elif command == 'delete_file': self._Delete( data )
|
|
elif command == 'inbox_file': self._Inbox()
|
|
elif command == 'manage_file_ratings': self._ManageRatings()
|
|
elif command == 'manage_file_tags': wx.CallAfter( self._ManageTags )
|
|
elif command == 'open_file_in_external_program': self._OpenExternally()
|
|
elif command == 'undelete': self._Undelete()
|
|
else: event.Skip()
|
|
|
|
|
|
|
|
|
|
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( wx.Colour( *HC.options[ 'gui_colours' ][ '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.scheme + '://' + 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 = wx.Colour( *HC.options[ 'gui_colours' ][ '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()
|
|
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
|
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 )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
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_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
|
elif 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 EventMenu( self, event ):
|
|
|
|
action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
( command, data ) = action
|
|
|
|
if command == 'archive_file': self._Keep()
|
|
elif command == 'back': self._Back()
|
|
elif command == 'close': self._Close()
|
|
elif command == 'delete_file': self.EventDelete( event )
|
|
elif command == 'switch_between_fullscreen_borderless_and_regular_framed_window': self.GetParent().FullscreenSwitch()
|
|
elif command == 'launch_the_archive_delete_filter': self._Close()
|
|
elif command == 'move_animation_to_previous_frame': self._media_container.GotoPreviousOrNextFrame( -1 )
|
|
elif command == 'move_animation_to_next_frame': self._media_container.GotoPreviousOrNextFrame( 1 )
|
|
elif command == 'manage_file_ratings': self._ManageRatings()
|
|
elif command == 'manage_file_tags': wx.CallAfter( self._ManageTags )
|
|
elif command in ( 'pan_up', 'pan_down', 'pan_left', 'pan_right' ):
|
|
|
|
if command == 'pan_up': self._DoManualPan( 0, -1 )
|
|
elif command == 'pan_down': self._DoManualPan( 0, 1 )
|
|
elif command == 'pan_left': self._DoManualPan( -1, 0 )
|
|
elif command == 'pan_right': self._DoManualPan( 1, 0 )
|
|
|
|
elif command == 'zoom_in': self._ZoomIn()
|
|
elif command == 'zoom_out': self._ZoomOut()
|
|
else: 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 )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
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()
|
|
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
|
else:
|
|
|
|
CanvasMediaListNavigable.EventCharHook( self, event )
|
|
|
|
|
|
else:
|
|
|
|
event.Skip()
|
|
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
# is None bit means this is prob from a keydown->menu event
|
|
if event.GetEventObject() is None:
|
|
|
|
event.Skip()
|
|
|
|
else:
|
|
|
|
action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
( command, data ) = action
|
|
|
|
if command == 'archive_file': self._Archive()
|
|
elif command == 'copy_bmp': self._CopyBMPToClipboard()
|
|
elif command == 'copy_files': self._CopyFileToClipboard()
|
|
elif command == 'copy_hash': self._CopyHashToClipboard( data )
|
|
elif command == 'copy_path': self._CopyPathToClipboard()
|
|
elif command == 'delete_file': self._Delete( data )
|
|
elif command == 'switch_between_fullscreen_borderless_and_regular_framed_window': self.GetParent().FullscreenSwitch()
|
|
elif command == 'view_first': self._ShowFirst()
|
|
elif command == 'view_last': self._ShowLast()
|
|
elif command == 'view_previous': self._ShowPrevious()
|
|
elif command == 'view_next': self._ShowNext()
|
|
elif command == 'move_animation_to_previous_frame': self._media_container.GotoPreviousOrNextFrame( -1 )
|
|
elif command == 'move_animation_to_next_frame': self._media_container.GotoPreviousOrNextFrame( 1 )
|
|
elif command == 'inbox_file': self._Inbox()
|
|
elif command == 'manage_file_ratings': self._ManageRatings()
|
|
elif command == 'manage_file_tags': wx.CallLater( 1, self._ManageTags )
|
|
elif command == 'open_file_in_external_program': self._OpenExternally()
|
|
elif command in ( 'pan_up', 'pan_down', 'pan_left', 'pan_right' ):
|
|
|
|
if command == 'pan_up': self._DoManualPan( 0, -1 )
|
|
elif command == 'pan_down': self._DoManualPan( 0, 1 )
|
|
elif command == 'pan_left': self._DoManualPan( -1, 0 )
|
|
elif command == 'pan_right': self._DoManualPan( 1, 0 )
|
|
|
|
elif command == 'remove_file_from_view': self._Remove()
|
|
elif command == 'slideshow': wx.CallLater( 1, self._StartSlideshow, data )
|
|
elif command == 'slideshow_pause_play': wx.CallLater( 1, self._PausePlaySlideshow )
|
|
elif command == 'undelete': self._Undelete()
|
|
elif command == 'zoom_in': self._ZoomIn()
|
|
elif command == 'zoom_out': self._ZoomOut()
|
|
elif command == 'switch_between_100_percent_and_canvas_zoom': self._ZoomSwitch()
|
|
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 )
|
|
|
|
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'open_file_in_external_program' ), '&open externally' )
|
|
|
|
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()
|
|
|
|
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_files' ), 'file' )
|
|
|
|
copy_hash_menu = wx.Menu()
|
|
|
|
copy_hash_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_hash', 'sha256' ) , 'sha256 (hydrus default)' )
|
|
copy_hash_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_hash', 'md5' ) , 'md5' )
|
|
copy_hash_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_hash', 'sha1' ) , 'sha1' )
|
|
copy_hash_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_hash', 'sha512' ) , 'sha512' )
|
|
|
|
copy_menu.AppendMenu( CC.ID_NULL, 'hash', copy_hash_menu )
|
|
|
|
if self._current_media.GetMime() in HC.IMAGES and self._current_media.GetDuration() is None:
|
|
|
|
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_bmp' ), 'image' )
|
|
|
|
|
|
copy_menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'copy_path' ), 'path' )
|
|
|
|
share_menu.AppendMenu( CC.ID_NULL, 'copy', copy_menu )
|
|
|
|
menu.AppendMenu( CC.ID_NULL, 'share', share_menu )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
slideshow = wx.Menu()
|
|
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 1000 ), '1 second' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 5000 ), '5 seconds' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 10000 ), '10 seconds' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 30000 ), '30 seconds' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 60000 ), '60 seconds' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow', 80 ), 'william gibson' )
|
|
slideshow.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow' ), 'custom interval' )
|
|
|
|
menu.AppendMenu( CC.ID_NULL, 'start slideshow', slideshow )
|
|
|
|
if self._timer_slideshow.IsRunning():
|
|
|
|
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'slideshow_pause_play' ), 'stop slideshow' )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if self.GetParent().IsFullScreen():
|
|
|
|
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'switch_between_fullscreen_borderless_and_regular_framed_window' ), 'exit fullscreen' )
|
|
|
|
else:
|
|
|
|
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetTemporaryId( 'switch_between_fullscreen_borderless_and_regular_framed_window' ), 'go fullscreen' )
|
|
|
|
|
|
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 )
|
|
|
|
|
|
def _Redraw( self, dc ):
|
|
|
|
( x, y ) = self.GetClientSize()
|
|
|
|
center_x = x / 2
|
|
center_y = y / 2
|
|
radius = min( 50, center_x, center_y ) - 5
|
|
|
|
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ '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.SetBackgroundColour( wx.Colour( *HC.options[ 'gui_colours' ][ '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 )
|
|
|
|
HydrusPaths.LaunchFile( 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 ):
|
|
|
|
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ '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
|
|
|
|
|
|
|