Version 381

This commit is contained in:
Hydrus Network Developer 2020-01-22 15:04:43 -06:00
parent 70671ee703
commit 3d3c2533ba
42 changed files with 1127 additions and 336 deletions

View File

@ -97,6 +97,8 @@ try:
HydrusPaths.SetEnvTempDir( result.temp_dir )
HydrusPaths.AddBaseDirToEnvPath()
from include import HydrusPy2To3
HydrusPy2To3.do_2to3_test()

View File

@ -97,6 +97,8 @@ try:
HydrusPaths.SetEnvTempDir( result.temp_dir )
HydrusPaths.AddBaseDirToEnvPath()
from include import HydrusPy2To3
HydrusPy2To3.do_2to3_test()

View File

@ -8,6 +8,52 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 381</h3></li>
<ul>
<li>mpv:</li>
<li>mpv is now available and the default for all windows users</li>
<li>I believed I have eliminated the final reported mpv crash</li>
<li>mpv load and unload delays are greatly reduced. initial load still takes about half a second, but subsequent loads are now as quick as native renderers</li>
<li>mpv seems to work well for gif and apng</li>
<li>added a very simple global volume slider and audio mute checkbox to the media viewer top hover window. this was a quick patch--much better controls and shortcuts will come in future</li>
<li>mpv windows now properly re-show the cursor on mouse movement</li>
<li>unified mpv mouse press/release handling with native animation--click down now does pause/play and starts a drag event</li>
<li>unfortunately, in some cases embedding mpv requires overriding local OS number rendering (e.g. 1,234 vs 1.234). hydrus number rendering is now coerced to the english style with commas until we can figure out a better solution--sorry!</li>
<li>cleared up an issue where simple clicks on page tabs would trigger micro-page drags that were immediately cancelled. this situation was exacerbated when the page being left had an active mpv window. the flicker of page drag cursor is now gone, and some weird situations where static clicks during busy time could move a tab should be fixed</li>
<li>eliminated the recent issue in the media viewer where transitioning from one media type to another through navigation, particularly mpv->other, would flicker a single frame of the last 'other' media shown(!)</li>
<li>fixed a bug where repeated mpv views in the preview viewer could disable client file drag and drop</li>
<li>the bug where thumbnails may not waterfall in unless the mouse is moving after some mpv videos are loaded for a page is relieved but not completely fixed</li>
<li>if the preview window is collapsed and hidden, media will no longer ever load into it</li>
<li>fixed an edge-case bug where the mpv window would not like being told to show nothing when it was already showing nothing</li>
<li>wrapped mpv load errors in a basic graceful catch</li>
<li>fixed an issue some users had with loading mpv's dll</li>
<li>.</li>
<li>file types:</li>
<li>a new file metatype, 'animation', is added, for gif and apng. these are no longer considered 'image' for a variety of purposes</li>
<li>the filetype selection panel, which is used in system:filetype and import folder UI, has had an overhaul--it now has tristate 'mime group' checkboxes to represent a half-filled group and expand/collapse buttons to hide the tall filetype lists. individual filetype lists will start hidden unless their default value is a partially filled group</li>
<li>the media view options have a similar overhaul: they are now collapsed to general filetypes by default. you set view and zoom options for the generalised 'video' type under options->media, and if you want to set specific options for webm or anything else, you can add/delete those types to override the general default</li>
<li>the new default options for a fresh client are just for these general types. if mpv is available, video, animations, and audio now start with mpv as the default viewer. video and animation zoom is now flexible (not fixed to 50%, 100%, 200%) and will fill the media canvas</li>
<li>all media view options will be reset to this simple default on update! if you have specific zoom or display preferences, please reset them after the update--but you might like to play with mpv a bit first, as it renders at large and smooth zooms very well</li>
<li>.</li>
<li>the rest:</li>
<li>the new thumbnail right-click file selection routine will now only focus and scroll to the first member of the selection if no other members of the new selection are already in view</li>
<li>fixed some caching code and sped up the new select/remove menu count generation (which can lag for very large pages) by two to six times</li>
<li>sped up file filter counting code by about ten percent</li>
<li>fixed weird layout on: migrate database panel, duplicates page (left and right), edit shortcuts, edit import folder, and the filename tagging panel</li>
<li>fixed an issue where the media viewer's hover windows might flicker into view for one frame when the mouse moved over the center of the media viewer for the first time</li>
<li>fixed a media viewer shutdown issue that would sometimes lead to the first file in the list being opened in the shutting-down viewer for an instant or highlighted as the new thumb focus</li>
<li>the file maintenance system that queues up missing/broken files' urls for redownload will no longer re-select the download page on every new url</li>
<li>fixed an issue where a downloader's tag blacklist was not being applied on the child files of certain kinds of multiple-file post (such as with pixiv)</li>
<li>deleting a very long tag should no longer create a very wide confirmation dialog in the manage tags dialog</li>
<li>fixed some 'the panel grew a bit, but the parent window didn't grow quite enough and now it has scrollbars for two pixels of extra content' sizing issues</li>
<li>fixed some dialog sizing calculations when the parent window was borderless fullscreen</li>
<li>maybe fixed a rare event processing bug</li>
<li>improved quality of some misc data comparison code across the program</li>
<li>did some significant backend event/pubsub code cleanup, mostly related to getting mpv working a bit cleaner</li>
<li>improved thumbnail rendering time</li>
<li>improved smoothness of thumbnail fade animations (at least for when they are working right, ha ha!)</li>
<li>misc fixes</li>
</ul>
<li><h3>version 380</h3></li>
<ul>
<li>basic mpv support is added. it comes with the windows build this week, and is a prototype meant for initial testing. the library is optional. users who run from source will want 'python-mpv' added via pip and libmpv available on their PATH, more details in running_from_source help</li>

View File

@ -955,6 +955,11 @@ class ThumbnailCache( object ):
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 3
for mime in HC.ANIMATIONS:
self._magic_mime_thumbnail_ease_score_lookup[ mime ] = 3
def _RecalcQueues( self ):
@ -1217,7 +1222,10 @@ class ThumbnailCache( object ):
page_keys_to_rendered_medias = collections.defaultdict( list )
while not HydrusData.TimeHasPassedPrecise( stop_time ):
num_done = 0
max_at_once = 16
while not HydrusData.TimeHasPassedPrecise( stop_time ) and num_done <= max_at_once:
with self._lock:
@ -1245,6 +1253,8 @@ class ThumbnailCache( object ):
page_keys_to_rendered_medias[ page_key ].append( media )
num_done += 1
if len( page_keys_to_rendered_medias ) > 0:

View File

@ -190,9 +190,15 @@ no_support = ( unsupported_media_actions, False, False )
media_viewer_capabilities = {}
media_viewer_capabilities[ HC.GENERAL_ANIMATION ] = animated_full_support
media_viewer_capabilities[ HC.GENERAL_IMAGE ] = static_full_support
media_viewer_capabilities[ HC.GENERAL_VIDEO ] = animated_full_support
media_viewer_capabilities[ HC.GENERAL_AUDIO ] = audio_full_support
media_viewer_capabilities[ HC.GENERAL_APPLICATION ] = no_support
for mime in HC.SEARCHABLE_MIMES:
if mime in HC.IMAGES_THAT_CAN_HAVE_ANIMATION:
if mime in HC.ANIMATIONS:
media_viewer_capabilities[ mime ] = animated_full_support

View File

@ -56,6 +56,42 @@ if not HG.twisted_is_broke:
from twisted.internet import threads, reactor, defer
class PubSubEvent( QC.QEvent ):
def __init__( self, pubsub ):
QC.QEvent.__init__( self, QC.QEvent.User )
self._pubsub = pubsub
def Execute( self ):
if self._pubsub.WorkToDo():
self._pubsub.Process()
class PubSubEventFilter( QC.QObject ):
def __init__( self, parent = None ):
QC.QObject.__init__( self, parent )
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.User and isinstance( event, PubSubEvent ):
event.Execute()
return True
return False
class App( QW.QApplication ):
def __init__( self, *args, **kwargs ):
@ -74,6 +110,10 @@ class App( QW.QApplication ):
self.call_after_catcher.installEventFilter( QP.CallAfterEventFilter( self.call_after_catcher ) )
self.pubsub_catcher = QC.QObject( self )
self.pubsub_catcher.installEventFilter( PubSubEventFilter( self.pubsub_catcher ) )
self.aboutToQuit.connect( self.EventEndSession )
@ -281,7 +321,7 @@ class Controller( HydrusController.HydrusController ):
raise HydrusExceptions.ShutdownException( 'Application is shutting down!' )
time.sleep( 0.05 )
time.sleep( 0.02 )
if job_key.HasVariable( 'result' ):
@ -1115,6 +1155,16 @@ class Controller( HydrusController.HydrusController ):
def do_gui_refs( gui ):
if self.gui is not None and QP.isValid( self.gui ):
self.gui.MaintainCanvasFrameReferences()
QP.CallAfter( do_gui_refs, self.gui )
def MenubarMenuIsOpen( self ):
@ -1170,6 +1220,9 @@ class Controller( HydrusController.HydrusController ):
self.CallBlockingToQt( self.app, self._pubsub.Process )
# this needs to be blocking in some way or the pubsub daemon goes nuts
#QW.QApplication.instance().postEvent( QW.QApplication.instance().pubsub_catcher, PubSubEvent( self._pubsub ) )
def RefreshServices( self ):

View File

@ -13428,6 +13428,28 @@ class DB( HydrusDB.HydrusDB ):
if version == 380:
try:
new_options = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_CLIENT_OPTIONS )
default_view_options = new_options.GetDefaultMediaViewOptions()
new_options.SetMediaViewOptions( default_view_options )
self._SetJSONDump( new_options )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update the media view options failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -631,7 +631,15 @@ class Credentials( HydrusData.HydrusYAMLBase ):
self._access_key = access_key
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __eq__( self, other ):
if isinstance( other, Credentials ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ): return ( self._host, self._port, self._access_key ).__hash__()

View File

@ -269,7 +269,12 @@ class GalleryIdentifier( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, GalleryIdentifier ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):

View File

