3728 lines
119 KiB
Python
3728 lines
119 KiB
Python
import itertools
|
|
import typing
|
|
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
from qtpy import QtGui as QG
|
|
from qtpy import QtMultimediaWidgets as QMW
|
|
from qtpy import QtMultimedia as QM
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusFileHandling
|
|
from hydrus.core import HydrusGlobals as HG
|
|
from hydrus.core import HydrusImageHandling
|
|
from hydrus.core import HydrusPaths
|
|
from hydrus.core import HydrusTime
|
|
|
|
from hydrus.client import ClientApplicationCommand as CAC
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientRendering
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIMedia
|
|
from hydrus.client.gui import ClientGUIMediaControls
|
|
from hydrus.client.gui import ClientGUIShortcuts
|
|
from hydrus.client.gui import QtInit
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.canvas import ClientGUIMPV
|
|
from hydrus.client.gui.widgets import ClientGUICommon
|
|
from hydrus.client.media import ClientMedia
|
|
|
|
ZOOM_CENTERPOINT_MEDIA_CENTER = 0
|
|
ZOOM_CENTERPOINT_VIEWER_CENTER = 1
|
|
ZOOM_CENTERPOINT_MOUSE = 2
|
|
ZOOM_CENTERPOINT_MEDIA_TOP_LEFT = 3
|
|
|
|
ZOOM_CENTERPOINT_TYPES = ( ZOOM_CENTERPOINT_VIEWER_CENTER, ZOOM_CENTERPOINT_MOUSE, ZOOM_CENTERPOINT_MEDIA_CENTER, ZOOM_CENTERPOINT_MEDIA_TOP_LEFT )
|
|
|
|
zoom_centerpoints_str_lookup = {}
|
|
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MEDIA_CENTER ] = 'media center'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_VIEWER_CENTER ] = 'viewer center'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MOUSE ] = 'mouse (or viewer center if mouse outside)'
|
|
zoom_centerpoints_str_lookup[ ZOOM_CENTERPOINT_MEDIA_TOP_LEFT ] = 'media top-left'
|
|
|
|
OPEN_EXTERNALLY_BUTTON_SIZE = ( 200, 45 )
|
|
|
|
def CalculateCanvasMediaSize( media, canvas_size: QC.QSize, show_action ):
|
|
|
|
canvas_width = canvas_size.width()
|
|
canvas_height = canvas_size.height()
|
|
|
|
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
|
|
|
|
canvas_height -= animated_scanbar_height
|
|
'''
|
|
|
|
canvas_width = max( canvas_width, 80 )
|
|
canvas_height = max( canvas_height, 60 )
|
|
|
|
return ( canvas_width, canvas_height )
|
|
|
|
|
|
def CalculateCanvasZooms( canvas_size: QC.QSize, canvas_type: int, device_pixel_ratio: float, media, show_action ):
|
|
|
|
if media is None:
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
if show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
( media_width, media_height ) = CalculateMediaSize( media, 1.0 )
|
|
|
|
if media_width == 0 or media_height == 0:
|
|
|
|
return ( 1.0, 1.0 )
|
|
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
( canvas_width, canvas_height ) = CalculateCanvasMediaSize( media, canvas_size, show_action )
|
|
|
|
raw_canvas_width = canvas_width * device_pixel_ratio
|
|
raw_canvas_height = canvas_height * device_pixel_ratio
|
|
|
|
width_zoom = raw_canvas_width / media_width
|
|
|
|
height_zoom = raw_canvas_height / media_height
|
|
|
|
canvas_zoom = min( ( width_zoom, height_zoom ) )
|
|
|
|
#
|
|
|
|
mime = media.GetMime()
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = new_options.GetMediaZoomOptions( mime )
|
|
|
|
if exact_zooms_only:
|
|
|
|
max_regular_zoom = 1.0
|
|
|
|
if canvas_zoom > 1.0:
|
|
|
|
while max_regular_zoom * 2 < canvas_zoom:
|
|
|
|
max_regular_zoom *= 2
|
|
|
|
|
|
elif canvas_zoom < 1.0:
|
|
|
|
while max_regular_zoom > canvas_zoom:
|
|
|
|
max_regular_zoom /= 2
|
|
|
|
|
|
|
|
else:
|
|
|
|
regular_zooms = new_options.GetMediaZooms()
|
|
|
|
valid_regular_zooms = [ zoom for zoom in regular_zooms if zoom < canvas_zoom ]
|
|
|
|
if len( valid_regular_zooms ) > 0:
|
|
|
|
max_regular_zoom = max( valid_regular_zooms )
|
|
|
|
else:
|
|
|
|
max_regular_zoom = canvas_zoom
|
|
|
|
|
|
|
|
if media.GetMime() in HC.AUDIO:
|
|
|
|
scale_up_action = CC.MEDIA_VIEWER_SCALE_100
|
|
scale_down_action = CC.MEDIA_VIEWER_SCALE_TO_CANVAS
|
|
|
|
elif canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
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 > raw_canvas_width or media_height > raw_canvas_height
|
|
can_be_scaled_up = media_width < raw_canvas_width and media_height < raw_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, device_pixel_ratio: float, zoom, show_action ) -> QC.QSize:
|
|
|
|
if show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
raise Exception( 'This media should not be shown in the media viewer!' )
|
|
|
|
elif show_action == CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON:
|
|
|
|
( width, height ) = OPEN_EXTERNALLY_BUTTON_SIZE
|
|
|
|
if media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
bounding_dimensions = HG.client_controller.options[ 'thumbnail_dimensions' ]
|
|
thumbnail_scale_type = HG.client_controller.new_options.GetInteger( 'thumbnail_scale_type' )
|
|
|
|
# we want the device independant size here, not actual pixels, so want to keep this 100
|
|
#thumbnail_dpr_percent = HG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' )
|
|
thumbnail_dpr_percent = 100
|
|
|
|
( clip_rect, ( thumb_width, thumb_height ) ) = HydrusImageHandling.GetThumbnailResolutionAndClipRegion( media.GetResolution(), bounding_dimensions, thumbnail_scale_type, thumbnail_dpr_percent )
|
|
|
|
height = height + thumb_height
|
|
|
|
|
|
return QC.QSize( width, height )
|
|
|
|
else:
|
|
|
|
( raw_media_width, raw_media_height ) = CalculateMediaSize( media, zoom )
|
|
|
|
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
|
|
|
|
media_height += animated_scanbar_height
|
|
'''
|
|
|
|
media_width = int( raw_media_width / device_pixel_ratio )
|
|
media_height = int( raw_media_height / device_pixel_ratio )
|
|
|
|
return QC.QSize( media_width, media_height )
|
|
|
|
|
|
|
|
def CalculateMediaSize( media, zoom ):
|
|
|
|
if media.GetMime() in HC.AUDIO:
|
|
|
|
( original_width, original_height ) = ( 360, 240 )
|
|
|
|
else:
|
|
|
|
( original_width, original_height ) = media.GetResolution()
|
|
|
|
|
|
media_width = int( round( zoom * original_width ) )
|
|
media_height = int( round( zoom * original_height ) )
|
|
|
|
media_width = max( 1, media_width )
|
|
media_height = max( 1, media_height )
|
|
|
|
return ( media_width, media_height )
|
|
|
|
|
|
def CanDisplayMedia( media: ClientMedia.MediaSingleton, canvas_type: int ) -> bool:
|
|
|
|
if media is None:
|
|
|
|
return False
|
|
|
|
|
|
media = media.GetDisplayMedia()
|
|
|
|
if media is None:
|
|
|
|
return False
|
|
|
|
|
|
locations_manager = media.GetLocationsManager()
|
|
|
|
if not locations_manager.IsLocal():
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
def GetShowAction( media: ClientMedia.MediaSingleton, canvas_type: int ):
|
|
|
|
# in the midst of a rewrite, feel free to refactor further
|
|
|
|
start_paused = False
|
|
start_with_embed = False
|
|
|
|
bad_result = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, start_paused, start_with_embed )
|
|
|
|
if media is None:
|
|
|
|
return bad_result
|
|
|
|
|
|
mime = media.GetMime()
|
|
|
|
if mime not in HC.ALLOWED_MIMES: # stopgap to catch a collection or application_unknown due to unusual import order/media moving
|
|
|
|
return bad_result
|
|
|
|
|
|
if canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
return HG.client_controller.new_options.GetPreviewShowAction( mime )
|
|
|
|
else:
|
|
|
|
return HG.client_controller.new_options.GetMediaShowAction( mime )
|
|
|
|
|
|
|
|
def ShouldHaveAnimationBar( media, show_action ):
|
|
|
|
if media is None:
|
|
|
|
return False
|
|
|
|
|
|
if show_action not in ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER ):
|
|
|
|
return False
|
|
|
|
|
|
is_animated_image = media.GetMime() in HC.ANIMATIONS
|
|
is_audio = media.GetMime() in HC.AUDIO
|
|
is_video = media.GetMime() in HC.VIDEO
|
|
|
|
if show_action in ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER ):
|
|
|
|
if ( is_animated_image or is_audio or is_video ) and media.HasDuration():
|
|
|
|
return True
|
|
|
|
|
|
elif show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE:
|
|
|
|
num_frames = media.GetNumFrames()
|
|
|
|
has_some_frames = num_frames is not None and num_frames > 1
|
|
|
|
if ( is_animated_image or is_video ) and has_some_frames:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def UserWantsUsToDisplayMedia( media: ClientMedia.MediaSingleton, canvas_type: int ) -> bool:
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = GetShowAction( media, canvas_type )
|
|
|
|
if media_show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
class Animation( QW.QWidget ):
|
|
|
|
launchMediaViewer = QC.Signal()
|
|
|
|
def __init__( self, parent, canvas_type, background_colour_generator ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._canvas_type = canvas_type
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
# pass up un-button-pressed mouse moves to parent, which wants to do cursor show/hide
|
|
self.setMouseTracking( True )
|
|
|
|
self._media = None
|
|
|
|
self._last_device_pixel_ratio = self.devicePixelRatio()
|
|
|
|
self._have_drawn_background_once = False
|
|
self._playthrough_count = 0
|
|
|
|
self._num_frames = 1
|
|
|
|
self._stop_for_slideshow = False
|
|
|
|
self._current_frame_index = 0
|
|
self._current_frame_drawn = False
|
|
self._current_timestamp_ms = None
|
|
self._next_frame_due_at = HydrusTime.GetNowPrecise()
|
|
self._slow_frame_score = 1.0
|
|
|
|
self._paused = True
|
|
|
|
self._video_container = None
|
|
|
|
self._canvas_qt_pixmap = None
|
|
|
|
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
shortcut_set = 'media_viewer_media_window'
|
|
|
|
else:
|
|
|
|
shortcut_set = 'preview_media_window'
|
|
|
|
|
|
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ shortcut_set ], catch_mouse = True )
|
|
|
|
|
|
def _ClearCanvasBitmap( self ):
|
|
|
|
if self._canvas_qt_pixmap is not None:
|
|
|
|
self._canvas_qt_pixmap = None
|
|
|
|
|
|
|
|
def _GetRawPixelSize( self ) -> QC.QSize:
|
|
|
|
return self.size() * self.devicePixelRatio()
|
|
|
|
|
|
def _ReinitForResizeOrDPRChange( self ):
|
|
|
|
my_raw_size = self._GetRawPixelSize()
|
|
|
|
my_raw_width = my_raw_size.width()
|
|
my_raw_height = my_raw_size.height()
|
|
|
|
self._ClearCanvasBitmap()
|
|
|
|
self._last_device_pixel_ratio = self.devicePixelRatio()
|
|
|
|
self._current_frame_drawn = False
|
|
self._have_drawn_background_once = False
|
|
|
|
self.update()
|
|
|
|
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_raw_width > renderer_width or my_raw_height > renderer_height
|
|
we_just_zoomed_out = my_raw_width < renderer_width or my_raw_height < renderer_height
|
|
|
|
if we_just_zoomed_in:
|
|
|
|
if self._video_container.IsScaled():
|
|
|
|
target_width = min( media_width, my_raw_width )
|
|
target_height = min( media_height, my_raw_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_raw_width < media_width or my_raw_height < media_height: # i.e. new zoom is scaled
|
|
|
|
self._video_container.Stop()
|
|
|
|
self._video_container = ClientRendering.RasterContainerVideo( self._media, ( my_raw_width, my_raw_height ), init_position = self._current_frame_index )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _TryToDrawCanvasBitmap( self ):
|
|
|
|
my_raw_size = self._GetRawPixelSize()
|
|
|
|
my_raw_width = my_raw_size.width()
|
|
my_raw_height = my_raw_size.height()
|
|
|
|
if self._video_container is None:
|
|
|
|
self._video_container = ClientRendering.RasterContainerVideo( self._media, ( my_raw_width, my_raw_height ), init_position = self._current_frame_index )
|
|
|
|
|
|
if not self._video_container.HasFrame( self._current_frame_index ):
|
|
|
|
return
|
|
|
|
|
|
if self._canvas_qt_pixmap is None:
|
|
|
|
self._canvas_qt_pixmap = HG.client_controller.bitmap_manager.GetQtPixmap( my_raw_width, my_raw_height )
|
|
|
|
self._canvas_qt_pixmap.setDevicePixelRatio( self.devicePixelRatio() )
|
|
|
|
painter = QG.QPainter( self._canvas_qt_pixmap )
|
|
|
|
self._DrawABlankFrame( painter )
|
|
|
|
else:
|
|
|
|
self._canvas_qt_pixmap.setDevicePixelRatio( self.devicePixelRatio() )
|
|
|
|
painter = QG.QPainter( self._canvas_qt_pixmap )
|
|
|
|
|
|
current_frame = self._video_container.GetFrame( self._current_frame_index )
|
|
|
|
current_frame_image = current_frame.GetQtImage()
|
|
|
|
# note we draw to self.rect(), which is in DPR coordinates. the pixmap needs to be DPR'd by here mate, this caught us up before
|
|
painter.drawImage( self.rect(), current_frame_image )
|
|
|
|
self._current_frame_drawn = True
|
|
|
|
next_frame_time_s = self._video_container.GetDurationMS( self._current_frame_index ) / 1000.0
|
|
|
|
next_frame_ideally_due = self._next_frame_due_at + next_frame_time_s
|
|
|
|
if HydrusTime.TimeHasPassedPrecise( next_frame_ideally_due ):
|
|
|
|
self._next_frame_due_at = HydrusTime.GetNowPrecise() + next_frame_time_s
|
|
|
|
else:
|
|
|
|
self._next_frame_due_at = next_frame_ideally_due
|
|
|
|
|
|
|
|
def _DrawABlankFrame( self, painter ):
|
|
|
|
if self._background_colour_generator.CanDoTransparencyCheckerboard():
|
|
|
|
light_grey = QG.QColor( 237, 237, 237 )
|
|
dark_grey = QG.QColor( 222, 222, 222 )
|
|
|
|
painter.setBackground( QG.QBrush( light_grey ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
# 16x16 boxes, light grey in top right
|
|
BOX_LENGTH = int( 16 * self.devicePixelRatio() )
|
|
|
|
painter_width = painter.viewport().width()
|
|
painter_height = painter.viewport().height()
|
|
|
|
num_cols = painter_width // BOX_LENGTH
|
|
|
|
if painter_width % BOX_LENGTH > 0:
|
|
|
|
num_cols += 1
|
|
|
|
|
|
num_rows = painter_height // BOX_LENGTH
|
|
|
|
if painter_height % BOX_LENGTH > 0:
|
|
|
|
num_rows += 1
|
|
|
|
|
|
painter.setBrush( QG.QBrush( dark_grey ) )
|
|
painter.setPen( QG.QPen( QC.Qt.NoPen ) )
|
|
|
|
for y_index in range( num_rows ):
|
|
|
|
for x_index in range( num_cols ):
|
|
|
|
if ( x_index + y_index ) % 2 == 1:
|
|
|
|
rect = QC.QRect( x_index * BOX_LENGTH, y_index * BOX_LENGTH, BOX_LENGTH, BOX_LENGTH )
|
|
|
|
if painter.viewport().intersects( rect ):
|
|
|
|
painter.drawRect( rect )
|
|
|
|
|
|
|
|
|
|
|
|
self._have_drawn_background_once = True
|
|
|
|
return
|
|
|
|
|
|
colour = self._background_colour_generator.GetColour()
|
|
|
|
painter.setBackground( QG.QBrush( colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
self._have_drawn_background_once = True
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self.SetMedia( None )
|
|
|
|
|
|
def CurrentFrame( self ):
|
|
|
|
return self._current_frame_index
|
|
|
|
|
|
def GetAnimationBarStatus( self ):
|
|
|
|
if self._video_container is None:
|
|
|
|
buffer_indices = None
|
|
|
|
else:
|
|
|
|
buffer_indices = self._video_container.GetBufferIndices()
|
|
|
|
if self._current_timestamp_ms is None and self._video_container.IsInitialised():
|
|
|
|
self._current_timestamp_ms = self._video_container.GetTimestampMS( self._current_frame_index )
|
|
|
|
|
|
|
|
return ( self._current_frame_index, self._current_timestamp_ms, self._paused, buffer_indices )
|
|
|
|
|
|
def GotoFrame( self, frame_index, pause_afterwards = True ):
|
|
|
|
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._current_timestamp_ms = None
|
|
|
|
self._next_frame_due_at = HydrusTime.GetNowPrecise()
|
|
|
|
self._video_container.GetReadyForFrame( self._current_frame_index )
|
|
|
|
self._current_frame_drawn = False
|
|
|
|
self.update()
|
|
|
|
|
|
if pause_afterwards:
|
|
|
|
self._paused = True
|
|
|
|
|
|
|
|
|
|
def GotoTimestamp( self, timestamp_ms, round_direction, pause_afterwards = True ):
|
|
|
|
if self._video_container is not None and self._video_container.IsInitialised():
|
|
|
|
frame_index = self._video_container.GetFrameIndex( timestamp_ms )
|
|
|
|
if frame_index == self._current_frame_index:
|
|
|
|
frame_index += round_direction
|
|
|
|
|
|
if frame_index > self._media.GetNumFrames() - 1:
|
|
|
|
frame_index = 0
|
|
|
|
|
|
self.GotoFrame( frame_index, pause_afterwards = pause_afterwards )
|
|
|
|
|
|
|
|
def HasPlayedOnceThrough( self ):
|
|
|
|
return self._playthrough_count > 0
|
|
|
|
|
|
def IsPaused( self ):
|
|
|
|
return self._paused
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
if self.devicePixelRatio() != self._last_device_pixel_ratio:
|
|
|
|
self._ReinitForResizeOrDPRChange()
|
|
|
|
|
|
if not self._current_frame_drawn:
|
|
|
|
self._TryToDrawCanvasBitmap()
|
|
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
if self._canvas_qt_pixmap is None:
|
|
|
|
self._DrawABlankFrame( painter )
|
|
|
|
else:
|
|
|
|
painter.drawPixmap( self.rect(), self._canvas_qt_pixmap )
|
|
|
|
|
|
|
|
def Pause( self ):
|
|
|
|
self._paused = True
|
|
|
|
|
|
def PausePlay( self ):
|
|
|
|
self._paused = not self._paused
|
|
|
|
|
|
def Play( self ):
|
|
|
|
self._paused = False
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_PAUSE_MEDIA:
|
|
|
|
self.Pause()
|
|
|
|
elif action == CAC.SIMPLE_PAUSE_PLAY_MEDIA:
|
|
|
|
self.PausePlay()
|
|
|
|
elif action == CAC.SIMPLE_MEDIA_SEEK_DELTA:
|
|
|
|
( direction, duration_ms ) = command.GetSimpleData()
|
|
|
|
self.SeekDelta( direction, duration_ms )
|
|
|
|
elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
|
|
|
|
if self._media is not None:
|
|
|
|
self.Pause()
|
|
|
|
ClientGUIMedia.OpenExternally( self._media )
|
|
|
|
|
|
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
self.window().close()
|
|
|
|
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
self.launchMediaViewer.emit()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def resizeEvent( self, event ):
|
|
|
|
size = self.size()
|
|
|
|
my_raw_width = size.width()
|
|
my_raw_height = size.height()
|
|
|
|
if my_raw_width > 0 and my_raw_height > 0:
|
|
|
|
if size != event.oldSize():
|
|
|
|
self._ReinitForResizeOrDPRChange()
|
|
|
|
|
|
|
|
|
|
def SeekDelta( self, direction, duration_ms ):
|
|
|
|
if self._current_timestamp_ms is not None and self._video_container is not None and self._video_container.IsInitialised():
|
|
|
|
new_ts = self._current_timestamp_ms + ( direction * duration_ms )
|
|
|
|
self.GotoTimestamp( new_ts, direction, pause_afterwards = False )
|
|
|
|
|
|
|
|
def SetBackgroundColourGenerator( self, background_colour_generator ):
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
|
|
def StopForSlideshow( self, value ):
|
|
|
|
self._stop_for_slideshow = value
|
|
|
|
|
|
def SetMedia( self, media: typing.Optional[ ClientMedia.MediaSingleton ], start_paused = False ):
|
|
|
|
if media == self._media:
|
|
|
|
return
|
|
|
|
|
|
self._media = media
|
|
|
|
self._ClearCanvasBitmap()
|
|
|
|
self._have_drawn_background_once = False
|
|
self._playthrough_count = 0
|
|
|
|
self._stop_for_slideshow = False
|
|
|
|
self._current_frame_index = int( ( self._num_frames - 1 ) * HC.options[ 'animation_start_position' ] )
|
|
self._current_frame_drawn = False
|
|
self._current_timestamp_ms = None
|
|
self._next_frame_due_at = HydrusTime.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
|
|
|
|
if self._media is None:
|
|
|
|
self._num_frames = 1
|
|
|
|
HG.client_controller.gui.UnregisterAnimationUpdateWindow( self )
|
|
|
|
else:
|
|
|
|
self._num_frames = self._media.GetNumFrames()
|
|
|
|
HG.client_controller.gui.RegisterAnimationUpdateWindow( self )
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def TIMERAnimationUpdate( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
if self.isVisible():
|
|
|
|
if self._current_frame_drawn:
|
|
|
|
if not self._paused and HydrusTime.TimeHasPassedPrecise( self._next_frame_due_at ):
|
|
|
|
num_frames = self._media.GetNumFrames()
|
|
|
|
next_frame_index = ( self._current_frame_index + 1 ) % num_frames
|
|
|
|
if next_frame_index == 0:
|
|
|
|
self._playthrough_count += 1
|
|
|
|
do_times_to_play_animation_pause = False
|
|
|
|
if self._media.GetMime() in HC.ANIMATIONS and not HG.client_controller.new_options.GetBoolean( 'always_loop_gifs' ):
|
|
|
|
times_to_play_animation = self._video_container.GetTimesToPlayAnimation()
|
|
|
|
# 0 is infinite
|
|
if times_to_play_animation != 0 and self._playthrough_count >= times_to_play_animation:
|
|
|
|
do_times_to_play_animation_pause = True
|
|
|
|
|
|
|
|
if self._stop_for_slideshow or do_times_to_play_animation_pause:
|
|
|
|
self._paused = True
|
|
|
|
else:
|
|
|
|
self._current_frame_index = next_frame_index
|
|
self._current_timestamp_ms = 0
|
|
|
|
|
|
else:
|
|
|
|
self._current_frame_index = next_frame_index
|
|
|
|
if self._current_timestamp_ms is not None and self._video_container is not None and self._video_container.IsInitialised():
|
|
|
|
duration_ms = self._video_container.GetDurationMS( self._current_frame_index - 1 )
|
|
|
|
self._current_timestamp_ms += duration_ms
|
|
|
|
|
|
|
|
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 ):
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
HG.client_controller.gui.UnregisterAnimationUpdateWindow( self )
|
|
|
|
raise
|
|
|
|
|
|
|
|
class AnimationBar( QW.QWidget ):
|
|
|
|
def __init__( self, parent ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._colours = {
|
|
'hab_border' : QG.QColor( 0, 0, 0 ),
|
|
'hab_background' : QG.QColor( 240, 240, 240 ),
|
|
'hab_nub' : QG.QColor( 96, 96, 96 )
|
|
}
|
|
|
|
self.setObjectName( 'HydrusAnimationBar' )
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.ArrowCursor ) )
|
|
|
|
self.setSizePolicy( QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed )
|
|
|
|
self._media_window = None
|
|
self._duration_ms = 1000
|
|
self._num_frames = 1
|
|
self._last_drawn_info = None
|
|
|
|
self._show_text = True
|
|
|
|
self._currently_in_a_drag = False
|
|
self._it_was_playing_before_drag = False
|
|
|
|
|
|
def _DrawBlank( self, painter ):
|
|
|
|
self.setProperty( 'playing', False )
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
background_colour = self._colours[ 'hab_background' ]
|
|
|
|
painter.setBackground( background_colour )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
|
|
def _GetAnimationBarStatus( self ):
|
|
|
|
return self._media_window.GetAnimationBarStatus()
|
|
|
|
|
|
def _GetXFromFrameIndex( self, index, width_offset = 0 ):
|
|
|
|
if self._num_frames is None or self._num_frames < 2:
|
|
|
|
return 0
|
|
|
|
|
|
my_width = self.size().width()
|
|
|
|
return int( ( my_width - width_offset ) * index / ( self._num_frames - 1 ) )
|
|
|
|
|
|
def _GetXFromTimestamp( self, timestamp_ms, width_offset = 0 ):
|
|
|
|
my_width = self.size().width()
|
|
|
|
return int( ( my_width - width_offset ) * timestamp_ms / self._duration_ms )
|
|
|
|
|
|
def _CurrentMediaWindowIsBad( self ):
|
|
|
|
if self._media_window is None:
|
|
|
|
return True
|
|
|
|
|
|
if not QP.isValid( self._media_window ):
|
|
|
|
self.ClearMedia()
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _Redraw( self, painter ):
|
|
|
|
self._last_drawn_info = self._GetAnimationBarStatus()
|
|
|
|
( current_frame_index, current_timestamp_ms, paused, buffer_indices ) = self._last_drawn_info
|
|
|
|
self.setProperty( 'playing', not paused )
|
|
|
|
my_width = self.size().width()
|
|
my_height = self.size().height()
|
|
|
|
background_colour = self._colours[ 'hab_background' ]
|
|
|
|
if paused:
|
|
|
|
background_colour = ClientGUIFunctions.GetLighterDarkerColour( background_colour )
|
|
|
|
|
|
painter.setBackground( QG.QBrush( background_colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
#
|
|
|
|
my_height = self.height()
|
|
|
|
if buffer_indices is not None:
|
|
|
|
( start_index, rendered_to_index, end_index ) = buffer_indices
|
|
|
|
if ClientRendering.FrameIndexOutOfRange( rendered_to_index, start_index, end_index ):
|
|
|
|
rendered_to_index = start_index
|
|
|
|
|
|
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 = ClientGUIFunctions.GetDifferentLighterDarkerColour( background_colour )
|
|
|
|
if rendered_to_x > start_x:
|
|
|
|
painter.fillRect( start_x, 0, rendered_to_x - start_x, my_height, rendered_colour )
|
|
|
|
else:
|
|
|
|
painter.fillRect( start_x, 0, my_width - start_x, my_height, rendered_colour )
|
|
|
|
painter.fillRect( 0, 0, rendered_to_x, my_height, rendered_colour )
|
|
|
|
|
|
|
|
if rendered_to_x != end_x:
|
|
|
|
to_be_rendered_colour = ClientGUIFunctions.GetDifferentLighterDarkerColour( background_colour, 1 )
|
|
|
|
if end_x > rendered_to_x:
|
|
|
|
painter.fillRect( rendered_to_x, 0, end_x - rendered_to_x, my_height, to_be_rendered_colour )
|
|
|
|
else:
|
|
|
|
painter.fillRect( rendered_to_x, 0, my_width - rendered_to_x, my_height, to_be_rendered_colour )
|
|
|
|
painter.fillRect( 0, 0, end_x, my_height, to_be_rendered_colour )
|
|
|
|
|
|
|
|
|
|
animated_scanbar_nub_width = HG.client_controller.new_options.GetInteger( 'animated_scanbar_nub_width' )
|
|
|
|
num_frames_are_useful = self._num_frames is not None and self._num_frames > 1
|
|
|
|
nub_x = None
|
|
|
|
if num_frames_are_useful and current_frame_index is not None:
|
|
|
|
nub_x = self._GetXFromFrameIndex( current_frame_index, width_offset = animated_scanbar_nub_width )
|
|
|
|
elif self._duration_ms is not None and current_timestamp_ms is not None:
|
|
|
|
nub_x = self._GetXFromTimestamp( current_timestamp_ms, width_offset = animated_scanbar_nub_width )
|
|
|
|
|
|
if nub_x is not None:
|
|
|
|
painter.fillRect( nub_x, 0, animated_scanbar_nub_width, my_height, self._colours[ 'hab_nub' ] )
|
|
|
|
|
|
#
|
|
|
|
if self._show_text:
|
|
|
|
progress_strings = []
|
|
|
|
if num_frames_are_useful:
|
|
|
|
progress_strings.append( HydrusData.ConvertValueRangeToPrettyString( current_frame_index + 1, self._num_frames ) )
|
|
|
|
|
|
if current_timestamp_ms is not None:
|
|
|
|
progress_strings.append( HydrusTime.ValueRangeToScanbarTimestampsMS( current_timestamp_ms, self._duration_ms ) )
|
|
|
|
|
|
s = ' - '.join( progress_strings )
|
|
|
|
if len( s ) > 0:
|
|
|
|
( text_size, s ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, s )
|
|
|
|
x = my_width - text_size.width() - 3
|
|
y = ( my_height - text_size.height() ) / 2
|
|
|
|
ClientGUIFunctions.DrawText( painter, x, y, s )
|
|
|
|
|
|
|
|
#
|
|
|
|
painter.setBrush( QC.Qt.NoBrush )
|
|
|
|
painter.setPen( QG.QPen( self._colours[ 'hab_border' ] ) )
|
|
|
|
painter.drawRect( 0, 0, my_width - 1, my_height - 1 )
|
|
|
|
|
|
def _ScanToCurrentMousePos( self ):
|
|
|
|
my_width = self.size().width()
|
|
|
|
mouse_pos = self.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
animated_scanbar_nub_width = HG.client_controller.new_options.GetInteger( 'animated_scanbar_nub_width' )
|
|
|
|
compensated_x_position = mouse_pos.x() - ( animated_scanbar_nub_width / 2 )
|
|
|
|
proportion = ( compensated_x_position ) / ( my_width - animated_scanbar_nub_width )
|
|
|
|
proportion = max( proportion, 0.0 )
|
|
proportion = min( 1.0, proportion )
|
|
|
|
self.update()
|
|
|
|
if isinstance( self._media_window, Animation ):
|
|
|
|
current_frame_index = int( proportion * ( self._num_frames - 1 ) + 0.5 )
|
|
|
|
self._media_window.GotoFrame( current_frame_index )
|
|
|
|
elif isinstance( self._media_window, ( ClientGUIMPV.MPVWidget, QtMediaPlayer ) ):
|
|
|
|
time_index_ms = int( proportion * self._duration_ms )
|
|
|
|
self._media_window.Seek( time_index_ms )
|
|
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self._media_window = None
|
|
|
|
HG.client_controller.gui.UnregisterAnimationUpdateWindow( self )
|
|
|
|
self.update()
|
|
|
|
|
|
def DoingADrag( self ):
|
|
|
|
return self._currently_in_a_drag
|
|
|
|
|
|
def mouseMoveEvent( self, event ):
|
|
|
|
if self._CurrentMediaWindowIsBad():
|
|
|
|
return
|
|
|
|
|
|
CC.CAN_HIDE_MOUSE = False
|
|
|
|
if self._currently_in_a_drag:
|
|
|
|
if event.buttons() == QC.Qt.NoButton:
|
|
|
|
self._currently_in_a_drag = False
|
|
|
|
return
|
|
|
|
|
|
self._ScanToCurrentMousePos()
|
|
|
|
|
|
|
|
def mousePressEvent( self, event ):
|
|
|
|
if self._CurrentMediaWindowIsBad():
|
|
|
|
return
|
|
|
|
|
|
CC.CAN_HIDE_MOUSE = False
|
|
|
|
self._it_was_playing_before_drag = not self._media_window.IsPaused()
|
|
|
|
if self._it_was_playing_before_drag:
|
|
|
|
self._media_window.Pause()
|
|
|
|
|
|
self._currently_in_a_drag = True
|
|
|
|
self._ScanToCurrentMousePos()
|
|
|
|
|
|
def mouseReleaseEvent( self, event ):
|
|
|
|
CC.CAN_HIDE_MOUSE = True
|
|
|
|
if self._currently_in_a_drag:
|
|
|
|
if self._it_was_playing_before_drag:
|
|
|
|
if not self._CurrentMediaWindowIsBad():
|
|
|
|
self._media_window.Play()
|
|
|
|
|
|
|
|
self._currently_in_a_drag = False
|
|
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
if self._CurrentMediaWindowIsBad():
|
|
|
|
self._DrawBlank( painter )
|
|
|
|
else:
|
|
|
|
self._Redraw( painter )
|
|
|
|
|
|
|
|
def SetMediaAndWindow( self, media, media_window ):
|
|
|
|
self._media_window = media_window
|
|
self._duration_ms = max( media.GetDurationMS(), 1 )
|
|
|
|
num_frames = media.GetNumFrames()
|
|
|
|
if num_frames is None:
|
|
|
|
self._num_frames = num_frames
|
|
|
|
else:
|
|
|
|
self._num_frames = max( num_frames, 1 )
|
|
|
|
|
|
self._last_drawn_info = None
|
|
|
|
self._currently_in_a_drag = False
|
|
self._it_was_playing_before_drag = False
|
|
|
|
HG.client_controller.gui.RegisterAnimationUpdateWindow( self )
|
|
|
|
self.update()
|
|
|
|
|
|
def SetShowText( self, show_text: bool ):
|
|
|
|
self._show_text = show_text
|
|
|
|
|
|
def TIMERAnimationUpdate( self ):
|
|
|
|
if self._CurrentMediaWindowIsBad():
|
|
|
|
self.ClearMedia()
|
|
|
|
return
|
|
|
|
|
|
if not self.isVisible():
|
|
|
|
return
|
|
|
|
|
|
if self._last_drawn_info != self._GetAnimationBarStatus():
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def get_hab_background( self ):
|
|
|
|
return self._colours[ 'hab_background' ]
|
|
|
|
|
|
def get_hab_border( self ):
|
|
|
|
return self._colours[ 'hab_border' ]
|
|
|
|
|
|
def get_hab_nub( self ):
|
|
|
|
return self._colours[ 'hab_nub' ]
|
|
|
|
|
|
def set_hab_background( self, colour ):
|
|
|
|
self._colours[ 'hab_background' ] = colour
|
|
|
|
|
|
def set_hab_border( self, colour ):
|
|
|
|
self._colours[ 'hab_border' ] = colour
|
|
|
|
|
|
def set_hab_nub( self, colour ):
|
|
|
|
self._colours[ 'hab_nub' ] = colour
|
|
|
|
|
|
hab_border = QC.Property( QG.QColor, get_hab_border, set_hab_border )
|
|
hab_background = QC.Property( QG.QColor, get_hab_background, set_hab_background )
|
|
hab_nub = QC.Property( QG.QColor, get_hab_nub, set_hab_nub )
|
|
|
|
|
|
# cribbing from here https://doc.qt.io/qt-5/layout.html#how-to-write-a-custom-layout-manager
|
|
class MediaContainerLayout( QW.QLayout ):
|
|
|
|
def __init__( self, static_image ):
|
|
|
|
QW.QLayout.__init__( self )
|
|
|
|
self._static_image = static_image
|
|
|
|
self._layout_items = [ static_image ]
|
|
|
|
|
|
|
|
class MediaContainer( QW.QWidget ):
|
|
|
|
launchMediaViewer = QC.Signal()
|
|
readyForNeighbourPrefetch = QC.Signal()
|
|
|
|
zoomChanged = QC.Signal( float )
|
|
|
|
def __init__( self, parent, canvas_type, background_colour_generator, additional_event_filter: QC.QObject ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._canvas_type = canvas_type
|
|
|
|
if HC.PLATFORM_MACOS and not HG.macos_antiflicker_test:
|
|
|
|
# does modern macOS still go 100% CPU when this is off?
|
|
# yes :^(
|
|
# try again with more layout tech on the full canvas
|
|
|
|
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
|
|
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
self.setSizePolicy( QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed )
|
|
|
|
self._media = None
|
|
self._show_action = CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW
|
|
self._start_paused = False
|
|
self._start_with_embed = False
|
|
|
|
self._default_zoom = 1.0
|
|
self._current_zoom = 1.0
|
|
self._canvas_zoom = 1.0
|
|
|
|
self._media_window = None
|
|
|
|
self._embed_button = EmbedButton( self, self._background_colour_generator )
|
|
self._embed_button_widget_event_filter = QP.WidgetEventFilter( self._embed_button )
|
|
self._embed_button_widget_event_filter.EVT_LEFT_DOWN( self.EventEmbedButton )
|
|
|
|
# pass up un-button-pressed mouse moves to parent, which wants to do cursor show/hide
|
|
self.setMouseTracking( True )
|
|
|
|
self._additional_event_filter = additional_event_filter
|
|
|
|
self._animation_window = Animation( self, self._canvas_type, self._background_colour_generator )
|
|
|
|
self._static_image_window = StaticImage( self, self._canvas_type, self._background_colour_generator )
|
|
|
|
self._static_image_window.readyForNeighbourPrefetch.connect( self.readyForNeighbourPrefetch )
|
|
|
|
self._controls_bar = QW.QWidget( self )
|
|
self._controls_bar_show_full = True
|
|
|
|
# We need this to force-fill some blanks at times
|
|
self.setAutoFillBackground( True )
|
|
|
|
self._animation_bar = AnimationBar( self._controls_bar )
|
|
self._volume_control = ClientGUIMediaControls.VolumeControl( self._controls_bar, self._canvas_type, direction = 'up' )
|
|
|
|
self._volume_control.setCursor( QC.Qt.ArrowCursor )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout( margin = 0, spacing = 0 )
|
|
|
|
QP.AddToLayout( hbox, self._animation_bar, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
QP.AddToLayout( hbox, self._volume_control, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
self._controls_bar.setLayout( hbox )
|
|
|
|
#
|
|
|
|
self._animation_window.hide()
|
|
|
|
self._controls_bar.hide()
|
|
|
|
self._static_image_window.hide()
|
|
self._embed_button.hide()
|
|
|
|
self.hide()
|
|
|
|
HG.client_controller.sub( self, 'Pause', 'pause_all_media' )
|
|
|
|
|
|
def _DestroyOrHideThisMediaWindow( self, media_window ):
|
|
|
|
if media_window is not None:
|
|
|
|
launch_media_viewer_classes = ( Animation, ClientGUIMPV.MPVWidget, StaticImage, QtMediaPlayer )
|
|
|
|
media_window.removeEventFilter( self._additional_event_filter )
|
|
|
|
if isinstance( media_window, launch_media_viewer_classes ):
|
|
|
|
try:
|
|
|
|
media_window.launchMediaViewer.disconnect( self.launchMediaViewer )
|
|
|
|
except RuntimeError:
|
|
|
|
pass # lmao, weird 'Failed to disconnect signal launchMediaViewer()' error I couldn't figure out, I guess some out-of-order deleteLater gubbins
|
|
|
|
|
|
media_window.ClearMedia()
|
|
|
|
if isinstance( media_window, StaticImage ):
|
|
|
|
media_window.repaint()
|
|
|
|
|
|
media_window.hide()
|
|
|
|
if isinstance( media_window, ClientGUIMPV.MPVWidget ):
|
|
|
|
HG.client_controller.gui.ReleaseMPVWidget( media_window )
|
|
|
|
|
|
if isinstance( media_window, QtMediaPlayer ):
|
|
|
|
media_window.deleteLater()
|
|
|
|
|
|
else:
|
|
|
|
media_window.deleteLater()
|
|
|
|
|
|
|
|
|
|
def _GetMaxZoomDimension( self ):
|
|
|
|
if self._show_action in ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER ) or isinstance( self._media_window, Animation ):
|
|
|
|
return 8000
|
|
|
|
else:
|
|
|
|
return 32000
|
|
|
|
|
|
|
|
def _MakeMediaWindow( self ):
|
|
|
|
old_media_window = self._media_window
|
|
destroy_old_media_window = True
|
|
|
|
do_neighbour_prefetch_emit = True
|
|
|
|
if self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV and not ClientGUIMPV.MPV_IS_AVAILABLE:
|
|
|
|
self._show_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
|
|
|
|
HydrusData.ShowText( 'MPV is not available!' )
|
|
|
|
|
|
if self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER and QtInit.WE_ARE_QT5:
|
|
|
|
self._show_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
|
|
|
|
HydrusData.ShowText( 'Qt Media Player is only available on Qt6!' )
|
|
|
|
|
|
if self._show_action in ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER ) and self._media.GetMime() == HC.IMAGE_GIF and not self._media.HasDuration():
|
|
|
|
self._show_action = CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE
|
|
|
|
|
|
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 )
|
|
|
|
elif self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE:
|
|
|
|
if self._media.IsStaticImage():
|
|
|
|
if isinstance( self._media_window, StaticImage ):
|
|
|
|
destroy_old_media_window = False
|
|
|
|
self._media_window.hide()
|
|
|
|
else:
|
|
|
|
self._media_window = self._static_image_window
|
|
|
|
|
|
self._media_window.SetMedia( self._media )
|
|
|
|
do_neighbour_prefetch_emit = False
|
|
|
|
else:
|
|
|
|
if isinstance( self._media_window, Animation ):
|
|
|
|
destroy_old_media_window = False
|
|
|
|
self._media_window.hide()
|
|
|
|
else:
|
|
|
|
self._media_window = self._animation_window
|
|
|
|
|
|
self._media_window.SetMedia( self._media, start_paused = self._start_paused )
|
|
|
|
self._media_window.lower()
|
|
|
|
|
|
elif self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV:
|
|
|
|
self._media_window = HG.client_controller.gui.GetMPVWidget( self )
|
|
|
|
self._media_window.SetCanvasType( self._canvas_type )
|
|
|
|
self._media_window.SetMedia( self._media, start_paused = self._start_paused )
|
|
|
|
self._media_window.lower()
|
|
|
|
elif self._show_action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER:
|
|
|
|
self._media_window = QtMediaPlayer( self, self._canvas_type, self._background_colour_generator )
|
|
|
|
self._media_window.SetMedia( self._media, start_paused = self._start_paused )
|
|
|
|
self._media_window.lower()
|
|
|
|
|
|
if ShouldHaveAnimationBar( self._media, self._show_action ):
|
|
|
|
self._animation_bar.SetMediaAndWindow( self._media, self._media_window )
|
|
|
|
else:
|
|
|
|
self._animation_bar.ClearMedia()
|
|
|
|
|
|
media_window_changed = old_media_window != self._media_window
|
|
|
|
# this has to go after setcanvastype on the mpv window so the filters are in the correct order
|
|
if media_window_changed:
|
|
|
|
self._media_window.installEventFilter( self._additional_event_filter )
|
|
|
|
launch_media_viewer_classes = ( Animation, ClientGUIMPV.MPVWidget, StaticImage, QtMediaPlayer )
|
|
|
|
if isinstance( self._media_window, launch_media_viewer_classes ):
|
|
|
|
self._media_window.launchMediaViewer.connect( self.launchMediaViewer )
|
|
|
|
|
|
self._DestroyOrHideThisMediaWindow( old_media_window )
|
|
|
|
# this forces a flush of the last valid background bmp, so we don't get a flicker of a file from five files ago when we last saw a static image
|
|
self.repaint()
|
|
|
|
|
|
if do_neighbour_prefetch_emit:
|
|
|
|
self.readyForNeighbourPrefetch.emit()
|
|
|
|
|
|
|
|
def _MoveDelta( self, delta: QC.QPoint ):
|
|
|
|
if delta.isNull():
|
|
|
|
return
|
|
|
|
|
|
self.move( self.pos() + delta )
|
|
|
|
|
|
def _SetZoom( self, zoom: float ):
|
|
|
|
self._current_zoom = zoom
|
|
|
|
size_hint = self.sizeHint()
|
|
|
|
if self.size() != size_hint:
|
|
|
|
self.resize( size_hint )
|
|
|
|
if HC.PLATFORM_MACOS:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
self.zoomChanged.emit( self._current_zoom )
|
|
|
|
|
|
def _SizeAndPositionChildren( self ):
|
|
|
|
if self._media is not None:
|
|
|
|
self._embed_button.setFixedSize( self.size() )
|
|
self._embed_button.move( QC.QPoint( 0, 0 ) )
|
|
|
|
if self._media_window is not None:
|
|
|
|
self._media_window.setFixedSize( self.size() )
|
|
self._media_window.move( QC.QPoint( 0, 0 ) )
|
|
|
|
|
|
controls_bar_rect = self.GetIdealControlsBarRect( full_size = self._controls_bar_show_full )
|
|
|
|
if controls_bar_rect.size() != self._controls_bar.size():
|
|
|
|
self._controls_bar.setFixedSize( controls_bar_rect.size() )
|
|
|
|
|
|
self._controls_bar.move( controls_bar_rect.topLeft() )
|
|
|
|
|
|
|
|
def _TryToChangeZoom( self, new_zoom, zoom_center_type_override = None ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
my_dpr = self.devicePixelRatio()
|
|
|
|
new_media_window_size = CalculateMediaContainerSize( self._media, my_dpr, new_zoom, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE )
|
|
|
|
new_my_width = new_media_window_size.width()
|
|
new_my_height = new_media_window_size.height()
|
|
|
|
max_zoom_dimension = self._GetMaxZoomDimension()
|
|
|
|
if new_my_width > max_zoom_dimension or new_my_height > max_zoom_dimension:
|
|
|
|
( limit_max_normal_zoom, limit_max_canvas_zoom ) = CalculateCanvasZooms( QC.QSize( max_zoom_dimension, max_zoom_dimension ), self._canvas_type, my_dpr, self._media, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE )
|
|
|
|
new_zoom = limit_max_canvas_zoom
|
|
|
|
new_media_window_size = CalculateMediaContainerSize( self._media, my_dpr, new_zoom, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE )
|
|
|
|
new_my_width = new_media_window_size.width()
|
|
new_my_height = new_media_window_size.height()
|
|
|
|
|
|
if new_zoom == self._current_zoom:
|
|
|
|
return
|
|
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
|
|
old_size_bigger = canvas_size.width() < my_width or canvas_size.height() < my_height
|
|
new_size_fits = canvas_size.width() >= new_my_width and canvas_size.height() >= new_my_height
|
|
|
|
#
|
|
|
|
if my_width > 0 and my_height > 0:
|
|
|
|
if zoom_center_type_override is None:
|
|
|
|
zoom_center_type = HG.client_controller.new_options.GetInteger( 'media_viewer_zoom_center' )
|
|
|
|
else:
|
|
|
|
zoom_center_type = zoom_center_type_override
|
|
|
|
|
|
my_pos = self.pos()
|
|
|
|
# viewer center is the default
|
|
zoom_centerpoint = QC.QPoint( canvas_size.width() // 2, canvas_size.height() // 2 )
|
|
|
|
if zoom_center_type == ZOOM_CENTERPOINT_MEDIA_CENTER:
|
|
|
|
zoom_centerpoint = my_pos + QC.QPoint( my_width // 2, my_height // 2 )
|
|
|
|
elif zoom_center_type == ZOOM_CENTERPOINT_MEDIA_TOP_LEFT:
|
|
|
|
zoom_centerpoint = my_pos
|
|
|
|
elif zoom_center_type == ZOOM_CENTERPOINT_MOUSE:
|
|
|
|
mouse_pos = self.parentWidget().mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
if self.parent().rect().contains( mouse_pos ):
|
|
|
|
zoom_centerpoint = mouse_pos
|
|
|
|
|
|
|
|
# probably a simpler way to calc this, but hey
|
|
widths_centerpoint_is_from_pos = ( zoom_centerpoint.x() - my_pos.x() ) / my_width
|
|
heights_centerpoint_is_from_pos = ( zoom_centerpoint.y() - my_pos.y() ) / my_height
|
|
|
|
zoom_width_delta = my_width - new_my_width
|
|
zoom_height_delta = my_height - new_my_height
|
|
|
|
centerpoint_adjusted_delta = QC.QPoint( int( zoom_width_delta * widths_centerpoint_is_from_pos ), int( zoom_height_delta * heights_centerpoint_is_from_pos ) )
|
|
|
|
self._MoveDelta( centerpoint_adjusted_delta )
|
|
|
|
|
|
#
|
|
|
|
self._SetZoom( new_zoom )
|
|
|
|
self.RescueIfOffScreen()
|
|
|
|
# due to the foolish 'giganto window' system for large zooms, some auto-update stuff doesn't work right if the convas rect is contained by the media rect, so do a refresh here
|
|
if new_zoom > self._canvas_zoom:
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def BeginDrag( self ):
|
|
|
|
self.parentWidget().BeginDrag()
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self._media = None
|
|
|
|
self._animation_bar.ClearMedia()
|
|
|
|
self._controls_bar.hide()
|
|
|
|
self._DestroyOrHideThisMediaWindow( self._media_window )
|
|
|
|
self._media_window = None
|
|
|
|
HG.client_controller.gui.UnregisterUIUpdateWindow( self )
|
|
|
|
self.hide()
|
|
|
|
|
|
def DoEdgePan( self, pan_type: int ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
my_size = self.size()
|
|
my_pos = self.pos()
|
|
|
|
delta_x = 0
|
|
delta_y = 0
|
|
|
|
if pan_type == CAC.SIMPLE_PAN_TOP_EDGE:
|
|
|
|
delta_y = - my_pos.y()
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_LEFT_EDGE:
|
|
|
|
delta_x = - my_pos.x()
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_BOTTOM_EDGE:
|
|
|
|
delta_y = canvas_size.height() - ( my_pos.y() + my_size.height() )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_RIGHT_EDGE:
|
|
|
|
delta_x = canvas_size.width() - ( my_pos.x() + my_size.width() )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_VERTICAL_CENTER:
|
|
|
|
delta_y = ( canvas_size.height() / 2 ) - ( my_pos.y() + ( my_size.height() / 2 ) )
|
|
|
|
elif pan_type == CAC.SIMPLE_PAN_HORIZONTAL_CENTER:
|
|
|
|
delta_x = ( canvas_size.width() / 2 ) - ( my_pos.x() + ( my_size.width() / 2 ) )
|
|
|
|
|
|
delta = QC.QPoint( delta_x, delta_y )
|
|
|
|
self._MoveDelta( delta )
|
|
|
|
|
|
def DoManualPan( self, delta_x_step, delta_y_step ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
my_size = self.size()
|
|
|
|
x_pan_distance = min( canvas_size.width(), my_size.width() ) // 12
|
|
y_pan_distance = min( canvas_size.height(), my_size.height() ) // 12
|
|
|
|
delta_x = delta_x_step * x_pan_distance
|
|
delta_y = delta_y_step * y_pan_distance
|
|
|
|
delta = QC.QPoint( delta_x, delta_y )
|
|
|
|
self._MoveDelta( delta )
|
|
|
|
|
|
def EventEmbedButton( self, event ):
|
|
|
|
self._embed_button.hide()
|
|
|
|
self._MakeMediaWindow()
|
|
|
|
self._SizeAndPositionChildren()
|
|
|
|
if self._media_window is not None:
|
|
|
|
self._media_window.show()
|
|
|
|
|
|
|
|
def GetCanvasZoom( self ) -> float:
|
|
|
|
return self._canvas_zoom
|
|
|
|
|
|
def GetCurrentZoom( self ) -> float:
|
|
|
|
return self._current_zoom
|
|
|
|
|
|
def GetIdealControlsBarRect( self, full_size = True ):
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
if full_size:
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
|
|
|
|
else:
|
|
|
|
animated_scanbar_height = HG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' )
|
|
|
|
if animated_scanbar_height is None:
|
|
|
|
animated_scanbar_height = 5
|
|
|
|
|
|
|
|
return QC.QRect(
|
|
QC.QPoint( 0, my_height - animated_scanbar_height ),
|
|
QC.QSize( my_width, animated_scanbar_height )
|
|
)
|
|
|
|
|
|
def GotoPreviousOrNextFrame( self, direction ):
|
|
|
|
if self._media is not None:
|
|
|
|
if ShouldHaveAnimationBar( self._media, self._show_action ):
|
|
|
|
if isinstance( self._media_window, Animation ):
|
|
|
|
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 )
|
|
|
|
elif isinstance( self._media_window, ClientGUIMPV.MPVWidget ):
|
|
|
|
self._media_window.GotoPreviousOrNextFrame( direction )
|
|
|
|
|
|
|
|
|
|
|
|
def IsAtMaxZoom( self ):
|
|
|
|
possible_zooms = HG.client_controller.new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
max_zoom_dimension = self._GetMaxZoomDimension()
|
|
|
|
return self._current_zoom == max_zoom or self.width() == max_zoom_dimension or self.height() == max_zoom_dimension
|
|
|
|
|
|
def IsPaused( self ):
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
return self._media_window.IsPaused()
|
|
|
|
|
|
return False
|
|
|
|
|
|
def IsZoomable( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return False
|
|
|
|
|
|
return self._show_action not in ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW )
|
|
|
|
|
|
def minimumSizeHint( self ) -> QC.QSize:
|
|
|
|
return self.sizeHint()
|
|
|
|
|
|
def MouseIsNearAnimationBar( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return False
|
|
|
|
|
|
if ShouldHaveAnimationBar( self._media, self._show_action ):
|
|
|
|
canvas_widget = self.parentWidget()
|
|
|
|
if not ClientGUIFunctions.MouseIsOverWidget( canvas_widget ):
|
|
|
|
return False
|
|
|
|
|
|
# there's some minor update stuff here now the scanbar can be hidden. its geometry may not update until later, so we need to map coordinates from widgets we know are in view instead!
|
|
|
|
container_mouse_pos = self.mapFromGlobal( QG.QCursor.pos() )
|
|
|
|
controls_bar_rect = self.GetIdealControlsBarRect()
|
|
|
|
buffer = 100
|
|
|
|
test_rect = controls_bar_rect.adjusted( -buffer // 2, -buffer, buffer // 2, buffer // 5 )
|
|
|
|
return test_rect.contains( container_mouse_pos )
|
|
|
|
|
|
return False
|
|
|
|
|
|
def MoveDelta( self, delta: QC.QPoint ):
|
|
|
|
self._MoveDelta( delta )
|
|
|
|
|
|
def Pause( self ):
|
|
|
|
if self._media is not None:
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
self._media_window.Pause()
|
|
|
|
|
|
|
|
|
|
def PausePlay( self ):
|
|
|
|
if self._media is not None:
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
self._media_window.PausePlay()
|
|
|
|
|
|
|
|
|
|
def ReadyToSlideshow( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
if not self._media_window.HasPlayedOnceThrough():
|
|
|
|
return False
|
|
|
|
|
|
|
|
if isinstance( self._media_window, StaticImage ):
|
|
|
|
if not self._media_window.IsRendered():
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def RescueIfOffScreen( self ):
|
|
|
|
my_ideal_size = self.sizeHint()
|
|
|
|
canvas_rect = self.parentWidget().rect()
|
|
ideal_media_rect = QC.QRect( self.pos(), my_ideal_size )
|
|
|
|
if not canvas_rect.intersects( ideal_media_rect ):
|
|
|
|
# up/down
|
|
|
|
height_buffer = min( ideal_media_rect.height(), self.height() // 5 )
|
|
|
|
if ideal_media_rect.bottom() < canvas_rect.top():
|
|
|
|
ideal_media_rect.moveBottom( canvas_rect.top() + height_buffer )
|
|
|
|
elif ideal_media_rect.top() > canvas_rect.bottom():
|
|
|
|
ideal_media_rect.moveTop( canvas_rect.bottom() - height_buffer )
|
|
|
|
|
|
# left/right
|
|
|
|
width_buffer = min( ideal_media_rect.width(), self.width() // 5 )
|
|
|
|
if ideal_media_rect.right() < canvas_rect.left():
|
|
|
|
ideal_media_rect.moveRight( canvas_rect.left() + width_buffer )
|
|
|
|
elif ideal_media_rect.left() > canvas_rect.right():
|
|
|
|
ideal_media_rect.moveLeft( canvas_rect.right() - width_buffer )
|
|
|
|
|
|
|
|
ideal_pos = ideal_media_rect.topLeft()
|
|
|
|
if ideal_pos != self.pos():
|
|
|
|
self.move( ideal_pos )
|
|
|
|
|
|
|
|
def ResetCenterPosition( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
|
|
ideal_size = self.sizeHint()
|
|
|
|
x = ( canvas_size.width() - ideal_size.width() ) // 2
|
|
y = ( canvas_size.height() - ideal_size.height() ) // 2
|
|
|
|
ideal_pos = QC.QPoint( x, y )
|
|
|
|
if ideal_pos != self.pos():
|
|
|
|
self.move( ideal_pos )
|
|
|
|
|
|
|
|
def resizeEvent( self, event ):
|
|
|
|
if self._media is not None:
|
|
|
|
self._SizeAndPositionChildren()
|
|
|
|
|
|
|
|
def SeekDelta( self, direction, duration_ms ):
|
|
|
|
if self._media is not None:
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
self._media_window.SeekDelta( direction, duration_ms )
|
|
|
|
|
|
|
|
|
|
def SetBackgroundColourGenerator( self, background_colour_generator ):
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
self._embed_button.SetBackgroundColourGenerator( self._background_colour_generator )
|
|
self._animation_window.SetBackgroundColourGenerator( self._background_colour_generator )
|
|
self._static_image_window.SetBackgroundColourGenerator( self._background_colour_generator )
|
|
|
|
|
|
def SetMedia( self, media: ClientMedia.MediaSingleton, maintain_zoom, maintain_pan ):
|
|
|
|
previous_media = self._media
|
|
|
|
self._media = media
|
|
|
|
( self._show_action, self._start_paused, self._start_with_embed ) = GetShowAction( self._media, self._canvas_type )
|
|
|
|
if self._show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
self._show_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
|
|
|
|
|
|
if self._start_with_embed:
|
|
|
|
self._animation_bar.ClearMedia()
|
|
|
|
self._controls_bar.hide()
|
|
|
|
self._DestroyOrHideThisMediaWindow( self._media_window )
|
|
|
|
self._media_window = None
|
|
|
|
self._embed_button.SetMedia( self._media )
|
|
|
|
self._embed_button.show()
|
|
|
|
else:
|
|
|
|
self._embed_button.hide()
|
|
|
|
self._MakeMediaWindow()
|
|
|
|
|
|
if maintain_zoom and previous_media is not None:
|
|
|
|
self.ZoomMaintain( previous_media )
|
|
|
|
else:
|
|
|
|
self.ZoomReinit()
|
|
|
|
|
|
if not maintain_pan:
|
|
|
|
self.ResetCenterPosition()
|
|
|
|
|
|
self._SizeAndPositionChildren()
|
|
|
|
if self._media_window is not None:
|
|
|
|
self._media_window.show()
|
|
|
|
|
|
HG.client_controller.gui.RegisterUIUpdateWindow( self )
|
|
|
|
self.show()
|
|
|
|
|
|
def ShouldHaveVolumeControl( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return False
|
|
|
|
|
|
return isinstance( self._media_window, ClientGUIMPV.MPVWidget ) and self._media.HasAudio()
|
|
|
|
|
|
def sizeHint(self) -> QC.QSize:
|
|
|
|
if self._media is None or self._show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
return QC.QSize( 0, 0 )
|
|
|
|
|
|
my_dpr = self.devicePixelRatio()
|
|
|
|
return CalculateMediaContainerSize( self._media, my_dpr, self._current_zoom, self._show_action )
|
|
|
|
|
|
def StopForSlideshow( self, value ):
|
|
|
|
if isinstance( self._media_window, ( Animation, ClientGUIMPV.MPVWidget ) ):
|
|
|
|
self._media_window.StopForSlideshow( value )
|
|
|
|
|
|
|
|
def ZoomIn( self, zoom_center_type_override = None ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = HG.client_controller.new_options.GetMediaZoomOptions( self._media.GetMime() )
|
|
|
|
possible_zooms = HG.client_controller.new_options.GetMediaZooms()
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
if exact_zoom <= self._current_zoom:
|
|
|
|
while exact_zoom <= self._current_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
else:
|
|
|
|
while exact_zoom / 2 > self._current_zoom:
|
|
|
|
exact_zoom /= 2
|
|
|
|
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
if exact_zoom > max_zoom:
|
|
|
|
return
|
|
|
|
|
|
possible_zooms = [ exact_zoom ]
|
|
|
|
|
|
possible_zooms.append( self._canvas_zoom )
|
|
|
|
bigger_zooms = [ zoom for zoom in possible_zooms if zoom > self._current_zoom ]
|
|
|
|
if len( bigger_zooms ) > 0:
|
|
|
|
new_zoom = min( bigger_zooms )
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
|
|
|
|
def ZoomMaintain( self, previous_media: ClientMedia.MediaSingleton ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
if previous_media is None:
|
|
|
|
self.ZoomReinit()
|
|
|
|
return
|
|
|
|
|
|
if previous_media.GetMime() not in HC.MIMES_WITH_THUMBNAILS or self._media.GetMime() not in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
return
|
|
|
|
|
|
# set up canvas zoom
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
|
|
my_dpr = self.devicePixelRatio()
|
|
|
|
previous_current_zoom = self._current_zoom
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = GetShowAction( previous_media, self._canvas_type )
|
|
|
|
if media_show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
media_show_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
|
|
|
|
|
|
( previous_default_zoom, previous_canvas_zoom ) = CalculateCanvasZooms( canvas_size, self._canvas_type, my_dpr, previous_media, media_show_action )
|
|
|
|
( media_show_action, media_start_paused, media_start_with_embed ) = GetShowAction( self._media, self._canvas_type )
|
|
|
|
if media_show_action in ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW ):
|
|
|
|
media_show_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
|
|
|
|
|
|
( gumpf_current_zoom, self._canvas_zoom ) = CalculateCanvasZooms( canvas_size, self._canvas_type, my_dpr, self._media, media_show_action )
|
|
|
|
# previously, we always matched width, but this causes a problem in dupe viewer when B has a little watermark on the bottom, spilling below bottom of screen
|
|
# I think in future we will have more options regarding all this, and this method will change significantly
|
|
# however for now we really just want a hardcoded ok solution for all situations, so let's just hook on default canvas zoom situation
|
|
|
|
( previous_width, previous_height ) = CalculateMediaSize( previous_media, self._current_zoom )
|
|
|
|
( previous_media_100_width, previous_media_100_height ) = previous_media.GetResolution()
|
|
( current_media_100_width, current_media_100_height ) = self._media.GetResolution()
|
|
|
|
width_locked_zoom = previous_width / current_media_100_width
|
|
height_locked_zoom = previous_height / current_media_100_height
|
|
|
|
width_locked_size = CalculateMediaContainerSize( self._media, my_dpr, width_locked_zoom, media_show_action )
|
|
height_locked_size = CalculateMediaContainerSize( self._media, my_dpr, height_locked_zoom, media_show_action )
|
|
|
|
# if landscape, go height, portrait, go width
|
|
if previous_media_100_width > previous_media_100_height and current_media_100_width > current_media_100_height:
|
|
|
|
lock_height = True
|
|
|
|
elif previous_media_100_width < previous_media_100_height and current_media_100_width < current_media_100_height:
|
|
|
|
lock_height = False
|
|
|
|
else:
|
|
|
|
# for weird stuff, we'll choose the smaller of the two ratios
|
|
|
|
width_difference = max( previous_media_100_width, current_media_100_width ) / min( previous_media_100_width, current_media_100_width )
|
|
height_difference = max( previous_media_100_height, current_media_100_height ) / min( previous_media_100_height, current_media_100_height )
|
|
|
|
lock_height = height_difference <= width_difference
|
|
|
|
|
|
# however we don't want to accidentally zoom in if the media we are switching to is larger. it'll spill over the bottom of the canvas
|
|
# therefore let's have a little safety check
|
|
|
|
if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.05:
|
|
|
|
# we were looking at the default zoom, near or at canvas edge(s), probably hadn't zoomed before switching comparison
|
|
# we want to make sure our comparison does not spill over the canvas edge
|
|
|
|
close_to_vertical_edge = canvas_size.height() * 0.95 <= self.height() <= canvas_size.height() * 1.05
|
|
vertical_spillover_could_be_deceptive = self.height() < width_locked_size.height() < self.height() * 1.1
|
|
|
|
# locking by width will spill over bottom of screen
|
|
if close_to_vertical_edge and vertical_spillover_could_be_deceptive:
|
|
|
|
lock_height = True
|
|
|
|
|
|
close_to_horizontal_edge = canvas_size.width() * 0.95 <= self.width() <= canvas_size.width() * 1.05
|
|
horizontal_spillover_could_be_deceptive = self.width() < height_locked_size.width() < self.width() * 1.1
|
|
|
|
# locking by height will spill over right of screen
|
|
if close_to_horizontal_edge and horizontal_spillover_could_be_deceptive:
|
|
|
|
lock_height = False
|
|
|
|
|
|
|
|
if lock_height:
|
|
|
|
current_zoom = height_locked_zoom
|
|
|
|
else:
|
|
|
|
current_zoom = width_locked_zoom
|
|
|
|
|
|
self._SetZoom( current_zoom )
|
|
|
|
|
|
def Zoom100( self ):
|
|
|
|
self._TryToChangeZoom( 1.0 )
|
|
|
|
|
|
def ZoomCanvas( self ):
|
|
|
|
self._TryToChangeZoom( self._canvas_zoom )
|
|
|
|
|
|
def ZoomDefault( self ):
|
|
|
|
self._TryToChangeZoom( self._default_zoom )
|
|
|
|
|
|
def ZoomMax( self ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
possible_zooms = HG.client_controller.new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = HG.client_controller.new_options.GetMediaZoomOptions( self._media.GetMime() )
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
while exact_zoom * 2 <= max_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
max_zoom = exact_zoom
|
|
|
|
|
|
new_zoom = max_zoom
|
|
|
|
if self._current_zoom != new_zoom:
|
|
|
|
self._TryToChangeZoom( new_zoom )
|
|
|
|
|
|
|
|
def ZoomOut( self, zoom_center_type_override = None ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = HG.client_controller.new_options.GetMediaZoomOptions( self._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 = HG.client_controller.new_options.GetMediaZooms()
|
|
|
|
|
|
possible_zooms.append( self._canvas_zoom )
|
|
|
|
smaller_zooms = [ zoom for zoom in possible_zooms if zoom < self._current_zoom ]
|
|
|
|
if len( smaller_zooms ) > 0:
|
|
|
|
new_zoom = max( smaller_zooms )
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
|
|
|
|
def ZoomReinit( self ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
canvas_size = self.parentWidget().size()
|
|
my_dpr = self.devicePixelRatio()
|
|
|
|
( self._default_zoom, self._canvas_zoom ) = CalculateCanvasZooms( canvas_size, self._canvas_type, my_dpr, self._media, self._show_action )
|
|
|
|
self._SetZoom( self._default_zoom )
|
|
|
|
|
|
def ZoomSwitch( self, zoom_center_type_override = None ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
if self._canvas_zoom == 1.0 and self._current_zoom == 1.0:
|
|
|
|
return
|
|
|
|
|
|
if self._current_zoom == 1.0:
|
|
|
|
new_zoom = self._canvas_zoom
|
|
|
|
else:
|
|
|
|
new_zoom = 1.0
|
|
|
|
|
|
self._TryToChangeZoom( new_zoom, zoom_center_type_override = zoom_center_type_override )
|
|
|
|
if new_zoom <= self._canvas_zoom:
|
|
|
|
self.ResetCenterPosition()
|
|
|
|
|
|
|
|
def ZoomSwitch100Max( self ):
|
|
|
|
self.ZoomSwitchMax( 1.0 )
|
|
|
|
|
|
def ZoomSwitchCanvasMax( self ):
|
|
|
|
self.ZoomSwitchMax( self._canvas_zoom )
|
|
|
|
|
|
def ZoomSwitchMax( self, switch_base: float ):
|
|
|
|
if not self.IsZoomable():
|
|
|
|
return
|
|
|
|
|
|
if self._current_zoom == switch_base:
|
|
|
|
possible_zooms = HG.client_controller.new_options.GetMediaZooms()
|
|
|
|
max_zoom = max( possible_zooms )
|
|
|
|
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = HG.client_controller.new_options.GetMediaZoomOptions( self._media.GetMime() )
|
|
|
|
if exact_zooms_only:
|
|
|
|
exact_zoom = 1.0
|
|
|
|
while exact_zoom * 2 <= max_zoom:
|
|
|
|
exact_zoom *= 2
|
|
|
|
|
|
max_zoom = exact_zoom
|
|
|
|
|
|
new_zoom = max_zoom
|
|
|
|
else:
|
|
|
|
new_zoom = switch_base
|
|
|
|
|
|
self._TryToChangeZoom( new_zoom )
|
|
|
|
if new_zoom == switch_base:
|
|
|
|
self.ResetCenterPosition()
|
|
|
|
|
|
|
|
def TIMERUIUpdate( self ):
|
|
|
|
is_near = False
|
|
show_small_instead_of_hiding = None
|
|
force_show = False
|
|
|
|
if not ShouldHaveAnimationBar( self._media, self._show_action ):
|
|
|
|
should_show_controls = False
|
|
|
|
else:
|
|
|
|
is_near = self.MouseIsNearAnimationBar()
|
|
show_small_instead_of_hiding = HG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) is not None
|
|
force_show = self._volume_control.PopupIsVisible() or self._animation_bar.DoingADrag() or HG.client_controller.new_options.GetBoolean( 'force_animation_scanbar_show' )
|
|
|
|
should_show_controls = is_near or show_small_instead_of_hiding or force_show
|
|
|
|
|
|
if should_show_controls:
|
|
|
|
should_show_full = is_near or force_show
|
|
|
|
if should_show_full != self._controls_bar_show_full:
|
|
|
|
self._controls_bar_show_full = should_show_full
|
|
|
|
self._animation_bar.SetShowText( self._controls_bar_show_full )
|
|
|
|
self._volume_control.setEnabled( self._controls_bar_show_full )
|
|
|
|
self._SizeAndPositionChildren()
|
|
|
|
|
|
if not self._controls_bar.isVisible():
|
|
|
|
self._animation_bar.SetMediaAndWindow( self._media, self._media_window )
|
|
|
|
self._controls_bar.show()
|
|
self._controls_bar.raise_()
|
|
|
|
|
|
should_show_volume = self.ShouldHaveVolumeControl()
|
|
|
|
if self._volume_control.isVisible() != should_show_volume:
|
|
|
|
self._volume_control.setVisible( should_show_volume )
|
|
|
|
self._controls_bar.layout()
|
|
|
|
|
|
else:
|
|
|
|
if self._controls_bar.isVisible():
|
|
|
|
# ok, repaint here forces a clear paint event NOW, before we hide.
|
|
# this ensures that when we show again, we won't have the nub in the wrong place for a frame before it repaints
|
|
# we'll have no nub, but this is less noticeable
|
|
|
|
self._animation_bar.ClearMedia()
|
|
|
|
self._animation_bar.repaint()
|
|
|
|
self._controls_bar.hide()
|
|
|
|
self._controls_bar.layout()
|
|
|
|
|
|
|
|
|
|
class EmbedButton( QW.QWidget ):
|
|
|
|
def __init__( self, parent, background_colour_generator ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
self._media = None
|
|
|
|
self._thumbnail_qt_pixmap = None
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.PointingHandCursor ) )
|
|
|
|
HG.client_controller.sub( self, 'update', 'notify_new_colourset' )
|
|
|
|
|
|
def _Redraw( self, painter ):
|
|
|
|
my_size = self.size()
|
|
|
|
my_width = my_size.width()
|
|
my_height = my_size.height()
|
|
|
|
center_x = my_width // 2
|
|
center_y = my_height // 2
|
|
radius = min( 50, center_x, center_y ) - 5
|
|
|
|
new_options = HG.client_controller.new_options
|
|
|
|
colour = self._background_colour_generator.GetColour()
|
|
|
|
painter.setBackground( QG.QBrush( colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
if self._thumbnail_qt_pixmap is not None:
|
|
|
|
scale = my_width / self._thumbnail_qt_pixmap.width()
|
|
|
|
painter.setTransform( QG.QTransform().scale( scale, scale ) )
|
|
|
|
painter.drawPixmap( 0, 0, self._thumbnail_qt_pixmap )
|
|
|
|
painter.setTransform( QG.QTransform().scale( 1.0, 1.0 ) )
|
|
|
|
|
|
painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.Button ) ) )
|
|
|
|
painter.drawEllipse( QC.QPointF( center_x, center_y ), radius, radius )
|
|
|
|
painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.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( QC.QPoint( center_x - third_triangle_width, center_y - half_triangle_side ) )
|
|
points.append( QC.QPoint( center_x + third_triangle_width * 2, center_y ) )
|
|
points.append( QC.QPoint( center_x - third_triangle_width, center_y + half_triangle_side ) )
|
|
|
|
painter.drawPolygon( QG.QPolygon( points ) )
|
|
|
|
#
|
|
|
|
painter.setPen( QG.QPen( QG.QPalette().color( QG.QPalette.Shadow ) ) )
|
|
|
|
painter.setBrush( QC.Qt.NoBrush )
|
|
|
|
painter.drawRect( 0, 0, my_width, my_height )
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self.SetMedia( None )
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
self._Redraw( painter )
|
|
|
|
|
|
def SetBackgroundColourGenerator( self, background_colour_generator ):
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
|
|
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:
|
|
|
|
thumbnail_path = HG.client_controller.client_files_manager.GetThumbnailPath( self._media )
|
|
|
|
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
|
|
|
|
self._thumbnail_qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, thumbnail_mime ).GetQtPixmap()
|
|
|
|
self.update()
|
|
|
|
else:
|
|
|
|
self._thumbnail_qt_pixmap = None
|
|
|
|
|
|
|
|
class OpenExternallyPanel( QW.QWidget ):
|
|
|
|
def __init__( self, parent, media ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._new_options = HG.client_controller.new_options
|
|
|
|
self._media = media
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
if self._media.GetLocationsManager().IsLocal() and self._media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
|
|
|
|
thumbnail_path = HG.client_controller.client_files_manager.GetThumbnailPath( self._media )
|
|
|
|
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
|
|
|
|
qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, thumbnail_mime ).GetQtPixmap()
|
|
|
|
thumbnail_dpr_percent = HG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' )
|
|
|
|
if thumbnail_dpr_percent != 100:
|
|
|
|
qt_pixmap.setDevicePixelRatio( thumbnail_dpr_percent / 100 )
|
|
|
|
|
|
thumbnail_window = ClientGUICommon.BufferedWindowIcon( self, qt_pixmap )
|
|
|
|
QP.AddToLayout( vbox, thumbnail_window, CC.FLAGS_CENTER )
|
|
|
|
|
|
m_text = HC.mime_string_lookup[ media.GetMime() ]
|
|
|
|
button = QW.QPushButton( 'open {} externally'.format( m_text ), self )
|
|
|
|
button.setFocusPolicy( QC.Qt.NoFocus )
|
|
|
|
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
self.setCursor( QG.QCursor( QC.Qt.PointingHandCursor ) )
|
|
|
|
button.clicked.connect( self.LaunchFile )
|
|
|
|
|
|
def mousePressEvent( self, event ):
|
|
|
|
if not ( event.modifiers() & ( QC.Qt.ShiftModifier | QC.Qt.ControlModifier | QC.Qt.AltModifier ) ) and event.button() == QC.Qt.LeftButton:
|
|
|
|
self.LaunchFile()
|
|
|
|
else:
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
def LaunchFile( self ):
|
|
|
|
hash = self._media.GetHash()
|
|
mime = self._media.GetMime()
|
|
|
|
client_files_manager = HG.client_controller.client_files_manager
|
|
|
|
path = client_files_manager.GetFilePath( hash, mime )
|
|
|
|
launch_path = self._new_options.GetMimeLaunch( mime )
|
|
|
|
HydrusPaths.LaunchFile( path, launch_path )
|
|
|
|
|
|
|
|
class QtMediaPlayer( QW.QWidget ):
|
|
|
|
launchMediaViewer = QC.Signal()
|
|
|
|
def __init__( self, parent: QW.QWidget, canvas_type, background_colour_generator ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
|
|
self._canvas_type = canvas_type
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
self._my_audio_output = QM.QAudioOutput( self )
|
|
self._my_video_output = QMW.QVideoWidget( self )
|
|
self._my_audio_placeholder = QW.QWidget( self )
|
|
|
|
QP.SetBackgroundColour( self._my_audio_placeholder, QG.QColor( 0, 0, 0 ) )
|
|
|
|
# perhaps this stops the always on top behaviour, says several places, but it doesn't for me!
|
|
#self._my_video_output.setAttribute( QC.Qt.WA_TranslucentBackground, False )
|
|
|
|
self._media_player = QM.QMediaPlayer( self )
|
|
|
|
self._media_player.mediaStatusChanged.connect( self._MediaStatusChanged )
|
|
|
|
self._we_are_initialised = True
|
|
|
|
vbox = QP.VBoxLayout( margin = 0 )
|
|
|
|
QP.AddToLayout( vbox, self._my_video_output, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._my_audio_placeholder, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
self.setLayout( vbox )
|
|
|
|
self._media = None
|
|
|
|
self._playthrough_count = 0
|
|
|
|
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
shortcut_set = 'media_viewer_media_window'
|
|
|
|
else:
|
|
|
|
shortcut_set = 'preview_media_window'
|
|
|
|
|
|
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ shortcut_set ], catch_mouse = True )
|
|
|
|
|
|
def _MediaStatusChanged( self, status ):
|
|
|
|
if status == QM.QMediaPlayer.MediaStatus.EndOfMedia:
|
|
|
|
self._playthrough_count += 1
|
|
|
|
self._media_player.setPosition( 0 )
|
|
|
|
self._media_player.play()
|
|
|
|
|
|
|
|
def GetAnimationBarStatus( self ):
|
|
|
|
buffer_indices = None
|
|
|
|
if self._media is None:
|
|
|
|
current_frame_index = 0
|
|
current_timestamp_ms = 0
|
|
paused = True
|
|
|
|
else:
|
|
|
|
current_timestamp_ms = self._media_player.position()
|
|
|
|
num_frames = self._media.GetNumFrames()
|
|
|
|
if num_frames is None or num_frames == 1:
|
|
|
|
current_frame_index = 0
|
|
|
|
else:
|
|
|
|
current_frame_index = int( round( ( current_timestamp_ms / self._media.GetDurationMS() ) * num_frames ) )
|
|
|
|
current_frame_index = min( current_frame_index, num_frames - 1 )
|
|
|
|
|
|
current_timestamp_ms = min( current_timestamp_ms, self._media.GetDurationMS() )
|
|
|
|
paused = self.IsPaused()
|
|
|
|
|
|
return ( current_frame_index, current_timestamp_ms, paused, buffer_indices )
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
if self._media is not None:
|
|
|
|
self._media = None
|
|
|
|
# ok in my experience setting media_player.setSource to anything after a first load is pretty buggy!
|
|
# it can just straight up hang forever. either to a null QUrl or another file
|
|
# it seems to be it doesn't like unloading some files
|
|
# so, let's spawn a new one every time
|
|
# EDIT: ok going from one vid to another can cause crashes, so we are moving to a system where each QtMediaPlayer just gets one use. we'll make a new one every time
|
|
|
|
self._media_player.stop()
|
|
|
|
#self._media_player.setParent( None )
|
|
|
|
#QP.CallAfter( self._media_player.deleteLater )
|
|
|
|
#self._media_player = QM.QMediaPlayer( self )
|
|
|
|
#self._media_player.mediaStatusChanged.connect( self._MediaStatusChanged )
|
|
|
|
|
|
|
|
def HasPlayedOnceThrough( self ):
|
|
|
|
return self._playthrough_count > 0
|
|
|
|
|
|
def IsPaused( self ):
|
|
|
|
return not self._media_player.isPlaying()
|
|
|
|
|
|
def Pause( self ):
|
|
|
|
self._media_player.pause()
|
|
|
|
|
|
def PausePlay( self ):
|
|
|
|
if self.IsPaused():
|
|
|
|
self.Play()
|
|
|
|
else:
|
|
|
|
self.Pause()
|
|
|
|
|
|
|
|
def Play( self ):
|
|
|
|
self._media_player.play()
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_PAUSE_MEDIA:
|
|
|
|
self.Pause()
|
|
|
|
elif action == CAC.SIMPLE_PAUSE_PLAY_MEDIA:
|
|
|
|
self.PausePlay()
|
|
|
|
elif action == CAC.SIMPLE_MEDIA_SEEK_DELTA:
|
|
|
|
( direction, duration_ms ) = command.GetSimpleData()
|
|
|
|
self.SeekDelta( direction, duration_ms )
|
|
|
|
elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
|
|
|
|
if self._media is not None:
|
|
|
|
self.Pause()
|
|
|
|
ClientGUIMedia.OpenExternally( self._media )
|
|
|
|
|
|
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
self.window().close()
|
|
|
|
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
self.launchMediaViewer.emit()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def Seek( self, position_ms ):
|
|
|
|
self._media_player.setPosition( position_ms )
|
|
|
|
|
|
def SeekDelta( self, direction, duration_ms ):
|
|
|
|
if self._media is None:
|
|
|
|
return
|
|
|
|
|
|
current_timestamp_ms = self._media_player.position()
|
|
|
|
new_timestamp_ms = max( 0, current_timestamp_ms + ( direction * duration_ms ) )
|
|
|
|
if new_timestamp_ms > self._media.GetDurationMS():
|
|
|
|
new_timestamp_ms = 0
|
|
|
|
|
|
self.Seek( new_timestamp_ms )
|
|
|
|
|
|
def SetBackgroundColourGenerator( self, background_colour_generator ):
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
|
|
def SetMedia( self, media: ClientMedia.MediaSingleton, start_paused = False ):
|
|
|
|
if media == self._media:
|
|
|
|
return
|
|
|
|
|
|
self.ClearMedia()
|
|
|
|
self._media = media
|
|
|
|
has_audio = self._media.HasAudio()
|
|
is_audio = self._media.GetMime() in HC.AUDIO
|
|
|
|
if has_audio:
|
|
|
|
self._media_player.setAudioOutput( self._my_audio_output )
|
|
|
|
|
|
if not is_audio:
|
|
|
|
self._media_player.setVideoOutput( self._my_video_output )
|
|
|
|
|
|
self._my_video_output.setVisible( not is_audio )
|
|
self._my_audio_placeholder.setVisible( is_audio )
|
|
|
|
path = HG.client_controller.client_files_manager.GetFilePath( self._media.GetHash(), self._media.GetMime() )
|
|
|
|
self._media_player.setSource( QC.QUrl.fromLocalFile( path ) )
|
|
|
|
if not start_paused:
|
|
|
|
self._media_player.play()
|
|
|
|
|
|
|
|
|
|
class StaticImage( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
|
|
|
|
launchMediaViewer = QC.Signal()
|
|
readyForNeighbourPrefetch = QC.Signal()
|
|
|
|
def __init__( self, parent, canvas_type, background_colour_generator ):
|
|
|
|
QW.QWidget.__init__( self, parent )
|
|
CAC.ApplicationCommandProcessorMixin.__init__( self )
|
|
|
|
self._canvas_type = canvas_type
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
if HC.PLATFORM_MACOS and not HG.macos_antiflicker_test:
|
|
|
|
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
|
|
|
|
|
|
# pass up un-button-pressed mouse moves to parent, which wants to do cursor show/hide
|
|
self.setMouseTracking( True )
|
|
|
|
self._media = None
|
|
self._i_know_if_media_has_transparency = False
|
|
self._media_has_transparency = False
|
|
|
|
self._image_renderer = None
|
|
|
|
self._last_device_pixel_ratio = self.devicePixelRatio()
|
|
|
|
self._tile_cache = HG.client_controller.GetCache( 'image_tiles' )
|
|
|
|
self._canvas_tiles = {}
|
|
|
|
self._is_rendered = False
|
|
|
|
self._raw_canvas_tile_size = QC.QSize( 768, 768 )
|
|
self._device_canvas_tile_size = self._raw_canvas_tile_size / self._last_device_pixel_ratio
|
|
|
|
self._zoom = 1.0
|
|
|
|
if self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
shortcut_set = 'media_viewer_media_window'
|
|
|
|
else:
|
|
|
|
shortcut_set = 'preview_media_window'
|
|
|
|
|
|
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ shortcut_set ], catch_mouse = True )
|
|
|
|
|
|
def _ClearCanvasTileCache( self ):
|
|
|
|
my_raw_size = self._GetRawPixelSize()
|
|
|
|
if self._media is None or self.width() == 0 or self.height() == 0:
|
|
|
|
self._zoom = 1.0
|
|
tile_dimension = 0
|
|
|
|
else:
|
|
|
|
( media_width, media_height ) = self._media.GetResolution()
|
|
|
|
self._zoom = my_raw_size.width() / media_width
|
|
|
|
# it is most convenient to have tiles that line up with the current zoom ratio
|
|
# 768 is a convenient size for meaty GPU blitting, but as a number it doesn't make for nice multiplication
|
|
|
|
# a 'nice' size is one that divides nicely by our zoom, so that integer translations between canvas and native res aren't losing too much in the float remainder
|
|
|
|
# the trick of going ( 123456 // 16 ) * 16 to give you a nice multiple of 16 does not work with floats like 1.4 lmao.
|
|
# what we can do instead is phrase 1.4 as 7/5 and use 7 as our int. any number cleanly divisible by 7 is cleanly divisible by 1.4
|
|
|
|
ideal_tile_dimension = HG.client_controller.new_options.GetInteger( 'ideal_tile_dimension' )
|
|
|
|
nice_number = HydrusData.GetNicelyDivisibleNumberForZoom( self._zoom / self.devicePixelRatio(), ideal_tile_dimension )
|
|
|
|
if nice_number == -1:
|
|
|
|
# we are in extreme zoom land. nice multiples are impossible with reasonable size tiles, so we'll have to settle for some problems
|
|
# a future solution is to get a bigger zoom and scale down
|
|
# a future solution is to just make overlapping screen covering tiles and never deal with seams lmao
|
|
|
|
tile_dimension = ideal_tile_dimension
|
|
|
|
else:
|
|
|
|
tile_dimension = ( ideal_tile_dimension // nice_number ) * nice_number
|
|
|
|
|
|
tile_dimension = max( min( tile_dimension, 2048 ), 1 )
|
|
|
|
if HG.canvas_tile_outline_mode:
|
|
|
|
HydrusData.ShowText( '{} from zoom {} and nice number {}'.format( tile_dimension, self._zoom, nice_number ) )
|
|
|
|
|
|
|
|
self._raw_canvas_tile_size = QC.QSize( tile_dimension, tile_dimension )
|
|
|
|
self._canvas_tiles = {}
|
|
|
|
self._last_device_pixel_ratio = self.devicePixelRatio()
|
|
|
|
self._device_canvas_tile_size = self._raw_canvas_tile_size / self._last_device_pixel_ratio
|
|
|
|
self._is_rendered = False
|
|
|
|
|
|
def _DrawBackground( self, painter, topLeftOffset = None ):
|
|
|
|
if not self._i_know_if_media_has_transparency:
|
|
|
|
if self._image_renderer is not None and self._image_renderer.IsReady():
|
|
|
|
self._media_has_transparency = self._image_renderer.HasTransparency()
|
|
self._i_know_if_media_has_transparency = True
|
|
|
|
|
|
|
|
if self._background_colour_generator.CanDoTransparencyCheckerboard() and self._i_know_if_media_has_transparency and self._media_has_transparency:
|
|
|
|
light_grey = QG.QColor( 237, 237, 237 )
|
|
dark_grey = QG.QColor( 222, 222, 222 )
|
|
|
|
painter.setBackground( QG.QBrush( light_grey ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
# 16x16 boxes, light grey in top right
|
|
BOX_LENGTH = int( 16 * self.devicePixelRatio() )
|
|
|
|
# there's a way to do this with viewports or transforms or something, but I don't know mate
|
|
if topLeftOffset is None:
|
|
|
|
rectTopLeftAdjust = QC.QPoint( 0, 0 )
|
|
|
|
else:
|
|
|
|
x = topLeftOffset.x() % ( BOX_LENGTH * 2 )
|
|
y = topLeftOffset.y() % ( BOX_LENGTH * 2 )
|
|
|
|
x_adjust = - x if x > 0 else 0
|
|
y_adjust = - y if y > 0 else 0
|
|
|
|
rectTopLeftAdjust = QC.QPoint( x_adjust, y_adjust )
|
|
|
|
|
|
painter_width = painter.viewport().width() + abs( rectTopLeftAdjust.x() )
|
|
painter_height = painter.viewport().height() + abs( rectTopLeftAdjust.y() )
|
|
|
|
num_cols = painter_width // BOX_LENGTH
|
|
|
|
if painter_width % BOX_LENGTH > 0:
|
|
|
|
num_cols += 1
|
|
|
|
|
|
num_rows = painter_height // BOX_LENGTH
|
|
|
|
if painter_height % BOX_LENGTH > 0:
|
|
|
|
num_rows += 1
|
|
|
|
|
|
painter.setBrush( QG.QBrush( dark_grey ) )
|
|
painter.setPen( QG.QPen( QC.Qt.NoPen ) )
|
|
|
|
for y_index in range( num_rows ):
|
|
|
|
for x_index in range( num_cols ):
|
|
|
|
if ( x_index + y_index ) % 2 == 1:
|
|
|
|
rect = QC.QRect( x_index * BOX_LENGTH, y_index * BOX_LENGTH, BOX_LENGTH, BOX_LENGTH )
|
|
|
|
rect.moveTo( rect.topLeft() + rectTopLeftAdjust )
|
|
|
|
if painter.viewport().intersects( rect ):
|
|
|
|
painter.drawRect( rect )
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
colour = self._background_colour_generator.GetColour()
|
|
|
|
painter.setBackground( QG.QBrush( colour ) )
|
|
|
|
painter.eraseRect( painter.viewport() )
|
|
|
|
|
|
def _DrawTile( self, tile_coordinate ):
|
|
|
|
( native_clip_rect, raw_canvas_clip_rect ) = self._GetRawClipRectsFromTileCoordinates( tile_coordinate )
|
|
|
|
raw_width = raw_canvas_clip_rect.width()
|
|
raw_height = raw_canvas_clip_rect.height()
|
|
|
|
tile_pixmap = HG.client_controller.bitmap_manager.GetQtPixmap( raw_width, raw_height )
|
|
|
|
painter = QG.QPainter( tile_pixmap )
|
|
|
|
self._DrawBackground( painter, topLeftOffset = raw_canvas_clip_rect.topLeft() )
|
|
|
|
tile = self._tile_cache.GetTile( self._image_renderer, self._media, native_clip_rect, raw_canvas_clip_rect.size() )
|
|
|
|
painter.drawPixmap( 0, 0, tile.qt_pixmap )
|
|
|
|
if HG.canvas_tile_outline_mode:
|
|
|
|
painter.setPen( QG.QPen( QG.QColor( 0, 127, 255 ) ) )
|
|
painter.setBrush( QC.Qt.NoBrush )
|
|
|
|
painter.drawRect( tile_pixmap.rect() )
|
|
|
|
|
|
self._canvas_tiles[ tile_coordinate ] = ( tile_pixmap, raw_canvas_clip_rect.topLeft() )
|
|
|
|
|
|
def _GetRawClipRectsFromTileCoordinates( self, tile_coordinate ) -> typing.Tuple[ QC.QRect, QC.QRect ]:
|
|
|
|
( tile_x, tile_y ) = tile_coordinate
|
|
|
|
my_raw_size = self._GetRawPixelSize()
|
|
|
|
my_raw_width = my_raw_size.width()
|
|
my_raw_height = my_raw_size.height()
|
|
|
|
( normal_raw_canvas_width, normal_raw_canvas_height ) = ( self._raw_canvas_tile_size.width(), self._raw_canvas_tile_size.height() )
|
|
|
|
( media_width, media_height ) = self._media.GetResolution()
|
|
|
|
raw_canvas_x = tile_x * self._raw_canvas_tile_size.width()
|
|
raw_canvas_y = tile_y * self._raw_canvas_tile_size.height()
|
|
|
|
raw_canvas_topLeft = QC.QPoint( raw_canvas_x, raw_canvas_y )
|
|
|
|
raw_canvas_width = normal_raw_canvas_width
|
|
|
|
if raw_canvas_x + normal_raw_canvas_width > my_raw_width:
|
|
|
|
# this is the rightmost tile and should be shrunk
|
|
|
|
raw_canvas_width = my_raw_width % normal_raw_canvas_width
|
|
|
|
|
|
raw_canvas_height = normal_raw_canvas_height
|
|
|
|
if raw_canvas_y + normal_raw_canvas_height > my_raw_height:
|
|
|
|
# this is the bottommost tile and should be shrunk
|
|
|
|
raw_canvas_height = my_raw_height % normal_raw_canvas_height
|
|
|
|
|
|
raw_canvas_width = max( 1, raw_canvas_width )
|
|
raw_canvas_height = max( 1, raw_canvas_height )
|
|
|
|
# if we are the last row/column our size is not this!
|
|
|
|
raw_canvas_size = QC.QSize( raw_canvas_width, raw_canvas_height )
|
|
|
|
raw_canvas_clip_rect = QC.QRect( raw_canvas_topLeft, raw_canvas_size )
|
|
|
|
native_clip_rect = QC.QRect( raw_canvas_topLeft / self._zoom, raw_canvas_size / self._zoom )
|
|
|
|
# dealing with rounding errors with zoom calc
|
|
if native_clip_rect.width() + native_clip_rect.x() > media_width:
|
|
|
|
native_clip_rect.setWidth( media_width - native_clip_rect.x() )
|
|
|
|
|
|
if native_clip_rect.height() + native_clip_rect.y() > media_height:
|
|
|
|
native_clip_rect.setHeight( media_height - native_clip_rect.y() )
|
|
|
|
|
|
if native_clip_rect.width() == 0:
|
|
|
|
native_clip_rect.setX( max( native_clip_rect.x() - 1, 0 ) )
|
|
native_clip_rect.setWidth( 1 )
|
|
|
|
|
|
if native_clip_rect.height() == 0:
|
|
|
|
native_clip_rect.setY( max( native_clip_rect.y() - 1, 0 ) )
|
|
native_clip_rect.setHeight( 1 )
|
|
|
|
|
|
return ( native_clip_rect, raw_canvas_clip_rect )
|
|
|
|
|
|
def _GetRawPixelSize( self ) -> QC.QSize:
|
|
|
|
return self.size() * self.devicePixelRatio()
|
|
|
|
|
|
def _GetTileCoordinateFromPoint( self, device_pos: QC.QPoint ):
|
|
|
|
raw_pos = device_pos * self.devicePixelRatio()
|
|
|
|
tile_x = raw_pos.x() // self._raw_canvas_tile_size.width()
|
|
tile_y = raw_pos.y() // self._raw_canvas_tile_size.height()
|
|
|
|
return ( tile_x, tile_y )
|
|
|
|
|
|
def _GetTileCoordinatesInView( self, device_rect: QC.QRect ):
|
|
|
|
if self.width() == 0 or self.height() == 0 or self._raw_canvas_tile_size.width() == 0 or self._raw_canvas_tile_size.height() == 0:
|
|
|
|
return []
|
|
|
|
|
|
topLeft_tile_coordinate = self._GetTileCoordinateFromPoint( device_rect.topLeft() )
|
|
bottomRight_tile_coordinate = self._GetTileCoordinateFromPoint( device_rect.bottomRight() )
|
|
|
|
i = itertools.product(
|
|
range( topLeft_tile_coordinate[0], bottomRight_tile_coordinate[0] + 1 ),
|
|
range( topLeft_tile_coordinate[1], bottomRight_tile_coordinate[1] + 1 )
|
|
)
|
|
|
|
return list( i )
|
|
|
|
|
|
def ClearMedia( self ):
|
|
|
|
self._media = None
|
|
self._image_renderer = None
|
|
|
|
self._ClearCanvasTileCache()
|
|
|
|
self.update()
|
|
|
|
|
|
def paintEvent( self, event ):
|
|
|
|
if self.devicePixelRatio() != self._last_device_pixel_ratio:
|
|
|
|
self._ClearCanvasTileCache()
|
|
|
|
|
|
painter = QG.QPainter( self )
|
|
|
|
if self._image_renderer is None or not self._image_renderer.IsReady():
|
|
|
|
self._DrawBackground( painter )
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
dirty_tile_coordinates = self._GetTileCoordinatesInView( event.rect() )
|
|
|
|
for dirty_tile_coordinate in dirty_tile_coordinates:
|
|
|
|
if dirty_tile_coordinate not in self._canvas_tiles:
|
|
|
|
self._DrawTile( dirty_tile_coordinate )
|
|
|
|
|
|
|
|
my_dpr = self.devicePixelRatio()
|
|
|
|
visible_bounding_rect = self.visibleRegion().boundingRect()
|
|
visible_bounding_rect_topLeft = visible_bounding_rect.topLeft()
|
|
|
|
for dirty_tile_coordinate in dirty_tile_coordinates:
|
|
|
|
( tile, raw_pos ) = self._canvas_tiles[ dirty_tile_coordinate ]
|
|
|
|
raw_pos_f = QC.QPointF( raw_pos )
|
|
|
|
device_pos_f = raw_pos_f / my_dpr
|
|
|
|
tile.setDevicePixelRatio( my_dpr )
|
|
|
|
adjust_pos_f = QC.QPointF()
|
|
|
|
if my_dpr % 1 != 0.0:
|
|
|
|
#
|
|
# ,ad8888ba, 88888888ba 88 88
|
|
# d8"' `"8b ,d 88 "8b "" 88
|
|
# d8' `8b 88 88 ,8P 88
|
|
# 88 88 MM88MMM 88aaaaaa8P' 88 8b, ,d8 ,adPPYba, 88 ,adPPYba,
|
|
# 88 88 88 88""""""' 88 `Y8, ,8P' a8P_____88 88 I8[ ""
|
|
# Y8, "88,,8P 88 88 88 )888( 8PP""""""" 88 `"Y8ba,
|
|
# Y8a. Y88P 88, 88 88 ,d8" "8b, "8b, ,aa 88 aa ]8I
|
|
# `"Y8888Y"Y8a "Y888 88 88 8P' `Y8 `"Ybbd8"' 88 `"YbbdP"'
|
|
#
|
|
|
|
# Ok, what is going on here is that if you have an ugly UI scale, like 125%, when a bitmap has to be drawn starting offscreen, Qt rounds the starting coordinate down every n pixels of offset
|
|
# something gets cut off somewhere, I guess every 5 pixels for 125%, 3 for 150%, and you get a blank/white line on the first set of tiles to be stitched as you drag around, flickering every n pixels
|
|
# therefore, we engage in some sordid hexxing here: if the current tile starts offscreen, we move it on a real pixel
|
|
# I haven't seen massive warping here, but maybe it stands out on cleaner vectors on low res displays. if so, I'll revisit and try to determine the actual nth pixel to activate this
|
|
|
|
if device_pos_f.x() < visible_bounding_rect_topLeft.x():
|
|
|
|
adjust_pos_f.setX( 1 / my_dpr )
|
|
|
|
|
|
if device_pos_f.y() < visible_bounding_rect_topLeft.y():
|
|
|
|
adjust_pos_f.setY( 1 / my_dpr )
|
|
|
|
|
|
|
|
painter.drawPixmap( device_pos_f + adjust_pos_f, tile )
|
|
|
|
tile.setDevicePixelRatio( 1.0 )
|
|
|
|
|
|
all_visible_tile_coordinates = self._GetTileCoordinatesInView( visible_bounding_rect )
|
|
|
|
deletee_tile_coordinates = set( self._canvas_tiles.keys() ).difference( all_visible_tile_coordinates )
|
|
|
|
for deletee_tile_coordinate in deletee_tile_coordinates:
|
|
|
|
del self._canvas_tiles[ deletee_tile_coordinate ]
|
|
|
|
|
|
if not self._is_rendered:
|
|
|
|
self.readyForNeighbourPrefetch.emit()
|
|
|
|
self._is_rendered = True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
HydrusData.PrintException( e, do_wait = False )
|
|
|
|
return
|
|
|
|
|
|
|
|
def resizeEvent( self, event ):
|
|
|
|
self._ClearCanvasTileCache()
|
|
|
|
|
|
def showEvent( self, event ):
|
|
|
|
self._ClearCanvasTileCache()
|
|
|
|
|
|
def IsRendered( self ):
|
|
|
|
return self._is_rendered
|
|
|
|
|
|
def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
|
|
|
|
command_processed = True
|
|
|
|
if command.IsSimpleCommand():
|
|
|
|
action = command.GetSimpleAction()
|
|
|
|
if action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
|
|
|
|
if self._media is not None:
|
|
|
|
ClientGUIMedia.OpenExternally( self._media )
|
|
|
|
|
|
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
|
|
|
|
self.window().close()
|
|
|
|
elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER and self._canvas_type == CC.CANVAS_PREVIEW:
|
|
|
|
self.launchMediaViewer.emit()
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
else:
|
|
|
|
command_processed = False
|
|
|
|
|
|
return command_processed
|
|
|
|
|
|
def SetBackgroundColourGenerator( self, background_colour_generator ):
|
|
|
|
self._background_colour_generator = background_colour_generator
|
|
|
|
|
|
def SetMedia( self, media ):
|
|
|
|
if media == self._media:
|
|
|
|
return
|
|
|
|
|
|
self._ClearCanvasTileCache()
|
|
|
|
self._media = media
|
|
self._i_know_if_media_has_transparency = False
|
|
self._media_has_transparency = False
|
|
|
|
image_cache = HG.client_controller.GetCache( 'images' )
|
|
|
|
self._image_renderer = image_cache.GetImageRenderer( self._media )
|
|
|
|
if not self._image_renderer.IsReady():
|
|
|
|
HG.client_controller.gui.RegisterAnimationUpdateWindow( self )
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
def TIMERAnimationUpdate( self ):
|
|
|
|
try:
|
|
|
|
if self._image_renderer is None or self._image_renderer.IsReady():
|
|
|
|
self.update()
|
|
|
|
HG.client_controller.gui.UnregisterAnimationUpdateWindow( self )
|
|
|
|
|
|
except:
|
|
|
|
HG.client_controller.gui.UnregisterAnimationUpdateWindow( self )
|
|
|
|
raise
|
|
|
|
|
|
|