Version 270

This commit is contained in:
Hydrus Network Developer 2017-08-23 16:34:25 -05:00
parent 9b22a0ac22
commit fac67766eb
26 changed files with 640 additions and 214 deletions

View File

@ -8,8 +8,37 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 270</h3></li>
<ul>
<li>?added page tab drag and drop (windows only, have to iron out critical bugs for linux/os x).</li>
<li>dragging onto the middle of a normal tab will put the source tab there</li>
<li>dragging onto the edge of a tab will try to insert the tab there</li>
<li>dragging onto the middle of a page of pages tab will insert the tab onto the end of that page's list</li>
<li>dragging a page up a notebook level will work ok</li>
<li>dragging a notebook into itself will do nothing and not crash the client :^)</li>
<li>new pages that come from the main gui level (such as from pages menu or file import) now open in the deepest open notebook (previously, they would always appear in the top row)</li>
<li>fixed some misc page of pages bugs</li>
<li>fixed a bandwidth calculation that meant 1s time delta rules were working at 50% capacity (e.g. 1rq per 1s rule for domains were actually running at 1rq per 2s)</li>
<li>improved a bandwidth estimate calculation that was cutting out early in some situations for large time deltas</li>
<li>tag manager's page up/down shortcuts no longer mistakenly navigate the archive/delete filter</li>
<li>added a network timeout option to the 'connection' options page</li>
<li>reduced some hover window show/hide flicker when the media viewer is fullscreen and the OS has a taskbar that pops in for non-fullscreen windows (this mostly affected Linux)</li>
<li>fixed a long-time issue with yes/no dialog layout</li>
<li>improved some rendering of EXIF-rotated and -flipped jpegs. I expect to add more support for this (and retroactive image metadata-parsing to figure out correct reversed resolution--atm rotated images remain stretched) in the coming weeks</li>
<li>fixed an error when cancelling the booru-picker dialog from the page chooser dialog</li>
<li>if no files or all files are selected, the 'invert' select choice will no longer be shown (in this case, it redundantly does the same job as 'all' and 'none')</li>
<li>fixed an issue where setting namespace sort as default would persist through a client reboot</li>
<li>fixed an issue where force idle debug mode was not waking sleeping daemons</li>
<li>increased frequency of mappings processing reporting</li>
<li>wrote an exception and the needed maintenance code for the 'repairfilesystem' dialog to allow a proceed action if the only remaining incorrect paths are thumbnail paths--in this case, the client will create empty prefix folders and prompt the user to regen thumbnails</li>
<li>export filenames are now clipped to make <255 total characters in path</li>
<li>removed the old 'processing phase' option, which was no longer in use</li>
<li>removed the proxy settings from the 'connection' options page--the new engine does not use this old system, but if there is demand, a more flexible system will return</li>
<li>misc image rendering pipeline updates</li>
<li>misc improvements</li>
</ul>
<li><h3>version 269</h3></li>
<ul>
<li>nested pages now supported!</li>
<li>moved all page management (session load/save, new/close page, page navigation, page name maintenance, etc...) code from the main gui to the new PagesNotebook object</li>
<li>expanded the session object to hold nested page information</li>

View File

@ -1840,9 +1840,11 @@ class ThumbnailCache( object ):
mime = display_media.GetMime()
try:
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path, mime )
except Exception as e:
@ -1854,7 +1856,7 @@ class ThumbnailCache( object ):
try:
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path, mime )
except Exception as e:
@ -1887,7 +1889,7 @@ class ThumbnailCache( object ):
self._controller.client_files_manager.RegenerateResizedThumbnail( hash )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( path, mime )
return hydrus_bitmap
@ -1937,7 +1939,7 @@ class ThumbnailCache( object ):
f.write( thumbnail )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( temp_path )
hydrus_bitmap = ClientRendering.GenerateHydrusBitmap( temp_path, HC.IMAGE_PNG )
self._special_thumbs[ name ] = hydrus_bitmap

View File

@ -466,14 +466,6 @@ class Controller( HydrusController.HydrusController ):
def ForceIdle( self ):
HG.force_idle_mode = not HG.force_idle_mode
self.pub( 'wake_daemons' )
self.pubimmediate( 'refresh_status' )
def GetApp( self ):
return self._app

View File

@ -1502,7 +1502,7 @@ class DB( HydrusDB.HydrusDB ):
try:
phashes = ClientImageHandling.GenerateShapePerceptualHashes( path )
phashes = ClientImageHandling.GenerateShapePerceptualHashes( path, mime )
except Exception as e:
@ -7315,7 +7315,7 @@ class DB( HydrusDB.HydrusDB ):
def _ProcessRepositoryContentUpdate( self, job_key, service_id, content_update ):
FILES_CHUNK_SIZE = 20
MAPPINGS_CHUNK_SIZE = 10000
MAPPINGS_CHUNK_SIZE = 1000
NEW_TAG_PARENTS_CHUNK_SIZE = 5
total_rows = content_update.GetNumRows()
@ -8054,6 +8054,10 @@ class DB( HydrusDB.HydrusDB ):
portable_incorrect_location = HydrusPaths.ConvertAbsPathToPortablePath( incorrect_location )
portable_correct_location = HydrusPaths.ConvertAbsPathToPortablePath( correct_location )
full_abs_correct_location = os.path.join( correct_location, prefix )
HydrusPaths.MakeSureDirectoryExists( full_abs_correct_location )
self._c.execute( 'UPDATE client_files_locations SET location = ? WHERE location = ? AND prefix = ?;', ( portable_correct_location, portable_incorrect_location, prefix ) )

View File

@ -392,7 +392,10 @@ def GetSortTypeChoices():
sort_choices = list( CC.SORT_CHOICES )
sort_choices.extend( HC.options[ 'sort_by' ] )
for ( namespaces_text, namespaces_list ) in HC.options[ 'sort_by' ]:
sort_choices.append( ( namespaces_text, tuple( namespaces_list ) ) )
service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
@ -852,6 +855,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'integers' ][ 'max_page_name_chars' ] = 20
self._dictionary[ 'integers' ][ 'page_file_count_display' ] = CC.PAGE_FILE_COUNT_DISPLAY_ALL
self._dictionary[ 'integers' ][ 'network_timeout' ] = 10
#
self._dictionary[ 'keys' ] = {}

View File

@ -196,8 +196,6 @@ def GetClientDefaultOptions():
options[ 'pause_repo_sync' ] = False
options[ 'pause_subs_sync' ] = False
options[ 'processing_phase' ] = 0
options[ 'rating_dialog_position' ] = ( False, None )
return options

View File

@ -3,20 +3,23 @@ import wx
class FileDropTarget( wx.PyDropTarget ):
def __init__( self, filenames_callable = None, url_callable = None ):
def __init__( self, filenames_callable = None, url_callable = None, page_callable = None ):
wx.PyDropTarget.__init__( self )
self._filenames_callable = filenames_callable
self._url_callable = url_callable
self._page_callable = page_callable
self._receiving_data_object = wx.DataObjectComposite()
self._hydrus_media_data_object = wx.CustomDataObject( 'application/hydrus-media' )
self._hydrus_page_tab_data_object = wx.CustomDataObject( 'application/hydrus-page-tab' )
self._file_data_object = wx.FileDataObject()
self._text_data_object = wx.TextDataObject()
self._receiving_data_object.Add( self._hydrus_media_data_object, True )
self._receiving_data_object.Add( self._hydrus_page_tab_data_object )
self._receiving_data_object.Add( self._file_data_object )
self._receiving_data_object.Add( self._text_data_object )
@ -59,6 +62,13 @@ class FileDropTarget( wx.PyDropTarget ):
pass
if format_id == 'application/hydrus-page-tab' and self._page_callable is not None:
page_key = self._hydrus_page_tab_data_object.GetData()
self._page_callable( page_key )
return result