@ -390,6 +390,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._canvas_frames = [] # Keep references to canvas frames so they won't get garbage collected (canvas frames don't have a parent)
self._persistent_mpv_widgets = []
self._closed_pages = []
self._lock = threading.Lock()
@ -473,6 +475,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._ui_update_windows = set()
self._animation_update_timer = QC.QTimer( self )
self._animation_update_timer.setTimerType( QC.Qt.PreciseTimer )
self._animation_update_timer.timeout.connect( self.TIMEREventAnimationUpdate )
self._animation_update_windows = set()
@ -4906,6 +4909,25 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
return self._notebook.GetSessionAPIInfoDict( is_selected = True )
def GetMPVWidget( self, parent ):
if len( self._persistent_mpv_widgets ) == 0:
mpv_widget = ClientGUIMPV.mpvWidget( parent )
self._persistent_mpv_widgets.append( mpv_widget )
mpv_widget = self._persistent_mpv_widgets.pop()
if mpv_widget.parent() is self:
mpv_widget.setParent( parent )
return mpv_widget
def GetPageAPIInfoDict( self, page_key, simple ):
page = self._notebook.GetPageFromPageKey( page_key )
@ -4995,7 +5017,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
try:
self._ImportURL( url, destination_page_name = destination_page_name )
self._ImportURL( url, destination_page_name = destination_page_name, show_destination_page = False )
except Exception as e:
@ -5148,7 +5170,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if tlw.isMinimized() and not self._notebook.HasMediaPageName( page_name ):
self._controller.CallLaterQtSafe(self, 10.0, self.PresentImportedFilesToPage, hashes, page_name)
self._controller.CallLaterQtSafe( self, 10.0, self.PresentImportedFilesToPage, hashes, page_name )
return
@ -5414,6 +5436,13 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def ReleaseMPVWidget( self, mpv_widget ):
mpv_widget.setParent( self )
self._persistent_mpv_widgets.append( mpv_widget )
def REPEATINGBandwidth( self ):
if not QP.isValid( self ) or self.isMinimized():

View File

@ -240,7 +240,7 @@ def ShouldHaveAnimationBar( media, show_action ):
return True
is_animated_image = media.GetMime() in HC.IMAGES_THAT_CAN_HAVE_ANIMATION and media.HasDuration()
is_animated_image = media.GetMime() in HC.ANIMATIONS and media.HasDuration()
is_video = media.GetMime() in HC.VIDEO
@ -1187,7 +1187,7 @@ class Canvas( QW.QWidget ):
if self._current_media is not None:
if self._current_media.GetMime() in HC.IMAGES and self._current_media.GetDuration() is None:
if self._current_media.GetMime() in HC.IMAGES:
HG.client_controller.pub( 'clipboard', 'bmp', self._current_media )
@ -2357,6 +2357,11 @@ class Canvas( QW.QWidget ):
def SetMedia( self, media ):
if not self.isVisible():
return
if media is not None:
media = media.GetDisplayMedia()
@ -2586,7 +2591,7 @@ class CanvasPanel( Canvas ):
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
if self._current_media.GetMime() in HC.IMAGES and self._current_media.GetDuration() is None:
if self._current_media.GetMime() in HC.IMAGES:
ClientGUIMenus.AppendMenuItem( copy_menu, 'image (bitmap)', 'Copy this file to your clipboard as a bmp.', self._CopyBMPToClipboard )
@ -4211,6 +4216,13 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
def ProcessContentUpdates( self, service_keys_to_content_updates ):
if self._current_media is None:
# probably a file view stats update as we close down--ignore it
return
if self.HasMedia( self._current_media ):
next_media = self._GetNext( self._current_media )
@ -5011,8 +5023,10 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
if self._current_media.GetMime() in HC.IMAGES and self._current_media.GetDuration() is None:
if self._current_media.GetMime() in HC.IMAGES:
ClientGUIMenus.AppendMenuItem( copy_menu, 'image (bitmap)', 'Copy this file to your clipboard as a BMP image.', self._CopyBMPToClipboard )
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy this file\'s path to your clipboard.', self._CopyPathToClipboard )
@ -5126,8 +5140,11 @@ class MediaContainer( QW.QWidget ):
# If I do not set this, macOS goes 100% CPU endless repaint events!
# My guess is it due to the borked layout
# it means 'I guarantee to cover my whole viewport with pixels, no need for automatic background clear'
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
self.setSizePolicy( QW.QSizePolicy.Fixed, QW.QSizePolicy.Fixed )
self._media = None
self._show_action = CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE
self._start_paused = False
@ -5161,6 +5178,7 @@ class MediaContainer( QW.QWidget ):
if isinstance( media_window, ( Animation, StaticImage ) ):
media_window.SetNoneMedia()
media_window.hide()
elif isinstance( media_window, ClientGUIMPV.mpvWidget ):
@ -5169,11 +5187,9 @@ class MediaContainer( QW.QWidget ):
media_window.SetNoneMedia()
time.sleep( 0.1 ) # anti crash wew
media_window.hide()
media_window.deleteLater()
time.sleep( 0.1 ) # anti crash wew
HG.client_controller.gui.ReleaseMPVWidget( media_window )
else:
@ -5219,8 +5235,6 @@ class MediaContainer( QW.QWidget ):
else:
self._static_image_window.show()
self._media_window = self._static_image_window
@ -5234,8 +5248,6 @@ class MediaContainer( QW.QWidget ):
else:
self._animation_window.show()
self._media_window = self._animation_window
@ -5252,23 +5264,21 @@ class MediaContainer( QW.QWidget ):
if self._mpv_window is None:
self._mpv_window = ClientGUIMPV.mpvWidget( self )
self._mpv_window = HG.client_controller.gui.GetMPVWidget( self )
self._mpv_window.show()
self._media_window = self._mpv_window
self._mpv_window.SetMedia( self._media, start_paused = self._start_paused )
self._media_window.SetMedia( self._media, start_paused = self._start_paused )
if ShouldHaveAnimationBar( self._media, self._show_action ):
self._animation_bar.show()
self._animation_bar.SetMediaAndWindow( self._media, self._media_window )
self._animation_bar.show()
else:
self._HideAnimationBar()
@ -5278,6 +5288,11 @@ class MediaContainer( QW.QWidget ):
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()
self._media_window.show()
def _SizeAndPositionChildren( self ):
@ -5288,8 +5303,8 @@ class MediaContainer( QW.QWidget ):
if self._media_window is None:
self._embed_button.setFixedSize( QP.TupleToQSize( (my_width,my_height) ) )
self._embed_button.move( QP.TupleToQPoint( (0,0) ) )
self._embed_button.setFixedSize( QP.TupleToQSize( ( my_width, my_height ) ) )
self._embed_button.move( QP.TupleToQPoint( ( 0, 0 ) ) )
else:
@ -5309,6 +5324,7 @@ class MediaContainer( QW.QWidget ):
self._media_window.setFixedSize( QP.TupleToQSize( ( media_width, media_height ) ) )
self._media_window.move( QP.TupleToQPoint( ( 0, 0 ) ) )
@ -5404,6 +5420,26 @@ class MediaContainer( QW.QWidget ):
def paintEvent( self, event ):
if self._media_window is not None and self._media_window.isVisible():
return
# this only happens when we are transitioning from one media to another. in the brief period when one media type is going to another, we'll get flicker of the last valid bmp
# mpv embed fun aggravates this
# so instead we do an explicit repaint after the hide and before the new show, to clear our window
painter = QG.QPainter( self )
background_colour = HG.client_controller.new_options.GetColour( CC.COLOUR_MEDIA_BACKGROUND )
painter.setBrush( QG.QBrush( background_colour ) )
painter.drawRect( painter.viewport() )
def Pause( self ):
if self._media is not None:
@ -5471,8 +5507,6 @@ class MediaContainer( QW.QWidget ):
self._media = media
self.hide()
self._show_action = show_action
self._start_paused = start_paused
self._start_with_embed = start_with_embed
@ -5707,6 +5741,8 @@ class StaticImage( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
self.setMouseTracking( True )
self._media = None
@ -5726,6 +5762,8 @@ class StaticImage( QW.QWidget ):
self._is_rendered = False
self._first_background_drawn = False
def _DrawBackground( self, painter ):
@ -5815,7 +5853,7 @@ class StaticImage( QW.QWidget ):
self._ClearCanvasBitmap()
self._first_background_drawn = False
self.update()
def TIMERAnimationUpdate( self ):

View File

@ -7,6 +7,7 @@ from . import ClientGUIDialogs
from . import ClientGUIFunctions
from . import ClientGUIListBoxes
from . import ClientGUIMenus
from . import ClientGUIMPV
from . import ClientGUITopLevelWindows
from . import ClientGUIScrolledPanelsEdit
from . import ClientGUIScrolledPanelsManagement
@ -47,7 +48,7 @@ class FullscreenHoverFrame( QW.QFrame ):
self._hide_until = None
self._SizeAndPosition()
self._position_initialised = False
HG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
@ -72,6 +73,8 @@ class FullscreenHoverFrame( QW.QFrame ):
self.move( QP.TupleToQPoint( my_ideal_position ) )
self._position_initialised = True
def keyPressEvent( self, event ):
@ -91,6 +94,11 @@ class FullscreenHoverFrame( QW.QFrame ):
def TIMERUIUpdate( self ):
if not self._position_initialised:
self._SizeAndPosition()
current_focus_tlw = QW.QApplication.activeWindow()
focus_is_on_descendant = ClientGUIFunctions.IsQtAncestor( current_focus_tlw, self._my_canvas.window(), through_tlws = True )
@ -241,7 +249,6 @@ class FullscreenHoverFrame( QW.QFrame ):
class FullscreenHoverFrameRightDuplicates( FullscreenHoverFrame ):
@ -554,6 +561,8 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
HG.client_controller.sub( self, 'SetCurrentZoom', 'canvas_new_zoom' )
HG.client_controller.sub( self, 'SetIndexString', 'canvas_new_index_string' )
HG.client_controller.sub( self, 'UpdateGlobalAudioMute', 'new_global_audio_mute' )
HG.client_controller.sub( self, 'UpdateGlobalAudioVolume', 'new_global_audio_volume' )
def _Archive( self ):
@ -570,6 +579,13 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
HG.client_controller.pub( 'canvas_application_command', command, self._canvas_key )
def _FlipGlobalMute( self ):
HG.client_controller.new_options.FlipBoolean( 'global_audio_mute' )
HG.client_controller.pub( 'new_global_audio_mute' )
def _GetIdealSizeAndPosition( self ):
# clip this and friends to availableScreenGeometry for size and position, not rely 100% on parent
@ -643,6 +659,29 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
zoom_switch = ClientGUICommon.BetterBitmapButton( self, CC.GlobalPixmaps.zoom_switch, HG.client_controller.pub, 'canvas_application_command', ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'switch_between_100_percent_and_canvas_zoom' ), self._canvas_key )
zoom_switch.SetToolTipWithShortcuts( 'zoom switch', 'switch_between_100_percent_and_canvas_zoom' )
self._global_audio_volume_slider = QW.QSlider( self )
self._global_audio_volume_slider.setOrientation( QC.Qt.Horizontal )
self._global_audio_volume_slider.setTickInterval( 1 )
self._global_audio_volume_slider.setTickPosition( QW.QSlider.TicksBothSides )
self._global_audio_volume_slider.setRange( 0, 100 )
self._global_audio_volume_slider.setValue( HG.client_controller.new_options.GetInteger( 'global_audio_volume' ) )
self._global_audio_volume_slider.valueChanged.connect( self._VolumeSliderMoved )
self._global_audio_mute_checkbox = QW.QCheckBox( 'mute', self )
self._global_audio_mute_checkbox.setChecked( HG.client_controller.new_options.GetBoolean( 'global_audio_mute' ) )
self._global_audio_mute_checkbox.clicked.connect( self._FlipGlobalMute )
if not ClientGUIMPV.MPV_IS_AVAILABLE:
self._global_audio_mute_checkbox.hide()
self._global_audio_volume_slider.hide()
shortcuts = ClientGUICommon.BetterBitmapButton( self, CC.GlobalPixmaps.keyboard, self._ShowShortcutMenu )
shortcuts.setToolTip( 'shortcuts' )
@ -670,6 +709,8 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
QP.AddToLayout( self._top_hbox, zoom_in, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, zoom_out, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, zoom_switch, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, self._global_audio_volume_slider, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, self._global_audio_mute_checkbox, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, shortcuts, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, fullscreen_switch, CC.FLAGS_VCENTER )
QP.AddToLayout( self._top_hbox, open_externally, CC.FLAGS_VCENTER )
@ -801,6 +842,13 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
HG.client_controller.PopupMenu( self, menu )
def _VolumeSliderMoved( self ):
HG.client_controller.new_options.SetInteger( 'global_audio_volume', self._global_audio_volume_slider.value() )
HG.client_controller.pub( 'new_global_audio_volume' )
def EventDragButton( self ):
if self._current_media is None:
@ -899,6 +947,16 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
def UpdateGlobalAudioMute( self ):
self._global_audio_mute_checkbox.setChecked( HG.client_controller.new_options.GetBoolean( 'global_audio_mute' ) )
def UpdateGlobalAudioVolume( self ):
self._global_audio_volume_slider.setValue( HG.client_controller.new_options.GetInteger( 'global_audio_volume' ) )
class FullscreenHoverFrameTopArchiveDeleteFilter( FullscreenHoverFrameTop ):
def _Archive( self ):

View File

@ -617,6 +617,8 @@ class FilenameTaggingOptionsPanel( QW.QWidget ):
self._checkboxes_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self._checkboxes_panel.Add( QW.QWidget( self._checkboxes_panel ), CC.FLAGS_EXPAND_BOTH_WAYS )
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._tags_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
@ -1183,7 +1185,7 @@ class EditImportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
QP.AddToLayout( vbox, self._folder_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._file_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._tag_box, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._tag_box, CC.FLAGS_EXPAND_BOTH_WAYS )
self.widget().setLayout( vbox )

View File

@ -952,9 +952,18 @@ class BetterListCtrlPanel( QW.QWidget ):
self.AddMenuButton( 'add defaults', import_menu_items )
def AddDeleteButton( self ):
def AddDeleteButton( self, enabled_check_func = None ):
self.AddButton( 'delete', self._listctrl.ProcessDeleteAction, enabled_only_on_selection = True )
if enabled_check_func is None:
enabled_only_on_selection = True
else:
enabled_only_on_selection = False
self.AddButton( 'delete', self._listctrl.ProcessDeleteAction, enabled_check_func = enabled_check_func, enabled_only_on_selection = enabled_only_on_selection )
def AddImportExportButtons( self, permitted_object_types, import_add_callable ):

View File

@ -40,7 +40,8 @@ class mpvWidget( QW.QWidget ):
# This is necessary since PyQT stomps over the locale settings needed by libmpv.
# This needs to happen after importing PyQT before creating the first mpv.MPV instance.
locale.setlocale( locale.LC_NUMERIC, 'C' )
# hydev is commenting this out for now since Windows doesn't seem to need it and it is set in the import for non-Windows
#locale.setlocale( locale.LC_NUMERIC, 'C' )
self.setAttribute( QC.Qt.WA_DontCreateNativeAncestors )
self.setAttribute( QC.Qt.WA_NativeWindow )
@ -63,9 +64,10 @@ class mpvWidget( QW.QWidget ):
self._player.loop = True
# this makes black screen for audio (rather than transparent)
self._player.force_window = True
#self.setMouseTracking( True )#Needed to get mouse move events
self.setMouseTracking( True )#Needed to get mouse move events
#self.setFocusPolicy(QC.Qt.StrongFocus)#Needed to get key events
self._player.input_cursor = False#Disable mpv mouse move/click event capture
self._player.input_vo_keyboard = False#Disable mpv key event capture, might also need to set input_x11_keyboard
@ -76,6 +78,9 @@ class mpvWidget( QW.QWidget ):
self.destroyed.connect( self._player.terminate )
HG.client_controller.sub( self, 'UpdateGlobalAudioMute', 'new_global_audio_mute' )
HG.client_controller.sub( self, 'UpdateGlobalAudioMute', 'new_global_audio_volume' )
def GetAnimationBarStatus( self ):
@ -181,34 +186,40 @@ class mpvWidget( QW.QWidget ):
event.ignore()
#def mouseMoveEvent( self, event ):
def mouseMoveEvent( self, event ):
# same deal here as with mousereleaseevent--osc is non-interactable with commands, so let's not use it for now
#self._player.command( 'mouse', event.x(), event.y() )
#event.ignore()
event.ignore()
def mousePressEvent( self, event ):
if not ( event.modifiers() & ( QC.Qt.ShiftModifier | QC.Qt.ControlModifier | QC.Qt.AltModifier) ):
if event.button() == QC.Qt.LeftButton:
self.PausePlay()
self.parentWidget().BeginDrag()
return
event.ignore()
def mouseReleaseEvent( self, event ):
if event.button() == QC.Qt.LeftButton:
self.PausePlay()
else:
event.ignore()
return
# left index = 0
# right index = 2
# the issue with using this guy is it sends a mouse press or mouse down event, and the OSC only responds to mouse up
#self._player.command( 'mouse', event.x(), event.y(), index, 'single' )
event.accept()
event.ignore()
def Pause( self ):
@ -234,7 +245,10 @@ class mpvWidget( QW.QWidget ):
self._player.pause = True
self._player.command( 'playlist-remove', 'current' )
if len( self._player.playlist ) > 0:
self._player.command( 'playlist-remove', 'current' )
else:
@ -249,16 +263,20 @@ class mpvWidget( QW.QWidget ):
self._player.visibility = 'always'
volume = 70
mute = False
self._player.pause = True
self._player.loadfile( path )
try:
self._player.loadfile( path )
except Exception as e:
HydrusData.ShowException( e )
self._player.volume = HG.client_controller.new_options.GetInteger( 'global_audio_volume' )
self._player.mute = HG.client_controller.new_options.GetBoolean( 'global_audio_mute' )
self._player.pause = start_paused
self._player.volume = volume
self._player.mute = mute
@ -267,3 +285,12 @@ class mpvWidget( QW.QWidget ):
self.SetMedia( None )
def UpdateGlobalAudioMute( self ):
self._player.mute = HG.client_controller.new_options.GetBoolean( 'global_audio_mute' )
def UpdateGlobalAudioVolume( self ):
self._player.volume = HG.client_controller.new_options.GetInteger( 'global_audio_volume' )

View File

@ -966,6 +966,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._searching_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, QW.QWidget( self._main_left_panel ), CC.FLAGS_EXPAND_BOTH_WAYS )
self._main_left_panel.setLayout( vbox )
@ -998,6 +999,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
QP.AddToLayout( vbox, self._edit_merge_options, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._filtering_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, random_filtering_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, QW.QWidget( self._main_right_panel ), CC.FLAGS_EXPAND_BOTH_WAYS )
self._main_right_panel.setLayout( vbox )

View File