View File

@ -11,9 +11,14 @@ import os
import re
import stat
def GenerateExportFilename( media, terms ):
MAX_PATH_LENGTH = 245 # bit of padding from 255 for .txt neigbouring and other surprises
def GenerateExportFilename( destination_directory, media, terms ):
mime = media.GetMime()
if len( destination_directory ) > ( MAX_PATH_LENGTH - 10 ):
raise Exception( 'The destination directory is too long!' )
filename = ''
@ -84,13 +89,28 @@ def GenerateExportFilename( media, terms ):
filename = re.sub( '/', '_', filename, flags = re.UNICODE )
#
mime = media.GetMime()
ext = HC.mime_ext_lookup[ mime ]
if not filename.endswith( ext ):
if filename.endswith( ext ):
filename += ext
filename = filename[ : - len( ext ) ]
example_dest_path = os.path.join( destination_directory, filename + ext )
excess_chars = len( example_dest_path ) - MAX_PATH_LENGTH
if excess_chars > 0:
filename = filename[ : - excess_chars ]
filename = filename + ext
return filename
def GetExportPath():
@ -299,7 +319,7 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
source_path = client_files_manager.GetFilePath( hash, mime )
filename = GenerateExportFilename( media_result, terms )
filename = GenerateExportFilename( folder_path, media_result, terms )
dest_path = os.path.join( folder_path, filename )

View File

@ -71,8 +71,6 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
ClientGUITopLevelWindows.FrameThatResizes.__init__( self, None, title, 'main_gui', float_on_parent = False )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self.ImportFiles, self.ImportURL ) )
bandwidth_width = ClientData.ConvertTextToPixelWidth( self, 17 )
idle_width = ClientData.ConvertTextToPixelWidth( self, 6 )
system_busy_width = ClientData.ConvertTextToPixelWidth( self, 13 )
@ -92,6 +90,8 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
self._notebook = ClientGUIPages.PagesNotebook( self, self._controller, 'top page notebook' )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self.ImportFiles, self.ImportURL, self._notebook.PageDragAndDropDropped ) )
wx.GetApp().SetTopWindow( self )
self._message_manager = ClientGUIPopupMessages.PopupMessageManager( self )
@ -278,7 +278,11 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
self._controller.SetServices( all_services )
HydrusData.ShowText( 'Auto repo setup done! Check services->review services to see your new services.' )
message = 'Auto repo setup done! Check services->review services to see your new services.'
message += os.linesep * 2
message += 'The PTR has a lot of tags and will sync a little bit at a time when you are not using the client. Expect it to take a few weeks to sync fully.'
HydrusData.ShowText( message )
text = 'This will attempt to set up your client with my repositories\' credentials, letting you tag on the public tag repository and see some files.'
@ -1010,12 +1014,12 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
petition_resolvable_repositories = [ repository for repository in repositories if True in ( repository.HasPermission( content_type, action ) for ( content_type, action ) in petition_permissions ) ]
ClientGUIMenus.AppendMenuItem( self, search_menu, 'my files', 'Open a new search tab for your files.', self._notebook.NewPageQuery, CC.LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( self, search_menu, 'trash', 'Open a new search tab for your recently deleted files.', self._notebook.NewPageQuery, CC.TRASH_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( self, search_menu, 'my files', 'Open a new search tab for your files.', self._notebook.NewPageQuery, CC.LOCAL_FILE_SERVICE_KEY, on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, search_menu, 'trash', 'Open a new search tab for your recently deleted files.', self._notebook.NewPageQuery, CC.TRASH_SERVICE_KEY, on_deepest_notebook = True )
for service in file_repositories:
ClientGUIMenus.AppendMenuItem( self, search_menu, service.GetName(), 'Open a new search tab for ' + service.GetName() + '.', self._notebook.NewPageQuery, service.GetServiceKey() )
ClientGUIMenus.AppendMenuItem( self, search_menu, service.GetName(), 'Open a new search tab for ' + service.GetName() + '.', self._notebook.NewPageQuery, service.GetServiceKey(), on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( menu, search_menu, 'new search page' )
@ -1028,7 +1032,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
for service in petition_resolvable_repositories:
ClientGUIMenus.AppendMenuItem( self, petition_menu, service.GetName(), 'Open a new petition page for ' + service.GetName() + '.', self._notebook.NewPagePetitions, service.GetServiceKey() )
ClientGUIMenus.AppendMenuItem( self, petition_menu, service.GetName(), 'Open a new petition page for ' + service.GetName() + '.', self._notebook.NewPagePetitions, service.GetServiceKey(), on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( menu, petition_menu, 'new petition page' )
@ -1038,23 +1042,23 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
download_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, download_menu, 'url download', 'Open a new tab to download some raw urls.', self._notebook.NewPageImportURLs )
ClientGUIMenus.AppendMenuItem( self, download_menu, 'thread watcher', 'Open a new tab to watch a thread.', self._notebook.NewPageImportThreadWatcher )
ClientGUIMenus.AppendMenuItem( self, download_menu, 'webpage of images', 'Open a new tab to download files from generic galleries or threads.', self._notebook.NewPageImportPageOfImages )
ClientGUIMenus.AppendMenuItem( self, download_menu, 'url download', 'Open a new tab to download some raw urls.', self._notebook.NewPageImportURLs, on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, download_menu, 'thread watcher', 'Open a new tab to watch a thread.', self._notebook.NewPageImportThreadWatcher, on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, download_menu, 'webpage of images', 'Open a new tab to download files from generic galleries or threads.', self._notebook.NewPageImportPageOfImages, on_deepest_notebook = True )
gallery_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'booru', 'Open a new tab to download files from a booru.', self._notebook.NewPageImportBooru )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'deviant art', 'Open a new tab to download files from Deviant Art.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_DEVIANT_ART ) )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'booru', 'Open a new tab to download files from a booru.', self._notebook.NewPageImportBooru, on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'deviant art', 'Open a new tab to download files from Deviant Art.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_DEVIANT_ART ), on_deepest_notebook = True )
hf_submenu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, hf_submenu, 'by artist', 'Open a new tab to download files from Hentai Foundry.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_HENTAI_FOUNDRY_ARTIST ) )
ClientGUIMenus.AppendMenuItem( self, hf_submenu, 'by tags', 'Open a new tab to download files from Hentai Foundry.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_HENTAI_FOUNDRY_TAGS ) )
ClientGUIMenus.AppendMenuItem( self, hf_submenu, 'by artist', 'Open a new tab to download files from Hentai Foundry.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_HENTAI_FOUNDRY_ARTIST ), on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, hf_submenu, 'by tags', 'Open a new tab to download files from Hentai Foundry.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_HENTAI_FOUNDRY_TAGS ), on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( gallery_menu, hf_submenu, 'hentai foundry' )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'newgrounds', 'Open a new tab to download files from Newgrounds.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_NEWGROUNDS ) )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'newgrounds', 'Open a new tab to download files from Newgrounds.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_NEWGROUNDS ), on_deepest_notebook = True )
result = self._controller.Read( 'serialisable_simple', 'pixiv_account' )
@ -1062,13 +1066,13 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
pixiv_submenu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, pixiv_submenu, 'by artist id', 'Open a new tab to download files from Pixiv.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_PIXIV_ARTIST_ID ) )
ClientGUIMenus.AppendMenuItem( self, pixiv_submenu, 'by tag', 'Open a new tab to download files from Pixiv.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_PIXIV_TAG ) )
ClientGUIMenus.AppendMenuItem( self, pixiv_submenu, 'by artist id', 'Open a new tab to download files from Pixiv.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_PIXIV_ARTIST_ID ), on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, pixiv_submenu, 'by tag', 'Open a new tab to download files from Pixiv.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_PIXIV_TAG ), on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( gallery_menu, pixiv_submenu, 'pixiv' )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'tumblr', 'Open a new tab to download files from tumblr.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_TUMBLR ) )
ClientGUIMenus.AppendMenuItem( self, gallery_menu, 'tumblr', 'Open a new tab to download files from tumblr.', self._notebook.NewPageImportGallery, ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_TUMBLR ), on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( download_menu, gallery_menu, 'gallery' )
ClientGUIMenus.AppendMenu( menu, download_menu, 'new download page' )
@ -1092,8 +1096,8 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
special_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, special_menu, 'page of pages', 'Open a new tab that can hold more tabs.', self._notebook.NewPagesNotebook )
ClientGUIMenus.AppendMenuItem( self, special_menu, 'duplicates processing', 'Open a new tab to discover and filter duplicate files.', self._notebook.NewPageDuplicateFilter )
ClientGUIMenus.AppendMenuItem( self, special_menu, 'page of pages', 'Open a new tab that can hold more tabs.', self._notebook.NewPagesNotebook, on_deepest_notebook = True )
ClientGUIMenus.AppendMenuItem( self, special_menu, 'duplicates processing', 'Open a new tab to discover and filter duplicate files.', self._notebook.NewPageDuplicateFilter, on_deepest_notebook = True )
ClientGUIMenus.AppendMenu( menu, special_menu, 'new special page' )
@ -1417,6 +1421,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
ClientGUIMenus.AppendMenuCheckItem( self, debug_modes, 'network report mode', 'Have the network engine report new jobs.', HG.network_report_mode, self._SwitchBoolean, 'network_report_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug_modes, 'pubsub profile mode', 'Run detailed \'profiles\' on every internal publisher/subscriber message and dump this information to the log. This can hammer your log with dozens of large dumps every second. Don\'t run it unless you know you need to.', HG.pubsub_profile_mode, self._SwitchBoolean, 'pubsub_profile_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug_modes, 'force idle mode', 'Make the client consider itself idle and fire all maintenance routines right now. This may hang the gui for a while.', HG.force_idle_mode, self._SwitchBoolean, 'force_idle_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug_modes, 'no page limit mode', 'Let the user create as many pages as they want with no warnings or prohibitions.', HG.no_page_limit_mode, self._SwitchBoolean, 'no_page_limit_mode' )
ClientGUIMenus.AppendMenu( debug, debug_modes, 'modes' )
@ -1637,7 +1642,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
if load_a_blank_page:
self._notebook.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY )
self._notebook.NewPageQuery( CC.LOCAL_FILE_SERVICE_KEY, on_deepest_notebook = True )
else:
@ -2356,6 +2361,13 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HG.force_idle_mode = not HG.force_idle_mode
self._controller.pub( 'wake_daemons' )
self._controller.pubimmediate( 'refresh_status' )
elif name == 'no_page_limit_mode':
HG.no_page_limit_mode = not HG.no_page_limit_mode
def _UnclosePage( self, closed_page_index = None ):
@ -2813,17 +2825,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def EventFrameNewPage( self, event ):
screen_position = self.ClientToScreen( event.GetPosition() )
new_position = self._notebook.ScreenToClient( screen_position )
self._notebook.EventNewPageFromMousePosition( new_position )
self._notebook.EventNewPageFromScreenPosition( screen_position )
def EventFrameNotebookMenu( self, event ):
screen_position = self.ClientToScreen( event.GetPosition() )
new_position = self._notebook.ScreenToClient( screen_position )
self._notebook.EventMenuFromMousePosition( new_position )
self._notebook.EventMenuFromScreenPosition( screen_position )
def TIMEREventBandwidth( self, event ):
@ -3020,7 +3030,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
# change this when thread watchers can support multiple sub-pages etc...
self._notebook.NewPageImportThreadWatcher( url )
self._notebook.NewPageImportThreadWatcher( url, on_deepest_notebook = True )
else:
@ -3055,7 +3065,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
management_controller = ClientGUIManagement.CreateManagementControllerImportHDD( paths, import_file_options, paths_to_tags, delete_after_success )
self._notebook.NewPage( management_controller )
self._notebook.NewPage( management_controller, on_deepest_notebook = True )
def NewPageQuery( self, service_key, initial_hashes = None, initial_predicates = None, page_name = None ):
@ -3070,7 +3080,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
initial_predicates = []
self._notebook.NewPageQuery( service_key, initial_hashes = initial_hashes, initial_predicates = initial_predicates, page_name = page_name )
self._notebook.NewPageQuery( service_key, initial_hashes = initial_hashes, initial_predicates = initial_predicates, page_name = page_name, on_deepest_notebook = True )
def NotifyClosedPage( self, page ):