@ -633,7 +633,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
media = self._focused_media.GetDisplayMedia()
if media.GetMime() in HC.IMAGES and media.GetDuration() is None:
if media.GetMime() in HC.IMAGES:
HG.client_controller.pub( 'clipboard', 'bmp', media )
@ -1181,6 +1181,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
return 'image'
elif len( classes.difference( HC.ANIMATIONS ) ) == 0:
return 'animation'
elif len( classes.difference( HC.VIDEO ) ) == 0:
return 'video'
@ -1438,6 +1442,11 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
def _MediaIsVisible( self, media ):
return True
def _ModifyUploaders( self, file_service_key ):
QW.QMessageBox.information( self, 'Information', 'this does not work yet!' )
@ -1777,18 +1786,25 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
else:
for m in self._sorted_media:
# let's not focus if one of the selectees is already visible
media_visible = True in ( self._MediaIsVisible( media ) for media in self._selected_media )
if not media_visible:
if m in self._selected_media:
for m in self._sorted_media:
ctrl = False
shift = False
self._HitMedia( m, ctrl, shift )
self._ScrollToMedia( m )
break
if m in self._selected_media:
ctrl = False
shift = False
self._HitMedia( m, ctrl, shift )
self._ScrollToMedia( m )
break
@ -3006,6 +3022,52 @@ class MediaPanelThumbnails( MediaPanel ):
return y_start
def _MediaIsInCleanPage( self, thumbnail ):
try:
index = self._sorted_media.index( thumbnail )
except HydrusExceptions.DataMissing:
return False
if self._GetPageIndexFromThumbnailIndex( index ) in self._clean_canvas_pages:
return True
else:
return False
def _MediaIsVisible( self, media ):
if media is not None:
( x, y ) = self._GetMediaCoordinates( media )
visible_rect = QP.ScrollAreaVisibleRect( self )
visible_rect_y = visible_rect.y()
visible_rect_height = visible_rect.height()
( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions()
bottom_edge_below_top_of_view = visible_rect_y < y + thumbnail_span_height
top_edge_above_bottom_of_view = y < visible_rect_y + visible_rect_height
is_visible = bottom_edge_below_top_of_view and top_edge_above_bottom_of_view
return is_visible
return True
def _MoveFocusedThumbnail( self, rows, columns, shift ):
if self._focused_media is not None:
@ -3106,7 +3168,7 @@ class MediaPanelThumbnails( MediaPanel ):
def _RedrawMedia( self, thumbnails ):
visible_thumbnails = [ thumbnail for thumbnail in thumbnails if self._ThumbnailIsVisible( thumbnail ) ]
visible_thumbnails = [ thumbnail for thumbnail in thumbnails if self._MediaIsInCleanPage( thumbnail ) ]
thumbnail_cache = HG.client_controller.GetCache( 'thumbnail' )
@ -3263,27 +3325,6 @@ class MediaPanelThumbnails( MediaPanel ):
def _ThumbnailIsVisible( self, thumbnail ):
try:
index = self._sorted_media.index( thumbnail )
except HydrusExceptions.DataMissing:
return False
if self._GetPageIndexFromThumbnailIndex( index ) in self._clean_canvas_pages:
return True
else:
return False
def _UpdateBackgroundColour( self ):
MediaPanel._UpdateBackgroundColour( self )
@ -4371,7 +4412,7 @@ class MediaPanelThumbnails( MediaPanel ):
if focused_is_local:
if self._focused_media.GetMime() in HC.IMAGES and self._focused_media.GetDuration() is None:
if self._focused_media.GetMime() in HC.IMAGES:
ClientGUIMenus.AppendMenuItem( copy_menu, 'image (bitmap)', 'Copy the selected file\'s image data to the clipboard (as a bmp).', self._CopyBMPToClipboard )
@ -4929,6 +4970,11 @@ class Thumbnail( Selectable ):
self._dump_status = CC.DUMPER_NOT_DUMPED
self._file_service_key = file_service_key
self._last_tags = None
self._last_upper_summary = None
self._last_lower_summary = None
def _ScaleUpThumbnailDimensions( self, thumbnail_dimensions, scale_up_dimensions ):
@ -5044,12 +5090,24 @@ class Thumbnail( Selectable ):
if len( tags ) > 0:
upper_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_top' )
upper_summary = upper_tag_summary_generator.GenerateSummary( tags )
lower_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_bottom_right' )
lower_summary = lower_tag_summary_generator.GenerateSummary( tags )
if self._last_tags is not None and self._last_tags == tags:
upper_summary = self._last_upper_summary
lower_summary = self._last_lower_summary
else:
upper_summary = upper_tag_summary_generator.GenerateSummary( tags )
lower_summary = lower_tag_summary_generator.GenerateSummary( tags )
self._last_tags = set( tags )
self._last_upper_summary = upper_summary
self._last_lower_summary = lower_summary
if len( upper_summary ) > 0 or len( lower_summary ) > 0:

View File

@ -1,7 +1,9 @@
from . import ClientConstants as CC
from . import ClientCaches
from . import ClientDefaults
from . import ClientGUICommon
from . import ClientGUIDialogs
from . import ClientGUIFunctions
from . import ClientImporting
from . import ClientTags
import collections
@ -26,6 +28,9 @@ class OptionsPanel( QW.QWidget ):
class OptionsPanelMimes( OptionsPanel ):
BUTTON_CURRENTLY_HIDDEN = '\u25B6'
BUTTON_CURRENTLY_SHOWING = '\u25BC'
def __init__( self, parent, selectable_mimes ):
OptionsPanel.__init__( self, parent )
@ -33,56 +38,54 @@ class OptionsPanelMimes( OptionsPanel ):
self._selectable_mimes = set( selectable_mimes )
self._mimes_to_checkboxes = {}
self._mime_groups_to_checkboxes = {}
self._mime_groups_to_values = {}
self._general_mime_types_to_checkboxes = {}
self._general_mime_types_to_buttons = {}
mime_groups = []
general_mime_types = []
mime_groups.append( ( HC.APPLICATIONS, HC.GENERAL_APPLICATION ) )
mime_groups.append( ( HC.AUDIO, HC.GENERAL_AUDIO ) )
mime_groups.append( ( HC.IMAGES, HC.GENERAL_IMAGE ) )
mime_groups.append( ( HC.VIDEO, HC.GENERAL_VIDEO ) )
general_mime_types.append( HC.GENERAL_IMAGE )
general_mime_types.append( HC.GENERAL_ANIMATION )
general_mime_types.append( HC.GENERAL_VIDEO )
general_mime_types.append( HC.GENERAL_AUDIO )
general_mime_types.append( HC.GENERAL_APPLICATION )
mime_groups_to_mimes = collections.defaultdict( list )
gridbox = QP.GridLayout( cols = 3 )
for mime in self._selectable_mimes:
gridbox.setColumnStretch( 2, 1 )
for general_mime_type in general_mime_types:
for ( mime_group, mime_group_type ) in mime_groups:
mimes_in_type = self._GetMimesForGeneralMimeType( general_mime_type )
if len( mimes_in_type ) == 0:
if mime in mime_group:
mime_groups_to_mimes[ mime_group ].append( mime )
break
continue
gridbox = QP.GridLayout( cols = 2 )
gridbox.setColumnStretch( 1, 1 )
for ( mime_group, mime_group_type ) in mime_groups:
general_mime_checkbox = QW.QCheckBox( HC.mime_string_lookup[ general_mime_type ], self )
general_mime_checkbox.clicked.connect( self.EventMimeGroupCheckbox )
mimes = mime_groups_to_mimes[ mime_group ]
self._general_mime_types_to_checkboxes[ general_mime_type ] = general_mime_checkbox
mg_checkbox = QW.QCheckBox( HC.mime_string_lookup[ mime_group_type ], self )
mg_checkbox.clicked.connect( self.EventMimeGroupCheckbox )
QP.AddToLayout( gridbox, general_mime_checkbox, CC.FLAGS_VCENTER )
self._mime_groups_to_checkboxes[ mime_group ] = mg_checkbox
self._mime_groups_to_values[ mime_group ] = mg_checkbox.isChecked()
show_hide_button = ClientGUICommon.BetterButton( self, self.BUTTON_CURRENTLY_SHOWING, self._ButtonShowHide, general_mime_type )
QP.AddToLayout( gridbox, mg_checkbox, CC.FLAGS_VCENTER )
max_width = ClientGUIFunctions.ConvertTextToPixelWidth( show_hide_button, 5 )
show_hide_button.setMaximumWidth( max_width )
self._general_mime_types_to_buttons[ general_mime_type ] = show_hide_button
QP.AddToLayout( gridbox, show_hide_button, CC.FLAGS_VCENTER )
vbox = QP.VBoxLayout()
for mime in mimes:
for mime in mimes_in_type:
m_checkbox = QW.QCheckBox( HC.mime_string_lookup[ mime ], self )
m_checkbox.clicked.connect( self.EventMimeCheckbox )
#m_checkbox.hide()
self._mimes_to_checkboxes[ mime ] = m_checkbox
QP.AddToLayout( vbox, m_checkbox, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -93,46 +96,90 @@ class OptionsPanelMimes( OptionsPanel ):
self.setLayout( gridbox )
self._HideShowSubCheckboxes()
def _DoInitialHideShow( self ):
for ( general_mime_type, general_mime_checkbox ) in list( self._general_mime_types_to_checkboxes.items() ):
mimes_in_type = self._GetMimesForGeneralMimeType( general_mime_type )
should_show = general_mime_checkbox.checkState() == QC.Qt.PartiallyChecked
if not should_show:
self._ButtonShowHide( general_mime_type )
def _HideShowSubCheckboxes( self ):
def _GetMimesForGeneralMimeType( self, general_mime_type ):
pass
mimes_in_type = HC.general_mimetypes_to_mime_groups[ general_mime_type ]
# this is insufficient. we need to have mime groups done by three-state checkboxes or something.
# and it'd be nice to have a border around groups or something.
mimes_in_type = [ mime for mime in mimes_in_type if mime in self._selectable_mimes ]
'''
for ( mime_group, all_true ) in self._mime_groups_to_values.items():
return mimes_in_type
def _ButtonShowHide( self, general_mime_type ):
button = self._general_mime_types_to_buttons[ general_mime_type ]
mimes_in_type = self._GetMimesForGeneralMimeType( general_mime_type )
should_show = button.text() == self.BUTTON_CURRENTLY_HIDDEN
for mime in mimes_in_type:
should_show = not all_true
self._mimes_to_checkboxes[ mime ].setVisible( should_show )
for mime in mime_group:
if mime not in self._selectable_mimes:
continue
self._mimes_to_checkboxes[ mime ].setVisible( should_show )
if should_show:
'''
button.setText( self.BUTTON_CURRENTLY_SHOWING )
else:
button.setText( self.BUTTON_CURRENTLY_HIDDEN )
def _UpdateMimeGroupCheckboxes( self ):
for ( mime_group, mg_checkbox ) in self._mime_groups_to_checkboxes.items():
for ( general_mime_type, general_mime_checkbox ) in self._general_mime_types_to_checkboxes.items():
respective_checkbox_values = [ m_checkbox.isChecked() for ( mime, m_checkbox ) in list( self._mimes_to_checkboxes.items() ) if mime in mime_group ]
mimes_in_type = self._GetMimesForGeneralMimeType( general_mime_type )
all_true = False not in respective_checkbox_values
all_checkbox_values = { self._mimes_to_checkboxes[ mime ].isChecked() for mime in mimes_in_type }
mg_checkbox.setChecked( all_true )
self._mime_groups_to_values[ mime_group ] = all_true
all_false = True not in all_checkbox_values
all_true = False not in all_checkbox_values
if all_false:
check_state = QC.Qt.Unchecked
elif all_true:
check_state = QC.Qt.Checked
else:
check_state = QC.Qt.PartiallyChecked
if check_state == QC.Qt.PartiallyChecked:
general_mime_checkbox.setTristate( True )
general_mime_checkbox.setCheckState( check_state )
if check_state != QC.Qt.PartiallyChecked:
general_mime_checkbox.setTristate( False )
self._HideShowSubCheckboxes()
def EventMimeCheckbox( self ):
@ -142,43 +189,45 @@ class OptionsPanelMimes( OptionsPanel ):
def EventMimeGroupCheckbox( self ):
# Obsolote comment from before the Qt port:
# this is a commandevent, which won't give up the checkbox object, so we have to do some jiggery pokery
for ( mime_group, mg_checkbox ) in list(self._mime_groups_to_checkboxes.items()):
for ( general_mime_type, general_mime_checkbox ) in list( self._general_mime_types_to_checkboxes.items() ):
expected_value = self._mime_groups_to_values[ mime_group ]
actual_value = mg_checkbox.isChecked()
check_state = general_mime_checkbox.checkState()
if actual_value != expected_value:
mime_check_state = None
if check_state == QC.Qt.Unchecked:
for ( mime, m_checkbox ) in list(self._mimes_to_checkboxes.items()):
mime_check_state = False
elif check_state == QC.Qt.Checked:
mime_check_state = True
if mime_check_state is not None:
mimes_in_type = self._GetMimesForGeneralMimeType( general_mime_type )
for mime in mimes_in_type:
if mime in mime_group:
m_checkbox.setChecked( actual_value )
self._mimes_to_checkboxes[ mime ].setChecked( mime_check_state )
self._mime_groups_to_values[ mime_group ] = actual_value
self._HideShowSubCheckboxes()
def GetValue( self ):
mimes = tuple( [ mime for ( mime, checkbox ) in list(self._mimes_to_checkboxes.items()) if checkbox.isChecked() == True ] )
mimes = tuple( [ mime for ( mime, checkbox ) in list( self._mimes_to_checkboxes.items() ) if checkbox.isChecked() ] )
return mimes
def SetValue( self, mimes ):
def SetValue( self, checked_mimes ):
for ( mime, checkbox ) in list(self._mimes_to_checkboxes.items()):
for ( mime, checkbox ) in self._mimes_to_checkboxes.items():
if mime in mimes:
if mime in checked_mimes:
checkbox.setChecked( True )
@ -190,4 +239,6 @@ class OptionsPanelMimes( OptionsPanel ):
self._UpdateMimeGroupCheckboxes()
self._DoInitialHideShow()

View File

@ -117,14 +117,16 @@ class ResizingScrolledPanel( QW.QScrollArea ):
width_increase = 0
height_increase = 0
# + 2 because it is late and that seems to stop scrollbars lmao
if width_larger:
width_increase = max( 0, widget_size_hint.width() - my_size.width() )
width_increase = max( 0, widget_size_hint.width() - my_size.width() + 2 )
if height_larger:
height_increase = max( 0, widget_size_hint.height() - my_size.height() )
height_increase = max( 0, widget_size_hint.height() - my_size.height() + 2 )
if width_increase > 0 or height_increase > 0:

View File

@ -2680,11 +2680,9 @@ class EditMediaViewOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
self._preview_start_paused = QW.QCheckBox( self )
self._preview_start_with_embed = QW.QCheckBox( self )
advanced_mode = HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
for action in possible_show_actions:
if action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV and ( not advanced_mode or not ClientGUIMPV.MPV_IS_AVAILABLE ):
if action == CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV and not ClientGUIMPV.MPV_IS_AVAILABLE:
continue
@ -2764,11 +2762,6 @@ class EditMediaViewOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
text += ' MPV is not available for this client.'
if not advanced_mode:
text += ' Only advanced mode users can select MPV for now.'
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,text), CC.FLAGS_EXPAND_PERPENDICULAR )
rows = []

View File

@ -3282,10 +3282,15 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer mime handling' )
self._media_viewer_options = ClientGUIListCtrl.BetterListCtrl( self._media_viewer_panel, 'media_viewer_options', 20, 20, [ ( 'mime', 21 ), ( 'media show action', 20 ), ( 'preview show action', 20 ), ( 'zoom info', -1 ) ], data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions )
media_viewer_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._media_viewer_panel )
self._media_viewer_edit_button = QW.QPushButton( 'edit', self._media_viewer_panel )
self._media_viewer_edit_button.clicked.connect( self.EditMediaViewerOptions )
self._media_viewer_options = ClientGUIListCtrl.BetterListCtrl( media_viewer_list_panel, 'media_viewer_options', 20, 20, [ ( 'mime', 21 ), ( 'media show action', 20 ), ( 'preview show action', 20 ), ( 'zoom info', -1 ) ], data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
media_viewer_list_panel.SetListCtrl( self._media_viewer_options )
media_viewer_list_panel.AddButton( 'add', self.AddMediaViewerOptions, enabled_check_func = self._CanAddMediaViewOption )
media_viewer_list_panel.AddButton( 'edit', self.EditMediaViewerOptions, enabled_only_on_selection = True )
media_viewer_list_panel.AddDeleteButton( enabled_check_func = self._CanDeleteMediaViewOptions )
#
@ -3302,13 +3307,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._media_zooms.setText( ','.join( ( str( media_zoom ) for media_zoom in media_zooms ) ) )
mimes_in_correct_order = ( HC.IMAGE_JPEG, HC.IMAGE_PNG, HC.IMAGE_APNG, HC.IMAGE_GIF, HC.IMAGE_WEBP, HC.IMAGE_TIFF, HC.IMAGE_ICON, HC.APPLICATION_FLASH, HC.APPLICATION_PDF, HC.APPLICATION_PSD, HC.APPLICATION_ZIP, HC.APPLICATION_RAR, HC.APPLICATION_7Z, HC.APPLICATION_HYDRUS_UPDATE_CONTENT, HC.APPLICATION_HYDRUS_UPDATE_DEFINITIONS, HC.VIDEO_AVI, HC.VIDEO_FLV, HC.VIDEO_MOV, HC.VIDEO_MP4, HC.VIDEO_MKV, HC.VIDEO_WEBM, HC.VIDEO_WMV, HC.VIDEO_MPEG, HC.VIDEO_REALMEDIA, HC.AUDIO_MP3, HC.AUDIO_M4A, HC.AUDIO_OGG, HC.AUDIO_FLAC, HC.AUDIO_WMA, HC.AUDIO_REALMEDIA, HC.AUDIO_TRUEAUDIO )
all_media_view_options = self._new_options.GetMediaViewOptions()
for mime in mimes_in_correct_order:
for ( mime, view_options ) in all_media_view_options.items():
items = self._new_options.GetMediaViewOptions( mime )
data = QP.ListsToTuples( [ mime ] + list( items ) )
data = QP.ListsToTuples( [ mime ] + list( view_options ) )
self._media_viewer_options.AddDatas( ( data, ) )
@ -3335,31 +3338,75 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self._media_viewer_panel.Add( self._media_viewer_options, CC.FLAGS_EXPAND_BOTH_WAYS )
self._media_viewer_panel.Add( self._media_viewer_edit_button, CC.FLAGS_LONE_BUTTON )
self._media_viewer_panel.Add( media_viewer_list_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._media_viewer_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
self.setLayout( vbox )
def _CanAddMediaViewOption( self ):
return len( self._GetUnsetMediaViewFiletypes() ) > 0
def _CanDeleteMediaViewOptions( self ):
deletable_mimes = set( HC.SEARCHABLE_MIMES )
selected_mimes = set()
for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData( only_selected = True ):
selected_mimes.add( mime )
if len( selected_mimes ) == 0:
return False
all_selected_are_deletable = selected_mimes.issubset( deletable_mimes )
return all_selected_are_deletable
def _GetCopyOfGeneralMediaViewOptions( self, desired_mime ):
general_mime_type = HC.mimes_to_general_mimetypes[ desired_mime ]
for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
if mime == general_mime_type:
view_options = ( desired_mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info )
return view_options
def _GetUnsetMediaViewFiletypes( self ):
editable_mimes = set( HC.SEARCHABLE_MIMES )
set_mimes = set()
for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
set_mimes.add( mime )
unset_mimes = editable_mimes.difference( set_mimes )
return unset_mimes
def _GetListCtrlData( self, data ):
( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = data
pretty_mime = HC.mime_string_lookup[ mime ]
for general_mime in HC.GENERAL_FILETYPES:
mime_group = HC.general_mimetypes_to_mime_groups[ general_mime ]
if mime in mime_group:
pretty_mime = '{}: {}'.format( HC.mime_string_lookup[ general_mime ], pretty_mime )
break
pretty_mime = self._GetPrettyMime( mime )
pretty_media_show_action = CC.media_viewer_action_string_lookup[ media_show_action ]
@ -3402,11 +3449,64 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
return ( display_tuple, sort_tuple )
def _GetPrettyMime( self, mime ):
pretty_mime = HC.mime_string_lookup[ mime ]
if mime not in HC.GENERAL_FILETYPES:
pretty_mime = '{}: {}'.format( HC.mime_string_lookup[ HC.mimes_to_general_mimetypes[ mime ] ], pretty_mime )
return pretty_mime
def AddMediaViewerOptions( self ):
unset_filetypes = self._GetUnsetMediaViewFiletypes()
if len( unset_filetypes ) == 0:
QW.QMessageBox.warning( self, 'Warning', 'You cannot add any more specific filetype options!' )
return
choice_tuples = [ ( self._GetPrettyMime( mime ), mime ) for mime in unset_filetypes ]
try:
mime = ClientGUIDialogsQuick.SelectFromList( self, 'select the filetype to add', choice_tuples, sort_tuples = True )
except HydrusExceptions.CancelledException:
return
data = self._GetCopyOfGeneralMediaViewOptions( mime )
title = 'add media view options information'
with ClientGUITopLevelWindows.DialogEdit( self, title ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditMediaViewOptionsPanel( dlg, data )
dlg.SetPanel( panel )
if dlg.exec() == QW.QDialog.Accepted:
new_data = panel.GetValue()
self._media_viewer_options.AddDatas( ( new_data, ) )
def EditMediaViewerOptions( self ):
for data in self._media_viewer_options.GetData( only_selected = True ):
title = 'set media view options information'
title = 'edit media view options information'
with ClientGUITopLevelWindows.DialogEdit( self, title ) as dlg:
@ -3469,6 +3569,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HydrusData.ShowText( 'Could not parse those zooms, so they were not saved!' )
mimes_to_media_view_options = {}
for data in self._media_viewer_options.GetData():
data = list( data )
@ -3477,9 +3579,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
value = data[1:]
self._new_options.SetMediaViewOptions( mime, value )
mimes_to_media_view_options[ mime ] = value
self._new_options.SetMediaViewOptions( mimes_to_media_view_options )
class _RegexPanel( QW.QWidget ):
@ -4912,7 +5016,7 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
reserved_panel = ClientGUICommon.StaticBox( self, 'built-in hydrus shortcut sets' )
self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrl( reserved_panel, 'reserved_shortcuts', 20, 30, [ ( 'name', -1 ), ( 'number of shortcuts', 20 ) ], data_to_tuples_func = self._GetTuples, activation_callback = self._EditReserved )
self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrl( reserved_panel, 'reserved_shortcuts', 6, 30, [ ( 'name', -1 ), ( 'number of shortcuts', 20 ) ], data_to_tuples_func = self._GetTuples, activation_callback = self._EditReserved )
self._reserved_shortcuts.setMinimumSize( QP.TupleToQSize( (320,200) ) )
@ -4922,7 +5026,7 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
custom_panel = ClientGUICommon.StaticBox( self, 'custom user sets' )
self._custom_shortcuts = ClientGUIListCtrl.BetterListCtrl( custom_panel, 'custom_shortcuts', 20, 30, [ ( 'name', -1 ), ( 'number of shortcuts', 20 ) ], data_to_tuples_func = self._GetTuples, delete_key_callback = self._Delete, activation_callback = self._EditCustom )
self._custom_shortcuts = ClientGUIListCtrl.BetterListCtrl( custom_panel, 'custom_shortcuts', 6, 30, [ ( 'name', -1 ), ( 'number of shortcuts', 20 ) ], data_to_tuples_func = self._GetTuples, delete_key_callback = self._Delete, activation_callback = self._EditCustom )
self._add_button = ClientGUICommon.BetterButton( custom_panel, 'add', self._Add )
self._edit_custom_button = ClientGUICommon.BetterButton( custom_panel, 'edit', self._EditCustom )
@ -4967,7 +5071,10 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
custom_panel_message = 'Custom shortcuts are advanced. They apply to the media viewer and must be turned on to take effect.'
custom_panel.Add( ClientGUICommon.BetterStaticText( custom_panel, custom_panel_message ), CC.FLAGS_EXPAND_PERPENDICULAR )
st = ClientGUICommon.BetterStaticText( custom_panel, custom_panel_message )
st.setWordWrap( True )
custom_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
custom_panel.Add( self._custom_shortcuts, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
custom_panel.Add( button_hbox, CC.FLAGS_BUTTON_SIZER )

View File

@ -161,7 +161,7 @@ class MigrateDatabasePanel( ClientGUIScrolledPanels.ReviewPanel ):
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, help_hbox, CC.FLAGS_BUTTON_SIZER )
QP.AddToLayout( vbox, info_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, info_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, migration_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )

View File

@ -250,7 +250,12 @@ class Shortcut( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, Shortcut ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):

View File

@ -1579,7 +1579,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
[ ( tag, count ) ] = tag_counts
text = choice_text_prefix + ' "' + tag + '" for ' + HydrusData.ToHumanInt( count ) + ' files'
text = choice_text_prefix + ' "' + HydrusText.ElideText( tag, 64 ) + '" for ' + HydrusData.ToHumanInt( count ) + ' files'
else:
@ -2127,7 +2127,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
message = 'Are you sure you want to remove these tags:'
message += os.linesep * 2
message += os.linesep.join( tags )
message += os.linesep.join( ( HydrusText.ElideText( tag, 64 ) for tag in tags ) )
else:

View File

@ -71,15 +71,27 @@ def GetSafeSize( tlw, min_size, gravity ):
parent_window = parent.window()
# when we initialise, we might not have a frame yet because we haven't done show() yet
# so borrow parent's
# so borrow main gui's
if frame_padding.isEmpty():
frame_padding = parent_window.frameGeometry().size() - parent_window.size()
main_gui = HG.client_controller.gui
if main_gui is not None and QP.isValid( main_gui ) and not main_gui.isFullScreen():
frame_padding = main_gui.frameGeometry().size() - main_gui.size()
parent_frame_size = parent_window.frameGeometry().size()
parent_available_size = parent_frame_size - frame_padding
if parent_window.isFullScreen():
parent_available_size = parent_window.size()
else:
parent_frame_size = parent_window.frameGeometry().size()
parent_available_size = parent_frame_size - frame_padding
parent_available_width = parent_available_size.width()
parent_available_height = parent_available_size.height()

View File

@ -445,7 +445,12 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, FileSeed ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):
@ -1357,6 +1362,8 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
if should_download_file:
self.CheckPreFetchMetadata( tag_import_options )
did_substantial_work = True
file_url = self.file_seed_data

View File

@ -100,7 +100,12 @@ class GallerySeed( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, GallerySeed ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):

View File

@ -411,7 +411,7 @@ class HydrusResourceBooruPage( HydrusResourceBooru ):
mime = media_result.GetMime()
if mime in HC.IMAGES:
if mime in HC.IMAGES or mime in HC.ANIMATIONS:
( width, height ) = media_result.GetResolution()

View File

@ -546,7 +546,7 @@ class MediaResult( object ):
def GetInbox( self ):
return self._locations_manager.GetInbox()
return self._locations_manager.inbox
def GetLocationsManager( self ):
@ -596,7 +596,10 @@ class MediaResult( object ):
def IsStaticImage( self ):
return self._file_info_manager.mime in HC.IMAGES and self._file_info_manager.duration in ( None, 0 )
image = self._file_info_manager.mime in HC.IMAGES
static_animation = self._file_info_manager.mime in HC.ANIMATIONS and self._file_info_manager.duration in ( 0, None )
return image or static_animation
def ProcessContentUpdate( self, service_key, content_update ):
@ -761,7 +764,7 @@ class LocationsManager( object ):
self._pending = pending
self._petitioned = petitioned
self._inbox = inbox
self.inbox = inbox
if urls is None:
@ -803,7 +806,7 @@ class LocationsManager( object ):
service_keys_to_filenames = dict( self._service_keys_to_filenames )
current_to_timestamps = dict( self._current_to_timestamps )
return LocationsManager( current, deleted, pending, petitioned, self._inbox, urls, service_keys_to_filenames, current_to_timestamps, self._file_modified_timestamp )
return LocationsManager( current, deleted, pending, petitioned, self.inbox, urls, service_keys_to_filenames, current_to_timestamps, self._file_modified_timestamp )
def GetCDPP( self ): return ( self._current, self._deleted, self._pending, self._petitioned )
@ -827,7 +830,7 @@ class LocationsManager( object ):
def GetInbox( self ):
return self._inbox
return self.inbox
def GetPending( self ): return self._pending
@ -931,11 +934,11 @@ class LocationsManager( object ):
if action == HC.CONTENT_UPDATE_ARCHIVE:
self._inbox = False
self.inbox = False
elif action == HC.CONTENT_UPDATE_INBOX:
self._inbox = True
self.inbox = True
elif action == HC.CONTENT_UPDATE_ADD:
@ -951,7 +954,7 @@ class LocationsManager( object ):
if CC.COMBINED_LOCAL_FILE_SERVICE_KEY not in self._current:
self._inbox = True
self.inbox = True
self._current.add( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
@ -978,7 +981,7 @@ class LocationsManager( object ):
elif service_key == CC.TRASH_SERVICE_KEY:
self._inbox = False
self.inbox = False
self._current.discard( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
self._deleted.add( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
@ -1047,7 +1050,15 @@ class Media( object ):
self._id_hash = self._id.__hash__()
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __eq__( self, other ):
if isinstance( other, Media ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):
@ -1447,6 +1458,68 @@ class MediaList( object ):
def GetFilteredFileCount( self, file_filter ):
if file_filter.filter_type == FILE_FILTER_ALL:
return self.GetNumFiles()
elif file_filter.filter_type == FILE_FILTER_SELECTED:
return sum( ( m.GetNumFiles() for m in self._selected_media ) )
elif file_filter.filter_type == FILE_FILTER_NOT_SELECTED:
return self.GetNumFiles() - sum( ( m.GetNumFiles() for m in self._selected_media ) )
elif file_filter.filter_type == FILE_FILTER_NONE:
return 0
elif file_filter.filter_type == FILE_FILTER_INBOX:
return sum( ( m.GetNumInbox() for m in self._selected_media ) )
elif file_filter.filter_type == FILE_FILTER_ARCHIVE:
return self.GetNumFiles() - sum( ( m.GetNumInbox() for m in self._selected_media ) )
else:
flat_media = self.GetFlatMedia()
if file_filter.filter_type == FILE_FILTER_FILE_SERVICE:
file_service_key = file_filter.filter_data
return sum( ( 1 for m in flat_media if file_service_key in m.GetLocationsManager().GetCurrent() ) )
elif file_filter.filter_type == FILE_FILTER_LOCAL:
return sum( ( 1 for m in flat_media if m.GetLocationsManager().IsLocal() ) )
elif file_filter.filter_type == FILE_FILTER_REMOTE:
return sum( ( 1 for m in flat_media if m.GetLocationsManager().IsRemote() ) )
elif file_filter.filter_type == FILE_FILTER_TAGS:
( and_or_or, select_tags ) = file_filter.filter_data
if and_or_or == 'AND':
return sum( ( 1 for m in flat_media if len( m.GetTagsManager().GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SIBLINGS_AND_PARENTS ).intersection( select_tags ) ) == len( select_tags ) ) )
elif and_or_or == 'OR':
return sum( ( 1 for m in flat_media if len( m.GetTagsManager().GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SIBLINGS_AND_PARENTS ).intersection( select_tags ) ) > 0 ) )
return 0
def GetFilteredHashes( self, file_filter ):
if file_filter.filter_type == FILE_FILTER_ALL:
@ -1527,6 +1600,8 @@ class MediaList( object ):
return hashes
return set()
def GetFilteredMedia( self, file_filter ):
@ -1587,6 +1662,8 @@ class MediaList( object ):
return filtered_media
return set()
def GenerateMediaResults( self, has_location = None, discriminant = None, selected_media = None, unrated = None, for_media_viewer = False ):
@ -1977,6 +2054,16 @@ class FileFilter( object ):
self.filter_data = filter_data
def __eq__( self, other ):
if isinstance( other, FileFilter ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):
if self.filter_data is None:
@ -2023,7 +2110,7 @@ class FileFilter( object ):
count = len( media_list.GetFilteredHashes( self ) )
count = media_list.GetFilteredFileCount( self )
filter_counts[ self ] = count
@ -2483,7 +2570,10 @@ class MediaSingleton( Media ):
def GetLocationsManager( self ): return self._media_result.GetLocationsManager()
def GetLocationsManager( self ):
return self._media_result.GetLocationsManager()
def GetMediaResult( self ): return self._media_result
@ -2685,7 +2775,7 @@ class MediaSingleton( Media ):
def IsImage( self ):
return self._media_result.GetMime() in HC.IMAGES and not self.HasDuration()
return self._media_result.GetMime() in HC.IMAGES
def IsSizeDefinite( self ): return self._media_result.GetSize() is not None

View File

@ -20,7 +20,12 @@ class NetworkContext( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, NetworkContext ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):

View File

@ -31,6 +31,79 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._InitialiseDefaults()
def _GetDefaultMediaViewOptions( self ):
media_view = HydrusSerialisable.SerialisableDictionary() # integer keys, so got to be cleverer dict
# media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, ( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) )
from . import ClientGUIMPV
if ClientGUIMPV.MPV_IS_AVAILABLE:
video_action = CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV
audio_action = CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV
video_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
else:
video_action = CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE
audio_action = CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON
video_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, True, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
image_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
gif_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
flash_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )
null_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )
media_start_paused = False
media_start_with_embed = False
preview_start_paused = False
preview_start_with_embed = False
media_view[ HC.GENERAL_IMAGE ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, preview_start_paused, preview_start_with_embed, image_zoom_info )
media_view[ HC.GENERAL_ANIMATION ] = ( video_action, media_start_paused, media_start_with_embed, video_action, preview_start_paused, preview_start_with_embed, gif_zoom_info )
media_view[ HC.GENERAL_VIDEO ] = ( video_action, media_start_paused, media_start_with_embed, video_action, preview_start_paused, preview_start_with_embed, video_zoom_info )
media_view[ HC.GENERAL_AUDIO ] = ( audio_action, media_start_paused, media_start_with_embed, audio_action, preview_start_paused, preview_start_with_embed, null_zoom_info )
media_view[ HC.GENERAL_APPLICATION ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, preview_start_paused, preview_start_with_embed, null_zoom_info )
return media_view
def _GetMediaViewOptions( self, mime ):
if mime in self._dictionary[ 'media_view' ]:
return self._dictionary[ 'media_view' ][ mime ]
else:
if mime not in HC.mimes_to_general_mimetypes:
null_result = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, False, False, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, False, False, ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR ) )
return null_result
general_mime_type = HC.mimes_to_general_mimetypes[ mime ]
if general_mime_type not in self._dictionary[ 'media_view' ]:
self._dictionary[ 'media_view' ] = self._GetDefaultMediaViewOptions()
return self._dictionary[ 'media_view' ][ general_mime_type ]
def _GetSerialisableInfo( self ):
serialisable_info = self._dictionary.GetSerialisableTuple()
@ -137,6 +210,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'autocomplete_float_main_gui' ] = True
self._dictionary[ 'booleans' ][ 'autocomplete_float_frames' ] = False
self._dictionary[ 'booleans' ][ 'global_audio_mute' ] = False
#
self._dictionary[ 'colours' ] = HydrusSerialisable.SerialisableDictionary()
@ -229,6 +304,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'integers' ][ 'video_thumbnail_percentage_in' ] = 35
self._dictionary[ 'integers' ][ 'global_audio_volume' ] = 70
self._dictionary[ 'integers' ][ 'duplicate_comparison_score_higher_jpeg_quality' ] = 10
self._dictionary[ 'integers' ][ 'duplicate_comparison_score_much_higher_jpeg_quality' ] = 20
self._dictionary[ 'integers' ][ 'duplicate_comparison_score_higher_filesize' ] = 10
@ -468,47 +545,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
#
self._dictionary[ 'media_view' ] = HydrusSerialisable.SerialisableDictionary() # integer keys, so got to be cleverer dict
# media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, ( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) )
image_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
gif_zoom_info = ( CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, True, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
flash_zoom_info = ( CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )
video_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_MAX_REGULAR, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, CC.MEDIA_VIEWER_SCALE_TO_CANVAS, True, CC.ZOOM_LANCZOS4, CC.ZOOM_AREA )
null_zoom_info = ( CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, CC.MEDIA_VIEWER_SCALE_100, False, CC.ZOOM_LINEAR, CC.ZOOM_LINEAR )
media_start_paused = False
media_start_with_embed = False
preview_start_paused = False
preview_start_with_embed = False
for mime in HC.SEARCHABLE_MIMES:
if mime in HC.IMAGES_THAT_CAN_HAVE_ANIMATION:
self._dictionary[ 'media_view' ][ mime ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, preview_start_paused, preview_start_with_embed, gif_zoom_info )
elif mime in HC.IMAGES:
self._dictionary[ 'media_view' ][ mime ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, preview_start_paused, preview_start_with_embed, image_zoom_info )
elif mime in HC.VIDEO:
self._dictionary[ 'media_view' ][ mime ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, preview_start_paused, preview_start_with_embed, video_zoom_info )
elif mime in HC.AUDIO:
self._dictionary[ 'media_view' ][ mime ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, preview_start_paused, preview_start_with_embed, null_zoom_info )
elif mime in HC.APPLICATIONS:
self._dictionary[ 'media_view' ][ mime ] = ( CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_SHOW_OPEN_EXTERNALLY_BUTTON, preview_start_paused, preview_start_with_embed, null_zoom_info )
self._dictionary[ 'media_view' ][ HC.APPLICATION_HYDRUS_UPDATE_CONTENT ] = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, preview_start_paused, preview_start_with_embed, null_zoom_info )
self._dictionary[ 'media_view' ][ HC.APPLICATION_HYDRUS_UPDATE_DEFINITIONS ] = ( CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, media_start_paused, media_start_with_embed, CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW, preview_start_paused, preview_start_with_embed, null_zoom_info )
self._dictionary[ 'media_view' ] = self._GetDefaultMediaViewOptions()
self._dictionary[ 'media_zooms' ] = [ 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 3.0, 5.0, 10.0, 20.0 ]
@ -749,6 +786,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetDefaultMediaViewOptions( self ):
with self._lock:
return self._GetDefaultMediaViewOptions()
def GetDefaultSort( self ):
with self._lock:
@ -848,7 +893,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
with self._lock:
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._GetMediaViewOptions( mime )
( possible_show_actions, can_start_paused, can_start_with_embed ) = CC.media_viewer_capabilities[ mime ]
@ -861,11 +906,11 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetMediaViewOptions( self, mime ):
def GetMediaViewOptions( self ):
with self._lock:
return self._dictionary[ 'media_view' ][ mime ]
return dict( self._dictionary[ 'media_view' ] )
@ -873,7 +918,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
with self._lock:
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._GetMediaViewOptions( mime )
return zoom_info
@ -891,7 +936,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
with self._lock:
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._GetMediaViewOptions( mime )
( media_scale_up, media_scale_down, preview_scale_up, preview_scale_down, exact_zooms_only, scale_up_quality, scale_down_quality ) = zoom_info
@ -932,7 +977,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
with self._lock:
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._dictionary[ 'media_view' ][ mime ]
( media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = self._GetMediaViewOptions( mime )
( possible_show_actions, can_start_paused, can_start_with_embed ) = CC.media_viewer_capabilities[ mime ]
@ -1125,11 +1170,11 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def SetMediaViewOptions( self, mime, value_tuple ):
def SetMediaViewOptions( self, mimes_to_media_view_options ):
with self._lock:
self._dictionary[ 'media_view' ][ mime ] = value_tuple
self._dictionary[ 'media_view' ] = HydrusSerialisable.SerialisableDictionary( mimes_to_media_view_options )

View File

@ -819,7 +819,12 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, Predicate ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):
@ -1461,6 +1466,10 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
mime_text = 'image'
elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.ANIMATIONS ) ):
mime_text = 'animation'
elif set( mimes ) == set( HC.SEARCHABLE_MIMES ).intersection( set( HC.VIDEO ) ):
mime_text = 'video'