View File

@ -4027,9 +4027,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
self.Bind( wx.EVT_MENU, self.EventMenu )
HG.client_controller.sub( self, 'Delete', 'canvas_delete' )
HG.client_controller.sub( self, 'Skip', 'canvas_show_next' )
HG.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
HG.client_controller.sub( self, 'Back', 'canvas_show_previous' )
wx.CallAfter( self.SetMedia, self._GetFirst() ) # don't set this until we have a size > (20, 20)!
@ -5418,10 +5416,11 @@ class EmbedButton( wx.Window ):
if needs_thumb:
hash = self._media.GetHash()
mime = self._media.GetMime()
thumbnail_path = HG.client_controller.client_files_manager.GetFullSizeThumbnailPath( hash )
self._thumbnail_bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path ).GetWxBitmap()
self._thumbnail_bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetWxBitmap()
self._SetDirty()
@ -5446,10 +5445,11 @@ class OpenExternallyPanel( wx.Panel ):
if self._media.GetLocationsManager().IsLocal() and self._media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
hash = self._media.GetHash()
mime = self._media.GetMime()
thumbnail_path = HG.client_controller.client_files_manager.GetFullSizeThumbnailPath( hash )
bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path ).GetWxBitmap()
bmp = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetWxBitmap()
thumbnail = ClientGUICommon.BufferedWindowIcon( self, bmp )

View File

@ -830,7 +830,7 @@ class DialogInputLocalFiles( Dialog ):
Dialog.__init__( self, parent, 'importing files' )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self._AddPathsToList, None ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( filenames_callable = self._AddPathsToList ) )
listctrl_panel = ClientGUIListCtrl.SaneListCtrlPanel( self )
@ -3229,7 +3229,7 @@ class DialogSetupExport( Dialog ):
directory = HydrusData.ToUnicode( self._directory_picker.GetPath() )
filename = ClientExporting.GenerateExportFilename( media, terms )
filename = ClientExporting.GenerateExportFilename( directory, media, terms )
return os.path.join( directory, filename )
@ -3595,7 +3595,7 @@ class DialogYesNo( Dialog ):
text.Wrap( 480 )
vbox.AddF( text, CC.FLAGS_BIG_INDENT )
vbox.AddF( text, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( hbox, CC.FLAGS_BUTTON_SIZER )
self.SetSizer( vbox )

View File

@ -120,7 +120,7 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
self.SetSizer( vbox )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self.Import, None ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( filenames_callable = self.Import ) )
( x, y ) = self.GetEffectiveMinSize()
@ -617,7 +617,7 @@ class DialogManageContacts( ClientGUIDialogs.Dialog ):
self.SetInitialSize( ( 980, y ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self.Import, None ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( filenames_callable = self.Import ) )
self.EventContactChanged( None )
@ -1517,7 +1517,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
self.SetInitialSize( ( 980, y ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self.Import, None ) )
self.SetDropTarget( ClientDragDrop.FileDropTarget( filenames_callable = self.Import ) )
wx.CallAfter( self._ok.SetFocus )

View File

@ -108,11 +108,30 @@ class FullscreenHoverFrame( wx.Frame ):
( should_resize, ( my_ideal_width, my_ideal_height ), ( my_ideal_x, my_ideal_y ) ) = self._GetIdealSizeAndPosition()
if my_ideal_width == -1: my_ideal_width = my_width
if my_ideal_height == -1: my_ideal_height = my_height
if my_ideal_width == -1:
my_ideal_width = max( my_width, 50 )
in_x = my_ideal_x <= mouse_x and mouse_x <= my_ideal_x + my_ideal_width
in_y = my_ideal_y <= mouse_y and mouse_y <= my_ideal_y + my_ideal_height
if my_ideal_height == -1:
my_ideal_height = max( my_height, 50 )
( my_x, my_y ) = self.GetPosition()
in_ideal_x = my_ideal_x <= mouse_x and mouse_x <= my_ideal_x + my_ideal_width
in_ideal_y = my_ideal_y <= mouse_y and mouse_y <= my_ideal_y + my_ideal_height
in_actual_x = my_x <= mouse_x and mouse_x <= my_x + my_width
in_actual_y = my_y <= mouse_y and mouse_y <= my_y + my_height
# we test both ideal and actual here because setposition is not always honoured by the OS
# for instance, in Linux on a fullscreen view, the top taskbar is hidden, but when hover window is shown, it takes focus and causes taskbar to reappear
# the reappearance shuffles the screen coordinates down a bit so the hover sits +20px y despite wanting to be lined up with the underlying fullscreen viewer
# wew lad
in_position = ( in_ideal_x or in_actual_x ) and ( in_ideal_y or in_actual_y )
menu_open = HG.client_controller.MenuIsOpen()
@ -130,8 +149,6 @@ class FullscreenHoverFrame( wx.Frame ):
mime = self._current_media.GetMime()
in_position = in_x and in_y
mouse_is_over_interactable_media = mime == HC.APPLICATION_FLASH and self.GetParent().MouseIsOverMedia()
mouse_is_near_animation_bar = self.GetParent().MouseIsNearAnimationBar()

View File

@ -436,7 +436,7 @@ def GenerateDumpMultipartFormDataCTAndBody( fields ):
with open( temp_path, 'wb' ) as f: f.write( jpeg )
self._bitmap = ClientRendering.GenerateHydrusBitmap( temp_path )
self._bitmap = ClientRendering.GenerateHydrusBitmap( temp_path, HC.IMAGE_JPEG )
finally:

View File

@ -1730,7 +1730,7 @@ class MediaPanelThumbnails( MediaPanel ):
self.SetScrollRate( 0, thumbnail_span_height )
self.Bind( wx.EVT_LEFT_DOWN, self.EventSelection )
self.Bind( wx.EVT_MOTION, self.EventDragTest )
self.Bind( wx.EVT_MOTION, self.EventDrag )
self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )
self.Bind( wx.EVT_LEFT_DCLICK, self.EventMouseFullScreen )
self.Bind( wx.EVT_MIDDLE_DOWN, self.EventMouseFullScreen )
@ -2342,7 +2342,7 @@ class MediaPanelThumbnails( MediaPanel ):
def EventDragTest( self, event ):
def EventDrag( self, event ):
if event.LeftIsDown() and self._drag_init_coordinates is not None:
@ -2656,8 +2656,11 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMenus.AppendMenuItem( self, select_menu, 'all', 'Select everything.', self._Select, 'all' )
ClientGUIMenus.AppendMenuItem( self, select_menu, 'invert', 'Swap what is and is not selected.', self._Select, 'invert' )
if len( self._selected_media ) > 0:
ClientGUIMenus.AppendMenuItem( self, select_menu, 'invert', 'Swap what is and is not selected.', self._Select, 'invert' )
if media_has_archive and media_has_inbox:
@ -3297,8 +3300,11 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMenus.AppendMenuItem( self, select_menu, 'all', 'Select everything.', self._Select, 'all' )
ClientGUIMenus.AppendMenuItem( self, select_menu, 'invert', 'Swap what is and is not selected.', self._Select, 'invert' )
if len( self._selected_media ) > 0:
ClientGUIMenus.AppendMenuItem( self, select_menu, 'invert', 'Swap what is and is not selected.', self._Select, 'invert' )
if media_has_archive and media_has_inbox:

View File

@ -184,6 +184,8 @@ class DialogPageChooser( ClientGUIDialogs.Dialog ):
self.EndModal( wx.ID_CANCEL )
return
elif entry_type == 'page_import_gallery':
@ -719,6 +721,8 @@ class PagesNotebook( wx.Notebook ):
self._next_new_page_index = None
self._potential_drag_page = None
self._closed_pages = []
self._page_key = HydrusData.GenerateKey()
@ -726,6 +730,12 @@ class PagesNotebook( wx.Notebook ):
self._controller.sub( self, 'RefreshPageName', 'refresh_page_name' )
self._controller.sub( self, 'NotifyPageUnclosed', 'notify_page_unclosed' )
if HC.PLATFORM_WINDOWS:
self.Bind( wx.EVT_MOTION, self.EventDrag )
self.Bind( wx.EVT_LEFT_DOWN, self.EventLeftDown )
self.Bind( wx.EVT_LEFT_DCLICK, self.EventLeftDoubleClick )
self.Bind( wx.EVT_MIDDLE_DOWN, self.EventMiddleClick )
self.Bind( wx.EVT_RIGHT_DOWN, self.EventMenu )
@ -819,10 +829,14 @@ class PagesNotebook( wx.Notebook ):
page.PrepareToHide()
self._closed_pages.append( ( index, page.GetPageKey() ) )
page_key = page.GetPageKey()
self._closed_pages.append( ( index, page_key ) )
self.RemovePage( index )
self._controller.pub( 'refresh_page_name', self._page_key )
self._controller.pub( 'notify_closed_page', page )
self._controller.pub( 'notify_new_undo' )
@ -916,35 +930,6 @@ class PagesNotebook( wx.Notebook ):
return results
def _GetNotebookFromMousePosition( self, position ):
current_page = self.GetCurrentPage()
if current_page is None or not isinstance( current_page, PagesNotebook ):
return ( self, position )
else:
( tab_index, flags ) = self.HitTest( position )
if tab_index != wx.NOT_FOUND:
return ( self, position )
if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE: # not on a label but inside my client area
screen_position = self.ClientToScreen( position )
new_position = current_page.ScreenToClient( screen_position )
return current_page._GetNotebookFromMousePosition( new_position )
return ( self, position )
def _GetIndex( self, page_key ):
for ( page, index ) in ( ( self.GetPage( index ), index ) for index in range( self.GetPageCount() ) ):
@ -958,11 +943,60 @@ class PagesNotebook( wx.Notebook ):
raise HydrusExceptions.DataMissing()
def _GetNotebookFromScreenPosition( self, screen_position ):
current_page = self.GetCurrentPage()
if current_page is None or not isinstance( current_page, PagesNotebook ):
return self
else:
position = self.ScreenToClient( screen_position )
( tab_index, flags ) = self.HitTest( position )
if tab_index != wx.NOT_FOUND:
return self
if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE: # not on a label but inside my client area
return current_page._GetNotebookFromScreenPosition( screen_position )
return self
def _GetPages( self ):
return [ self.GetPage( i ) for i in range( self.GetPageCount() ) ]
def _GetPageFromPageKey( self, page_key ):
for page in self._GetPages():
if page.GetPageKey() == page_key:
return page
if isinstance( page, PagesNotebook ):
if page.HasPageKey( page_key ):
return page._GetPageFromPageKey( page_key )
return None
def _MovePage( self, page_index, delta = None, new_index = None ):
new_page_index = page_index
@ -1056,7 +1090,9 @@ class PagesNotebook( wx.Notebook ):
def _ShowMenu( self, position ):
def _ShowMenu( self, screen_position ):
position = self.ScreenToClient( screen_position )
( tab_index, flags ) = self.HitTest( position )
@ -1258,6 +1294,50 @@ class PagesNotebook( wx.Notebook ):
def EventDrag( self, event ):
if event.LeftIsDown() and self._potential_drag_page is not None:
drop_source = wx.DropSource( self )
data_object = wx.DataObjectComposite()
#
hydrus_page_tab_data_object = wx.CustomDataObject( 'application/hydrus-page-tab' )
data = self._potential_drag_page.GetPageKey()
hydrus_page_tab_data_object.SetData( data )
data_object.Add( hydrus_page_tab_data_object, True )
#
drop_source.SetData( data_object )
drop_source.DoDragDrop()
self._potential_drag_page = None
def EventLeftDown( self, event ):
position = event.GetPosition()
( tab_index, flags ) = self.HitTest( position )
if tab_index != -1:
page = self.GetPage( tab_index )
self._potential_drag_page = page
event.Skip()
def EventLeftDoubleClick( self, event ):
position = event.GetPosition()
@ -1268,9 +1348,11 @@ class PagesNotebook( wx.Notebook ):
if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE:
( notebook, new_position ) = self._GetNotebookFromMousePosition( position )
screen_position = self.ClientToScreen( position )
notebook.EventNewPageMousePosition( new_position )
notebook = self._GetNotebookFromScreenPosition( screen_position )
notebook.EventNewPageFromScreenPosition( screen_position )
else:
@ -1281,27 +1363,33 @@ class PagesNotebook( wx.Notebook ):
def EventMenu( self, event ):
self._ShowMenu( event.GetPosition() )
screen_position = self.ClientToScreen( event.GetPosition() )
self._ShowMenu( screen_position )
def EventMenuFromMousePosition( self, position ):
def EventMenuFromScreenPosition( self, position ):
( notebook, new_position ) = self._GetNotebookFromMousePosition( position )
notebook = self._GetNotebookFromScreenPosition( position )
notebook._ShowMenu( position )
def EventMiddleClick( self, event ):
( tab_index, flags ) = self.HitTest( event.GetPosition() )
position = event.GetPosition()
( tab_index, flags ) = self.HitTest( position )
if tab_index == wx.NOT_FOUND:
if flags & wx.NB_HITTEST_NOWHERE and flags & wx.NB_HITTEST_ONPAGE:
( notebook, new_position ) = self._GetNotebookFromMousePosition( event.GetPosition() )
screen_position = self.ClientToScreen( position )
notebook.EventNewPageFromMousePosition( new_position )
notebook = self._GetNotebookFromScreenPosition( screen_position )
notebook.EventNewPageFromScreenPosition( screen_position )
else:
@ -1314,9 +1402,9 @@ class PagesNotebook( wx.Notebook ):
def EventNewPageFromMousePosition( self, position ):
def EventNewPageFromScreenPosition( self, position ):
( notebook, new_position ) = self._GetNotebookFromMousePosition( position )
notebook = self._GetNotebookFromScreenPosition( position )
notebook._ChooseNewPage()
@ -1421,7 +1509,7 @@ class PagesNotebook( wx.Notebook ):
# import page does not exist
return self.NewPageImportURLs()
return self.NewPageImportURLs( on_deepest_notebook = True )
def GetPageKey( self ):
@ -1500,27 +1588,39 @@ class PagesNotebook( wx.Notebook ):
self.AppendGUISession( name )
def NewPage( self, management_controller, initial_hashes = None, forced_insertion_index = None ):
def NewPage( self, management_controller, initial_hashes = None, forced_insertion_index = None, on_deepest_notebook = False ):
MAX_TOTAL_PAGES = 150
current_page = self.GetCurrentPage()
( total_active_page_count, total_closed_page_count ) = self._controller.gui.GetTotalPageCounts()
if total_active_page_count + total_closed_page_count >= MAX_TOTAL_PAGES:
if on_deepest_notebook and isinstance( current_page, PagesNotebook ):
self._controller.gui.DeleteAllClosedPages()
if total_active_page_count >= MAX_TOTAL_PAGES:
HydrusData.ShowText( 'The client cannot have more than ' + str( MAX_TOTAL_PAGES ) + ' pages open! For system stability reasons, please close some now!' )
current_page.NewPage( management_controller, initial_hashes = initial_hashes, forced_insertion_index = forced_insertion_index, on_deepest_notebook = on_deepest_notebook )
return
if total_active_page_count == MAX_TOTAL_PAGES - 5:
if not HG.no_page_limit_mode:
HydrusData.ShowText( 'You have ' + str( total_active_page_count ) + ' pages open! You can only open a few more before system stability is affected! Please close some now!' )
MAX_TOTAL_PAGES = 150
( total_active_page_count, total_closed_page_count ) = self._controller.gui.GetTotalPageCounts()
if total_active_page_count + total_closed_page_count >= MAX_TOTAL_PAGES:
self._controller.gui.DeleteAllClosedPages()
if total_active_page_count >= MAX_TOTAL_PAGES:
HydrusData.ShowText( 'The client cannot have more than ' + str( MAX_TOTAL_PAGES ) + ' pages open! For system stability reasons, please close some now!' )
return
if total_active_page_count == MAX_TOTAL_PAGES - 5:
HydrusData.ShowText( 'You have ' + str( total_active_page_count ) + ' pages open! You can only open a few more before system stability is affected! Please close some now!' )
self._controller.ResetIdleTimer()
@ -1563,14 +1663,14 @@ class PagesNotebook( wx.Notebook ):
return page
def NewPageDuplicateFilter( self ):
def NewPageDuplicateFilter( self, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerDuplicateFilter()
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPageImportBooru( self ):
def NewPageImportBooru( self, on_deepest_notebook = False ):
with ClientGUIDialogs.DialogSelectBooru( self ) as dlg:
@ -1578,47 +1678,47 @@ class PagesNotebook( wx.Notebook ):
gallery_identifier = dlg.GetGalleryIdentifier()
return self.NewPageImportGallery( gallery_identifier )
return self.NewPageImportGallery( gallery_identifier, on_deepest_notebook = on_deepest_notebook )
def NewPageImportGallery( self, gallery_identifier ):
def NewPageImportGallery( self, gallery_identifier, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerImportGallery( gallery_identifier )
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPageImportPageOfImages( self ):
def NewPageImportPageOfImages( self, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerImportPageOfImages()
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPageImportThreadWatcher( self, thread_url = None ):
def NewPageImportThreadWatcher( self, thread_url = None, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerImportThreadWatcher( thread_url )
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPageImportURLs( self ):
def NewPageImportURLs( self, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerImportURLs()
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPagePetitions( self, service_key ):
def NewPagePetitions( self, service_key, on_deepest_notebook = False ):
management_controller = ClientGUIManagement.CreateManagementControllerPetitions( service_key )
return self.NewPage( management_controller )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
def NewPageQuery( self, file_service_key, initial_hashes = None, initial_predicates = None, page_name = None ):
def NewPageQuery( self, file_service_key, initial_hashes = None, initial_predicates = None, page_name = None, on_deepest_notebook = False ):
if initial_hashes is None:
@ -1650,10 +1750,19 @@ class PagesNotebook( wx.Notebook ):
management_controller = ClientGUIManagement.CreateManagementControllerQuery( page_name, file_service_key, file_search_context, search_enabled )
return self.NewPage( management_controller, initial_hashes = initial_hashes )
return self.NewPage( management_controller, initial_hashes = initial_hashes, on_deepest_notebook = on_deepest_notebook )
def NewPagesNotebook( self, name = 'pages', forced_insertion_index = None ):
def NewPagesNotebook( self, name = 'pages', forced_insertion_index = None, on_deepest_notebook = False ):
current_page = self.GetCurrentPage()
if on_deepest_notebook and isinstance( current_page, PagesNotebook ):
current_page.NewPagesNotebook( name = name, forced_insertion_index = forced_insertion_index, on_deepest_notebook = on_deepest_notebook )
return
self._controller.ResetIdleTimer()
self._controller.ResetPageChangeTimer()
@ -1732,6 +1841,135 @@ class PagesNotebook( wx.Notebook ):
def PageDragAndDropDropped( self, page_key ):
page = self._GetPageFromPageKey( page_key )
if page is None:
return
source_notebook = page.GetParent()
screen_position = wx.GetMousePosition()
dest_notebook = self._GetNotebookFromScreenPosition( screen_position )
( x, y ) = dest_notebook.ScreenToClient( screen_position )
( tab_index, flags ) = dest_notebook.HitTest( ( x, y ) )
EDGE_PADDING = 10
( left_tab_index, gumpf ) = dest_notebook.HitTest( ( x - EDGE_PADDING, y ) )
( right_tab_index, gumpf ) = dest_notebook.HitTest( ( x + EDGE_PADDING, y ) )
landed_near_left_edge = left_tab_index != tab_index
landed_near_right_edge = right_tab_index != tab_index
landed_on_edge = landed_near_right_edge or landed_near_left_edge
landed_in_middle = not landed_on_edge
there_is_a_page_to_the_left = tab_index > 0
there_is_a_page_to_the_right = tab_index < dest_notebook.GetPageCount() - 1
page_on_left_is_source = there_is_a_page_to_the_left and dest_notebook.GetPage( tab_index - 1 ) == page
page_on_right_is_source = there_is_a_page_to_the_right and dest_notebook.GetPage( tab_index + 1 ) == page
if tab_index == wx.NOT_FOUND:
# if it isn't dropped on anything, put it on the end
tab_index = dest_notebook.GetPageCount()
if tab_index > 0:
if dest_notebook.GetPage( tab_index - 1 ) == page:
return
else:
# dropped on source and not on the right edge: do nothing
landee_page = dest_notebook.GetPage( tab_index )
if landee_page == page:
if landed_near_right_edge and there_is_a_page_to_the_right:
tab_index += 1
else:
return
# dropped just to the left of source: do nothing
if landed_near_right_edge and page_on_right_is_source:
return
# dropped on left side of an edge: insert on right side
if landed_near_right_edge:
tab_index += 1
if landed_in_middle and isinstance( landee_page, PagesNotebook ):
dest_notebook = landee_page
tab_index = dest_notebook.GetPageCount()
if dest_notebook == page or ClientGUICommon.IsWXAncestor( dest_notebook, page ):
# can't drop a notebook beneath itself!
return
#
insertion_tab_index = tab_index
for ( index, p ) in enumerate( source_notebook._GetPages() ):
if p == page:
if source_notebook == dest_notebook and index + 1 < insertion_tab_index:
# we are just about to remove it from earlier in the same list, which shuffles the inserting index up one
insertion_tab_index -= 1
source_notebook.RemovePage( index )
break
if source_notebook != dest_notebook:
page.Reparent( dest_notebook )
dest_notebook.InsertPage( insertion_tab_index, page, 'page' )
self.ShowPage( page )
self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )
self._controller.pub( 'refresh_page_name', page.GetPageKey() )
def PrepareToHide( self ):
for page in self._GetPages():

View File

@ -1220,12 +1220,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._listbook = ClientGUICommon.ListBook( self )
self._listbook.AddPage( 'gui', 'gui', self._GUIPanel( self._listbook ) ) # leave this at the top, to make it default page
self._listbook.AddPage( 'connection', 'connection', self._ConnectionPanel( self._listbook ) )
self._listbook.AddPage( 'files and trash', 'files and trash', self._FilesAndTrashPanel( self._listbook ) )
self._listbook.AddPage( 'speed and memory', 'speed and memory', self._SpeedAndMemoryPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'maintenance and processing', 'maintenance and processing', self._MaintenanceAndProcessingPanel( self._listbook ) )
self._listbook.AddPage( 'media', 'media', self._MediaPanel( self._listbook ) )
self._listbook.AddPage( 'gui', 'gui', self._GUIPanel( self._listbook ) )
#self._listbook.AddPage( 'sound', 'sound', self._SoundPanel( self._listbook ) )
self._listbook.AddPage( 'default file system predicates', 'default file system predicates', self._DefaultFileSystemPredicatesPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'default tag import options', 'default tag import options', self._DefaultTagImportOptionsPanel( self._listbook, self._new_options ) )
@ -1663,6 +1663,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._external_host = wx.TextCtrl( self )
self._external_host.SetToolTipString( 'If you have trouble parsing your external ip using UPnP, you can force it to be this.' )
self._network_timeout = wx.SpinCtrl( self, min = 3, max = 300 )
self._network_timeout.SetToolTipString( 'If a network connection experiences any uninterrupted inactivity for this duration, it will throw an error.' )
proxy_panel = ClientGUICommon.StaticBox( self, 'proxy settings' )
self._proxy_type = ClientGUICommon.BetterChoice( proxy_panel )
@ -1680,6 +1683,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._external_host.SetValue( HC.options[ 'external_host' ] )
self._new_options = HG.client_controller.GetNewOptions()
self._network_timeout.SetValue( self._new_options.GetInteger( 'network_timeout' ) )
self._proxy_type.Append( 'http', 'http' )
self._proxy_type.Append( 'socks4', 'socks4' )
self._proxy_type.Append( 'socks5', 'socks5' )
@ -1730,11 +1737,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
proxy_panel.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
proxy_panel.Hide() # proxy settings no longer in use for new engine
#
rows = []
rows.append( ( 'external ip/host override: ', self._external_host ) )
rows.append( ( 'network timeout (seconds): ', self._network_timeout ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
@ -1775,6 +1785,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'external_host' ] = external_host
self._new_options.SetInteger( 'network_timeout', self._network_timeout.GetValue() )
class _DownloadingPanel( wx.Panel ):
@ -1871,7 +1883,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._jobs_panel = ClientGUICommon.StaticBox( self, 'when to run high cpu jobs' )
self._maintenance_panel = ClientGUICommon.StaticBox( self, 'maintenance period' )
self._processing_panel = ClientGUICommon.StaticBox( self, 'processing' )
self._idle_panel = ClientGUICommon.StaticBox( self._jobs_panel, 'idle' )
self._shutdown_panel = ClientGUICommon.StaticBox( self._jobs_panel, 'shutdown' )
@ -1904,11 +1915,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
self._processing_phase = wx.SpinCtrl( self._processing_panel, min = 0, max = 100000 )
self._processing_phase.SetToolTipString( 'how long this client will delay processing updates after they are due. useful if you have multiple clients and do not want them to process at the same time' )
#
self._idle_normal.SetValue( HC.options[ 'idle_normal' ] )
self._idle_period.SetValue( HC.options[ 'idle_period' ] )
self._idle_mouse_period.SetValue( HC.options[ 'idle_mouse_period' ] )
@ -1919,8 +1925,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._maintenance_vacuum_period_days.SetValue( self._new_options.GetNoneableInteger( 'maintenance_vacuum_period_days' ) )
self._processing_phase.SetValue( HC.options[ 'processing_phase' ] )
#
rows = []
@ -1975,21 +1979,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
#
rows = []
rows.append( ( 'Delay repository update processing by (s): ', self._processing_phase ) )
gridbox = ClientGUICommon.WrapInGrid( self._processing_panel, rows )
self._processing_panel.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._jobs_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._maintenance_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._processing_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
@ -2046,8 +2039,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'idle_shutdown' ] = self._idle_shutdown.GetChoice()
HC.options[ 'idle_shutdown_max_minutes' ] = self._idle_shutdown_max_minutes.GetValue()
HC.options[ 'processing_phase' ] = self._processing_phase.GetValue()
self._new_options.SetNoneableInteger( 'maintenance_vacuum_period_days', self._maintenance_vacuum_period_days.GetValue() )
@ -2563,7 +2554,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._disable_cv_for_gifs.SetToolTipString( 'OpenCV is good at rendering gifs, but if you have problems with it and your graphics card, check this and the less reliable and slower PIL will be used instead.' )
self._load_images_with_pil = wx.CheckBox( self )
self._load_images_with_pil.SetToolTipString( 'OpenCV is much faster than PIL, but the current release crashes on certain images. You can try turning this off, but switch it back on if you have any problems.' )
self._load_images_with_pil.SetToolTipString( 'OpenCV is much faster than PIL, but it is sometimes less reliable. Switch this on if you experience crashes or other unusual problems while importing or viewing certain images.' )
self._use_system_ffmpeg = wx.CheckBox( self )
self._use_system_ffmpeg.SetToolTipString( 'Check this to always default to the system ffmpeg in your path, rather than using the static ffmpeg in hydrus\'s bin directory. (requires restart)' )
@ -6093,14 +6084,30 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
self._only_thumbs = True
for ( incorrect_location, prefix ) in missing_locations:
if prefix.startswith( 'f' ):
self._only_thumbs = False
text = 'This dialog has launched because some expected file storage directories were not found. This is a serious error. You have two options:'
text += os.linesep * 2
text += '1) If you know what these should be (e.g. you recently remapped their external drive to another location), update the paths here manually. For most users, this will likely be a simple ctrl+a->correct, but if you have a more complicated system or store your thumbnails different to your files, make sure you skim the whole list. Check everything reports _ok!_'
text += os.linesep * 2
text += 'Then hit \'apply\', and the client will launch. You should double-check your \'preferred\' file storage locations under options->file storage locations immediately.'
text += 'Then hit \'apply\', and the client will launch. You should double-check your \'preferred\' file storage locations under database->migrate database immediately.'
text += os.linesep * 2
text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help.'
if self._only_thumbs:
text += os.linesep * 2
text += 'SPECIAL NOTE FOR YOUR SITUATION: The only paths missing are thumbnail paths. If you cannot recover these folders, you can hit apply to create empty paths at the original or corrected locations and then run a maintenance routine to regenerate the thumbnails from their originals.'
st = ClientGUICommon.BetterStaticText( self, text )
st.Wrap( 640 )
@ -6120,6 +6127,8 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
self._locations.Append( t, t )
# sort by prefix
#self._locations.SortListItems( 1 ) # subdirs secondary
#self._locations.SortListItems( 0 ) # missing location primary
@ -6170,29 +6179,67 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
def CommitChanges( self ):
user_was_warned = False
correct_rows = []
for ( incorrect_location, prefix, correct_location, ok ) in self._locations.GetClientData():
if self._only_thumbs:
if correct_location == '':
problems = False
for ( incorrect_location, prefix, correct_location, ok ) in self._locations.GetClientData():
wx.MessageBox( 'You did not correct all the locations!' )
raise HydrusExceptions.VetoException()
elif ok != 'ok!':
wx.MessageBox( 'You did not find all the correct locations!' )
raise HydrusExceptions.VetoException()
else:
if correct_location == '':
problems = True
correct_location = incorrect_location
elif ok != 'ok!':
problems = True
correct_rows.append( ( incorrect_location, prefix, correct_location ) )
if problems:
message = 'Some or all of your incorrect paths have not been corrected, but they are all thumbnail paths.'
message += os.linesep * 2
message += 'Would you like instead to create new empty folders at the previous (or corrected, if you have entered them) locations?'
message += os.linesep * 2
message += 'You can run database->regenerate->all thumbnails to fill them up again.'
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
if dlg.ShowModal() != wx.ID_YES:
raise HydrusExceptions.VetoException()
else:
for ( incorrect_location, prefix, correct_location, ok ) in self._locations.GetClientData():
if correct_location == '':
wx.MessageBox( 'You did not correct all the locations!' )
raise HydrusExceptions.VetoException()
elif ok != 'ok!':
wx.MessageBox( 'You did not find all the correct locations!' )
raise HydrusExceptions.VetoException()
else:
correct_rows.append( ( incorrect_location, prefix, correct_location ) )
HG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows )

View File

@ -1,16 +1,19 @@
import numpy.core.multiarray # important this comes before cv!
import ClientConstants as CC
import cv2
import HydrusConstants as HC
import HydrusImageHandling
import HydrusGlobals as HG
if cv2.__version__.startswith( '2' ):
IMREAD_UNCHANGED = cv2.CV_LOAD_IMAGE_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.CV_LOAD_IMAGE_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.CV_LOAD_IMAGE_ANYDEPTH | cv2.CV_LOAD_IMAGE_ANYCOLOR
else:
IMREAD_UNCHANGED = cv2.IMREAD_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2.IMREAD_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR # this preserves colour info but does EXIF reorientation and flipping
cv_interpolation_enum_lookup = {}
@ -41,7 +44,7 @@ def EfficientlyThumbnailNumpyImage( numpy_image, ( target_x, target_y ) ):
return cv2.resize( numpy_image, ( target_x, target_y ), interpolation = cv2.INTER_AREA )
def GenerateNumpyImage( path ):
def GenerateNumpyImage( path, mime ):
if HG.client_controller.GetNewOptions().GetBoolean( 'load_images_with_pil' ):
@ -54,7 +57,16 @@ def GenerateNumpyImage( path ):
else:
numpy_image = cv2.imread( path, flags = IMREAD_UNCHANGED )
if mime == HC.IMAGE_JPEG:
flags = CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION
else:
flags = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
numpy_image = cv2.imread( path, flags = flags )
if numpy_image is None: # doesn't support static gifs and some random other stuff
@ -109,9 +121,9 @@ def GenerateNumPyImageFromPILImage( pil_image ):
return numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, len( s ) // ( w * h ) ) )
def GenerateShapePerceptualHashes( path ):
def GenerateShapePerceptualHashes( path, mime ):
numpy_image = GenerateNumpyImage( path )
numpy_image = GenerateNumpyImage( path, mime )
( y, x, depth ) = numpy_image.shape

View File

@ -365,7 +365,7 @@ class FileImportJob( object ):
if mime in HC.MIMES_WE_CAN_PHASH:
self._phashes = ClientImageHandling.GenerateShapePerceptualHashes( self._temp_path )
self._phashes = ClientImageHandling.GenerateShapePerceptualHashes( self._temp_path, mime )
self._extra_hashes = HydrusFileHandling.GetExtraHashesFromPath( self._temp_path )

View File

@ -1793,7 +1793,14 @@ class NetworkEngine( object ):
self._jobs_downloading = filter( ProcessDownloadingJob, self._jobs_downloading )
self._new_work_to_do.wait( 1 )
# we want to catch the rollover of the second for bandwidth jobs
now_with_subsecond = time.time()
subsecond_part = now_with_subsecond % 1
time_until_next_second = 1.0 - subsecond_part
self._new_work_to_do.wait( time_until_next_second )
self._new_work_to_do.clear()
@ -1900,7 +1907,9 @@ class NetworkJob( object ):
self._status_text = u'sending request\u2026'
response = session.request( method, url, data = data, headers = headers, stream = True, timeout = 10 )
timeout = HG.client_controller.GetNewOptions().GetInteger( 'network_timeout' )
response = session.request( method, url, data = data, headers = headers, stream = True, timeout = timeout )
connection_successful = True
@ -2083,7 +2092,7 @@ class NetworkJob( object ):
waiting_duration = self.engine.bandwidth_manager.GetWaitingEstimate( self._network_contexts )
if waiting_duration < 1:
if waiting_duration < 2:
self._status_text = u'bandwidth free imminently\u2026'

View File

@ -14,9 +14,9 @@ import threading
import time
import wx
def GenerateHydrusBitmap( path, compressed = True ):
def GenerateHydrusBitmap( path, mime, compressed = True ):
numpy_image = ClientImageHandling.GenerateNumpyImage( path )
numpy_image = ClientImageHandling.GenerateNumpyImage( path, mime )
return GenerateHydrusBitmapFromNumPyImage( numpy_image, compressed = compressed )
@ -57,12 +57,12 @@ class ImageRenderer( object ):
self._media = media
self._numpy_image = None
hash = self._media.GetHash()
mime = self._media.GetMime()
self._hash = self._media.GetHash()
self._mime = self._media.GetMime()
client_files_manager = HG.client_controller.client_files_manager
self._path = client_files_manager.GetFilePath( hash, mime )
self._path = client_files_manager.GetFilePath( self._hash, self._mime )
HG.client_controller.CallToThread( self._Initialise )
@ -71,7 +71,7 @@ class ImageRenderer( object ):
time.sleep( 0.00001 )
self._numpy_image = ClientImageHandling.GenerateNumpyImage( self._path )
self._numpy_image = ClientImageHandling.GenerateNumpyImage( self._path, self._mime )
def GetEstimatedMemoryFootprint( self ):

View File

@ -49,7 +49,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 269
SOFTWARE_VERSION = 270
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -14,6 +14,7 @@ gui_report_mode = False
network_report_mode = False
pubsub_profile_mode = False
force_idle_mode = False
no_page_limit_mode = False
server_busy = False
do_idle_shutdown_work = False

View File

@ -205,7 +205,10 @@ def GetResolutionAndNumFrames( path ):
except: break
except: num_frames = 1
except:
num_frames = 1
return ( ( x, y ), num_frames )

View File

@ -429,14 +429,35 @@ class BandwidthTracker( HydrusSerialisable.SerialisableBase ):
( window, counter ) = self._GetWindowAndCounter( bandwidth_type, time_delta )
# we need the 'window' because this tracks brackets from the first timestamp and we want to include if 'since' lands anywhere in the bracket
# e.g. if it is 1200 and we want the past 1,000, we also need the bracket starting at 0, which will include 200-999
time_delta += window
since = HydrusData.GetNow() - time_delta
return sum( ( value for ( key, value ) in counter.items() if key >= since ) )
if time_delta == 1:
# the case of 1 poses a problem as our min block width is also 1. we can't have a window of 0.1s to make the transition smooth
# if we include the last second's data in an effort to span the whole previous 1000ms, we end up not doing anything until the next second rolls over
# this causes 50% consumption as we consume in the second after the one we verified was clear
# so, let's just check the current second and be happy with it
now = HydrusData.GetNow()
if now in counter:
return counter[ now ]
else:
return 0
else:
# we need the 'window' because this tracks brackets from the first timestamp and we want to include if 'since' lands anywhere in the bracket
# e.g. if it is 1200 and we want the past 1,000, we also need the bracket starting at 0, which will include 200-999
search_time_delta = time_delta + window
since = HydrusData.GetNow() - search_time_delta
return sum( ( value for ( timestamp, value ) in counter.items() if timestamp >= since ) )
def _GetTimes( self, dt ):
@ -615,6 +636,8 @@ class BandwidthTracker( HydrusSerialisable.SerialisableBase ):
( window, counter ) = self._GetWindowAndCounter( bandwidth_type, time_delta )
time_delta_in_which_bandwidth_counts = time_delta + window
time_and_values = counter.items()
time_and_values.sort( reverse = True )
@ -626,7 +649,7 @@ class BandwidthTracker( HydrusSerialisable.SerialisableBase ):
current_search_time_delta = now - timestamp
if current_search_time_delta > time_delta: # we are searching beyond our time delta. no need to wait
if current_search_time_delta > time_delta_in_which_bandwidth_counts: # we are searching beyond our time delta. no need to wait
break
@ -635,7 +658,7 @@ class BandwidthTracker( HydrusSerialisable.SerialisableBase ):
if usage >= max_allowed:
return time_delta - current_search_time_delta
return time_delta_in_which_bandwidth_counts - current_search_time_delta

View File

@ -9,7 +9,7 @@ class TestImageHandling( unittest.TestCase ):
def test_phash( self ):
phashes = ClientImageHandling.GenerateShapePerceptualHashes( os.path.join( HC.STATIC_DIR, 'hydrus.png' ) )
phashes = ClientImageHandling.GenerateShapePerceptualHashes( os.path.join( HC.STATIC_DIR, 'hydrus.png' ), HC.IMAGE_PNG )
self.assertEqual( phashes, set( [ '\xb4M\xc7\xb2M\xcb8\x1c' ] ) )