View File

@ -405,7 +405,12 @@ class TagFilter( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self._tag_slices_to_rules == other._tag_slices_to_rules
if isinstance( other, TagFilter ):
return self._tag_slices_to_rules == other._tag_slices_to_rules
return NotImplemented
def _GetTagSlices( self, tag ):

View File

@ -48,11 +48,20 @@ class JobKey( object ):
self._variables = dict()
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __eq__( self, other ):
if isinstance( other, JobKey ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ): return self._key.__hash__()
def __ne__( self, other ): return self.__hash__() != other.__hash__()
def __hash__( self ):
return self._key.__hash__()
def _CheckCancelTests( self ):
@ -535,19 +544,19 @@ class QtAwareRepeatingJob(HydrusThreading.RepeatingJob):
self._window = window
def _QTWork( self ):
if not self._window or not QP.isValid( self._window ):
return
self.Work()
def _BootWorker( self ):
def qt_code():
if not self._window or not QP.isValid( self._window ):
return
self.Work()
QP.CallAfter( qt_code )
QP.CallAfter( self._QTWork )
def _MyWindowDead( self ):

View File

@ -18,6 +18,9 @@ if getattr( sys, 'frozen', False ):
else:
# maybe this is better?
# os.path.dirname( __file__ )
RUNNING_FROM_FROZEN_BUILD = False
BASE_DIR = sys.path[0]
@ -67,7 +70,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 380
SOFTWARE_VERSION = 381
CLIENT_API_VERSION = 11
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -488,20 +491,23 @@ GENERAL_AUDIO = 40
GENERAL_IMAGE = 41
GENERAL_VIDEO = 42
GENERAL_APPLICATION = 43
GENERAL_ANIMATION = 44
APPLICATION_OCTET_STREAM = 100
APPLICATION_UNKNOWN = 101
GENERAL_FILETYPES = ( GENERAL_APPLICATION, GENERAL_AUDIO, GENERAL_IMAGE, GENERAL_VIDEO )
GENERAL_FILETYPES = { GENERAL_APPLICATION, GENERAL_AUDIO, GENERAL_IMAGE, GENERAL_VIDEO, GENERAL_ANIMATION }
SEARCHABLE_MIMES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, APPLICATION_FLASH, VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_MPEG, APPLICATION_PSD, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z, AUDIO_M4A, AUDIO_MP3, AUDIO_REALMEDIA, AUDIO_OGG, AUDIO_FLAC, AUDIO_TRUEAUDIO, AUDIO_WMA, VIDEO_WMV }
ALLOWED_MIMES = set( SEARCHABLE_MIMES ).union( { IMAGE_BMP, APPLICATION_HYDRUS_UPDATE_CONTENT, APPLICATION_HYDRUS_UPDATE_DEFINITIONS } )
STORABLE_MIMES = set( SEARCHABLE_MIMES ).union( { APPLICATION_HYDRUS_UPDATE_CONTENT, APPLICATION_HYDRUS_UPDATE_DEFINITIONS } )
ALLOWED_MIMES = set( STORABLE_MIMES ).union( { IMAGE_BMP } )
DECOMPRESSION_BOMB_IMAGES = ( IMAGE_JPEG, IMAGE_PNG )
IMAGES = ( IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON )
IMAGES = ( IMAGE_JPEG, IMAGE_PNG, IMAGE_BMP, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON )
IMAGES_THAT_CAN_HAVE_ANIMATION = ( IMAGE_GIF, IMAGE_APNG )
ANIMATIONS = ( IMAGE_GIF, IMAGE_APNG )
AUDIO = ( AUDIO_M4A, AUDIO_MP3, AUDIO_OGG, AUDIO_FLAC, AUDIO_WMA, AUDIO_REALMEDIA, AUDIO_TRUEAUDIO )
@ -515,6 +521,17 @@ general_mimetypes_to_mime_groups[ GENERAL_APPLICATION ] = APPLICATIONS
general_mimetypes_to_mime_groups[ GENERAL_AUDIO ] = AUDIO
general_mimetypes_to_mime_groups[ GENERAL_IMAGE ] = IMAGES
general_mimetypes_to_mime_groups[ GENERAL_VIDEO ] = VIDEO
general_mimetypes_to_mime_groups[ GENERAL_ANIMATION ] = ANIMATIONS
mimes_to_general_mimetypes = {}
for ( general_mime_type, mimes_in_type ) in general_mimetypes_to_mime_groups.items():
for mime in mimes_in_type:
mimes_to_general_mimetypes[ mime ] = general_mime_type
MIMES_THAT_DEFINITELY_HAVE_AUDIO = tuple( [ APPLICATION_FLASH ] + list( AUDIO ) )
MIMES_THAT_MAY_HAVE_AUDIO = tuple( list( MIMES_THAT_DEFINITELY_HAVE_AUDIO ) + list( VIDEO ) )
@ -627,6 +644,7 @@ mime_string_lookup[ GENERAL_APPLICATION ] = 'application'
mime_string_lookup[ GENERAL_AUDIO ] = 'audio'
mime_string_lookup[ GENERAL_IMAGE ] = 'image'
mime_string_lookup[ GENERAL_VIDEO ] = 'video'
mime_string_lookup[ GENERAL_ANIMATION ] = 'animation'
mime_mimetype_string_lookup = {}
@ -675,6 +693,7 @@ mime_mimetype_string_lookup[ GENERAL_APPLICATION ] = 'application'
mime_mimetype_string_lookup[ GENERAL_AUDIO ] = 'audio'
mime_mimetype_string_lookup[ GENERAL_IMAGE ] = 'image'
mime_mimetype_string_lookup[ GENERAL_VIDEO ] = 'video'
mime_mimetype_string_lookup[ GENERAL_ANIMATION ] = 'animation'
mime_ext_lookup = {}

View File

@ -1325,7 +1325,10 @@ def ToHumanBytes( size ):
def ToHumanInt( num ):
text = locale.format_string( '%d', num, grouping = True )
# this got stomped on by mpv, which resets locale
#text = locale.format_string( '%d', num, grouping = True )
text = '{:,}'.format( num )
return text
@ -1375,12 +1378,18 @@ class AccountIdentifier( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __eq__( self, other ):
if isinstance( other, AccountIdentifier ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ): return ( self._type, self._data ).__hash__()
def __ne__( self, other ): return self.__hash__() != other.__hash__()
def __repr__( self ): return 'Account Identifier: ' + str( ( self._type, self._data ) )
def _GetSerialisableInfo( self ):
@ -1539,10 +1548,13 @@ class ContentUpdate( object ):
def __eq__( self, other ):
return hash( self ) == hash( other )
if isinstance( other, ContentUpdate ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __ne__( self, other ): return not self.__eq__( other )
def __hash__( self ):

View File

@ -1021,12 +1021,18 @@ class Content( HydrusSerialisable.SerialisableBase ):
self._content_data = content_data
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __eq__( self, other ):
if isinstance( other, Content ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ): return ( self._content_type, self._content_data ).__hash__()
def __ne__( self, other ): return self.__hash__() != other.__hash__()
def __repr__( self ): return 'Content: ' + self.ToString()
def _GetSerialisableInfo( self ):
@ -1378,7 +1384,12 @@ class Credentials( HydrusSerialisable.SerialisableBase ):
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
if isinstance( other, Credentials ):
return self.__hash__() == other.__hash__()
return NotImplemented
def __hash__( self ):
@ -1386,11 +1397,6 @@ class Credentials( HydrusSerialisable.SerialisableBase ):
return ( self._host, self._port, self._access_key ).__hash__()
def __ne__( self, other ):
return self.__hash__() != other.__hash__()
def __repr__( self ):
if self._access_key is None:

View File

@ -20,6 +20,15 @@ import traceback
TEMP_PATH_LOCK = threading.Lock()
IN_USE_TEMP_PATHS = set()
def AddBaseDirToEnvPath():
# this is a thing to get mpv working, loading the dll/so from the base dir using ctypes
if 'PATH' in os.environ:
os.environ[ 'PATH' ] = HC.BASE_DIR + os.pathsep + os.environ[ 'PATH' ]
def AppendPathUntilNoConflicts( path ):
( path_absent_ext, ext ) = os.path.splitext( path )

View File

@ -360,6 +360,7 @@ class TabBar( QW.QTabBar ):
self._supplementary_drop_target = None
self._last_clicked_tab_index = -1
self._last_clicked_global_pos = None
def AddSupplementaryTabBarDropTarget( self, drop_target ):
@ -367,10 +368,12 @@ class TabBar( QW.QTabBar ):
self._supplementary_drop_target = drop_target
def clearLastClickedTabIndex( self ):
def clearLastClickedTabInfo( self ):
self._last_clicked_tab_index = -1
self._last_clicked_global_pos = None
def mouseMoveEvent( self, e ):
@ -383,6 +386,8 @@ class TabBar( QW.QTabBar ):
self._last_clicked_tab_index = self.tabAt( event.pos() )
self._last_clicked_global_pos = event.globalPos()
QW.QTabBar.mousePressEvent( self, event )
@ -416,9 +421,9 @@ class TabBar( QW.QTabBar ):
def lastClickedTabIndex( self ):
def lastClickedTabInfo( self ):
return self._last_clicked_tab_index
return ( self._last_clicked_tab_index, self._last_clicked_global_pos )
def dropEvent( self, event ):
@ -504,7 +509,7 @@ class TabWidgetWithDnD( QW.QTabWidget ):
def mouseMoveEvent( self, e ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
QW.QTabWidget.mouseMoveEvent( self, e )
@ -529,13 +534,20 @@ class TabWidgetWithDnD( QW.QTabWidget ):
return
clicked_tab_index = self._tab_bar.lastClickedTabIndex()
( clicked_tab_index, clicked_global_pos ) = self._tab_bar.lastClickedTabInfo()
if clicked_tab_index == -1:
return
if e.globalPos() == clicked_global_pos:
# don't start a drag until movement
return
tab_rect = self._tab_bar.tabRect( clicked_tab_index )
pixmap = QG.QPixmap( tab_rect.size() )
@ -655,9 +667,9 @@ class TabWidgetWithDnD( QW.QTabWidget ):
return
source_page_index = source_tab_bar.lastClickedTabIndex()
( source_page_index, source_page_click_global_pos ) = source_tab_bar.lastClickedTabInfo()
source_tab_bar.clearLastClickedTabIndex()
source_tab_bar.clearLastClickedTabInfo()
source_notebook = source_tab_bar.parentWidget()
source_page = source_notebook.widget( source_page_index )
@ -1183,11 +1195,15 @@ class CallAfterEvent( QC.QEvent ):
self._args = args
self._kwargs = kwargs
def Execute( self ):
self._fn( *self._args, **self._kwargs )
if self._fn is not None:
self._fn( *self._args, **self._kwargs )
class CallAfterEventFilter( QC.QObject ):
def __init__( self, parent = None ):

View File

@ -836,7 +836,7 @@ class TestClientDB( unittest.TestCase ):
( mr_hash_id, mr_hash, mr_size, mr_mime, mr_width, mr_height, mr_duration, mr_num_frames, mr_has_audio, mr_num_words ) = mr_file_info_manager.ToTuple()
mr_inbox = mr_locations_manager.GetInbox()
mr_inbox = mr_locations_manager.inbox
now = HydrusData.GetNow()
@ -985,7 +985,7 @@ class TestClientDB( unittest.TestCase ):
( mr_hash_id, mr_hash, mr_size, mr_mime, mr_width, mr_height, mr_duration, mr_num_frames, mr_has_audio, mr_num_words ) = mr_file_info_manager.ToTuple()
mr_inbox = mr_locations_manager.GetInbox()
mr_inbox = mr_locations_manager.inbox
now = HydrusData.GetNow()
@ -1007,7 +1007,7 @@ class TestClientDB( unittest.TestCase ):
( mr_hash_id, mr_hash, mr_size, mr_mime, mr_width, mr_height, mr_duration, mr_num_frames, mr_has_audio, mr_num_words ) = mr_file_info_manager.ToTuple()
mr_inbox = mr_locations_manager.GetInbox()
mr_inbox = mr_locations_manager.inbox
now = HydrusData.GetNow()

View File

@ -573,6 +573,11 @@ class Controller( object ):
return True
def isFullScreen( self ):
return True # hackery for another test
def IShouldRegularlyUpdate( self, window ):
return True

View File

@ -113,6 +113,8 @@ try:
HydrusPaths.SetEnvTempDir( result.temp_dir )
HydrusPaths.AddBaseDirToEnvPath()
#
try: