changelog
-
+
-
+
version 570
+-
+
UI stuff
+ - wrote a thing to wrap tooltips and applied it everywhere. every tooltip in the program should now wrap to 80 characters +
- the thumbnail view is now better about pausing the current video if you open it externally in various ways +
- the 'open' submenu you get off of a file right-click is now exactly the same for the thumbnail menu and the media viewer menu, with all commands working in either place, the labels are also brushed up a little +
- added a shortcut action for 'open file in web browser' to the media shortcut set +
- added a shortcut action for 'open files in a new duplicates filter page' to the media shortcut set +
- added/updated the shortcut action for 'open similar looking files in a new page' in the media shortcut set. this is now one job that lets you set any distance, and it now works from the media viewer too. all existing `show similar files: 0 (exact)` fixed-distance simple actions will be converted to the new action when you update +
- I removed 'open externally' and 'open in file explorer' shortcuts from the media viewer/preview viewer/thumbnails sets. these sets are technically awkward and were really meant for a different thing, like pause/play or 'close media viewer', and having the media command code duplicated here was getting spammy. if you have any of these now-defunct commands set, please move them up to the general 'media' set, where it'll work everywhere. sorry if this breaks a very complicated set you have, but let's KISS! +
- the 'files' submenu off thumbnails or the media viewer is flattened one level. the 'upload to' remote services stuff still isn't available for the media viewer, but I'll do the same as I did above for that in the near future +
misc
+ - fixed an issue with the 'manage tag siblings/parents' dialogs where the mass-import button was, in 'add or delete' mode, not doing any deletes/rescinds if there were any new pairs in what was being imported. this was probably applying to large regular adds in the UI, also +
- this mass-import button of 'manage tag siblings/parents' also dedupes the pairs coming in. it now shouldn't do anything like 'add, then ask to remove' if you have the same pair twice! +
- the nitter downloaders are removed from the defaults. I can't keep up with whatever the situation is there +
- the style and stylesheet names in the options are now sorted +
- sidecar importers will now work on sidecars that have uppercase .TXT or .JSON extensions +
more URL stuff (advanced, can be ignored by most)
+ - fixed up the recent URL encoding tech to properly follow the encoding exceptions as under RFC 3986. an '@' in an URL shouldn't get messed up now. thanks to the user(s) who helped here +
- incoming URLs can now have a mix of encoded and non-encoded characters and the 'ensure URL is encoded' process will accept it and encode the non-encoded parts, idempotently. it only fails on ingesting a legit decoded percent character that happens to be followed by two hex chars, but that's rare enough we don't really have to worry +
- you can similarly now enter multiple tags in a query text that are a mix of encoded and non-encoded, a mix of %20 and spaces, and it should figure it out +
- the 'ensure URL is encoded' process now applies to GUG-generated URLs, and in the edit GUG UI, you now see the normalised 'for server' URL, with any additional tokens or whatever the URL class has +
- GUGs also try to recognise if their replacement phrase is going into the path or the parameters now, and only force-encodes everything if it looks like our tags are going into a query param +
- ensured that what you paste into an 'edit URL Class' panel's 'example url' section gets encoded before normalisation just as it would in engine +
- the file log right-click now shows both the normalised and request urls under the 'additional urls' section, if they differ from the pretty human URL in the list +
- right-clicking a single item in the downloader search log now previews the specific request URL to be copied +
boring stuff
+ - all instances of URL path or parameter encoding now go through one location that obeys RFC 3986 +
- replaced my various uses of the unusual `ParseResult` with `urllib.parse.urlunparse` +
- added a couple unit tests for the improved URL encoding tech +
- added some unit tests for the GUGs' new encoding tech +
- harmonised how a file is opened in the OS file explorer in the media results and media canvas pages. what was previously random hardcode, duplicated internal method calls, and ancient pubsub redirects now all goes thorugh the application command system to a singular isolated media-actioning method +
- did the same harmonisation for opening files externally +
- and for opening files in your web browser, which gets additional new infrastructure so it can plug into the shortcuts system +
- and to a lesser degree the 'open in a new page' and 'open in a new duplicates filter page' commands +
- moved the various gui-side media python files to a new 'gui.media' module. renamed `ClientGUIMedia` to `ClientGUISimpleActions` and `ClientGUIMediaActions` to `ClientGUIModalActions` and shuffled their methods back and forth a bit +
- cleaned up `ClientGUIFunctions` and `ClientGUICommon` and their imports a little with some similar shuffle-refactoring +
- broke up `ClientGUIControls` into a bunch of smaller, defined files, mostly to untangle imports +
- cleaned up how some text and exceptions are split by newlines to handle different sorts of newline, and cleaned up how I fetch the first 'summary' line of text in all cases across the program +
- replaced `os.linesep` with `\n` across the program. Qt only wants `\n` anyway, most logging wants `\n` (and sometimes converts on the fly behind the scenes), and this helps KISS otherwise. I might bring back `os.linesep` for sidecars and stuff if it proves a problem, but most text editors and scripting languages are very happy with `\n`, so we'll see +
- multi-column lists now show multiline tooltips if the underlying text in the cell was originally multiline (although tbh this is rare) +
-
version 569
-
diff --git a/hydrus/client/ClientApplicationCommand.py b/hydrus/client/ClientApplicationCommand.py
index fdf596ca7..bb4ba13f5 100644
--- a/hydrus/client/ClientApplicationCommand.py
+++ b/hydrus/client/ClientApplicationCommand.py
@@ -6,6 +6,7 @@
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTime
+from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
SIMPLE_ARCHIVE_DELETE_FILTER_BACK = 0
@@ -161,6 +162,9 @@
SIMPLE_REARRANGE_THUMBNAILS = 150
SIMPLE_MAC_QUICKLOOK = 151
SIMPLE_COPY_URLS = 152
+SIMPLE_OPEN_FILE_IN_WEB_BROWSER = 153
+SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE = 154
+SIMPLE_OPEN_SIMILAR_LOOKING_FILES = 155
REARRANGE_THUMBNAILS_TYPE_FIXED = 0
REARRANGE_THUMBNAILS_TYPE_COMMAND = 1
@@ -265,8 +269,11 @@
SIMPLE_NEW_WATCHER_DOWNLOADER_PAGE : 'open a new page: thread watcher',
SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM : 'open file in external program',
SIMPLE_OPEN_FILE_IN_FILE_EXPLORER : 'open file in file explorer',
+ SIMPLE_OPEN_FILE_IN_WEB_BROWSER : 'open file in web browser',
SIMPLE_OPEN_KNOWN_URL : 'open known url',
SIMPLE_OPEN_SELECTION_IN_NEW_PAGE : 'open files in a new page',
+ SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE : 'open files in a new duplicates filter page',
+ SIMPLE_OPEN_SIMILAR_LOOKING_FILES : 'open similar looking files in a new page',
SIMPLE_PAN_BOTTOM_EDGE : 'pan file to bottom edge',
SIMPLE_PAN_DOWN : 'pan file down',
SIMPLE_PAN_HORIZONTAL_CENTER : 'pan file left/right to center',
@@ -465,7 +472,7 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND
SERIALISABLE_NAME = 'Application Command'
- SERIALISABLE_VERSION = 5
+ SERIALISABLE_VERSION = 6
def __init__( self, command_type = None, data = None ):
@@ -656,6 +663,49 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 5, new_serialisable_info )
+ if version == 5:
+
+ ( command_type, serialisable_data ) = old_serialisable_info
+
+ if command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
+
+ data_dict = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_data )
+
+ simple_action = data_dict[ 'simple_action' ]
+
+ if simple_action in ( SIMPLE_GET_SIMILAR_TO_EXACT, SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR, SIMPLE_GET_SIMILAR_TO_SIMILAR, SIMPLE_GET_SIMILAR_TO_SPECULATIVE ):
+
+ hamming_distance = 0
+
+ if simple_action == SIMPLE_GET_SIMILAR_TO_EXACT:
+
+ hamming_distance = 0
+
+ elif simple_action == SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR:
+
+ hamming_distance = 2
+
+ elif simple_action == SIMPLE_GET_SIMILAR_TO_SIMILAR:
+
+ hamming_distance = 4
+
+ elif simple_action == SIMPLE_GET_SIMILAR_TO_SPECULATIVE:
+
+ hamming_distance = 8
+
+
+ data_dict[ 'simple_action' ] = SIMPLE_OPEN_SIMILAR_LOOKING_FILES
+ data_dict[ 'simple_data' ] = hamming_distance
+
+
+ serialisable_data = data_dict.GetSerialisableTuple()
+
+
+ new_serialisable_info = ( command_type, serialisable_data )
+
+ return ( 6, new_serialisable_info )
+
+
def GetCommandType( self ):
@@ -756,6 +806,19 @@ def ToString( self ):
s = f'{s} ({direction_s} {ms_s})'
+ elif action == SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
+
+ hamming_distance = self.GetSimpleData()
+
+ if hamming_distance in CC.hamming_string_lookup:
+
+ s = f'{s} ({hamming_distance} - {CC.hamming_string_lookup[ hamming_distance ]})'
+
+ else:
+
+ s = f'{s} ({hamming_distance})'
+
+
elif action == SIMPLE_MOVE_THUMBNAIL_FOCUS:
( move_direction, selection_status ) = self.GetSimpleData()
diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py
index 81816177b..5366a9ca5 100644
--- a/hydrus/client/ClientController.py
+++ b/hydrus/client/ClientController.py
@@ -566,7 +566,7 @@ def qt_code():
from hydrus.client.gui import ClientGUIDialogsQuick
message = 'It looks like another instance of this client is already running, so this instance cannot start.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If the old instance is closing and does not quit for a _very_ long time, it is usually safe to force-close it from task manager.'
result = ClientGUIDialogsQuick.GetYesNo( self._splash, message, title = 'The client is already running.', yes_label = 'wait a bit, then try again', no_label = 'forget it' )
@@ -1686,9 +1686,9 @@ def RestoreDatabase( self ):
path = dlg.GetPath()
text = 'Are you sure you want to restore a backup from "{}"?'.format( path )
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Everything in your current database will be deleted!'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'The gui will shut down, and then it will take a while to complete the restore. Once it is done, the client will restart.'
result = ClientGUIDialogsQuick.GetYesNo( self.gui, text )
diff --git a/hydrus/client/ClientDBMaintenanceManager.py b/hydrus/client/ClientDBMaintenanceManager.py
index 3d17e7554..1075b01c6 100644
--- a/hydrus/client/ClientDBMaintenanceManager.py
+++ b/hydrus/client/ClientDBMaintenanceManager.py
@@ -155,7 +155,7 @@ def check_shutdown():
HydrusData.PrintException( e )
message = 'There was an unexpected problem during deferred table delete database maintenance work! This maintenance system will not run again this boot. A full traceback of this error should be written to the log.'
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
HydrusData.ShowText( message )
diff --git a/hydrus/client/ClientData.py b/hydrus/client/ClientData.py
index e08d44452..b84d3ca6a 100644
--- a/hydrus/client/ClientData.py
+++ b/hydrus/client/ClientData.py
@@ -30,15 +30,15 @@ def CatchExceptionClient( etype, value, tb ):
pretty_value = str( value )
- if os.linesep in pretty_value:
-
- ( first_line, anything_else ) = pretty_value.split( os.linesep, 1 )
-
- trace = trace + os.linesep + anything_else
+ all_lines = pretty_value.splitlines()
+
+ first_line = all_lines[0]
+
+ if len( all_lines ) > 1:
- else:
+ the_rest = all_lines[1:]
- first_line = pretty_value
+ trace = trace + '\n' + '\n'.join( the_rest )
job_status = ClientThreading.JobStatus()
@@ -68,7 +68,7 @@ def CatchExceptionClient( etype, value, tb ):
text = 'Encountered an error I could not parse:'
- text += os.linesep
+ text += '\n'
text += str( ( etype, value, tb ) )
@@ -153,7 +153,7 @@ def ShowExceptionTupleClient( etype, value, tb, do_wait = True ):
if tb is None:
- trace = 'No error trace--here is the stack:' + os.linesep + ''.join( traceback.format_stack() )
+ trace = 'No error trace--here is the stack:' + '\n' + ''.join( traceback.format_stack() )
else:
@@ -162,15 +162,15 @@ def ShowExceptionTupleClient( etype, value, tb, do_wait = True ):
pretty_value = str( value )
- if os.linesep in pretty_value:
-
- ( first_line, anything_else ) = pretty_value.split( os.linesep, 1 )
-
- trace = trace + os.linesep + anything_else
+ all_lines = pretty_value.splitlines()
+
+ first_line = all_lines[0]
+
+ if len( all_lines ) > 1:
- else:
+ the_rest = all_lines[1:]
- first_line = pretty_value
+ trace = trace + '\n' + '\n'.join( the_rest )
job_status = ClientThreading.JobStatus()
diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py
index 2dc02ed9d..d1d3569f8 100644
--- a/hydrus/client/ClientFiles.py
+++ b/hydrus/client/ClientFiles.py
@@ -522,9 +522,9 @@ def _AttemptToHealMissingLocations( self ):
summaries = sorted( ( '{} folders seem to have moved from {} to {}'.format( HydrusData.ToHumanInt( count ), missing_base_location, correct_base_location ) for ( ( missing_base_location, correct_base_location ), count ) in fixes_counter.items() ) )
summary_message = 'Some client file folders were missing, but they appear to be in other known locations! The folders are:'
- summary_message += os.linesep * 2
- summary_message += os.linesep.join( summaries )
- summary_message += os.linesep * 2
+ summary_message += '\n' * 2
+ summary_message += '\n'.join( summaries )
+ summary_message += '\n' * 2
summary_message += 'Assuming you did this on purpose, or hydrus recently inserted stub values after database corruption, Hydrus is ready to update its internal knowledge to reflect these new mappings as soon as this dialog closes. If you know these proposed fixes are incorrect, terminate the program now.'
HydrusData.Print( summary_message )
@@ -1116,11 +1116,11 @@ def _Reinit( self ):
missing_prefixes = sorted( missing_dict[ missing_base_location ] )
- missing_prefixes_string = ' ' + os.linesep.join( ( ', '.join( block ) for block in HydrusLists.SplitListIntoChunks( missing_prefixes, 32 ) ) )
+ missing_prefixes_string = ' ' + '\n'.join( ( ', '.join( block ) for block in HydrusLists.SplitListIntoChunks( missing_prefixes, 32 ) ) )
- missing_string += os.linesep
+ missing_string += '\n'
missing_string += str( missing_base_location )
- missing_string += os.linesep
+ missing_string += '\n'
missing_string += missing_prefixes_string
@@ -1129,7 +1129,7 @@ def _Reinit( self ):
if len( self._missing_subfolders ) > 4:
text = 'When initialising the client files manager, some file locations did not exist! They have all been written to the log!'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If this is happening on client boot, you should now be presented with a dialog to correct this manually!'
self._controller.BlockingSafeShowCriticalMessage( 'missing locations', text )
@@ -1140,9 +1140,9 @@ def _Reinit( self ):
else:
text = 'When initialising the client files manager, these file locations did not exist:'
- text += os.linesep * 2
+ text += '\n' * 2
text += missing_string
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If this is happening on client boot, you should now be presented with a dialog to correct this manually!'
self._controller.BlockingSafeShowCriticalMessage( 'missing locations', text )
@@ -2226,7 +2226,7 @@ def _CheckFileIntegrity( self, media_result, job_type ):
self._pubbed_message_about_invalid_file_export = True
message = 'During file maintenance, a file was found to be invalid. It and any known URLs have been moved to "{}".'.format( error_dir )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'More files may be invalid, but this message will not appear again during this boot.'
HydrusData.ShowText( message )
@@ -2279,7 +2279,7 @@ def qt_add_url( url ):
message = 'During file maintenance, a file was found to be missing or invalid. {} Its file hash and any known URLs have been written to "{}".'.format( m, error_dir )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This may happen to more files in the near future, but this message will not appear again during this boot.'
HydrusData.ShowText( message )
@@ -2881,7 +2881,7 @@ def _RunJob( self, media_results_to_job_types, job_status, job_done_hook = None
job_status = ClientThreading.JobStatus()
message = 'Hey, while performing file maintenance task "{}" on file {}, the client ran into an I/O Error! This could be just some media library moaning about a weird (probably truncated) file, but it could also be a significant hard drive problem. Look at the error yourself. If it looks serious, you should shut the client down and check your hard drive health immediately. Just to be safe, no more file maintenance jobs will be run this boot, and a full traceback has been written to the log.'.format( regen_file_enum_to_str_lookup[ job_type ], hash.hex() )
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
job_status.SetStatusText( message )
@@ -2902,7 +2902,7 @@ def _RunJob( self, media_results_to_job_types, job_status, job_done_hook = None
job_status = ClientThreading.JobStatus()
message = 'There was an unexpected problem performing maintenance task "{}" on file {}! The job will not be reattempted. A full traceback of this error should be written to the log.'.format( regen_file_enum_to_str_lookup[ job_type ], hash.hex() )
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
job_status.SetStatusText( message )
diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py
index 5b0101d23..ea2766175 100644
--- a/hydrus/client/ClientParsing.py
+++ b/hydrus/client/ClientParsing.py
@@ -104,7 +104,7 @@ def ConvertParseResultToPrettyString( result ):
note_name = additional_info
- return 'note "{}":{}{}'.format( note_name, os.linesep, parsed_text )
+ return 'note "{}":\n{}'.format( note_name, parsed_text )
elif content_type == HC.CONTENT_TYPE_HASH:
@@ -393,7 +393,7 @@ def GetHTMLTagString( tag: bs4.Tag ):
if sub_tag.name in ( 'br', 'p' ):
- all_strings.append( os.linesep )
+ all_strings.append( '\n' )
continue
@@ -765,7 +765,7 @@ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
def _GetParsePrettySeparator( self ):
- return os.linesep
+ return '\n'
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
@@ -982,9 +982,9 @@ def ToPrettyMultilineString( self ):
s.append( 'and substitute into ' + self._sub_phrase )
- separator = os.linesep * 2
+ separator = '\n' * 2
- text = '--COMPOUND--' + os.linesep * 2 + separator.join( s )
+ text = '--COMPOUND--' + '\n' * 2 + separator.join( s )
return text
@@ -1074,9 +1074,9 @@ def ToPrettyMultilineString( self ):
s.append( 'fetch the "' + self._variable_name + '" variable from the parsing context' )
- separator = os.linesep * 2
+ separator = '\n' * 2
- text = '--CONTEXT VARIABLE--' + os.linesep * 2 + separator.join( s )
+ text = '--CONTEXT VARIABLE--' + '\n' * 2 + separator.join( s )
return text
@@ -1137,11 +1137,11 @@ def _GetParsePrettySeparator( self ):
if self._content_to_fetch == HTML_CONTENT_HTML:
- return os.linesep * 2
+ return '\n' * 2
else:
- return os.linesep
+ return '\n'
@@ -1428,9 +1428,9 @@ def ToPrettyMultilineString( self ):
pretty_strings.extend( self._string_processor.GetProcessingStrings() )
- separator = os.linesep + 'and then '
+ separator = '\n' + 'and then '
- pretty_multiline_string = '--HTML--' + os.linesep + separator.join( pretty_strings )
+ pretty_multiline_string = '--HTML--' + '\n' + separator.join( pretty_strings )
return pretty_multiline_string
@@ -1737,11 +1737,11 @@ def _GetParsePrettySeparator( self ):
if self._content_to_fetch == JSON_CONTENT_JSON:
- return os.linesep * 2
+ return '\n' * 2
else:
- return os.linesep
+ return '\n'
@@ -2018,9 +2018,9 @@ def ToPrettyMultilineString( self ):
pretty_strings.extend( self._string_processor.GetProcessingStrings() )
- separator = os.linesep + 'and then '
+ separator = '\n' + 'and then '
- pretty_multiline_string = '--JSON--' + os.linesep + separator.join( pretty_strings )
+ pretty_multiline_string = '--JSON--' + '\n' + separator.join( pretty_strings )
return pretty_multiline_string
@@ -2389,7 +2389,7 @@ def remove_pre_url_gubbins( u ):
- clean_url = ClientNetworkingFunctions.WashURL( unclean_url )
+ clean_url = ClientNetworkingFunctions.EnsureURLIsEncoded( unclean_url )
clean_urls.append( clean_url )
@@ -2452,7 +2452,7 @@ def ParsePretty( self, parsing_context, parsing_text: str ):
result_lines.append( '*** RESULTS END ***' )
- results_text = os.linesep.join( result_lines )
+ results_text = '\n'.join( result_lines )
return results_text
@@ -2786,9 +2786,9 @@ def ParsePretty( self, parsing_context, parsing_text ):
all_parse_results = self.Parse( parsing_context, parsing_text )
- pretty_groups_of_parse_results = [ os.linesep.join( [ ConvertParseResultToPrettyString( parse_result ) for parse_result in parse_results ] ) for parse_results in all_parse_results ]
+ pretty_groups_of_parse_results = [ '\n'.join( [ ConvertParseResultToPrettyString( parse_result ) for parse_result in parse_results ] ) for parse_results in all_parse_results ]
- group_separator = os.linesep * 2 + '*** SEPARATE FILE RESULTS BREAK ***' + os.linesep * 2
+ group_separator = '\n' * 2 + '*** SEPARATE FILE RESULTS BREAK ***' + '\n' * 2
pretty_parse_result_text = group_separator.join( pretty_groups_of_parse_results )
@@ -2801,13 +2801,13 @@ def ParsePretty( self, parsing_context, parsing_text ):
result_lines = []
- result_lines.append( '*** ' + HydrusData.ToHumanInt( len( all_parse_results ) ) + ' RESULTS BEGIN ***' + os.linesep )
+ result_lines.append( '*** ' + HydrusData.ToHumanInt( len( all_parse_results ) ) + ' RESULTS BEGIN ***' + '\n' )
result_lines.append( pretty_parse_result_text )
- result_lines.append( os.linesep + '*** RESULTS END ***' )
+ result_lines.append( '\n' + '*** RESULTS END ***' )
- results_text = os.linesep.join( result_lines )
+ results_text = '\n'.join( result_lines )
return results_text
diff --git a/hydrus/client/ClientRendering.py b/hydrus/client/ClientRendering.py
index 4b408b34c..e7747852f 100644
--- a/hydrus/client/ClientRendering.py
+++ b/hydrus/client/ClientRendering.py
@@ -273,7 +273,7 @@ def _Initialise( self ):
self._hash.hex()
)
- m += os.linesep * 2
+ m += '\n' * 2
m += 'Jobs to check its integrity and metadata have been scheduled. If it is damaged, it may be redownloaded or removed from the client completely. If it is not damaged, it may be fixed automatically or further action may be required.'
HydrusData.ShowText( m )
@@ -295,7 +295,7 @@ def _Initialise( self ):
my_numpy_size
)
- m += os.linesep * 2
+ m += '\n' * 2
m += 'You may see some black squares in the image. A metadata regeneration has been scheduled, so with luck the image will fix itself soon.'
HydrusData.ShowText( m )
@@ -581,7 +581,7 @@ def __init__( self, media, target_resolution = None, init_position = 0 ):
if duration is None or duration == 0:
message = 'The file with hash ' + media.GetHash().hex() + ', had an invalid duration.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You may wish to try regenerating its metadata through the advanced mode right-click menu.'
HydrusData.ShowText( message )
@@ -592,7 +592,7 @@ def __init__( self, media, target_resolution = None, init_position = 0 ):
if num_frames_in_video is None or num_frames_in_video == 0:
message = 'The file with hash ' + media.GetHash().hex() + ', had an invalid number of frames.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You may wish to try regenerating its metadata through the advanced mode right-click menu.'
HydrusData.ShowText( message )
diff --git a/hydrus/client/ClientSerialisable.py b/hydrus/client/ClientSerialisable.py
index 31cc4b885..32e2b7a88 100644
--- a/hydrus/client/ClientSerialisable.py
+++ b/hydrus/client/ClientSerialisable.py
@@ -341,7 +341,7 @@ def LoadFromNumPyImage( numpy_image: numpy.array ):
HydrusData.PrintException( e )
message = 'The image loaded, but it did not seem to be a hydrus serialised png! The error was: {}'.format( repr( e ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you believe this is a legit non-resized, non-converted hydrus serialised png, please send it to hydrus_dev.'
raise Exception( message )
diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py
index 183fda509..ba97c00d2 100644
--- a/hydrus/client/ClientServices.py
+++ b/hydrus/client/ClientServices.py
@@ -1490,7 +1490,7 @@ def SyncAccount( self, force = False ):
if message != '' and message_created != original_message_created and not HydrusTime.TimeHasPassed( message_created + ( 86400 * 5 ) ):
m = 'New message for your account on {}:'.format( self._name )
- m += os.linesep * 2
+ m += '\n' * 2
m += message
HydrusData.ShowText( m )
@@ -1538,7 +1538,7 @@ def SyncAccount( self, force = False ):
summary = tag_filter.GetChangesSummaryText( old_tag_filter )
- message = 'The tag filter for "{}" just changed! Changes are:{}{}'.format( self._name, os.linesep * 2, summary )
+ message = 'The tag filter for "{}" just changed! Changes are:{}{}'.format( self._name, '\n' * 2, summary )
HydrusData.ShowText( message )
@@ -1928,7 +1928,7 @@ def _SyncDownloadUpdates( self, stop_time ):
message = 'Update ' + update_hash.hex() + ' downloaded from the ' + self._name + ' repository failed to load! This is a serious error!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The repository has been paused for now. Please look into what could be wrong and report this to the hydrus dev.'
HydrusData.ShowText( message )
@@ -1954,7 +1954,7 @@ def _SyncDownloadUpdates( self, stop_time ):
message = 'Update ' + update_hash.hex() + ' downloaded from the ' + self._name + ' was not a valid update--it was a ' + repr( update ) + '! This is a serious error!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The repository has been paused for now. Please look into what could be wrong and report this to the hydrus dev.'
HydrusData.ShowText( message )
diff --git a/hydrus/client/ClientThreading.py b/hydrus/client/ClientThreading.py
index 5c023f235..3143ac888 100644
--- a/hydrus/client/ClientThreading.py
+++ b/hydrus/client/ClientThreading.py
@@ -414,7 +414,7 @@ def ToString( self ):
try:
- return os.linesep.join( stuff_to_print )
+ return '\n'.join( stuff_to_print )
except:
diff --git a/hydrus/client/caches/ClientCaches.py b/hydrus/client/caches/ClientCaches.py
index dac92795a..7dd1dec2d 100644
--- a/hydrus/client/caches/ClientCaches.py
+++ b/hydrus/client/caches/ClientCaches.py
@@ -626,9 +626,9 @@ def _HandleThumbnailException( self, hash, e, summary ):
self._thumbnail_error_occurred = True
message = 'A thumbnail error has occurred. The problem thumbnail will appear with the default \'hydrus\' symbol. You may need to take hard drive recovery actions, and if the error is not obviously fixable, you can contact hydrus dev for additional help. Specific information for this first error follows. Subsequent thumbnail errors in this session will be silently printed to the log.'
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
- message += os.linesep * 2
+ message += '\n' * 2
message += summary
job_status = ClientThreading.JobStatus()
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index f5b511533..93960e754 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -1758,7 +1758,7 @@ def _DeleteServiceInfo( self, service_key = None, types_to_delete = None ):
def _DisplayCatastrophicError( self, text ):
message = 'The db encountered a serious error! This is going to be written to the log as well, but here it is for a screenshot:'
- message += os.linesep * 2
+ message += '\n' * 2
message += text
HydrusData.DebugPrint( message )
@@ -3926,7 +3926,7 @@ def _GetPending( self, service_key, content_types, ideal_weight = 100 ):
if pending_mapping_weight != addable_pending_mapping_weight:
message = 'Hey, while going through the pending tags to upload, it seemed some were simultaneously already in the \'current\' state. This looks like a bug.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please run _database->check and repair->fix logically inconsistent mappings_. If everything seems good after that and you do not get this message again, you should be all fixed. If not, you may need to regenerate your mappings storage cache under the \'database\' menu. If that does not work, hydev would like to know about it!'
HydrusData.ShowText( message )
@@ -3964,7 +3964,7 @@ def _GetPending( self, service_key, content_types, ideal_weight = 100 ):
if petitioned_mapping_weight != deletable_petitioned_mapping_weight:
message = 'Hey, while going through the petitioned tags to upload, it seemed some were simultaneously already in the \'deleted\' state. This looks like a bug.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please run _database->check and repair->fix logically inconsistent mappings_. If everything seems good after that and you do not get this message again, you should be all fixed. If not, you may need to regenerate your mappings storage cache under the \'database\' menu. If that does not work, hydev would like to know about it!'
HydrusData.ShowText( message )
@@ -7542,9 +7542,9 @@ def _RepairDB( self, version ):
missing_tag_service_ids = sorted( missing_tag_service_ids )
message = 'On boot, some important tag mapping tables for the storage context were missing! You should have already had a notice about this. You may have had other problems earlier, but this particular problem is completely recoverable and results in no lost data. The relevant tables have been recreated and will now be repopulated. The services about to be worked on are:'
- message += os.linesep * 2
- message += os.linesep.join( ( str( t ) for t in missing_tag_service_ids ) )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( ( str( t ) for t in missing_tag_service_ids ) )
+ message += '\n' * 2
message += 'If you want to go ahead, click ok on this message and the client will fill these tables with the correct data. It may take some time. If you want to solve this problem otherwise, kill the hydrus process now.'
self._controller.BlockingSafeShowMessage( message )
@@ -7574,9 +7574,9 @@ def _RepairDB( self, version ):
missing_tag_service_ids = sorted( missing_tag_service_ids )
message = 'On boot, some important tag mapping tables for the display context were missing! You should have already had a notice about this. You may have had other problems earlier, but this particular problem is completely recoverable and results in no lost data. The relevant tables have been recreated and will now be repopulated. The services about to be worked on are:'
- message += os.linesep * 2
- message += os.linesep.join( ( str( t ) for t in missing_tag_service_ids ) )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( ( str( t ) for t in missing_tag_service_ids ) )
+ message += '\n' * 2
message += 'If you want to go ahead, click ok on this message and the client will fill these tables with the correct data. It may take some time. If you want to solve this problem otherwise, kill the hydrus process now.'
self._controller.BlockingSafeShowMessage( message )
@@ -7609,9 +7609,9 @@ def _RepairDB( self, version ):
missing_tag_service_ids = sorted( missing_tag_service_ids )
message = 'On boot, some important tag count tables for the storage context were missing! You should have already had a notice about this. You may have had other problems earlier, but this particular problem is completely recoverable and results in no lost data. The relevant tables have been recreated and will now be repopulated. The services about to be worked on are:'
- message += os.linesep * 2
- message += os.linesep.join( ( str( t ) for t in missing_tag_service_ids ) )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( ( str( t ) for t in missing_tag_service_ids ) )
+ message += '\n' * 2
message += 'If you want to go ahead, click ok on this message and the client will fill these tables with the correct data. It may take some time. If you want to solve this problem otherwise, kill the hydrus process now.'
self._controller.BlockingSafeShowMessage( message )
@@ -7639,9 +7639,9 @@ def _RepairDB( self, version ):
missing_tag_service_ids = sorted( missing_tag_service_ids )
message = 'On boot, some important tag count tables for the display context were missing! You should have already had a notice about this. You may have had other problems earlier, but this particular problem is completely recoverable and results in no lost data. The relevant tables have been recreated and will now be repopulated. The services about to be worked on are:'
- message += os.linesep * 2
- message += os.linesep.join( ( str( t ) for t in missing_tag_service_ids ) )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( ( str( t ) for t in missing_tag_service_ids ) )
+ message += '\n' * 2
message += 'If you want to go ahead, click ok on this message and the client will fill these tables with the correct data. It may take some time. If you want to solve this problem otherwise, kill the hydrus process now.'
self._controller.BlockingSafeShowMessage( message )
@@ -7671,9 +7671,9 @@ def _RepairDB( self, version ):
missing_tag_search_service_pairs = sorted( missing_tag_search_service_pairs )
message = 'On boot, some important tag search tables were missing! You should have already had a notice about this. You may have had other problems earlier, but this particular problem is completely recoverable and results in no lost data. The relevant tables have been recreated and will now be repopulated. The service pairs about to be worked on are:'
- message += os.linesep * 2
- message += os.linesep.join( ( str( t ) for t in missing_tag_search_service_pairs ) )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( ( str( t ) for t in missing_tag_search_service_pairs ) )
+ message += '\n' * 2
message += 'If you want to go ahead, click ok on this message and the client will fill these tables with the correct data. It may take some time. If you want to solve this problem otherwise, kill the hydrus process now.'
self._controller.BlockingSafeShowMessage( message )
@@ -7697,9 +7697,9 @@ def _RepairDB( self, version ):
if new_options is None:
message = 'On boot, your main options object was missing!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you wish, click ok on this message and the client will re-add fresh options with default values. But if you want to solve this problem otherwise, kill the hydrus process now.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not already know what caused this, it was likely a hard drive fault--either due to a recent abrupt power cut or actual hardware failure. Check \'help my db is broke.txt\' in the install_dir/db directory as soon as you can.'
self._controller.BlockingSafeShowMessage( message )
@@ -7882,7 +7882,7 @@ def _RepopulateMappingsFromCache( self, tag_service_key = None, job_status = Non
if job_status is not None:
message = 'Doing "{}": {}'.format( name, HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Total rows recovered: {}'.format( HydrusData.ToHumanInt( num_rows_recovered ) )
job_status.SetStatusText( message )
@@ -8493,270 +8493,6 @@ def _UpdateDB( self, version ):
self._controller.frame_splash_status.SetText( 'updating db to v' + str( version + 1 ) )
- if version == 500:
-
- try:
-
- domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
-
- domain_manager.Initialise()
-
- #
-
- domain_manager.OverwriteDefaultURLClasses( (
- 'deviant art file page',
- ) )
-
- #
-
- domain_manager.TryToLinkURLClassesAndParsers()
-
- #
-
- self.modules_serialisable.SetJSONDump( domain_manager )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
-
- if version == 502:
-
- try:
-
- domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
-
- domain_manager.Initialise()
-
- #
-
- domain_manager.OverwriteDefaultURLClasses( (
- 'deviant art embedded video player',
- ) )
-
- domain_manager.OverwriteDefaultParsers( (
- 'deviant art file page parser',
- 'deviantart backend video embed parser'
- ) )
-
- #
-
- domain_manager.TryToLinkURLClassesAndParsers()
-
- #
-
- self.modules_serialisable.SetJSONDump( domain_manager )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
-
- if version == 503:
-
- try:
-
- domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
-
- domain_manager.Initialise()
-
- #
-
- # no longer supported, they nuked the open api
-
- domain_manager.DeleteGUGs( (
- 'deviant art tag search',
- ) )
-
- #
-
- domain_manager.TryToLinkURLClassesAndParsers()
-
- #
-
- self.modules_serialisable.SetJSONDump( domain_manager )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
-
- if version == 504:
-
- try:
-
- domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
-
- domain_manager.Initialise()
-
- #
-
- domain_manager.OverwriteDefaultURLClasses( (
- 'furry.booru.org file page',
- 'furry.booru.org gallery page',
- 'twitter tweet',
- 'twitter syndication api tweet-result'
- ) )
-
- domain_manager.OverwriteDefaultParsers( (
- 'twitter syndication api tweet parser',
- 'gelbooru 0.1.11 file page parser'
- ) )
-
- #
-
- domain_manager.TryToLinkURLClassesAndParsers()
-
- #
-
- self.modules_serialisable.SetJSONDump( domain_manager )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
- try:
-
- table_join = self.modules_files_storage.GetTableJoinLimitedByFileDomain( self.modules_services.combined_local_file_service_id, 'files_info', HC.CONTENT_STATUS_CURRENT )
-
- self._controller.frame_splash_status.SetSubtext( 'scheduling files for embedded metadata scan' )
-
- result = self._Execute( 'SELECT 1 FROM sqlite_master WHERE name = ?;', ( 'has_exif', ) ).fetchone()
-
- if result is None:
-
- self._Execute( 'CREATE TABLE IF NOT EXISTS main.has_exif ( hash_id INTEGER PRIMARY KEY );' )
-
- hash_ids = self._STL( self._Execute( 'SELECT hash_id FROM {} WHERE mime IN {};'.format( table_join, HydrusData.SplayListForDB( HC.FILES_THAT_CAN_HAVE_EXIF ) ) ) )
-
- self.modules_files_maintenance_queue.AddJobs( hash_ids, ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF )
-
-
- result = self._Execute( 'SELECT 1 FROM sqlite_master WHERE name = ?;', ( 'has_human_readable_embedded_metadata', ) ).fetchone()
-
- if result is None:
-
- self._Execute( 'CREATE TABLE IF NOT EXISTS main.has_human_readable_embedded_metadata ( hash_id INTEGER PRIMARY KEY );' )
-
- hash_ids = self._STL( self._Execute( 'SELECT hash_id FROM {} WHERE mime IN {};'.format( table_join, HydrusData.SplayListForDB( HC.FILES_THAT_CAN_HAVE_HUMAN_READABLE_EMBEDDED_METADATA ) ) ) )
-
- self.modules_files_maintenance_queue.AddJobs( hash_ids, ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA )
-
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to schedule image files for embedded metadata maintenance failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
-
- if version == 508:
-
- try:
-
- domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
-
- domain_manager.Initialise()
-
- #
-
- domain_manager.RenameGUG( 'twitter syndication collection lookup', 'twitter collection lookup' )
- domain_manager.RenameGUG( 'twitter syndication likes lookup', 'twitter likes lookup' )
- domain_manager.RenameGUG( 'twitter syndication list lookup', 'twitter list lookup' )
- domain_manager.RenameGUG( 'twitter syndication profile lookup', 'twitter profile lookup' )
- domain_manager.RenameGUG( 'twitter syndication profile lookup (with replies)', 'twitter profile lookup (with replies)' )
-
- #
-
- domain_manager.TryToLinkURLClassesAndParsers()
-
- #
-
- self.modules_serialisable.SetJSONDump( domain_manager )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
-
- self.pub_initial_message( message )
-
-
-
- if version == 509:
-
- try:
-
- new_options = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_CLIENT_OPTIONS )
-
- from hydrus.client.importing.options import NoteImportOptions
-
- duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( HC.DUPLICATE_BETTER )
-
- duplicate_content_merge_options.SetSyncNotesAction( HC.CONTENT_MERGE_ACTION_COPY )
-
- note_import_options = NoteImportOptions.NoteImportOptions()
-
- note_import_options.SetIsDefault( False )
- note_import_options.SetGetNotes( True )
- note_import_options.SetExtendExistingNoteIfPossible( True )
- note_import_options.SetConflictResolution( NoteImportOptions.NOTE_IMPORT_CONFLICT_RENAME )
-
- duplicate_content_merge_options.SetSyncNoteImportOptions( note_import_options )
-
- new_options.SetDuplicateContentMergeOptions( HC.DUPLICATE_BETTER, duplicate_content_merge_options )
-
- duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( HC.DUPLICATE_SAME_QUALITY )
-
- duplicate_content_merge_options.SetSyncNotesAction( HC.CONTENT_MERGE_ACTION_TWO_WAY_MERGE )
-
- note_import_options = NoteImportOptions.NoteImportOptions()
-
- note_import_options.SetIsDefault( False )
- note_import_options.SetGetNotes( True )
- note_import_options.SetExtendExistingNoteIfPossible( True )
- note_import_options.SetConflictResolution( NoteImportOptions.NOTE_IMPORT_CONFLICT_RENAME )
-
- duplicate_content_merge_options.SetSyncNoteImportOptions( note_import_options )
-
- new_options.SetDuplicateContentMergeOptions( HC.DUPLICATE_SAME_QUALITY, duplicate_content_merge_options )
-
- self.modules_serialisable.SetJSONDump( new_options )
-
- except Exception as e:
-
- HydrusData.PrintException( e )
-
- message = 'Updating your duplicate metadata merge options for the new note-merge support failed! This is not super important, but hydev would be interested in seeing the error that was printed to the log.'
-
- self.pub_initial_message( message )
-
-
-
if version == 513:
try:
@@ -10914,7 +10650,7 @@ def _Vacuum( self, names: typing.Collection[ str ], maintenance_mode = HC.MAINTE
HydrusData.ShowException( e )
text = 'An attempt to vacuum the database failed.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If the error is not obvious, please contact the hydrus developer.'
HydrusData.ShowText( text )
diff --git a/hydrus/client/db/ClientDBFilesPhysicalStorage.py b/hydrus/client/db/ClientDBFilesPhysicalStorage.py
index 614291518..0d390e673 100644
--- a/hydrus/client/db/ClientDBFilesPhysicalStorage.py
+++ b/hydrus/client/db/ClientDBFilesPhysicalStorage.py
@@ -62,9 +62,9 @@ def GetClientFilesSubfolders( self ):
if len( missing_prefixes_f ) > 0 or len( missing_prefixes_t ) > 0:
message = 'When fetching the directories where your files are stored, the database discovered that some entries were missing! If you did not fiddle with the database yourself, this probably happened due to database corruption.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Default values will now be inserted. If you have previously migrated your files or thumbnails, and assuming this is occuring on boot, you will next be presented with a dialog to remap them to the correct location.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If this is not happening on client boot, you should kill the hydrus process right now, as a serious hard drive fault has likely recently occurred.'
self._DisplayCatastrophicError( message )
diff --git a/hydrus/client/db/ClientDBModule.py b/hydrus/client/db/ClientDBModule.py
index a767f4da9..d2b3a29a8 100644
--- a/hydrus/client/db/ClientDBModule.py
+++ b/hydrus/client/db/ClientDBModule.py
@@ -12,7 +12,7 @@ class ClientDBModule( HydrusDBModule.HydrusDBModule ):
def _DisplayCatastrophicError( self, text: str ):
message = 'The db encountered a serious error! This is going to be written to the log as well, but here it is for a screenshot:'
- message += os.linesep * 2
+ message += '\n' * 2
message += text
HydrusData.DebugPrint( message )
@@ -25,7 +25,7 @@ def _PresentMissingIndicesWarningToUser( self, index_names: typing.Collection[ s
index_names = sorted( index_names )
HydrusData.DebugPrint( 'The "{}" database module is missing the following indices:'.format( self.name ) )
- HydrusData.DebugPrint( os.linesep.join( index_names ) )
+ HydrusData.DebugPrint( '\n'.join( index_names ) )
message = 'Your "{}" database module was missing {} indices. More information has been written to the log. This may or may not be a big deal, and on its own it is completely recoverable. If you do not have further problems, hydev does not need to know about it. The indices will be regenerated once you proceed--it may take some time.'.format( self.name, len( index_names ) )
@@ -39,14 +39,14 @@ def _PresentMissingTablesWarningToUser( self, table_names: typing.Collection[ st
table_names = sorted( table_names )
HydrusData.DebugPrint( 'The "{}" database module is missing the following tables:'.format( self.name ) )
- HydrusData.DebugPrint( os.linesep.join( table_names ) )
+ HydrusData.DebugPrint( '\n'.join( table_names ) )
message = 'Your "{}" database module was missing {} tables. More information has been written to the log. This is a serious problem.'.format( self.name, len( table_names ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If this is happening on the first boot after an update, it is likely a fault in the update code. If you updated many versions in one go, kill the hydrus process now and update in a smaller version increment.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If this is just a normal boot, you most likely encountered hard drive damage. You should check "install_dir/db/help my db is broke.txt" for background reading. Whatever happens next, you need to check that your hard drive is healthy.'
- message += os.linesep * 2
+ message += '\n' * 2
if self.CAN_REPOPULATE_ALL_MISSING_DATA:
diff --git a/hydrus/client/db/ClientDBRepositories.py b/hydrus/client/db/ClientDBRepositories.py
index 39be7cc62..4d5d51d0a 100644
--- a/hydrus/client/db/ClientDBRepositories.py
+++ b/hydrus/client/db/ClientDBRepositories.py
@@ -169,7 +169,7 @@ def _HandleCriticalRepositoryDefinitionError( self, service_id, name, bad_ids ):
self._cursor_transaction_wrapper.CommitAndBegin()
message = 'A critical error was discovered with one of your repositories: its definition reference is in an invalid state. Your repository should now be paused, and all update files have been scheduled for an integrity and metadata check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository. Once unpaused, it will reprocess your definition files and attempt to fill the missing entries. If this error occurs again once that is complete, please inform hydrus dev.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Error: {}: {}'.format( name, bad_ids )
raise Exception( message )
diff --git a/hydrus/client/db/ClientDBSerialisable.py b/hydrus/client/db/ClientDBSerialisable.py
index 4ef32011d..53f047276 100644
--- a/hydrus/client/db/ClientDBSerialisable.py
+++ b/hydrus/client/db/ClientDBSerialisable.py
@@ -67,11 +67,11 @@ def DealWithBrokenJSONDump( db_dir, dump, dump_object_descriptor, dump_descripto
message = 'A serialised object failed to load! Its description is "{}".'.format( dump_descriptor )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This error could be due to several factors, but is most likely a hard drive fault (perhaps your computer recently had a bad power cut?).'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The database has attempted to delete the broken object, and the object\'s dump written to {}. Depending on the object, your client may no longer be able to boot, or it may have lost something like a session or a subscription.'.format( path )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please review the \'help my db is broke.txt\' file in your install_dir/db directory as background reading, and if the situation or fix here is not obvious, please contact hydrus dev.'
HydrusData.ShowText( message )
@@ -254,11 +254,11 @@ def GetHashedJSONDumps( self, hashes ):
if not shown_missing_dump_message:
message = 'A hashed serialised object was missing! Its hash is "{}".'.format( hash.hex() )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This error could be due to several factors, but is most likely a hard drive fault (perhaps your computer recently had a bad power cut?).'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Your client may have lost one or more session pages.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please review the \'help my db is broke.txt\' file in your install_dir/db directory as background reading, and if the situation or fix here is not obvious, please contact hydrus dev.'
HydrusData.ShowText( message )
@@ -293,11 +293,11 @@ def GetHashedJSONDumps( self, hashes ):
if not shown_broken_dump_message:
message = 'A hashed serialised object failed to load! Its hash is "{}".'.format( hash.hex() )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This error could be due to several factors, but is most likely a hard drive fault (perhaps your computer recently had a bad power cut?).'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The database has attempted to delete the broken object, and the object\'s dump written to your database directory. Your client may have lost one or more session pages.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please review the \'help my db is broke.txt\' file in your install_dir/db directory as background reading, and if the situation or fix here is not obvious, please contact hydrus dev.'
HydrusData.ShowText( message )
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index a237cc643..f6d6005f0 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -3,11 +3,9 @@
import random
import re
import ssl
-import subprocess
import sys
import threading
import time
-import traceback
import cv2
import PIL
@@ -36,7 +34,6 @@
from hydrus.core.files import HydrusVideoHandling
from hydrus.core.files.images import HydrusImageHandling
from hydrus.core.networking import HydrusNetwork
-from hydrus.core.networking import HydrusNetworking
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@@ -60,7 +57,6 @@
from hydrus.client.gui import ClientGUIFrames
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUILogin
-from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIPopupMessages
from hydrus.client.gui import ClientGUIScrolledPanels
@@ -84,6 +80,7 @@
from hydrus.client.gui.canvas import ClientGUIMPV
from hydrus.client.gui.exporting import ClientGUIExport
from hydrus.client.gui.importing import ClientGUIImportFolders
+from hydrus.client.gui.media import ClientGUIMediaControls
from hydrus.client.gui.networking import ClientGUIHydrusNetwork
from hydrus.client.gui.networking import ClientGUINetwork
from hydrus.client.gui.pages import ClientGUIManagementController
@@ -202,9 +199,9 @@ def THREADUploadPending( service_key ):
', '.join( ( HC.content_type_string_lookup[ content_type ] for content_type in unauthorised_content_types ) )
)
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you are currently using a public, read-only account (such as with the PTR), you may be able to generate your own private account with more permissions. Please hit the button below to open this service in _manage services_ and see if you can generate a new account. If accounts cannot be automatically created, you may have to contact the server owner directly to get this permission.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you think your account does have this permission, try refreshing it under _review services_.'
unauthorised_job_status = ClientThreading.JobStatus()
@@ -886,7 +883,7 @@ def _AboutWindow( self ):
description = 'This is the media management application of the hydrus software suite.'
- description += os.linesep * 2 + os.linesep.join( library_version_lines )
+ description += '\n' * 2 + '\n'.join( library_version_lines )
#
@@ -916,9 +913,9 @@ def _AboutWindow( self ):
def _AnalyzeDatabase( self ):
message = 'This will gather statistical information on the database\'s indices, helping the query planner perform efficiently. It typically happens automatically every few days, but you can force it here. If you have a large database, it will take a few minutes, during which your gui may hang. A popup message will show its status.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'A \'soft\' analyze will only reanalyze those indices that are due for a check in the normal db maintenance cycle. If nothing is due, it will return immediately.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'A \'full\' analyze will force a run over every index in the database. This can take substantially longer. If you do not have a specific reason to select this, it is probably pointless.'
( result, was_cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose how thorough your analyze will be.', yes_label = 'soft', no_label = 'full', check_for_cancelled = True )
@@ -968,7 +965,7 @@ def do_it():
self._controller.SetServices( all_services )
message = 'PTR setup done! Check services->review services to see it.'
- message += os.linesep * 2
+ message += '\n' * 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 )
@@ -991,16 +988,16 @@ def do_it():
text = 'This will automatically set up your client with public shared \'read-only\' account for the Public Tag Repository, just as if you had added it manually under services->manage services.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Over the coming weeks, your client will download updates and then process them into your database in idle time, and the PTR\'s tags will increasingly appear across your files. If you decide to upload tags, it is just a couple of clicks (under services->manage services again) to generate your own account that has permission to do so.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Be aware that the PTR has been growing since 2011 and now has more than a billion mappings. As of 2021-06, it requires about 6GB of bandwidth and file storage, and your database itself will grow by 50GB! Processing also takes a lot of CPU and HDD work, and, due to the unavoidable mechanical latency of HDDs, will only work in reasonable time if your hydrus database is on an SSD.'
- text += os.linesep * 2
+ text += '\n' * 2
text += '++++If you are on a mechanical HDD or will not be able to free up enough space on your SSD, cancel out now.++++'
if have_it_already:
- text += os.linesep * 2
+ text += '\n' * 2
text += 'You seem to have the PTR already. If it is paused or desynchronised, this is best fixed under services->review services. Are you sure you want to add a duplicate?'
@@ -1042,7 +1039,7 @@ def _BackupDatabase( self ):
text = action + ' backup at "' + path + '"?'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'The database will be locked while the backup occurs, which may lock up your gui as well.'
result = ClientGUIDialogsQuick.GetYesNo( self, text )
@@ -1199,9 +1196,9 @@ def _ClearFileViewingStats( self ):
def _ClearOrphanFiles( self ):
text = 'This job will iterate through every file in your database\'s file storage, extracting any it does not expect to be there. This is particularly useful for \'re-syncing\' your file storage to what it should be, and is particularly useful if you are marrying an older/newer database with a newer/older file storage.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'You can choose to move the orphans in your file directories somewhere or delete them. Orphans in your thumbnail directories will always be deleted.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Files and thumbnails will be inaccessible while this runs, so it is best to leave the client alone until it is done. It may take some time.'
yes_tuples = []
@@ -1241,11 +1238,11 @@ def _ClearOrphanFiles( self ):
def _ClearOrphanFileRecords( self ):
text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'This will instruct the database to review its file records\' integrity. If anything appears to be in a specific domain (e.g. my files) but not an umbrella domain (e.g. all my files), and the actual file also exists on disk, it will try to recover the record. If the file does not actually exist on disk, or the record is in the umbrella domain and not in the specific domain, or if recovery data cannot be found, the record will be deleted.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'You typically do not ever see these files and they are basically harmless, but they can offset some file counts confusingly and may break other maintenance routines. You probably only need to run this if you can\'t process the apparent last handful of duplicate filter pairs or hydrus dev otherwise told you to try it.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'It will create a popup message while it works and inform you of the number of orphan records found. It may lock up the client for a bit.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )
@@ -1259,7 +1256,7 @@ def _ClearOrphanFileRecords( self ):
def _ClearOrphanHashedSerialisables( self ):
text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'This force-runs a routine that regularly removes some spare data from the database. You most likely do not need to run it.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )
@@ -1291,9 +1288,9 @@ def do_it():
def _ClearOrphanTables( self ):
text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'This will instruct the database to review its service tables and delete any orphans. This will typically do nothing, but hydrus dev may tell you to run this, just to check. Be sure you have a recent backup before you run this--if it deletes something important by accident, you will want to roll back!'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'It will create popups if it finds anything to delete.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )
@@ -1307,11 +1304,11 @@ def _ClearOrphanTables( self ):
def _CullFileViewingStats( self ):
text = 'If your file viewing statistics have some erroneous values due to many short views or accidental long views, this routine will cull your current numbers to compensate. For instance:'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you have a file with 100 views over 100 seconds and a minimum view time of 2 seconds, this will cull the views to 50.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you have a file with 10 views over 100000 seconds and a maximum view time of 60 seconds, this will cull the total viewtime to 600 seconds.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'It will work for both preview and media views based on their separate rules.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )
@@ -1783,7 +1780,7 @@ def _DeleteServiceInfo( self, only_pending = False ):
types_to_delete = None
message = 'This clears the cached counts for things like the number of files or tags on a service. Due to unusual situations and little counting bugs, these numbers can sometimes become unsynced. Clearing them forces an accurate recount from source.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Some GUI elements (review services, mainly) may be slow the next time they launch. Especially if you clear for all services.'
@@ -1876,7 +1873,7 @@ def _FetchIP( self, service_key ):
local_time = HydrusTime.TimestampToPrettyTime( timestamp )
text = 'File Hash: ' + hash.hex()
- text += os.linesep
+ text += '\n'
text += 'Uploader\'s IP: ' + ip
text += 'Upload Time (UTC): ' + utc_time
text += 'Upload Time (Your time): ' + local_time
@@ -1904,7 +1901,7 @@ def _FindMenuBarIndex( self, name ):
def _FixLogicallyInconsistentMappings( self ):
message = 'This will check for tags that are occupying mutually exclusive states--either current & pending or deleted & petitioned.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please run this if you attempt to upload some tags and get a related error. You may need some follow-up regeneration work to correct autocomplete or \'num pending\' counts.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -2214,7 +2211,7 @@ def _ImportURL(
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
- url = ClientNetworkingFunctions.WashURL( unclean_url )
+ url = ClientNetworkingFunctions.EnsureURLIsEncoded( unclean_url )
url = CG.client_controller.network_engine.domain_manager.NormaliseURL( url, for_server = True )
@@ -2223,7 +2220,7 @@ def _ImportURL(
if url_type in ( HC.URL_TYPE_GALLERY, HC.URL_TYPE_POST, HC.URL_TYPE_WATCHABLE ) and not can_parse:
message = 'This URL was recognised as a "{}" but it cannot be parsed: {}'.format( match_name, cannot_parse_reason )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Since this URL cannot be parsed, a downloader cannot be created for it! Please check your url class links under the \'networking\' menu.'
raise HydrusExceptions.URLClassException( message )
@@ -3136,7 +3133,7 @@ def _InitialiseMenuInfoDatabase( self ):
self._menubar_database_restore_backup = ClientGUIMenus.AppendMenuItem( menu, 'restore from a database backup' + HC.UNICODE_ELLIPSIS, 'Restore the database from an external location.', self._controller.RestoreDatabase )
message = 'Your database is stored across multiple locations. The in-client backup routine can only handle simple databases (in one location), so the menu commands to backup have been hidden. To back up, please use a third-party program that will work better than anything I can write.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Check the help for more info on how best to backup manually.'
self._menubar_database_multiple_location_label = ClientGUIMenus.AppendMenuItem( menu, 'database is stored in multiple locations', 'The database is migrated, and internal backups are not possible--click for more info.', HydrusData.ShowText, message )
@@ -3402,11 +3399,11 @@ def _InitialiseMenuInfoHelp( self ):
profiling = ClientGUIMenus.GenerateMenu( debug_menu )
profile_mode_message = 'If something is running slow, you can turn on profile mode to have hydrus gather information on how long many jobs take to run.'
- profile_mode_message += os.linesep * 2
+ profile_mode_message += '\n' * 2
profile_mode_message += 'Turn the mode on, do the slow thing for a bit, and then turn it off. In your database directory will be a new profile log, which is really helpful for hydrus dev to figure out what is running slow for you and how to fix it.'
- profile_mode_message += os.linesep * 2
+ profile_mode_message += '\n' * 2
profile_mode_message += 'A new Query Planner mode also makes very detailed database analysis. This is an alternate profiling mode hydev is testing.'
- profile_mode_message += os.linesep * 2
+ profile_mode_message += '\n' * 2
profile_mode_message += 'More information is available in the help, under \'reducing program lag\'.'
ClientGUIMenus.AppendMenuItem( profiling, 'what is this?', 'Show profile info.', ClientGUIDialogsMessage.ShowInformation, self, profile_mode_message )
@@ -3582,7 +3579,7 @@ def _InitialiseMenuInfoNetwork( self ):
if not ClientParsing.HTML5LIB_IS_OK:
message = 'The client was unable to import html5lib on boot. This is an important parsing library that performs better than the usual backup, lxml. Without it, some downloaders will not work well and you will miss tags and files.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You are likely running from source, so I recommend you close the client, run \'pip install html5lib\' (or whatever is appropriate for your environment) and try again. You can double-check what imported ok under help->about.'
ClientGUIMenus.AppendMenuItem( submenu, '*** html5lib not found! ***', 'Your client does not have an important library.', ClientGUIDialogsMessage.ShowWarning, self, message )
@@ -3849,9 +3846,9 @@ def _InitialiseSession( self ):
# this can be upgraded to a nicer checkboxlist dialog to select pages or w/e
message = 'It looks like the last instance of the client did not shut down cleanly.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Would you like to try loading your default session "' + default_gui_session + '", or just a blank page?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will auto-choose to open your default session in 15 seconds.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Previous shutdown was bad', yes_label = 'try to load "' + default_gui_session + '"', no_label = 'just load a blank page', auto_yes_time = 15 )
@@ -4716,9 +4713,9 @@ def qt_do_it( subscriptions, missing_query_log_container_names, surplus_query_lo
if len( missing_query_log_container_names ) > 0:
text = '{} subscription queries had missing database data! This is a serious error!'.format( HydrusData.ToHumanInt( len( missing_query_log_container_names ) ) )
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you continue, the client will now create and save empty file/search logs for those queries, essentially resetting them, but if you know you need to exit and fix your database in a different way, cancel out now.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you do not know why this happened, you may have had a hard drive fault. Please consult "install_dir/db/help my db is broke.txt", and you may want to contact hydrus dev.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, title = 'Missing Query Logs!', yes_label = 'continue', no_label = 'back out' )
@@ -4756,9 +4753,9 @@ def qt_do_it( subscriptions, missing_query_log_container_names, surplus_query_lo
if len( surplus_query_log_container_names ) > 0:
text = 'When loading subscription data, the client discovered surplus orphaned subscription data for {} queries! This data is harmless and no longer used. The situation is however unusual, and probably due to an unusual deletion routine or a bug.'.format( HydrusData.ToHumanInt( len( surplus_query_log_container_names ) ) )
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you continue, this surplus data will backed up to your database directory and then safely deleted from the database itself, but if you recently did manual database editing and know you need to exit and fix your database in a different way, cancel out now.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you do not know why this happened, hydrus dev would be interested in being told about it and the surrounding circumstances.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, title = 'Orphan Query Logs!', yes_label = 'continue', no_label = 'back out' )
@@ -5190,7 +5187,7 @@ def _RefreshStatusBar( self ):
db_tooltip = None
- self._statusbar.setToolTip( job_name )
+ self._statusbar.setToolTip( ClientGUIFunctions.WrapToolTip( job_name ) )
self._statusbar.SetStatusText( media_status, 0 )
self._statusbar.SetStatusText( idle_status, 2, tooltip = idle_tooltip )
@@ -5202,9 +5199,9 @@ def _RefreshStatusBar( self ):
def _RegenerateCombinedDeletedFiles( self ):
message = 'This will resynchronise the "all deleted files" cache to the actual records in the database, ensuring that various tag searches over the deleted files domain give correct counts and file results. It isn\'t super important, but this routine fixes it if it is desynchronised.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It should not take all that long, but if you have a lot of deleted files, it can take a little while, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5218,9 +5215,9 @@ def _RegenerateCombinedDeletedFiles( self ):
def _RegenerateTagCache( self ):
message = 'This will delete and then recreate the fast search cache for one or all tag services.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a little while, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless. It fixes missing autocomplete or tag search results.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5243,9 +5240,9 @@ def _RegenerateTagCache( self ):
def _RegenerateLocalHashCache( self ):
message = 'This will delete and then recreate the local hash cache, which keeps a small record of hashes for files on your hard drive. It isn\'t super important, but it speeds most operations up, and this routine fixes it when broken.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of files, it can take a long time, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5259,9 +5256,9 @@ def _RegenerateLocalHashCache( self ):
def _RegenerateLocalTagCache( self ):
message = 'This will delete and then recreate the local tag cache, which keeps a small record of tags for files on your hard drive. It isn\'t super important, but it speeds most operations up, and this routine fixes it when broken.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a long time, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5275,9 +5272,9 @@ def _RegenerateLocalTagCache( self ):
def _RegenerateTagDisplayMappingsCache( self ):
message = 'This will delete and then recreate the tag \'display\' mappings cache, which is used for user-presented tag searching, loading, and autocomplete counts. This is useful if miscounting (particularly related to siblings/parents) has somehow occurred.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a long time, during which the gui may hang. All siblings and parents will have to be resynced.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5300,9 +5297,9 @@ def _RegenerateTagDisplayMappingsCache( self ):
def _RegenerateTagDisplayPendingMappingsCache( self ):
message = 'This will delete and then recreate the pending tags on the tag \'display\' mappings cache, which is used for user-presented tag searching, loading, and autocomplete counts. This is useful if you have \'ghost\' pending tags or counts hanging around.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a millions of tags, pending or current, it can take a long time, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5325,11 +5322,11 @@ def _RegenerateTagDisplayPendingMappingsCache( self ):
def _RegenerateTagMappingsCache( self ):
message = 'WARNING: Do not run this for no reason! On a large database, this could take hours to finish!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will delete and then recreate the entire tag \'storage\' mappings cache, which is used for tag calculation based on actual values and autocomplete counts in editing contexts like _manage tags_. This is useful if miscounting has somehow occurred.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a long time, during which the gui may hang. It necessarily involves a regeneration of the tag display mappings cache, which relies on the storage cache, and the tag text search cache. All siblings and parents will have to be resynced.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5352,9 +5349,9 @@ def _RegenerateTagMappingsCache( self ):
def _RegenerateTagPendingMappingsCache( self ):
message = 'This will delete and then recreate the pending tags on the whole tag mappings cache, which is used for multiple kinds of tag searching, loading, and autocomplete counts. This is useful if you have \'ghost\' pending tags or counts hanging around.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a millions of tags, pending or current, it can take a long time, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5377,9 +5374,9 @@ def _RegenerateTagPendingMappingsCache( self ):
def _RegenerateSimilarFilesTree( self ):
message = 'This will delete and then recreate the similar files search tree. This is useful if it has somehow become unbalanced and similar files searches are running slow.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of files, it can take a little while, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
( result, was_cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it', check_for_cancelled = True )
@@ -5393,9 +5390,9 @@ def _RegenerateSimilarFilesTree( self ):
def _RegenerateTagCacheSearchableSubtagsMaps( self ):
message = 'This will regenerate the fast search cache\'s \'unusual character logic\' lookup map, for one or all tag services.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags, it can take a little while, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless. It fixes missing autocomplete search results.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5418,9 +5415,9 @@ def _RegenerateTagCacheSearchableSubtagsMaps( self ):
def _RegenerateTagParentsLookupCache( self ):
message = 'This will delete and then recreate the tag parents lookup cache, which is used for all basic tag parents operations. This is useful if it has become damaged or otherwise desynchronised.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It should only take a second or two.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5434,9 +5431,9 @@ def _RegenerateTagParentsLookupCache( self ):
def _RegenerateTagSiblingsLookupCache( self ):
message = 'This will delete and then recreate the tag siblings lookup cache, which is used for all basic tag sibling operations. This is useful if it has become damaged or otherwise desynchronised.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It should only take a second or two. It necessarily involves a regeneration of the tag parents lookup cache.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5450,9 +5447,9 @@ def _RegenerateTagSiblingsLookupCache( self ):
def _RepairInvalidTags( self ):
message = 'This will scan all your tags and repair any that are invalid. This might mean taking out unrenderable characters or cleaning up improper whitespace. If there is a tag collision once cleaned, it may add a (1)-style number on the end.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags, it can take a long time, during which the gui may hang. If it finds bad tags, you should restart the program once it is complete.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have not had tag rendering problems, there is no reason to run this.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -5472,9 +5469,9 @@ def _RepairInvalidTags( self ):
def _RepopulateMappingsTables( self ):
message = 'WARNING: Do not run this for no reason!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have significant local tags (e.g. \'my tags\') storage, recently had a \'malformed\' client.mappings.db file, and have since gone through clone/repair and now have a truncated file, this routine will attempt to recover missing tags from the smaller tag cache stored in client.caches.db.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It can only recover tags for files currently stored by your client. It will take some time, during which the gui may hang. Once it is done, you probably want to regenerate your tag mappings cache, so that you are completely synced again.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'I have a reason to run this, let\'s do it--now choose which service', no_label = 'forget it' )
@@ -5503,9 +5500,9 @@ def _RepopulateMappingsTables( self ):
def _RepopulateTagCacheMissingSubtags( self ):
message = 'This will repopulate the fast search cache\'s subtag search, filling in missing entries, for one or all tag services.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a little while, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless. It fixes missing autocomplete or tag search results.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5528,9 +5525,9 @@ def _RepopulateTagCacheMissingSubtags( self ):
def _RepopulateTagDisplayMappingsCache( self ):
message = 'This will go through your mappings cache and fill in any missing files. It is radically faster than a full regen, and adds siblings and parents instantly, but it only solves the problem of missing file rows.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a millions of tags, pending or current, it can take a long time, during which the gui may hang.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -5558,9 +5555,9 @@ def _RestoreSplitterPositions( self ):
def _ResyncTagMappingsCacheFiles( self ):
message = 'This will scan your mappings cache for surplus or missing files and correct them. This is useful if you see ghost files or if searches miss files that have the tag.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have a lot of tags and files, it can take a long time, during which the gui may hang. It should be much faster than the full regen options though!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you do not have a specific reason to run this, it is pointless.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it--now choose which service', no_label = 'forget it' )
@@ -6433,19 +6430,19 @@ def _SetupBackupPath( self ):
if existing_backup_path is None:
backup_intro = 'Everything in your client is stored in the \'database\', which consists of a handful of .db files and a single subdirectory that contains all your media files. It is a very good idea to maintain a regular backup schedule--to save from hard drive failure, serious software fault, accidental deletion, or any other unexpected problem. It sucks to lose all your work, so make sure it can\'t happen!'
- backup_intro += os.linesep * 2
+ backup_intro += '\n' * 2
backup_intro += 'If you prefer to create a manual backup with an external program like FreeFileSync, then please cancel out of the dialog after this and set up whatever you like, but if you would rather a simple solution, simply select a directory and the client will remember it as the designated backup location. Creating or updating your backup can be triggered at any time from the database menu.'
- backup_intro += os.linesep * 2
+ backup_intro += '\n' * 2
backup_intro += 'An ideal backup location is initially empty and on a different hard drive.'
- backup_intro += os.linesep * 2
+ backup_intro += '\n' * 2
backup_intro += 'If you have a large database (100,000+ files) or a slow hard drive, creating the initial backup may take a long time--perhaps an hour or more--but updating an existing backup should only take a couple of minutes (since the client only has to copy new or modified files). Try to update your backup every week!'
- backup_intro += os.linesep * 2
+ backup_intro += '\n' * 2
backup_intro += 'If you would like some more info on making or restoring backups, please consult the help\'s \'installing and updating\' page.'
else:
backup_intro = 'Your current backup location is "{}".'.format( existing_backup_path )
- backup_intro += os.linesep * 2
+ backup_intro += '\n' * 2
backup_intro += 'If your client is getting large and/or complicated, I recommend you start backing up with a proper external program like FreeFileSync. If you would like some more info on making or restoring backups, please consult the help\'s \'installing and updating\' page.'
@@ -6501,9 +6498,9 @@ def _SetupBackupPath( self ):
text = 'You chose "' + path + '". Here is what I understand about it:'
- text += os.linesep * 2
+ text += '\n' * 2
text += extra_info
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Are you sure this is the correct directory?'
result = ClientGUIDialogsQuick.GetYesNo( self, text )
@@ -6570,13 +6567,13 @@ def _ShowPageWeightInfo( self ):
total_closed_num_seeds_weight = ClientGUIPages.ConvertNumSeedsToWeight( total_closed_num_seeds )
message = 'Session weight is a simple representation of your pages combined memory and CPU load. A file counts as 1, and a URL counts as 20.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Try to keep the total below 10 million! It is also generally better to spread it around--have five download pages each of 500k weight rather than one page with 2.5M.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Your {} open pages\' total is: {}'.format( total_active_page_count, HydrusData.ToHumanInt( total_active_num_hashes_weight + total_active_num_seeds_weight ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Specifically, your file weight is {} and URL weight is {}.'.format( HydrusData.ToHumanInt( total_active_num_hashes_weight ), HydrusData.ToHumanInt( total_active_num_seeds_weight ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'For extra info, your {} closed pages (in the undo list) have total weight {}, being file weight {} and URL weight {}.'.format(
total_closed_page_count,
HydrusData.ToHumanInt( total_closed_num_hashes_weight + total_closed_num_seeds_weight ),
@@ -6894,11 +6891,11 @@ def _UpdateSystemTrayIcon( self, currently_booting = False ):
def _VacuumDatabase( self ):
text = 'This will rebuild the database, rewriting all indices and tables to be contiguous and optimising most operations. It also truncates the database files, recovering unused space back to your hard drive. It typically happens automatically every few months, but you can force it here.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If you have no reason to run this, it is usually pointless. If you have a very large database on an HDD instead of an SSD, it may take upwards of an hour, during which your gui may hang. A popup message will show its status.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'A \'soft\' vacuum will only reanalyze those databases that are due for a check in the normal db maintenance cycle. If nothing is due, it will return immediately.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'A \'full\' vacuum will immediately force a vacuum for the entire database. This can take substantially longer.'
( result, was_cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, text, title = 'Choose how thorough your vacuum will be.', yes_label = 'soft', no_label = 'full', check_for_cancelled = True )
@@ -8401,7 +8398,7 @@ def TryToExit( self, restart = False, force_shutdown_maintenance = False ):
if able_to_close_statement is not None:
- text += os.linesep * 2
+ text += '\n' * 2
text += able_to_close_statement
@@ -8452,17 +8449,17 @@ def TryToExit( self, restart = False, force_shutdown_maintenance = False ):
if len( work_to_do ) > 0:
text = 'Is now a good time for the client to do up to ' + HydrusData.ToHumanInt( idle_shutdown_max_minutes ) + ' minutes\' maintenance work? (Will auto-no in 15 seconds)'
- text += os.linesep * 2
+ text += '\n' * 2
if CG.client_controller.IsFirstStart():
text += 'Since this is your first session, this maintenance should just be some quick initialisation work. It should only take a few seconds.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'The outstanding jobs appear to be:'
- text += os.linesep * 2
- text += os.linesep.join( work_to_do )
+ text += '\n' * 2
+ text += '\n'.join( work_to_do )
( result, was_cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, text, title = 'Maintenance is due', auto_no_time = 15, check_for_cancelled = True )
diff --git a/hydrus/client/gui/ClientGUIAPI.py b/hydrus/client/gui/ClientGUIAPI.py
index 218c92704..67219ef53 100644
--- a/hydrus/client/gui/ClientGUIAPI.py
+++ b/hydrus/client/gui/ClientGUIAPI.py
@@ -96,7 +96,7 @@ def __init__( self, parent, api_permissions ):
search_tag_filter = api_permissions.GetSearchTagFilter()
message = 'The API will only permit searching for tags that pass through this filter.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you want to allow all tags, just leave it as is, permitting everything. If you want to limit it to just one tag, such as "do waifu2x on this", set up a whitelist with only that tag allowed.'
self._search_tag_filter = ClientGUITags.TagFilterButton( self, message, search_tag_filter, label_prefix = 'permitted tags: ' )
diff --git a/hydrus/client/gui/ClientGUIAsync.py b/hydrus/client/gui/ClientGUIAsync.py
index cbf0446e9..b1e1a4c97 100644
--- a/hydrus/client/gui/ClientGUIAsync.py
+++ b/hydrus/client/gui/ClientGUIAsync.py
@@ -31,9 +31,9 @@ def _DefaultErrback( self, etype, value, tb ):
HydrusData.ShowExceptionTuple( etype, value, tb )
message = 'An error occured in a background task. If you had UI waiting on a fetch job, the dialog/panel may need to be closed and re-opened.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The error info will show as a popup and also be printed to log. Hydev may want to know about this error, at least to improve error handling.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Error summary: {}'.format( value )
ClientGUIDialogsMessage.ShowCritical( self._win, 'Error', message )
diff --git a/hydrus/client/gui/ClientGUIDialogs.py b/hydrus/client/gui/ClientGUIDialogs.py
index 736e8843f..3e221e32c 100644
--- a/hydrus/client/gui/ClientGUIDialogs.py
+++ b/hydrus/client/gui/ClientGUIDialogs.py
@@ -349,9 +349,9 @@ def __init__( self, parent, share_key, name, text, timeout, hashes, new_share =
vbox = QP.VBoxLayout()
intro = 'Sharing ' + HydrusData.ToHumanInt( len( self._hashes ) ) + ' files.'
- intro += os.linesep + 'Title and text are optional.'
+ intro += '\n' + 'Title and text are optional.'
- if new_share: intro += os.linesep + 'The link will not work until you ok this dialog.'
+ if new_share: intro += '\n' + 'The link will not work until you ok this dialog.'
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,intro), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@@ -459,7 +459,7 @@ def __init__( self, parent, namespace = '', regex = '' ):
vbox = QP.VBoxLayout()
- intro = r'Put the namespace (e.g. page) on the left.' + os.linesep + r'Put the regex (e.g. [1-9]+\d*(?=.{4}$)) on the right.' + os.linesep + r'All files will be tagged with "namespace:regex".'
+ intro = r'Put the namespace (e.g. page) on the left.' + '\n' + r'Put the regex (e.g. [1-9]+\d*(?=.{4}$)) on the right.' + '\n' + r'All files will be tagged with "namespace:regex".'
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,intro), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, control_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@@ -495,7 +495,7 @@ def EventOK( self ):
except Exception as e:
text = 'That regex would not compile!'
- text += os.linesep * 2
+ text += '\n' * 2
text += str( e )
ClientGUIDialogsMessage.ShowCritical( self, 'Error', text )
diff --git a/hydrus/client/gui/ClientGUIDialogsManage.py b/hydrus/client/gui/ClientGUIDialogsManage.py
index e00c274d2..27c977ac2 100644
--- a/hydrus/client/gui/ClientGUIDialogsManage.py
+++ b/hydrus/client/gui/ClientGUIDialogsManage.py
@@ -68,10 +68,10 @@ def __init__( self, parent, media ):
self._copy_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().copy, self._Copy )
- self._copy_button.setToolTip( 'Copy ratings to the clipboard.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy ratings to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste ratings from the clipboard.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste ratings from the clipboard.' ) )
self._apply = QW.QPushButton( 'apply', self )
self._apply.clicked.connect( self.EventOK )
@@ -150,7 +150,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON pairs of service keys and rating values', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON pairs of service keys and rating values', e )
return
@@ -885,7 +885,7 @@ def publish_callable( result ):
def _Remove( self ):
text = 'Remove these port mappings?'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If a mapping does not disappear after a remove, it may be hard-added in the router\'s settings interface. In this case, you will have to go there to fix it.'
result = ClientGUIDialogsQuick.GetYesNo( self, text, yes_label = 'do it', no_label = 'forget it' )
diff --git a/hydrus/client/gui/ClientGUIDialogsQuick.py b/hydrus/client/gui/ClientGUIDialogsQuick.py
index 834bac307..1b1dc983f 100644
--- a/hydrus/client/gui/ClientGUIDialogsQuick.py
+++ b/hydrus/client/gui/ClientGUIDialogsQuick.py
@@ -4,12 +4,14 @@
from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
+from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientPaths
+from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIScrolledPanelsButtonQuestions
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@@ -43,6 +45,7 @@ def GetDeleteFilesJobs( win, media, default_reason, suggested_file_service_key =
+
def GetFinishArchiveDeleteFilteringAnswer( win, kept_label, deletion_options ):
with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, 'filtering done?' ) as dlg:
@@ -58,6 +61,7 @@ def GetFinishArchiveDeleteFilteringAnswer( win, kept_label, deletion_options ):
return ( result, location_context, was_cancelled )
+
def GetFinishFilteringAnswer( win, label ):
with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, label ) as dlg:
@@ -162,6 +166,77 @@ def GetYesYesNo( win, message, title = 'Are you sure?', yes_tuples = None, no_la
+def OpenDocumentation( win: QW.QWidget, documentation_path: str ):
+
+ local_path = os.path.join( HC.HELP_DIR, documentation_path )
+ remote_url = "/".join( ( HC.REMOTE_HELP.rstrip( '/' ), documentation_path.lstrip( '/' ) ) )
+
+ local_launch_path = local_path
+
+ if "#" in local_path:
+
+ local_path = local_path[ : local_path.find( '#' ) ]
+
+
+ if os.path.isfile( local_path ):
+
+ ClientPaths.LaunchPathInWebBrowser( local_launch_path )
+
+ else:
+
+ message = 'You do not have a local help! Are you running from source? Would you like to open the online help or see a guide on how to build your own?'
+
+ yes_tuples = []
+
+ yes_tuples.append( ( 'open online help', 0 ) )
+ yes_tuples.append( ( 'open how to build guide', 1 ) )
+
+ try:
+
+ result = GetYesYesNo( win, message, yes_tuples = yes_tuples, no_label = 'forget it' )
+
+ except HydrusExceptions.CancelledException:
+
+ return
+
+
+ if result == 0:
+
+ url = remote_url
+
+ else:
+
+ url = '/'.join( ( HC.REMOTE_HELP.rstrip( '/' ), HC.DOCUMENTATION_ABOUT_DOCS.lstrip( '/' ) ) )
+
+
+ ClientPaths.LaunchURLInWebBrowser( url )
+
+
+
+def PresentClipboardParseError( win: QW.QWidget, content: str, expected_content_description: str, e: Exception ):
+
+ MAX_CONTENT_SIZE = 1024
+
+ log_message = 'Clipboard Error!\nI was expecting: {}'.format( expected_content_description )
+
+ if len( content ) > MAX_CONTENT_SIZE:
+
+ log_message += '\nFirst {} of content received (total was {}):\n'.format( HydrusData.ToHumanBytes( MAX_CONTENT_SIZE ), HydrusData.ToHumanBytes( len( content ) ) ) + content[:MAX_CONTENT_SIZE]
+
+ else:
+
+ log_message += '\nContent received ({}):\n'.format( HydrusData.ToHumanBytes( len( content ) ) ) + content[:MAX_CONTENT_SIZE]
+
+
+ HydrusData.DebugPrint( log_message )
+
+ HydrusData.PrintException( e, do_wait = False )
+
+ message = 'Sorry, I could not understand what was in the clipboard. I was expecting "{}" but received this text:\n\n{}\n\nMore details have been written to the log, but the general error was:\n\n{}'.format( expected_content_description, HydrusText.ElideText( content, 64 ), repr( e ) )
+
+ ClientGUIDialogsMessage.ShowCritical( win, 'Clipboard Error!', message )
+
+
def SelectFromList( win, title, choice_tuples, value_to_select = None, sort_tuples = True ):
if len( choice_tuples ) == 1:
@@ -189,6 +264,7 @@ def SelectFromList( win, title, choice_tuples, value_to_select = None, sort_tupl
+
def SelectFromListButtons( win, title, choice_tuples, message = '' ):
if len( choice_tuples ) == 1:
@@ -216,6 +292,7 @@ def SelectFromListButtons( win, title, choice_tuples, message = '' ):
+
def SelectMultipleFromList( win, title, choice_tuples ):
with ClientGUITopLevelWindowsPanels.DialogEdit( win, title ) as dlg:
@@ -236,6 +313,7 @@ def SelectMultipleFromList( win, title, choice_tuples ):
+
def SelectServiceKey( service_types = None, service_keys = None, unallowed = None, message = 'select service' ):
if service_types is None:
@@ -287,50 +365,3 @@ def SelectServiceKey( service_types = None, service_keys = None, unallowed = Non
-
-def OpenDocumentation( win: QW.QWidget, documentation_path: str ):
-
- local_path = os.path.join( HC.HELP_DIR, documentation_path )
- remote_url = "/".join( ( HC.REMOTE_HELP.rstrip( '/' ), documentation_path.lstrip( '/' ) ) )
-
- local_launch_path = local_path
-
- if "#" in local_path:
-
- local_path = local_path[ : local_path.find( '#' ) ]
-
-
- if os.path.isfile( local_path ):
-
- ClientPaths.LaunchPathInWebBrowser( local_launch_path )
-
- else:
-
- message = 'You do not have a local help! Are you running from source? Would you like to open the online help or see a guide on how to build your own?'
-
- yes_tuples = []
-
- yes_tuples.append( ( 'open online help', 0 ) )
- yes_tuples.append( ( 'open how to build guide', 1 ) )
-
- try:
-
- result = GetYesYesNo( win, message, yes_tuples = yes_tuples, no_label = 'forget it' )
-
- except HydrusExceptions.CancelledException:
-
- return
-
-
- if result == 0:
-
- url = remote_url
-
- else:
-
- url = '/'.join( ( HC.REMOTE_HELP.rstrip( '/' ), HC.DOCUMENTATION_ABOUT_DOCS.lstrip( '/' ) ) )
-
-
- ClientPaths.LaunchURLInWebBrowser( url )
-
-
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index 2d9d06310..e9a000797 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -297,6 +297,8 @@ def __init__( self, parent: QW.QWidget, gug: ClientNetworkingGUG.GalleryURLGener
self._example_url = QW.QLineEdit( self )
self._example_url.setReadOnly( True )
+ self._example_url.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is the Request URL--what will be sent to the server, with any additional normalisation the matching URL Class will add.' ) )
+
self._matched_url_class = QW.QLineEdit( self )
self._matched_url_class.setReadOnly( True )
@@ -327,7 +329,7 @@ def __init__( self, parent: QW.QWidget, gug: ClientNetworkingGUG.GalleryURLGener
rows.append( ( 'search terms separator: ', self._search_terms_separator ) )
rows.append( ( 'initial search text (to prompt user): ', self._initial_search_text ) )
rows.append( ( 'example search text: ', self._example_search_text ) )
- rows.append( ( 'example url: ', self._example_url ) )
+ rows.append( ( 'example request url: ', self._example_url ) )
rows.append( ( 'matches as a: ', self._matched_url_class ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
@@ -797,9 +799,9 @@ def _DeleteGUG( self ):
affected_ngug_names.sort()
message = 'The GUG "' + deletee.GetName() + '" is in the NGUGs:'
- message += os.linesep * 2
- message += os.linesep.join( affected_ngug_names )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( affected_ngug_names )
+ message += '\n' * 2
message += 'Deleting this GUG will ultimately remove it from those NGUGs--are you sure that is ok?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -928,13 +930,13 @@ def __init__( self, parent: QW.QWidget, string_match: ClientStrings.StringMatch,
string_match_panel = ClientGUICommon.StaticBox( self, 'value test' )
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( string_match_panel, string_match )
- self._string_match.setToolTip( 'If the encoded value of the component matches this, the URL Class matches!' )
+ self._string_match.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the encoded value of the component matches this, the URL Class matches!' ) )
self._pretty_default_value = ClientGUICommon.NoneableTextCtrl( self )
- self._pretty_default_value.setToolTip( 'If the URL is missing this component, you can add it here, and the URL Class will still match and will normalise by adding this default value. This can be useful if you need to add a /art or similar to a URL that ends with either /username or /username/art--sometimes it is better to make that stuff explicit in all cases.' )
+ self._pretty_default_value.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the URL is missing this component, you can add it here, and the URL Class will still match and will normalise by adding this default value. This can be useful if you need to add a /art or similar to a URL that ends with either /username or /username/art--sometimes it is better to make that stuff explicit in all cases.' ) )
self._default_value = ClientGUICommon.NoneableTextCtrl( self )
- self._default_value.setToolTip( 'What actual value will be embedded into the URL sent to the server.' )
+ self._default_value.setToolTip( ClientGUIFunctions.WrapToolTip( 'What actual value will be embedded into the URL sent to the server.' ) )
#
@@ -991,7 +993,7 @@ def _PrettyDefaultValueChanged( self ):
pretty_default_value = self._pretty_default_value.GetValue()
- default_value = pretty_default_value if pretty_default_value is None else urllib.parse.quote( pretty_default_value )
+ default_value = pretty_default_value if pretty_default_value is None else ClientNetworkingFunctions.ensure_path_component_is_encoded( pretty_default_value )
self._default_value.blockSignals( True )
@@ -1051,35 +1053,35 @@ def __init__( self, parent: QW.QWidget, parameter: ClientNetworkingURLClass.URLC
self._dupe_names = dupe_names
self._pretty_name = QW.QLineEdit( self )
- self._pretty_name.setToolTip( 'The "key" of the key=value pair.' )
+ self._pretty_name.setToolTip( ClientGUIFunctions.WrapToolTip( 'The "key" of the key=value pair.' ) )
self._name = QW.QLineEdit( self )
- self._name.setToolTip( 'The "key" of the key=value pair. This encoded form is what is actually sent to the server!' )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'The "key" of the key=value pair. This encoded form is what is actually sent to the server!' ) )
value_string_match_panel = ClientGUICommon.StaticBox( self, 'value test' )
from hydrus.client.gui import ClientGUIStringPanels
self._value_string_match = ClientGUIStringPanels.EditStringMatchPanel( value_string_match_panel, parameter.GetValueStringMatch() )
- self._value_string_match.setToolTip( 'If the encoded value of the key=value pair matches this, the URL Class matches!' )
+ self._value_string_match.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the encoded value of the key=value pair matches this, the URL Class matches!' ) )
self._is_ephemeral = QW.QCheckBox( self )
tt = 'THIS IS ADVANCED, DO NOT SET IF YOU ARE UNSURE! If this parameter is a one-time token or similar needed for the server request but not something you want to keep or use to compare, you can define it here.'
tt += '\n' * 2
tt += 'These tokens are also allowed _en masse_ in the main URL Class by setting "allow extra parameters for server", BUT if you need a whitelist, you will want to define them here. Also, if you need to pass this token on to an API/redirect converter, you have to define it here!'
- self._is_ephemeral.setToolTip( tt )
+ self._is_ephemeral.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._pretty_default_value = ClientGUICommon.NoneableTextCtrl( self )
- self._pretty_default_value.setToolTip( 'If the URL is missing this key=value pair, you can add it here, and the URL Class will still match and will normalise with this default value. This can be useful for gallery URLs that have an implicit page=1 or index=0 for their first result--sometimes it is better to make that stuff explicit in all cases.' )
+ self._pretty_default_value.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the URL is missing this key=value pair, you can add it here, and the URL Class will still match and will normalise with this default value. This can be useful for gallery URLs that have an implicit page=1 or index=0 for their first result--sometimes it is better to make that stuff explicit in all cases.' ) )
self._default_value = ClientGUICommon.NoneableTextCtrl( self )
- self._default_value.setToolTip( 'What actual value will be embedded into the URL sent to the server.' )
+ self._default_value.setToolTip( ClientGUIFunctions.WrapToolTip( 'What actual value will be embedded into the URL sent to the server.' ) )
self._default_value_string_processor = ClientGUIStringControls.StringProcessorButton( self, parameter.GetDefaultValueStringProcessor(), self._GetTestData )
tt = 'WARNING WARNING: Extremely Big Brain'
tt += '/n' * 2
tt += 'You can apply the parsing system\'s normal String Processor steps to your fixed default value here. For instance, you could append/replace the default value with random hex or today\'s date. This is obviously super advanced, so be careful.'
- self._default_value_string_processor.setToolTip( tt )
+ self._default_value_string_processor.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1196,7 +1198,7 @@ def _PrettyDefaultValueChanged( self ):
pretty_default_value = self._pretty_default_value.GetValue()
- default_value = pretty_default_value if pretty_default_value is None else urllib.parse.quote( pretty_default_value )
+ default_value = pretty_default_value if pretty_default_value is None else ClientNetworkingFunctions.ensure_param_component_is_encoded( pretty_default_value )
self._default_value.blockSignals( True )
@@ -1209,7 +1211,7 @@ def _PrettyNameChanged( self ):
pretty_name = self._pretty_name.text()
- name = pretty_name if pretty_name is None else urllib.parse.quote( pretty_name )
+ name = pretty_name if pretty_name is None else ClientNetworkingFunctions.ensure_param_component_is_encoded( pretty_name )
self._name.blockSignals( True )
@@ -1334,12 +1336,12 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
self._match_subdomains = QW.QCheckBox( self._matching_panel )
tt = 'Should this class apply to subdomains as well?'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'For instance, if this url class has domain \'example.com\', should it match a url with \'boards.example.com\' or \'artistname.example.com\'?'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Any subdomain starting with \'www\' is automatically matched, so do not worry about having to account for that.'
- self._match_subdomains.setToolTip( tt )
+ self._match_subdomains.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1369,7 +1371,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt = 'Some URLs have parameters with just a key or a value, not a "key=value" pair. Normally these are removed on normalisation, but if you turn this on, then this URL will keep them and require at least one.'
- self._has_single_value_parameters.setToolTip( tt )
+ self._has_single_value_parameters.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._has_single_value_parameters.setChecked( has_single_value_parameters )
@@ -1390,68 +1392,70 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
self._keep_matched_subdomains = QW.QCheckBox( self._options_panel )
tt = 'Should this url keep its matched subdomains when it is normalised?'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'This is typically useful for direct file links that are often served on a numbered CDN subdomain like \'img3.example.com\' but are also valid on the neater main domain.'
- self._keep_matched_subdomains.setToolTip( tt )
+ self._keep_matched_subdomains.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._alphabetise_get_parameters = QW.QCheckBox( self._options_panel )
tt = 'Normally, to ensure the same URLs are merged, hydrus will alphabetise GET parameters as part of the normalisation process.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Almost all servers support GET params in any order. One or two do not. Uncheck this if you know there is a problem.'
- self._alphabetise_get_parameters.setToolTip( tt )
+ self._alphabetise_get_parameters.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._no_more_path_components_than_this = QW.QCheckBox( self._options_panel )
tt = 'Normally, hydrus will match a URL that has a longer path than is defined here. site.com/index/123456/cool-pic-by-artist will match a URL class that looks for site.com/index/123456, and it will remove that extra cruft on normalisation.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Checking this turns that behaviour off. It will only match if the given URL satisfies all defined path component tests, and no more. If you have multiple URL Classes matching on different levels of a tree, and hydrus is having difficulty matching them up in the right order (neighbouring Gallery/Post URLs can do this), try this.'
- self._no_more_path_components_than_this.setToolTip( tt )
+ self._no_more_path_components_than_this.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._no_more_parameters_than_this = QW.QCheckBox( self._options_panel )
tt = 'Normally, hydrus will match a URL that has more parameters than is defined here. site.com/index?p=123456&orig_tags=skirt will match a URL class that looks for site.com/index?p=123456. Post URLs will remove that extra cruft on normalisation.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Checking this turns that behaviour off. It will only match if the given URL satisfies all defined parameter tests, and no more. If you have multiple URL Classes matching on the same base URL path but with different query params, and hydrus is having difficulty matching them up in the right order (neighbouring Gallery/Post URLs can do this), try this.'
- self._no_more_parameters_than_this.setToolTip( tt )
+ self._no_more_parameters_than_this.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._keep_extra_parameters_for_server = QW.QCheckBox( self._options_panel )
- tt = 'If checked, the URL not strip out undefined parameters in the normalisation process that occurs before a URL is sent to the server. In general, you probably want to keep this on, since these extra parameters can include temporary tokens and so on. Undefined parameters are removed when URLs are compared to each other (to detect dupes) or saved to the "known urls" storage in the database.'
+ tt = 'Only available if not using an API/redirect converter!'
+ tt += '\n' * 2
+ tt += 'If checked, the URL not strip out undefined parameters in the normalisation process that occurs before a URL is sent to the server. In general, you probably want to keep this on, since these extra parameters can include temporary tokens and so on. Undefined parameters are removed when URLs are compared to each other (to detect dupes) or saved to the "known urls" storage in the database.'
- self._keep_extra_parameters_for_server.setToolTip( tt )
+ self._keep_extra_parameters_for_server.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._can_produce_multiple_files = QW.QCheckBox( self._options_panel )
tt = 'If checked, the client will not rely on instances of this URL class to predetermine \'already in db\' or \'previously deleted\' outcomes. This is important for post types like pixiv pages (which can ultimately be manga, and represent many pages) and tweets (which can have multiple images).'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Most booru-type Post URLs only produce one file per URL and should not have this checked. Checking this avoids some bad logic where the client would falsely think it if it had seen one file at the URL, it had seen them all, but it then means the client has to download those pages\' content again whenever it sees them (so it can check against the direct File URLs, which are always considered one-file each).'
- self._can_produce_multiple_files.setToolTip( tt )
+ self._can_produce_multiple_files.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._should_be_associated_with_files = QW.QCheckBox( self._options_panel )
tt = 'If checked, the client will try to remember this url with any files it ends up importing. It will present this url in \'known urls\' ui across the program.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If this URL is a File or Post URL and the client comes across it after having already downloaded it once, it can skip the redundant download since it knows it already has (or has already deleted) the file once before.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Turning this on is only useful if the URL is non-ephemeral (i.e. the URL will produce the exact same file(s) in six months\' time). It is usually not appropriate for booru gallery or thread urls, which alter regularly, but is for static Post URLs or some fixed doujin galleries.'
- self._should_be_associated_with_files.setToolTip( tt )
+ self._should_be_associated_with_files.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._keep_fragment = QW.QCheckBox( self._options_panel )
tt = 'If checked, fragment text will be kept. This is the component sometimes after an URL that starts with a "#", such as "#kwGFb3xhA3k8B".'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'This data is never sent to a server, so in normal cases should never be kept, but for some clever services such as Mega, with complicated javascript navigation, it may contain unique clientside navigation data if you open the URL in your browser.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Only turn this on if you know it is needed. For almost all sites, it only hurts the normalisation process.'
- self._keep_fragment.setToolTip( tt )
+ self._keep_fragment.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1466,13 +1470,13 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt = 'Do not change this unless you know you need to. It fixes complicated problems.'
- self._send_referral_url.setToolTip( tt )
+ self._send_referral_url.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._referral_url_converter = ClientGUIStringControls.StringConverterButton( self._referral_url_panel, referral_url_converter )
tt = 'This will generate a referral URL from the original URL. If the URL needs a referral URL, and you can infer what that would be from just this URL, this will let hydrus download this URL without having to previously visit the referral URL (e.g. letting the user drag-and-drop import). It also lets you set up alternate referral URLs for perculiar situations.'
- self._referral_url_converter.setToolTip( tt )
+ self._referral_url_converter.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._referral_url = QW.QLineEdit( self._referral_url_panel )
self._referral_url.setReadOnly( True )
@@ -1485,7 +1489,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt = 'This will let you generate an alternate URL for the client to use for the actual download whenever it encounters a URL in this class. You must have a separate URL class to match the API type (which will link to parsers).'
- self._api_lookup_converter.setToolTip( tt )
+ self._api_lookup_converter.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._api_url = QW.QLineEdit( self._api_url_panel )
self._api_url.setReadOnly( True )
@@ -1524,7 +1528,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt = 'This is what should actually be sent to the server. It has some elements of full normalisation, but depending on your options, there may be additional, "ephemeral" data included. If you use an API/redirect, it will be that.'
- self._for_server_normalised_url.setToolTip( tt )
+ self._for_server_normalised_url.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._normalised_url = QW.QLineEdit( self )
self._normalised_url.setReadOnly( True )
@@ -1533,7 +1537,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
tt += '/n' * 2
tt += 'We want to normalise to a single reliable URL because the same URL can be expressed in different ways. The parameters can be reordered, and descriptive \'sugar\' like "/123456/bodysuit-samus_aran" can be altered at a later date, say to "/123456/bodysuit-green_eyes-samus_aran". In order to collapse all the different expressions of a url down to a single comparable form, we remove any cruft and "normalise" things. The preferred scheme (http/https) will be switched to, and, typically, parameters will be alphabetised and non-defined elements will be removed.'
- self._normalised_url.setToolTip( tt )
+ self._normalised_url.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1656,14 +1660,14 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
rows = []
- rows.append( ( 'if matching by subdomain, keep it when normalising?: ', self._keep_matched_subdomains ) )
- rows.append( ( 'alphabetise GET parameters when normalising?: ', self._alphabetise_get_parameters ) )
- rows.append( ( 'do not match on any extra path components?: ', self._no_more_path_components_than_this ) )
- rows.append( ( 'do not match on any extra parameters?: ', self._no_more_parameters_than_this ) )
- rows.append( ( 'keep extra parameters for server?: ', self._keep_extra_parameters_for_server ) )
- rows.append( ( 'keep fragment when normalising?: ', self._keep_fragment ) )
- rows.append( ( 'post page can produce multiple files?: ', self._can_produce_multiple_files ) )
- rows.append( ( 'associate a \'known url\' with resulting files?: ', self._should_be_associated_with_files ) )
+ rows.append( ( 'if matching by subdomain, keep it when normalising: ', self._keep_matched_subdomains ) )
+ rows.append( ( 'alphabetise GET parameters when normalising: ', self._alphabetise_get_parameters ) )
+ rows.append( ( 'disallow match on any extra path components: ', self._no_more_path_components_than_this ) )
+ rows.append( ( 'disallow match on any extra parameters: ', self._no_more_parameters_than_this ) )
+ rows.append( ( 'keep extra parameters for server: ', self._keep_extra_parameters_for_server ) )
+ rows.append( ( 'keep fragment when normalising: ', self._keep_fragment ) )
+ rows.append( ( 'post page can produce multiple files: ', self._can_produce_multiple_files ) )
+ rows.append( ( 'associate a \'known url\' with resulting files: ', self._should_be_associated_with_files ) )
gridbox = ClientGUICommon.WrapInGrid( self._options_panel, rows )
@@ -2051,6 +2055,8 @@ def _UpdateControls( self ):
example_url = self._example_url.text()
+ example_url = ClientNetworkingFunctions.EnsureURLIsEncoded( example_url )
+
url_class.Test( example_url )
self._example_url_classes.setText( 'Example matches ok!' )
@@ -2186,7 +2192,7 @@ def EventAssociationUpdate( self ):
if self._url_type.GetValue() in ( HC.URL_TYPE_GALLERY, HC.URL_TYPE_WATCHABLE ):
message = 'Please note that it is only appropriate to associate a Gallery or Watchable URL with a file if that URL is non-ephemeral. It is only appropriate if the exact same URL will definitely give the same files in six months\' time (like a fixed doujin chapter gallery).'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you are not sure what this means, turn this back off.'
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -2197,7 +2203,7 @@ def EventAssociationUpdate( self ):
if self._url_type.GetValue() in ( HC.URL_TYPE_FILE, HC.URL_TYPE_POST ):
message = 'Hydrus uses these file associations to make sure not to re-download the same file when it comes across the same URL in future. It is only appropriate to not associate a file or post url with a file if that url is particularly ephemeral, such as if the URL includes a non-removable random key that becomes invalid after a few minutes.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you are not sure what this means, turn this back on.'
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -2459,7 +2465,7 @@ def _UpdateURLClassCheckerText( self ):
else:
- url = ClientNetworkingFunctions.WashURL( unclean_url )
+ url = ClientNetworkingFunctions.EnsureURLIsEncoded( unclean_url )
url_classes = self.GetValue()
diff --git a/hydrus/client/gui/ClientGUIDuplicates.py b/hydrus/client/gui/ClientGUIDuplicates.py
index 5fe49a476..27b024f5b 100644
--- a/hydrus/client/gui/ClientGUIDuplicates.py
+++ b/hydrus/client/gui/ClientGUIDuplicates.py
@@ -12,17 +12,17 @@ def ClearFalsePositives( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to clear this file of its false-positive relations?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'False-positive relations are recorded between alternate groups, so this change will also affect any files this file is alternate to.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'All affected files will be queued up for another potential duplicates search, so you will likely see at least one of them again in the duplicate filter.'
else:
message = 'Are you sure you want to clear these {} files of their false-positive relations?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'False-positive relations are recorded between alternate groups, so this change will also affect all alternate files to your selection.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'All affected files will be queued up for another potential duplicates search, so you will likely see some of them again in the duplicate filter.'
@@ -38,17 +38,17 @@ def DissolveAlternateGroup( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to dissolve this file\'s entire alternates group?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will completely remove all duplicate, alternate, and false-positive relations for all files in the group and set them to come up again in the duplicate filter.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is a potentially big change that throws away many previous decisions and cannot be undone. If you can achieve your result just by removing some alternate members, do that instead.'
else:
message = 'Are you sure you want to dissolve these {} files\' entire alternates groups?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will completely remove all duplicate, alternate, and false-positive relations for all alternate groups of all files selected and set them to come up again in the duplicate filter.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is a potentially huge change that throws away many previous decisions and cannot be undone. If you can achieve your result just by removing some alternate members, do that instead.'
@@ -64,17 +64,17 @@ def DissolveDuplicateGroup( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to dissolve this file\'s duplicate group?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will split the duplicates group back into individual files and remove any alternate relations they have. They will be queued back up in the duplicate filter for reprocessing.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This could be a big change that throws away many previous decisions and cannot be undone. If you can achieve your result just by removing one or two members, do that instead.'
else:
message = 'Are you sure you want to dissolve these {} files\' duplicate groups?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will split all the files\' duplicates groups back into individual files and remove any alternate relations they have. They will all be queued back up in the duplicate filter for reprocessing.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This could be a huge change that throws away many previous decisions and cannot be undone. If you can achieve your result just by removing some members, do that instead.'
@@ -90,17 +90,17 @@ def RemoveFromAlternateGroup( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to remove this file from its alternates group?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Alternate relationships are stored between duplicate groups, so this will pull any duplicates of your file with it.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The removed file (and any duplicates) will be queued up for another potential duplicates search, so you will likely see at least one again in the duplicate filter.'
else:
message = 'Are you sure you want to remove these {} files from their alternates groups?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Alternate relationships are stored between duplicate groups, so this will pull any duplicates of these files with them.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The removed files (and any duplicates) will be queued up for another potential duplicates search, so you will likely see some again in the duplicate filter.'
@@ -116,17 +116,17 @@ def RemoveFromDuplicateGroup( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to remove this file from its duplicate group?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The remaining group will be otherwise unaffected and will keep its alternate relationships.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The removed file will be queued up for another potential duplicates search, so you will likely see it again in the duplicate filter.'
else:
message = 'Are you sure you want to remove these {} files from their duplicate groups?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The remaining groups will be otherwise unaffected and keep their alternate relationships.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The removed files will be queued up for another potential duplicates search, so you will likely see them again in the duplicate filter.'
@@ -142,13 +142,13 @@ def RemovePotentials( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to remove all of this file\'s potentials?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will mean it (or any of its duplicates) will not appear in the duplicate filter unless new potentials are found with new files. Use this command if the file has accidentally received many false positive potential relationships.'
else:
message = 'Are you sure you want to remove all of these {} files\' potentials?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will mean they (or any of their duplicates) will not appear in the duplicate filter unless new potentials are found with new files. Use this command if the files have accidentally received many false positive potential relationships.'
@@ -164,13 +164,13 @@ def ResetPotentialSearch( win, hashes ):
if len( hashes ) == 1:
message = 'Are you sure you want to search this file for potential duplicates again?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will not remove any existing potential pairs, and will typically not find any new relationships unless an error has occured.'
else:
message = 'Are you sure you want to search these {} files for potential duplicates again?'.format( HydrusData.ToHumanInt( len( hashes ) ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This will not remove any existing potential pairs, and will typically not find any new relationships unless an error has occured.'
diff --git a/hydrus/client/gui/ClientGUIFileSeedCache.py b/hydrus/client/gui/ClientGUIFileSeedCache.py
index 94b9ce44d..46ab0549a 100644
--- a/hydrus/client/gui/ClientGUIFileSeedCache.py
+++ b/hydrus/client/gui/ClientGUIFileSeedCache.py
@@ -24,6 +24,7 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.importing import ClientImportFileSeeds
from hydrus.client.importing.options import PresentationImportOptions
@@ -61,13 +62,13 @@ def GetExportableSourcesString( file_seed_cache: ClientImportFileSeeds.FileSeedC
sources = [ file_seed.file_seed_data for file_seed in file_seeds ]
- return os.linesep.join( sources )
+ return '\n'.join( sources )
def GetSourcesFromSourcesString( sources_string ):
sources = HydrusText.DeserialiseNewlinedTexts( sources_string )
- sources = [ ClientNetworkingFunctions.WashURL( source ) for source in sources ]
+ sources = [ ClientNetworkingFunctions.EnsureURLIsEncoded( source ) for source in sources ]
return sources
@@ -111,7 +112,7 @@ def ImportFromClipboard( win: QW.QWidget, file_seed_cache: ClientImportFileSeeds
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( win, raw_text, 'Lines of URLs or file paths', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( win, raw_text, 'Lines of URLs or file paths', e )
@@ -404,10 +405,10 @@ def _ConvertFileSeedToListCtrlTuples( self, file_seed: ClientImportFileSeeds.Fil
sort_source_time = ClientGUIListCtrl.SafeNoneInt( source_time )
- pretty_note = note.split( os.linesep )[0]
+ pretty_note = HydrusText.GetFirstLine( note )
display_tuple = ( pretty_file_seed_index, pretty_file_seed_data, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note )
- sort_tuple = ( file_seed_index, file_seed_data, status, added, modified, sort_source_time, note )
+ sort_tuple = ( file_seed_index, pretty_file_seed_data, status, added, modified, sort_source_time, note )
return ( display_tuple, sort_tuple )
@@ -428,7 +429,7 @@ def _CopySelectedNotes( self ):
if len( notes ) > 0:
- separator = os.linesep * 2
+ separator = '\n' * 2
text = separator.join( notes )
@@ -442,7 +443,7 @@ def _CopySelectedFileSeedData( self ):
if len( file_seeds ) > 0:
- separator = os.linesep * 2
+ separator = '\n' * 2
text = separator.join( ( file_seed.file_seed_data for file_seed in file_seeds ) )
@@ -529,11 +530,12 @@ def _GetListCtrlMenu( self ):
request_url = selected_file_seed.file_seed_data
normalised_url = selected_file_seed.file_seed_data_for_comparison
+ pretty_url = ClientNetworkingFunctions.ConvertURLToHumanString( normalised_url )
referral_url = selected_file_seed.GetReferralURL()
primary_urls = sorted( selected_file_seed.GetPrimaryURLs() )
source_urls = sorted( selected_file_seed.GetSourceURLs() )
- nothing_interesting_going_on = request_url == normalised_url and referral_url is None and len( primary_urls ) == 0 and len( source_urls ) == 0
+ nothing_interesting_going_on = normalised_url == pretty_url and request_url == normalised_url and referral_url is None and len( primary_urls ) == 0 and len( source_urls ) == 0
if nothing_interesting_going_on:
@@ -543,6 +545,11 @@ def _GetListCtrlMenu( self ):
url_submenu = ClientGUIMenus.GenerateMenu( menu )
+ if normalised_url != pretty_url:
+
+ ClientGUIMenus.AppendMenuLabel( url_submenu, f'normalised url: {normalised_url}', copy_text = normalised_url )
+
+
if request_url != normalised_url:
ClientGUIMenus.AppendMenuLabel( url_submenu, f'request url: {request_url}', copy_text = request_url )
@@ -712,9 +719,9 @@ def _SetSelected( self, status_to_set ):
deletee_hashes = { file_seed.GetHash() for file_seed in deleted_and_clearable_file_seeds }
- from hydrus.client.gui import ClientGUIMediaActions
+ from hydrus.client.gui.media import ClientGUIMediaModalActions
- ClientGUIMediaActions.UndeleteFiles( deletee_hashes )
+ ClientGUIMediaSimpleActions.UndeleteFiles( deletee_hashes )
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_CLEAR_DELETE_RECORD, deletee_hashes )
@@ -749,7 +756,7 @@ def _ShowSelectionInNewPage( self ):
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
- CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes )
+ ClientGUIMediaSimpleActions.ShowFilesInNewPage( hashes, location_context )
@@ -829,7 +836,7 @@ def __init__( self, parent, controller, file_seed_cache_get_callable, file_seed_
action = QW.QAction()
action.setText( 'file log' )
- action.setToolTip( 'open detailed file log' )
+ action.setToolTip( ClientGUIFunctions.WrapToolTip( 'open detailed file log' ) )
action.triggered.connect( self._ShowFileSeedCacheFrame )
diff --git a/hydrus/client/gui/ClientGUIFrames.py b/hydrus/client/gui/ClientGUIFrames.py
index a5a862d75..f26dab462 100644
--- a/hydrus/client/gui/ClientGUIFrames.py
+++ b/hydrus/client/gui/ClientGUIFrames.py
@@ -40,7 +40,7 @@ def __init__( self, key_type, keys ):
if key_type == 'registration': prepend = 'r'
else: prepend = ''
- self._text = os.linesep.join( [ prepend + key.hex() for key in self._keys ] )
+ self._text = '\n'.join( [ prepend + key.hex() for key in self._keys ] )
self._text_ctrl.setPlainText( self._text )
diff --git a/hydrus/client/gui/ClientGUIFunctions.py b/hydrus/client/gui/ClientGUIFunctions.py
index 198c664f5..5432e05f6 100644
--- a/hydrus/client/gui/ClientGUIFunctions.py
+++ b/hydrus/client/gui/ClientGUIFunctions.py
@@ -7,11 +7,9 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusText
from hydrus.client import ClientGlobals as CG
-from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import QtInit
from hydrus.client.gui import QtPorting as QP
from hydrus.core.files.images import HydrusImageNormalisation
@@ -388,30 +386,6 @@ def NotebookScreenToHitTest( notebook, screen_position ):
return notebook.tabBar().tabAt( tab_pos )
-def PresentClipboardParseError( win: QW.QWidget, content: str, expected_content_description: str, e: Exception ):
-
- MAX_CONTENT_SIZE = 1024
-
- log_message = 'Clipboard Error!\nI was expecting: {}'.format( expected_content_description )
-
- if len( content ) > MAX_CONTENT_SIZE:
-
- log_message += '\nFirst {} of content received (total was {}):\n'.format( HydrusData.ToHumanBytes( MAX_CONTENT_SIZE ), HydrusData.ToHumanBytes( len( content ) ) ) + content[:MAX_CONTENT_SIZE]
-
- else:
-
- log_message += '\nContent received ({}):\n'.format( HydrusData.ToHumanBytes( len( content ) ) ) + content[:MAX_CONTENT_SIZE]
-
-
- HydrusData.DebugPrint( log_message )
-
- HydrusData.PrintException( e, do_wait = False )
-
- message = 'Sorry, I could not understand what was in the clipboard. I was expecting "{}" but received this text:\n\n{}\n\nMore details have been written to the log, but the general error was:\n\n{}'.format( expected_content_description, HydrusText.ElideText( content, 64 ), repr( e ) )
-
- ClientGUIDialogsMessage.ShowCritical( win, 'Clipboard Error!', message )
-
-
def SetBitmapButtonBitmap( button, bitmap ):
# old wx stuff, but still basically relevant
@@ -510,3 +484,48 @@ def WidgetOrAnyTLWChildHasFocus( window ):
return False
+
+def WrapToolTip( s: str, max_line_length = 80 ):
+
+ wrapped_lines = []
+
+ for line in s.splitlines():
+
+ if len( line ) == 0:
+
+ wrapped_lines.append( line )
+
+ continue
+
+
+ words = line.split( ' ' )
+
+ words_of_current_line = []
+ num_chars_in_current_line = 0
+
+ for word in words:
+
+ if num_chars_in_current_line + len( words_of_current_line ) + len( word ) > max_line_length and len( words_of_current_line ) > 0:
+
+ wrapped_lines.append( ' '.join( words_of_current_line ) )
+
+ words_of_current_line = [ word ]
+ num_chars_in_current_line = len( word )
+
+ else:
+
+ words_of_current_line.append( word )
+ num_chars_in_current_line += len( word )
+
+
+
+ if len( words_of_current_line ) > 0:
+
+ wrapped_lines.append( ' '.join( words_of_current_line ) )
+
+
+
+ wrapped_tt = '\n'.join( wrapped_lines )
+
+ return wrapped_tt
+
diff --git a/hydrus/client/gui/ClientGUIGallerySeedLog.py b/hydrus/client/gui/ClientGUIGallerySeedLog.py
index bfe4b9ef3..07f8979bc 100644
--- a/hydrus/client/gui/ClientGUIGallerySeedLog.py
+++ b/hydrus/client/gui/ClientGUIGallerySeedLog.py
@@ -2,6 +2,7 @@
from qtpy import QtWidgets as QW
+from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
@@ -45,7 +46,7 @@ def GetExportableURLsString( gallery_seed_log: ClientImportGallerySeeds.GalleryS
urls = [ gallery_seed.url for gallery_seed in gallery_seeds ]
- return os.linesep.join( urls )
+ return '\n'.join( urls )
def GetURLsFromURLsString( urls_string ):
@@ -75,7 +76,7 @@ def ImportFromClipboard( win: QW.QWidget, gallery_seed_log: ClientImportGalleryS
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( win, raw_text, 'Lines of URLs', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( win, raw_text, 'Lines of URLs', e )
@@ -302,10 +303,11 @@ def _ConvertGallerySeedToListCtrlTuples( self, gallery_seed ):
pretty_status = CC.status_string_lookup[ status ] if status != CC.STATUS_UNKNOWN else ''
pretty_added = ClientTime.TimestampToPrettyTimeDelta( added )
pretty_modified = ClientTime.TimestampToPrettyTimeDelta( modified )
- pretty_note = note.split( os.linesep )[0]
+
+ pretty_note = HydrusText.GetFirstLine( note )
display_tuple = ( pretty_gallery_seed_index, pretty_url, pretty_status, pretty_added, pretty_modified, pretty_note )
- sort_tuple = ( gallery_seed_index, url, status, added, modified, note )
+ sort_tuple = ( gallery_seed_index, pretty_url, status, added, modified, note )
return ( display_tuple, sort_tuple )
@@ -316,7 +318,7 @@ def _CopySelectedGalleryURLs( self ):
if len( gallery_seeds ) > 0:
- separator = os.linesep * 2
+ separator = '\n' * 2
text = separator.join( ( gallery_seed.url for gallery_seed in gallery_seeds ) )
@@ -340,7 +342,7 @@ def _CopySelectedNotes( self ):
if len( notes ) > 0:
- separator = os.linesep * 2
+ separator = '\n' * 2
text = separator.join( notes )
@@ -378,13 +380,34 @@ def _GetListCtrlMenu( self ):
return menu
- ClientGUIMenus.AppendMenuItem( menu, 'copy urls', 'Copy all the selected urls to clipboard.', self._CopySelectedGalleryURLs )
- ClientGUIMenus.AppendMenuItem( menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )
+ if len( selected_gallery_seeds ) == 1:
+
+ ( gallery_seed, ) = selected_gallery_seeds
+
+ url = gallery_seed.url
+
+ ClientGUIMenus.AppendMenuItem( menu, f'copy "{url}"', 'Copy all the selected urls to clipboard.', self._CopySelectedGalleryURLs )
+
+ note = gallery_seed.note
+
+ if len( note ) > 0:
+
+ note_preview = HydrusText.GetFirstLine( gallery_seed.note ) + HC.UNICODE_ELLIPSIS
+
+ ClientGUIMenus.AppendMenuItem( menu, f'copy "{note_preview}"', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )
+
+
+ else:
+
+ ClientGUIMenus.AppendMenuItem( menu, 'copy urls', 'Copy all the selected urls to clipboard.', self._CopySelectedGalleryURLs )
+ ClientGUIMenus.AppendMenuItem( menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )
+
ClientGUIMenus.AppendSeparator( menu )
-
+
ClientGUIMenus.AppendMenuItem( menu, 'open urls', 'Open all the selected urls in your web browser.', self._OpenSelectedGalleryURLs )
+
ClientGUIMenus.AppendSeparator( menu )
if not self._read_only:
@@ -538,7 +561,7 @@ def __init__( self, parent, controller, read_only: bool, can_generate_more_pages
action = QW.QAction()
action.setText( '{} log'.format( gallery_type_string ) )
- action.setToolTip( 'open detailed {} log'.format( self._gallery_type_string ) )
+ action.setToolTip( ClientGUIFunctions.WrapToolTip( 'open detailed {} log'.format( self._gallery_type_string ) ) )
action.triggered.connect( self._ShowGallerySeedLogFrame )
diff --git a/hydrus/client/gui/ClientGUILogin.py b/hydrus/client/gui/ClientGUILogin.py
index 0b89a5080..135e4bd6f 100644
--- a/hydrus/client/gui/ClientGUILogin.py
+++ b/hydrus/client/gui/ClientGUILogin.py
@@ -168,8 +168,8 @@ def UserIsOKToOK( self ):
if len( veto_errors ) > 0:
message = 'These values are invalid--are you sure this is ok?'
- message += os.linesep * 2
- message += os.linesep.join( veto_errors )
+ message += '\n' * 2
+ message += '\n'.join( veto_errors )
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -300,7 +300,7 @@ def __init__( self, parent, engine, login_scripts, domains_to_login_info ):
vbox = QP.VBoxLayout()
warning = 'WARNING: Your credentials are stored in plaintext! For this and other reasons, I recommend you use throwaway accounts with hydrus!'
- warning += os.linesep * 2
+ warning += '\n' * 2
warning += 'If a login script does not work for you, or the site you want has a complicated captcha, check out the Hydrus Companion web browser add-on--it can copy login cookies to hydrus! Pixiv recently changed their login system and now require this!'
warning_st = ClientGUICommon.BetterStaticText( self, warning )
@@ -767,9 +767,9 @@ def _DoLogin( self ):
domains_to_login.sort()
message = 'It looks like the following domains can log in:'
- message += os.linesep * 2
- message += os.linesep.join( domains_to_login )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( domains_to_login )
+ message += '\n' * 2
message += 'The dialog will ok and the login attempts will start. Is this ok?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -1227,7 +1227,7 @@ def __init__( self, parent, test_result ):
QP.SetMinClientSize( self._data_preview, min_size )
self._data_copy_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().copy, self._CopyData )
- self._data_copy_button.setToolTip( 'Copy the current example data to the clipboard.' )
+ self._data_copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy the current example data to the clipboard.' ) )
self._temp_variables = QW.QPlainTextEdit( self )
self._temp_variables.setReadOnly( True )
@@ -1263,8 +1263,8 @@ def __init__( self, parent, test_result ):
self._data_preview.setPlainText( str( self._downloaded_data[:1024] ) )
- self._temp_variables.setPlainText( os.linesep.join( new_temp_strings ) )
- self._cookies.setPlainText( os.linesep.join( new_cookie_strings ) )
+ self._temp_variables.setPlainText( '\n'.join( new_temp_strings ) )
+ self._cookies.setPlainText( '\n'.join( new_cookie_strings ) )
#
@@ -1907,9 +1907,9 @@ def GetValue( self ):
except HydrusExceptions.ValidationException as e:
message = 'There is a problem with this script. The reason is:'
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Do you want to proceed with this invalid script, or go back and fix it?'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'ok as invalid', no_label = 'go back' )
diff --git a/hydrus/client/gui/ClientGUIMedia.py b/hydrus/client/gui/ClientGUIMedia.py
deleted file mode 100644
index 9da374d23..000000000
--- a/hydrus/client/gui/ClientGUIMedia.py
+++ /dev/null
@@ -1,431 +0,0 @@
-import itertools
-import os
-import time
-import typing
-
-from qtpy import QtWidgets as QW
-
-from hydrus.core import HydrusConstants as HC
-from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusPaths
-from hydrus.core import HydrusData
-from hydrus.core import HydrusGlobals as HG
-
-from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientGlobals as CG
-from hydrus.client import ClientLocation
-from hydrus.client import ClientPaths
-from hydrus.client import ClientThreading
-from hydrus.client.gui import ClientGUIDialogsMessage
-from hydrus.client.gui import ClientGUIDialogsQuick
-from hydrus.client.gui import ClientGUIScrolledPanelsEdit
-from hydrus.client.gui import ClientGUITopLevelWindowsPanels
-from hydrus.client.media import ClientMedia
-from hydrus.client.metadata import ClientContentUpdates
-
-def CopyHashesToClipboard( win: QW.QWidget, hash_type: str, medias: typing.Sequence[ ClientMedia.Media ] ):
-
- hex_it = True
-
- desired_hashes = []
-
- flat_media = ClientMedia.FlattenMedia( medias )
-
- sha256_hashes = [ media.GetHash() for media in flat_media ]
-
- if hash_type in ( 'pixel_hash', 'blurhash' ):
-
- file_info_managers = [ media.GetFileInfoManager() for media in flat_media ]
-
- if hash_type == 'pixel_hash':
-
- desired_hashes = [ fim.pixel_hash for fim in file_info_managers if fim.pixel_hash is not None ]
-
- elif hash_type == 'blurhash':
-
- desired_hashes = [ fim.blurhash for fim in file_info_managers if fim.blurhash is not None ]
-
- hex_it = False
-
-
- elif hash_type == 'sha256':
-
- desired_hashes = sha256_hashes
-
- else:
-
- num_hashes = len( sha256_hashes )
- num_remote_medias = len( [ not media.GetLocationsManager().IsLocal() for media in flat_media ] )
-
- source_to_desired = CG.client_controller.Read( 'file_hashes', sha256_hashes, 'sha256', hash_type )
-
- desired_hashes = [ source_to_desired[ source_hash ] for source_hash in sha256_hashes if source_hash in source_to_desired ]
-
- num_missing = num_hashes - len( desired_hashes )
-
- if num_missing > 0:
-
- if num_missing == num_hashes:
-
- message = 'Unfortunately, none of the {} hashes could be found.'.format( hash_type )
-
- else:
-
- message = 'Unfortunately, {} of the {} hashes could not be found.'.format( HydrusData.ToHumanInt( num_missing ), hash_type )
-
-
- if num_remote_medias > 0:
-
- message += ' {} of the files you wanted are not currently in this client. If they have never visited this client, the lookup is impossible.'.format( HydrusData.ToHumanInt( num_remote_medias ) )
-
-
- if num_remote_medias < num_hashes:
-
- message += ' It could be that some of the local files are currently missing this information in the hydrus database. A file maintenance job (under the database menu) can repopulate this data.'
-
-
- ClientGUIDialogsMessage.ShowWarning( win, message )
-
-
-
- if len( desired_hashes ) > 0:
-
- if hex_it:
-
- text_lines = [ desired_hash.hex() for desired_hash in desired_hashes ]
-
- else:
-
- text_lines = desired_hashes
-
-
- if CG.client_controller.new_options.GetBoolean( 'prefix_hash_when_copying' ):
-
- text_lines = [ '{}:{}'.format( hash_type, hex_hash ) for hex_hash in text_lines ]
-
-
- hex_hashes_text = os.linesep.join( text_lines )
-
- CG.client_controller.pub( 'clipboard', 'text', hex_hashes_text )
-
- job_status = ClientThreading.JobStatus()
-
- job_status.SetStatusText( '{} {} hashes copied'.format( HydrusData.ToHumanInt( len( desired_hashes ) ), hash_type ) )
-
- CG.client_controller.pub( 'message', job_status )
-
- job_status.FinishAndDismiss( 2 )
-
-
-def CopyMediaURLs( medias ):
-
- urls = set()
-
- for media in medias:
-
- media_urls = media.GetLocationsManager().GetURLs()
-
- urls.update( media_urls )
-
-
- urls = sorted( urls )
-
- urls_string = os.linesep.join( urls )
-
- CG.client_controller.pub( 'clipboard', 'text', urls_string )
-
-
-def CopyMediaURLClassURLs( medias, url_class ):
-
- urls = set()
-
- for media in medias:
-
- media_urls = media.GetLocationsManager().GetURLs()
-
- for url in media_urls:
-
- # can't do 'url_class.matches', as it will match too many
- if CG.client_controller.network_engine.domain_manager.GetURLClass( url ) == url_class:
-
- urls.add( url )
-
-
-
-
- urls = sorted( urls )
-
- urls_string = os.linesep.join( urls )
-
- CG.client_controller.pub( 'clipboard', 'text', urls_string )
-
-def DoClearFileViewingStats( win: QW.QWidget, flat_medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
-
- if len( flat_medias ) == 0:
-
- return
-
-
- if len( flat_medias ) == 1:
-
- insert = 'this file'
-
- else:
-
- insert = 'these {} files'.format( HydrusData.ToHumanInt( len( flat_medias ) ) )
-
-
- message = 'Clear the file viewing count/duration and \'last viewed time\' for {}?'.format( insert )
-
- result = ClientGUIDialogsQuick.GetYesNo( win, message )
-
- if result == QW.QDialog.Accepted:
-
- hashes = { m.GetHash() for m in flat_medias }
-
- content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_DELETE, hashes )
-
- CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update ) )
-
-
-def DoOpenKnownURLFromShortcut( win, media ):
-
- urls = media.GetLocationsManager().GetURLs()
-
- matched_labels_and_urls = []
- unmatched_urls = []
-
- if len( urls ) > 0:
-
- for url in urls:
-
- try:
-
- url_class = CG.client_controller.network_engine.domain_manager.GetURLClass( url )
-
- except HydrusExceptions.URLClassException:
-
- continue
-
-
- if url_class is None:
-
- unmatched_urls.append( url )
-
- else:
-
- label = url_class.GetName() + ': ' + url
-
- matched_labels_and_urls.append( ( label, url ) )
-
-
-
- matched_labels_and_urls.sort()
- unmatched_urls.sort()
-
-
- if len( matched_labels_and_urls ) == 0:
-
- return
-
- elif len( matched_labels_and_urls ) == 1:
-
- url = matched_labels_and_urls[0][1]
-
- else:
-
- matched_labels_and_urls.extend( ( url, url ) for url in unmatched_urls )
-
- try:
-
- url = ClientGUIDialogsQuick.SelectFromList( win, 'Select which URL', matched_labels_and_urls, sort_tuples = False )
-
- except HydrusExceptions.CancelledException:
-
- return
-
-
-
- ClientPaths.LaunchURLInWebBrowser( url )
-
-
-def EditDuplicateContentMergeOptions( win: QW.QWidget, duplicate_type: int ):
-
- new_options = CG.client_controller.new_options
-
- duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type )
-
- with ClientGUITopLevelWindowsPanels.DialogEdit( win, 'edit duplicate merge options' ) as dlg:
-
- panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options )
-
- dlg.SetPanel( panel )
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- duplicate_content_merge_options = panel.GetValue()
-
- new_options.SetDuplicateContentMergeOptions( duplicate_type, duplicate_content_merge_options )
-
-
-
-
-def OpenExternally( media ):
-
- hash = media.GetHash()
- mime = media.GetMime()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- new_options = CG.client_controller.new_options
-
- launch_path = new_options.GetMimeLaunch( mime )
-
- HydrusPaths.LaunchFile( path, launch_path )
-
-
-def OpenFileLocation( media ):
-
- hash = media.GetHash()
- mime = media.GetMime()
-
- path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
-
- HydrusPaths.OpenFileLocation( path )
-
-
-def OpenURLs( urls ):
-
- urls = sorted( urls )
-
- if len( urls ) > 1:
-
- message = 'Open the {} URLs in your web browser?'.format( len( urls ) )
-
- if len( urls ) > 10:
-
- message += ' This will take some time.'
-
-
- tlw = CG.client_controller.GetMainTLW()
-
- result = ClientGUIDialogsQuick.GetYesNo( tlw, message )
-
- if result != QW.QDialog.Accepted:
-
- return
-
-
-
- def do_it( urls ):
-
- job_status = None
-
- num_urls = len( urls )
-
- if num_urls > 5:
-
- job_status = ClientThreading.JobStatus( pausable = True, cancellable = True )
-
- job_status.SetStatusTitle( 'Opening URLs' )
-
- CG.client_controller.pub( 'message', job_status )
-
-
- try:
-
- for ( i, url ) in enumerate( urls ):
-
- if job_status is not None:
-
- ( i_paused, should_quit ) = job_status.WaitIfNeeded()
-
- if should_quit:
-
- return
-
-
- job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i + 1, num_urls ) )
- job_status.SetVariable( 'popup_gauge_1', ( i + 1, num_urls ) )
-
-
- ClientPaths.LaunchURLInWebBrowser( url )
-
- time.sleep( 1 )
-
-
- finally:
-
- if job_status is not None:
-
- job_status.FinishAndDismiss( 1 )
-
-
-
-
- CG.client_controller.CallToThread( do_it, urls )
-
-def OpenMediaURLs( medias ):
-
- urls = set()
-
- for media in medias:
-
- media_urls = media.GetLocationsManager().GetURLs()
-
- urls.update( media_urls )
-
-
- OpenURLs( urls )
-
-def OpenMediaURLClassURLs( medias, url_class ):
-
- urls = set()
-
- for media in medias:
-
- media_urls = media.GetLocationsManager().GetURLs()
-
- for url in media_urls:
-
- # can't do 'url_class.matches', as it will match too many
- if CG.client_controller.network_engine.domain_manager.GetURLClass( url ) == url_class:
-
- urls.add( url )
-
-
-
-
- OpenURLs( urls )
-
-
-def ShowDuplicatesInNewPage( location_context: ClientLocation.LocationContext, hash, duplicate_type ):
-
- # TODO: this can be replaced by a call to the MediaResult when it holds these hashes
- # don't forget to return itself in position 0!
- hashes = CG.client_controller.Read( 'file_duplicate_hashes', location_context, hash, duplicate_type )
-
- if hashes is not None and len( hashes ) > 1:
-
- CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes )
-
- else:
-
- location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_FILE_SERVICE_KEY )
-
- hashes = CG.client_controller.Read( 'file_duplicate_hashes', location_context, hash, duplicate_type )
-
- if hashes is not None and len( hashes ) > 1:
-
- HydrusData.ShowText( 'Could not find the members of this group in this location, so searched all known files and found more.' )
-
- CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes )
-
- else:
-
- HydrusData.ShowText( 'Sorry, could not find the members of this group either at the given location or in all known files. There may be a problem here, so let hydev know.' )
-
-
-
diff --git a/hydrus/client/gui/ClientGUIMenus.py b/hydrus/client/gui/ClientGUIMenus.py
index 043912917..962b30eff 100644
--- a/hydrus/client/gui/ClientGUIMenus.py
+++ b/hydrus/client/gui/ClientGUIMenus.py
@@ -13,6 +13,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui import ClientGUIFunctions
def AppendMenu( menu, submenu, label ):
@@ -262,11 +263,11 @@ def SetMenuTexts( menu_item: QW.QAction, label: str, description: str ):
if label != elided_label:
- menu_item.setToolTip( label )
+ menu_item.setToolTip( ClientGUIFunctions.WrapToolTip( label ) )
elif description != label and description != '':
- menu_item.setToolTip( description )
+ menu_item.setToolTip( ClientGUIFunctions.WrapToolTip( description ) )
menu_item.setWhatsThis( description )
diff --git a/hydrus/client/gui/ClientGUIPopupMessages.py b/hydrus/client/gui/ClientGUIPopupMessages.py
index 72555a2a8..9dcdd51f7 100644
--- a/hydrus/client/gui/ClientGUIPopupMessages.py
+++ b/hydrus/client/gui/ClientGUIPopupMessages.py
@@ -228,7 +228,7 @@ def _ProcessText( self, text ):
new_text = 'The text is too long to display here. Here is the start of it (the rest is printed to the log):'
- new_text += os.linesep * 2
+ new_text += '\n' * 2
new_text += text[ : self.TEXT_CUTOFF ]
@@ -271,7 +271,7 @@ def CopyTB( self ):
info = 'v{}, {}, {}'.format( HC.SOFTWARE_VERSION, sys.platform.lower(), 'frozen' if HC.RUNNING_FROM_FROZEN_BUILD else 'source' )
trace = self._job_status.ToString()
- full_text = info + os.linesep + trace
+ full_text = info + '\n' + trace
CG.client_controller.pub( 'clipboard', 'text', full_text )
diff --git a/hydrus/client/gui/ClientGUIRatings.py b/hydrus/client/gui/ClientGUIRatings.py
index 96b3d4816..0b91da43a 100644
--- a/hydrus/client/gui/ClientGUIRatings.py
+++ b/hydrus/client/gui/ClientGUIRatings.py
@@ -6,10 +6,10 @@
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientGlobals as CG
from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.metadata import ClientRatings
@@ -317,7 +317,7 @@ def _UpdateTooltip( self ):
tt = ''
- self.setToolTip( tt )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
def mouseDoubleClickEvent( self, event ):
@@ -508,7 +508,7 @@ def _UpdateTooltip( self ):
tt = ''
- self.setToolTip( tt )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
def EventLeftDown( self, event ):
@@ -727,7 +727,7 @@ def _UpdateTooltip( self ):
tt = ''
- self.setToolTip( tt )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
def EventLeftDown( self, event ):
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
index b7c14f282..0eb228218 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
@@ -105,7 +105,7 @@ def __init__( self, parent, kept_label: typing.Optional[ str ], deletion_options
if kept_label is not None:
- label = '{}{}-and-'.format( kept_label, os.linesep )
+ label = f'{kept_label}\n-and-'
st = ClientGUICommon.BetterStaticText( self, label )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
index 650b5e800..5e7941472 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
@@ -528,7 +528,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'An instance of JSON-serialised tag or note import options', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'An instance of JSON-serialised tag or note import options', e )
@@ -1930,10 +1930,10 @@ def __init__( self, parent: QW.QWidget, names_to_notes: typing.Dict[ str, str ],
self._delete_button = ClientGUICommon.BetterButton( self, 'delete current note', self._DeleteNote )
self._copy_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().copy, self._Copy )
- self._copy_button.setToolTip( 'Copy all notes to the clipboard.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy all notes to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste from a copy from another notes dialog.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste from a copy from another notes dialog.' ) )
#
@@ -2106,7 +2106,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON names and notes, either as an Object or a list of pairs', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON names and notes, either as an Object or a list of pairs', e )
return
@@ -2258,7 +2258,7 @@ def UserIsOKToOK( self ):
if len( empty_note_names ) > 0:
message = 'These notes are empty, and will not be saved--is this ok?'
- message += os.linesep * 2
+ message += '\n' * 2
message += ', '.join( empty_note_names )
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -2500,10 +2500,10 @@ def __init__( self, parent: QW.QWidget, ordered_medias: typing.List[ ClientMedia
menu_items.append( ( 'normal', 'all file service times', 'Copy every imported/deleted/previously imported time here for pasting in another file\'s dialog.', c ) )
self._copy_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().copy, menu_items )
- self._copy_button.setToolTip( 'Copy timestamps to the clipboard.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy timestamps to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste timestamps from another timestamps dialog.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste timestamps from another timestamps dialog.' ) )
#
@@ -2906,7 +2906,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'A list of JSON-serialised Timestamp Data objects', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'A list of JSON-serialised Timestamp Data objects', e )
return
@@ -3361,7 +3361,7 @@ def __init__( self, parent: QW.QWidget, info ):
self._exact_zooms_only = QW.QCheckBox( 'only permit half and double zooms', self )
- self._exact_zooms_only.setToolTip( 'This limits zooms to 25%, 50%, 100%, 200%, 400%, and so on. It makes for fast resize and is useful for files that often have flat colours and hard edges, which often scale badly otherwise. The \'canvas fit\' zoom will still be inserted.' )
+ self._exact_zooms_only.setToolTip( ClientGUIFunctions.WrapToolTip( 'This limits zooms to 25%, 50%, 100%, 200%, 400%, and so on. It makes for fast resize and is useful for files that often have flat colours and hard edges, which often scale badly otherwise. The \'canvas fit\' zoom will still be inserted.' ) )
self._scale_up_quality = ClientGUICommon.BetterChoice( self )
@@ -3838,11 +3838,11 @@ def __init__( self, parent: QW.QWidget, choices, message = '' ):
first_focused = False
- for ( text, data, tooltip ) in choices:
+ for ( text, data, tt ) in choices:
button = ClientGUICommon.BetterButton( self, text, self._ButtonChoice, data )
- button.setToolTip( tooltip )
+ button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_BOTH_WAYS )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index 787800b68..ca54a2160 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -46,7 +46,7 @@
from hydrus.client.gui.search import ClientGUILocation
from hydrus.client.gui.widgets import ClientGUIColourPicker
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIBytes
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
@@ -117,14 +117,14 @@ def __init__( self, parent, new_options ):
#
tt = 'If unchecked, this media canvas will use the \'global\' audio volume slider. If checked, this media canvas will have its own separate one.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Keep this on if you would like the preview viewer to be quieter than the main media viewer.'
#self._media_viewer_uses_its_own_audio_volume.setChecked( self._new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ) )
self._preview_uses_its_own_audio_volume.setChecked( self._new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ) )
- #self._media_viewer_uses_its_own_audio_volume.setToolTip( tt )
- self._preview_uses_its_own_audio_volume.setToolTip( tt )
+ #self._media_viewer_uses_its_own_audio_volume.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+ self._preview_uses_its_own_audio_volume.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._has_audio_label.setText( self._new_options.GetString( 'has_audio_label' ) )
@@ -314,19 +314,19 @@ def __init__( self, parent ):
self._max_connection_attempts_allowed = ClientGUICommon.BetterSpinBox( general, min = 1, max = 10 )
- self._max_connection_attempts_allowed.setToolTip( 'This refers to timeouts when actually making the initial connection.' )
+ self._max_connection_attempts_allowed.setToolTip( ClientGUIFunctions.WrapToolTip( 'This refers to timeouts when actually making the initial connection.' ) )
self._max_request_attempts_allowed_get = ClientGUICommon.BetterSpinBox( general, min = 1, max = 10 )
- self._max_request_attempts_allowed_get.setToolTip( 'This refers to timeouts when waiting for a response to our GET requests, whether that is the start or an interruption part way through.' )
+ self._max_request_attempts_allowed_get.setToolTip( ClientGUIFunctions.WrapToolTip( 'This refers to timeouts when waiting for a response to our GET requests, whether that is the start or an interruption part way through.' ) )
self._network_timeout = ClientGUICommon.BetterSpinBox( general, min = network_timeout_min, max = network_timeout_max )
- self._network_timeout.setToolTip( 'If a network connection cannot be made in this duration or, once started, it experiences inactivity for six times this duration, it will be considered dead and retried or abandoned.' )
+ self._network_timeout.setToolTip( ClientGUIFunctions.WrapToolTip( 'If a network connection cannot be made in this duration or, once started, it experiences inactivity for six times this duration, it will be considered dead and retried or abandoned.' ) )
self._connection_error_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
- self._connection_error_wait_time.setToolTip( 'If a network connection times out as above, it will wait increasing multiples of this base time before retrying.' )
+ self._connection_error_wait_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If a network connection times out as above, it will wait increasing multiples of this base time before retrying.' ) )
self._serverside_bandwidth_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
- self._serverside_bandwidth_wait_time.setToolTip( 'If a server returns a failure status code indicating it is short on bandwidth, the network job will wait increasing multiples of this base time before retrying.' )
+ self._serverside_bandwidth_wait_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If a server returns a failure status code indicating it is short on bandwidth, the network job will wait increasing multiples of this base time before retrying.' ) )
self._domain_network_infrastructure_error_velocity = ClientGUITime.VelocityCtrl( general, 0, 100, 30, hours = True, minutes = True, seconds = True, per_phrase = 'within', unit = 'errors' )
@@ -491,10 +491,10 @@ def __init__( self, parent, new_options ):
self._max_simultaneous_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=100 )
self._subscription_file_error_cancel_threshold = ClientGUICommon.NoneableSpinCtrl( subscriptions, min = 1, max = 1000000, unit = 'errors' )
- self._subscription_file_error_cancel_threshold.setToolTip( 'This is a simple patch and will be replaced with a better "retry network errors later" system at some point, but is useful to increase if you have subs to unreliable websites.' )
+ self._subscription_file_error_cancel_threshold.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a simple patch and will be replaced with a better "retry network errors later" system at some point, but is useful to increase if you have subs to unreliable websites.' ) )
self._process_subs_in_random_order = QW.QCheckBox( subscriptions )
- self._process_subs_in_random_order.setToolTip( 'Processing in random order is useful whenever bandwidth is tight, as it stops an \'aardvark\' subscription from always getting first whack at what is available. Otherwise, they will be processed in alphabetical order.' )
+ self._process_subs_in_random_order.setToolTip( ClientGUIFunctions.WrapToolTip( 'Processing in random order is useful whenever bandwidth is tight, as it stops an \'aardvark\' subscription from always getting first whack at what is available. Otherwise, they will be processed in alphabetical order.' ) )
checker_options = self._new_options.GetDefaultSubscriptionCheckerOptions()
@@ -536,25 +536,25 @@ def __init__( self, parent, new_options ):
#
gallery_page_tt = 'Gallery page fetches are heavy requests with unusual fetch-time requirements. It is important they not wait too long, but it is also useful to throttle them:'
- gallery_page_tt += os.linesep * 2
+ gallery_page_tt += '\n' * 2
gallery_page_tt += '- So they do not compete with file downloads for bandwidth, leading to very unbalanced 20/4400-type queues.'
- gallery_page_tt += os.linesep
+ gallery_page_tt += '\n'
gallery_page_tt += '- So you do not get 1000 items in your queue before realising you did not like that tag anyway.'
- gallery_page_tt += os.linesep
+ gallery_page_tt += '\n'
gallery_page_tt += '- To give servers a break (some gallery pages can be CPU-expensive to generate).'
- gallery_page_tt += os.linesep * 2
+ gallery_page_tt += '\n' * 2
gallery_page_tt += 'These delays/lots are per-domain.'
- gallery_page_tt += os.linesep * 2
+ gallery_page_tt += '\n' * 2
gallery_page_tt += 'If you do not understand this stuff, you can just leave it alone.'
self._gallery_page_wait_period_pages.setValue( self._new_options.GetInteger( 'gallery_page_wait_period_pages' ) )
- self._gallery_page_wait_period_pages.setToolTip( gallery_page_tt )
+ self._gallery_page_wait_period_pages.setToolTip( ClientGUIFunctions.WrapToolTip( gallery_page_tt ) )
self._gallery_file_limit.SetValue( HC.options['gallery_file_limit'] )
self._highlight_new_query.setChecked( self._new_options.GetBoolean( 'highlight_new_query' ) )
self._gallery_page_wait_period_subscriptions.setValue( self._new_options.GetInteger( 'gallery_page_wait_period_subscriptions' ) )
- self._gallery_page_wait_period_subscriptions.setToolTip( gallery_page_tt )
+ self._gallery_page_wait_period_subscriptions.setToolTip( ClientGUIFunctions.WrapToolTip( gallery_page_tt ) )
self._max_simultaneous_subscriptions.setValue( self._new_options.GetInteger( 'max_simultaneous_subscriptions' ) )
self._subscription_file_error_cancel_threshold.SetValue( self._new_options.GetNoneableInteger( 'subscription_file_error_cancel_threshold' ) )
@@ -567,7 +567,7 @@ def __init__( self, parent, new_options ):
self._show_deleted_on_file_seed_short_summary.setChecked( self._new_options.GetBoolean( 'show_deleted_on_file_seed_short_summary' ) )
self._watcher_page_wait_period.setValue( self._new_options.GetInteger( 'watcher_page_wait_period' ) )
- self._watcher_page_wait_period.setToolTip( gallery_page_tt )
+ self._watcher_page_wait_period.setToolTip( ClientGUIFunctions.WrapToolTip( gallery_page_tt ) )
self._highlight_new_watcher.setChecked( self._new_options.GetBoolean( 'highlight_new_watcher' ) )
self._subscription_network_error_delay.SetValue( self._new_options.GetInteger( 'subscription_network_error_delay' ) )
@@ -695,7 +695,7 @@ def __init__( self, parent, new_options ):
self._duplicate_comparison_score_nicer_ratio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_has_audio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_nicer_ratio.setToolTip( 'For instance, 16:9 vs 640:357.')
+ self._duplicate_comparison_score_nicer_ratio.setToolTip( ClientGUIFunctions.WrapToolTip( 'For instance, 16:9 vs 640:357.') )
batches_panel = ClientGUICommon.StaticBox( self, 'duplicate filter batches' )
@@ -704,13 +704,13 @@ def __init__( self, parent, new_options ):
colours_panel = ClientGUICommon.StaticBox( self, 'colours' )
self._duplicate_background_switch_intensity_a = ClientGUICommon.NoneableSpinCtrl( colours_panel, none_phrase = 'do not change', min = 1, max = 9 )
- self._duplicate_background_switch_intensity_a.setToolTip( 'This changes the background colour when you are looking at A. If you have a pure white/black background and do not have transparent images to show with checkerboard, it helps to highlight transparency vs opaque white/black image background.' )
+ self._duplicate_background_switch_intensity_a.setToolTip( ClientGUIFunctions.WrapToolTip( 'This changes the background colour when you are looking at A. If you have a pure white/black background and do not have transparent images to show with checkerboard, it helps to highlight transparency vs opaque white/black image background.' ) )
self._duplicate_background_switch_intensity_b = ClientGUICommon.NoneableSpinCtrl( colours_panel, none_phrase = 'do not change', min = 1, max = 9 )
- self._duplicate_background_switch_intensity_b.setToolTip( 'This changes the background colour when you are looking at B. Making it different to the A value helps to highlight switches between the two.' )
+ self._duplicate_background_switch_intensity_b.setToolTip( ClientGUIFunctions.WrapToolTip( 'This changes the background colour when you are looking at B. Making it different to the A value helps to highlight switches between the two.' ) )
self._draw_transparency_checkerboard_media_canvas_duplicates = QW.QCheckBox( colours_panel )
- self._draw_transparency_checkerboard_media_canvas_duplicates.setToolTip( 'Same as the setting in _media_, but only for the duplicate filter. Only applies if that _media_ setting is unchecked.' )
+ self._draw_transparency_checkerboard_media_canvas_duplicates.setToolTip( ClientGUIFunctions.WrapToolTip( 'Same as the setting in _media_, but only for the duplicate filter. Only applies if that _media_ setting is unchecked.' ) )
#
@@ -749,7 +749,7 @@ def __init__( self, parent, new_options ):
gridbox = ClientGUICommon.WrapInGrid( weights_panel, rows )
label = 'When processing potential duplicate pairs in the duplicate filter, the client tries to present the \'best\' file first. It judges the two files on a variety of potential differences, each with a score. The file with the greatest total score is presented first. Here you can tinker with these scores.'
- label += os.linesep * 2
+ label += '\n' * 2
label += 'I recommend you leave all these as positive numbers, but if you wish, you can set a negative number to reduce the score.'
st = ClientGUICommon.BetterStaticText( weights_panel, label )
@@ -858,9 +858,9 @@ def __init__( self, parent ):
vbox = QP.VBoxLayout()
text = 'By default, when you ask to open a URL, hydrus will send it to your OS, and that figures out what your "default" web browser is. These OS launch commands can be buggy, though, and sometimes drop #anchor components. If this happens to you, set the specific launch command for your web browser here.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'The command here must include a "%path%" component, normally ideally within those quote marks, which is where hydrus will place the URL when it executes the command. A good example would be:'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'C:\\program files\\firefox\\firefox.exe "%path%"'
st = ClientGUICommon.BetterStaticText( browser_panel, text )
@@ -881,7 +881,7 @@ def __init__( self, parent ):
vbox = QP.VBoxLayout()
text = 'Similarly, when you ask to open a file "externally", hydrus will send it to your OS, and that figures out your "default" program. This may fail or direct to a program you do not want for several reasons, so you can set a specific override here.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Again, make sure you include the "%path%" component. Most programs are going to be like \'program_exe "%path%"\', but some may need a profile switch or "-o" open command or similar.'
st = ClientGUICommon.BetterStaticText( mime_panel, text )
@@ -927,9 +927,9 @@ def _EditMimeLaunch( self ):
for ( mime, launch_path ) in self._mime_launch_listctrl.GetData( only_selected = True ):
message = 'Enter the new launch path for {}'.format( HC.mime_string_lookup[ mime ] )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Hydrus will insert the file\'s full path wherever you put %path%, even multiple times!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Set as blank to reset to default.'
if launch_path is None:
@@ -1016,13 +1016,13 @@ def __init__( self, parent ):
self._export_location = QP.DirPickerCtrl( self )
self._prefix_hash_when_copying = QW.QCheckBox( self )
- self._prefix_hash_when_copying.setToolTip( 'If you often paste hashes into boorus, check this to automatically prefix with the type, like "md5:2496dabcbd69e3c56a5d8caabb7acde5".' )
+ self._prefix_hash_when_copying.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you often paste hashes into boorus, check this to automatically prefix with the type, like "md5:2496dabcbd69e3c56a5d8caabb7acde5".' ) )
self._delete_to_recycle_bin = QW.QCheckBox( self )
self._ms_to_wait_between_physical_file_deletes = ClientGUICommon.BetterSpinBox( self, min=20, max = 5000 )
tt = 'Deleting a file from a hard disk can be resource expensive, so when files leave the trash, the actual physical file delete happens later, in the background. The operation is spread out so as not to give you lag spikes.'
- self._ms_to_wait_between_physical_file_deletes.setToolTip( tt )
+ self._ms_to_wait_between_physical_file_deletes.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._confirm_trash = QW.QCheckBox( self )
self._confirm_archive = QW.QCheckBox( self )
@@ -1044,13 +1044,13 @@ def __init__( self, parent ):
advanced_file_deletion_panel = ClientGUICommon.StaticBox( self, 'advanced file deletion and custom reasons' )
self._use_advanced_file_deletion_dialog = QW.QCheckBox( advanced_file_deletion_panel )
- self._use_advanced_file_deletion_dialog.setToolTip( 'If this is set, the client will present a more complicated file deletion confirmation dialog that will permit you to set your own deletion reason and perform \'clean\' deletes that leave no deletion record (making later re-import easier).' )
+ self._use_advanced_file_deletion_dialog.setToolTip( ClientGUIFunctions.WrapToolTip( 'If this is set, the client will present a more complicated file deletion confirmation dialog that will permit you to set your own deletion reason and perform \'clean\' deletes that leave no deletion record (making later re-import easier).' ) )
self._remember_last_advanced_file_deletion_special_action = QW.QCheckBox( advanced_file_deletion_panel )
- self._remember_last_advanced_file_deletion_special_action.setToolTip( 'This will try to remember and restore the last action you set, whether that was trash, physical delete, or physical delete and clear history.')
+ self._remember_last_advanced_file_deletion_special_action.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will try to remember and restore the last action you set, whether that was trash, physical delete, or physical delete and clear history.') )
self._remember_last_advanced_file_deletion_reason = QW.QCheckBox( advanced_file_deletion_panel )
- self._remember_last_advanced_file_deletion_reason.setToolTip( 'This will remember and restore the last reason you set for a delete.' )
+ self._remember_last_advanced_file_deletion_reason.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will remember and restore the last reason you set for a delete.' ) )
self._advanced_file_deletion_reasons = ClientGUIListBoxes.QueueListBox( advanced_file_deletion_panel, 5, str, add_callable = self._AddAFDR, edit_callable = self._EditAFDR )
@@ -1074,7 +1074,7 @@ def __init__( self, parent ):
self._confirm_trash.setChecked( HC.options[ 'confirm_trash' ] )
tt = 'If there is only one place to delete the file from, you will get no delete dialog--it will just be deleted immediately. Applies the same way to undelete.'
- self._confirm_trash.setToolTip( tt )
+ self._confirm_trash.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._confirm_archive.setChecked( HC.options[ 'confirm_archive' ] )
@@ -1238,11 +1238,11 @@ def __init__( self, parent ):
self._file_viewing_statistics_media_min_time = ClientGUICommon.NoneableSpinCtrl( self )
self._file_viewing_statistics_media_max_time = ClientGUICommon.NoneableSpinCtrl( self )
max_tt = 'If you view a file for a very long time, the amount of viewtime recorded is clipped to this. This stops an outrageous viewtime being saved because you left something open in the background. If the media you view has duration, like a video, the max viewtime is five times its length or this, whichever is larger.'
- self._file_viewing_statistics_media_max_time.setToolTip( max_tt )
+ self._file_viewing_statistics_media_max_time.setToolTip( ClientGUIFunctions.WrapToolTip( max_tt ) )
self._file_viewing_statistics_preview_min_time = ClientGUICommon.NoneableSpinCtrl( self )
self._file_viewing_statistics_preview_max_time = ClientGUICommon.NoneableSpinCtrl( self )
- self._file_viewing_statistics_preview_max_time.setToolTip( max_tt )
+ self._file_viewing_statistics_preview_max_time.setToolTip( ClientGUIFunctions.WrapToolTip( max_tt ) )
self._file_viewing_stats_menu_display = ClientGUICommon.BetterChoice( self )
@@ -1311,7 +1311,7 @@ def __init__( self, parent ):
self._main_gui_panel = ClientGUICommon.StaticBox( self, 'main window' )
self._app_display_name = QW.QLineEdit( self._main_gui_panel )
- self._app_display_name.setToolTip( 'This is placed in every window title, with current version name. Rename if you want to personalise or differentiate.' )
+ self._app_display_name.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is placed in every window title, with current version name. Rename if you want to personalise or differentiate.' ) )
self._confirm_client_exit = QW.QCheckBox( self._main_gui_panel )
@@ -1319,7 +1319,7 @@ def __init__( self, parent ):
tt = 'Middle-clicking one or more tags in a taglist will cause the creation of a new search page for those tags. If you do this from the media viewer or a child manage tags dialog, do you want to switch immediately to the main gui?'
- self._activate_window_on_tag_search_page_activation.setToolTip( tt )
+ self._activate_window_on_tag_search_page_activation.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1327,40 +1327,40 @@ def __init__( self, parent ):
self._always_show_iso_time = QW.QCheckBox( self._misc_panel )
tt = 'In many places across the program (typically import status lists), the client will state a timestamp as "5 days ago". If you would prefer a standard ISO string, like "2018-03-01 12:40:23", check this.'
- self._always_show_iso_time.setToolTip( tt )
+ self._always_show_iso_time.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._menu_choice_buttons_can_mouse_scroll = QW.QCheckBox( self._misc_panel )
tt = 'Many buttons that produce menus when clicked are also "scrollable", so if you wheel your mouse over them, the selection will scroll through the underlying menu. If this is annoying for you, turn it off here!'
- self._menu_choice_buttons_can_mouse_scroll.setToolTip( tt )
+ self._menu_choice_buttons_can_mouse_scroll.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._use_native_menubar = QW.QCheckBox( self._misc_panel )
tt = 'macOS and some Linux allows to embed the main GUI menubar into the OS. This can be buggy! Requires restart.'
- self._use_native_menubar.setToolTip( tt )
+ self._use_native_menubar.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._human_bytes_sig_figs = ClientGUICommon.BetterSpinBox( self._misc_panel, min = 1, max = 6 )
- self._human_bytes_sig_figs.setToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.')
+ self._human_bytes_sig_figs.setToolTip( ClientGUIFunctions.WrapToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.') )
self._discord_dnd_fix = QW.QCheckBox( self._misc_panel )
- self._discord_dnd_fix.setToolTip( 'This makes small file drag-and-drops a little laggier in exchange for Discord support. It also lets you set custom filenames for drag and drop exports.' )
+ self._discord_dnd_fix.setToolTip( ClientGUIFunctions.WrapToolTip( 'This makes small file drag-and-drops a little laggier in exchange for Discord support. It also lets you set custom filenames for drag and drop exports.' ) )
self._discord_dnd_filename_pattern = QW.QLineEdit( self._misc_panel )
- self._discord_dnd_filename_pattern.setToolTip( 'When the above is enabled, this export phrase will rename your files. If no filename can be generated, hash will be used instead.' )
+ self._discord_dnd_filename_pattern.setToolTip( ClientGUIFunctions.WrapToolTip( 'When the above is enabled, this export phrase will rename your files. If no filename can be generated, hash will be used instead.' ) )
self._secret_discord_dnd_fix = QW.QCheckBox( self._misc_panel )
- self._secret_discord_dnd_fix.setToolTip( 'This saves the lag but is potentially dangerous, as it (may) treat the from-db-files-drag as a move rather than a copy and hence only works when the drop destination will not consume the files. It requires an additional secret Alternate key to unlock.' )
+ self._secret_discord_dnd_fix.setToolTip( ClientGUIFunctions.WrapToolTip( 'This saves the lag but is potentially dangerous, as it (may) treat the from-db-files-drag as a move rather than a copy and hence only works when the drop destination will not consume the files. It requires an additional secret Alternate key to unlock.' ) )
self._do_macos_debug_dialog_menus = QW.QCheckBox( self._misc_panel )
- self._do_macos_debug_dialog_menus.setToolTip( 'There is a bug in Big Sur Qt regarding interacting with some menus in dialogs. The menus show but cannot be clicked. This shows the menu items in a debug dialog instead.' )
+ self._do_macos_debug_dialog_menus.setToolTip( ClientGUIFunctions.WrapToolTip( 'There is a bug in Big Sur Qt regarding interacting with some menus in dialogs. The menus show but cannot be clicked. This shows the menu items in a debug dialog instead.' ) )
self._use_qt_file_dialogs = QW.QCheckBox( self._misc_panel )
- self._use_qt_file_dialogs.setToolTip( 'If you get crashes opening file/directory dialogs, try this.' )
+ self._use_qt_file_dialogs.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you get crashes opening file/directory dialogs, try this.' ) )
#
frame_locations_panel = ClientGUICommon.StaticBox( self, 'frame locations' )
self._disable_get_safe_position_test = QW.QCheckBox( self._misc_panel )
- self._disable_get_safe_position_test.setToolTip( 'If your windows keep getting \'rescued\' despite being in a good location, try this.' )
+ self._disable_get_safe_position_test.setToolTip( ClientGUIFunctions.WrapToolTip( 'If your windows keep getting \'rescued\' despite being in a good location, try this.' ) )
self._frame_locations = ClientGUIListCtrl.BetterListCtrl( frame_locations_panel, CGLC.COLUMN_LIST_FRAME_LOCATIONS.ID, 15, data_to_tuples_func = lambda x: (self._GetPrettyFrameLocationInfo( x ), self._GetPrettyFrameLocationInfo( x )), activation_callback = self.EditFrameLocations )
@@ -1438,7 +1438,7 @@ def __init__( self, parent ):
self._misc_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
text = 'Here you can override the current and default values for many frame and dialog sizing and positioning variables.'
- text += os.linesep
+ text += '\n'
text += 'This is an advanced control. If you aren\'t confident of what you are doing here, come back later!'
st = ClientGUICommon.BetterStaticText( frame_locations_panel, label = text )
@@ -1565,15 +1565,15 @@ def __init__( self, parent, new_options ):
self._only_save_last_session_during_idle = QW.QCheckBox( self._sessions_panel )
- self._only_save_last_session_during_idle.setToolTip( 'This is useful if you usually have a very large session (200,000+ files/import items open) and a client that is always on.' )
+ self._only_save_last_session_during_idle.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is useful if you usually have a very large session (200,000+ files/import items open) and a client that is always on.' ) )
self._number_of_gui_session_backups = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 32 )
- self._number_of_gui_session_backups.setToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' )
+ self._number_of_gui_session_backups.setToolTip( ClientGUIFunctions.WrapToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' ) )
self._show_session_size_warnings = QW.QCheckBox( self._sessions_panel )
- self._show_session_size_warnings.setToolTip( 'This will give you a once-per-boot warning popup if your active session contains more than 10M weight.' )
+ self._show_session_size_warnings.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will give you a once-per-boot warning popup if your active session contains more than 10M weight.' ) )
#
@@ -1594,25 +1594,25 @@ def __init__( self, parent, new_options ):
self._page_drop_chase_normally = QW.QCheckBox( self._pages_panel )
- self._page_drop_chase_normally.setToolTip( 'When you drop a page to a new location, should hydrus follow the page selection to the new location?' )
+ self._page_drop_chase_normally.setToolTip( ClientGUIFunctions.WrapToolTip( 'When you drop a page to a new location, should hydrus follow the page selection to the new location?' ) )
self._page_drop_chase_with_shift = QW.QCheckBox( self._pages_panel )
- self._page_drop_chase_with_shift.setToolTip( 'When you drop a page to a new location with shift held down, should hydrus follow the page selection to the new location?' )
+ self._page_drop_chase_with_shift.setToolTip( ClientGUIFunctions.WrapToolTip( 'When you drop a page to a new location with shift held down, should hydrus follow the page selection to the new location?' ) )
self._page_drag_change_tab_normally = QW.QCheckBox( self._pages_panel )
- self._page_drag_change_tab_normally.setToolTip( 'When you drag media or a page to a new location, should hydrus navigate and change tabs as you move the mouse around?' )
+ self._page_drag_change_tab_normally.setToolTip( ClientGUIFunctions.WrapToolTip( 'When you drag media or a page to a new location, should hydrus navigate and change tabs as you move the mouse around?' ) )
self._page_drag_change_tab_with_shift = QW.QCheckBox( self._pages_panel )
- self._page_drag_change_tab_with_shift.setToolTip( 'When you drag media or a page to a new location with shift held down, should hydrus navigate and change tabs as you move the mouse around?' )
+ self._page_drag_change_tab_with_shift.setToolTip( ClientGUIFunctions.WrapToolTip( 'When you drag media or a page to a new location with shift held down, should hydrus navigate and change tabs as you move the mouse around?' ) )
self._wheel_scrolls_tab_bar = QW.QCheckBox( self._pages_panel )
- self._wheel_scrolls_tab_bar.setToolTip( 'When you scroll your mouse wheel over some tabs, the normal behaviour is to change the tab selection. If you often have overloaded tab bars, you might like to have the mouse wheel actually scroll the tab bar itself.' )
+ self._wheel_scrolls_tab_bar.setToolTip( ClientGUIFunctions.WrapToolTip( 'When you scroll your mouse wheel over some tabs, the normal behaviour is to change the tab selection. If you often have overloaded tab bars, you might like to have the mouse wheel actually scroll the tab bar itself.' ) )
self._disable_page_tab_dnd = QW.QCheckBox( self._pages_panel )
- self._disable_page_tab_dnd.setToolTip( 'Trying to debug some client hangs!' )
+ self._disable_page_tab_dnd.setToolTip( ClientGUIFunctions.WrapToolTip( 'Trying to debug some client hangs!' ) )
self._force_hide_page_signal_on_new_page = QW.QCheckBox( self._pages_panel )
- self._force_hide_page_signal_on_new_page.setToolTip( 'If your video still plays with sound in the preview viewer when you create a new page, please try this.' )
+ self._force_hide_page_signal_on_new_page.setToolTip( ClientGUIFunctions.WrapToolTip( 'If your video still plays with sound in the preview viewer when you create a new page, please try this.' ) )
#
@@ -1736,16 +1736,16 @@ def __init__( self, parent, new_options ):
page_names_gridbox = ClientGUICommon.WrapInGrid( self._page_names_panel, rows )
label = 'If you have enough pages in a row, left/right arrows will appear to navigate them back and forth.'
- label += os.linesep
+ label += '\n'
label += 'Due to an unfortunate Qt issue, the tab bar will scroll so the current tab is right-most visible whenever you change page or a page is renamed. This is very annoying to live with.'
- label += os.linesep
+ label += '\n'
label += 'Therefore, do not put import pages in a long row of tabs, as it will reset scroll position on every progress update. Try to avoid long rows in general.'
- label += os.linesep
+ label += '\n'
label += 'Just make some nested \'page of pages\' so they are not all in the same row.'
st = ClientGUICommon.BetterStaticText( self._page_names_panel, label )
- st.setToolTip( 'https://bugreports.qt.io/browse/QTBUG-45381' )
+ st.setToolTip( ClientGUIFunctions.WrapToolTip( 'https://bugreports.qt.io/browse/QTBUG-45381' ) )
st.setWordWrap( True )
@@ -1926,11 +1926,11 @@ def __init__( self, parent ):
self._file_maintenance_active_throttle_velocity = ClientGUITime.VelocityCtrl( self._file_maintenance_panel, min_unit_value, max_unit_value, min_time_delta, minutes = True, seconds = True, per_phrase = 'every', unit = 'heavy work units' )
tt = 'Different jobs will count for more or less weight. A file metadata reparse will count as one work unit, but quicker jobs like checking for file presence will count as fractions of one and will will work more frequently.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Please note that this throttle is not rigorous for long timescales, as file processing history is not currently saved on client exit. If you restart the client, the file manager thinks it has run 0 jobs and will be happy to run until the throttle kicks in again.'
- self._file_maintenance_idle_throttle_velocity.setToolTip( tt )
- self._file_maintenance_active_throttle_velocity.setToolTip( tt )
+ self._file_maintenance_idle_throttle_velocity.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+ self._file_maintenance_active_throttle_velocity.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1938,27 +1938,27 @@ def __init__( self, parent ):
self._repository_processing_work_time_very_idle = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. Very Idle is after an hour of idle mode.'
- self._repository_processing_work_time_very_idle.setToolTip( tt )
+ self._repository_processing_work_time_very_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._repository_processing_rest_percentage_very_idle = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. Very Idle is after an hour of idle mode.'
- self._repository_processing_rest_percentage_very_idle.setToolTip( tt )
+ self._repository_processing_rest_percentage_very_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._repository_processing_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
- self._repository_processing_work_time_idle.setToolTip( tt )
+ self._repository_processing_work_time_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._repository_processing_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
- self._repository_processing_rest_percentage_idle.setToolTip( tt )
+ self._repository_processing_rest_percentage_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._repository_processing_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
- self._repository_processing_work_time_normal.setToolTip( tt )
+ self._repository_processing_work_time_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._repository_processing_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
- self._repository_processing_rest_percentage_normal.setToolTip( tt )
+ self._repository_processing_rest_percentage_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1967,31 +1967,31 @@ def __init__( self, parent ):
self._tag_display_maintenance_during_idle = QW.QCheckBox( self._tag_display_processing_panel )
self._tag_display_maintenance_during_active = QW.QCheckBox( self._tag_display_processing_panel )
tt = 'This can be a real killer. If you are catching up with the PTR and notice a lot of lag bumps, sometimes several seconds long, try turning this off.'
- self._tag_display_maintenance_during_active.setToolTip( tt )
+ self._tag_display_maintenance_during_active.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
- self._tag_display_processing_work_time_idle.setToolTip( tt )
+ self._tag_display_processing_work_time_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
- self._tag_display_processing_rest_percentage_idle.setToolTip( tt )
+ self._tag_display_processing_rest_percentage_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
- self._tag_display_processing_work_time_normal.setToolTip( tt )
+ self._tag_display_processing_work_time_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
- self._tag_display_processing_rest_percentage_normal.setToolTip( tt )
+ self._tag_display_processing_rest_percentage_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_work_time_work_hard = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force it to work hard through the dialog.'
- self._tag_display_processing_work_time_work_hard.setToolTip( tt )
+ self._tag_display_processing_work_time_work_hard.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._tag_display_processing_rest_percentage_work_hard = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force it to work hard through the dialog.'
- self._tag_display_processing_rest_percentage_work_hard.setToolTip( tt )
+ self._tag_display_processing_rest_percentage_work_hard.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -2001,11 +2001,11 @@ def __init__( self, parent ):
self._potential_duplicates_search_work_time = ClientGUITime.TimeDeltaCtrl( self._duplicates_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Potential search operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this, and on large databases the minimum work time may be upwards of several seconds.'
- self._potential_duplicates_search_work_time.setToolTip( tt )
+ self._potential_duplicates_search_work_time.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._potential_duplicates_search_rest_percentage = ClientGUICommon.BetterSpinBox( self._duplicates_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Potential search operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, as a percentage of the last work time.'
- self._potential_duplicates_search_rest_percentage.setToolTip( tt )
+ self._potential_duplicates_search_rest_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -2013,27 +2013,27 @@ def __init__( self, parent ):
self._deferred_table_delete_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
- self._deferred_table_delete_work_time_idle.setToolTip( tt )
+ self._deferred_table_delete_work_time_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._deferred_table_delete_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
- self._deferred_table_delete_rest_percentage_idle.setToolTip( tt )
+ self._deferred_table_delete_rest_percentage_idle.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._deferred_table_delete_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
- self._deferred_table_delete_work_time_normal.setToolTip( tt )
+ self._deferred_table_delete_work_time_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._deferred_table_delete_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
- self._deferred_table_delete_rest_percentage_normal.setToolTip( tt )
+ self._deferred_table_delete_rest_percentage_normal.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._deferred_table_delete_work_time_work_hard = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force it to work hard through the dialog.'
- self._deferred_table_delete_work_time_work_hard.setToolTip( tt )
+ self._deferred_table_delete_work_time_work_hard.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._deferred_table_delete_rest_percentage_work_hard = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force it to work hard through the dialog.'
- self._deferred_table_delete_rest_percentage_work_hard.setToolTip( tt )
+ self._deferred_table_delete_rest_percentage_work_hard.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -2142,17 +2142,17 @@ def __init__( self, parent ):
#
text = '***'
- text += os.linesep
+ text += '\n'
text +='If you are a new user or do not completely understand these options, please do not touch them! Do not set the client to be idle all the time unless you know what you are doing or are testing something and are prepared for potential problems!'
- text += os.linesep
+ text += '\n'
text += '***'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Sometimes, the client needs to do some heavy maintenance. This could be reformatting the database to keep it running fast or processing a large number of tags from a repository. Typically, these jobs will not allow you to use the gui while they run, and on slower computers--or those with not much memory--they can take a long time to complete.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'You can set these jobs to run only when the client is idle, or only during shutdown, or neither, or both. If you leave the client on all the time in the background, focusing on \'idle time\' processing is often ideal. If you have a slow computer, relying on \'shutdown\' processing (which you can manually start when convenient), is often better.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If the client switches from idle to not idle during a job, it will try to abandon it and give you back control. This is not always possible, and even when it is, it will sometimes take several minutes, particularly on slower machines or those on HDDs rather than SSDs.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'If the client believes the system is busy, it will generally not start jobs.'
st = ClientGUICommon.BetterStaticText( self._jobs_panel, label = text )
@@ -2390,7 +2390,7 @@ def __init__( self, parent ):
self._animation_start_position = ClientGUICommon.BetterSpinBox( animations_panel, min=0, max=100 )
self._always_loop_animations = QW.QCheckBox( animations_panel )
- self._always_loop_animations.setToolTip( 'Some GIFS and APNGs have metadata specifying how many times they should be played, usually 1. Uncheck this to obey that number.' )
+ self._always_loop_animations.setToolTip( ClientGUIFunctions.WrapToolTip( 'Some GIFS and APNGs have metadata specifying how many times they should be played, usually 1. Uncheck this to obey that number.' ) )
#
@@ -2399,10 +2399,10 @@ def __init__( self, parent ):
self._mpv_conf_path = QP.FilePickerCtrl( system_panel, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
self._use_system_ffmpeg = QW.QCheckBox( system_panel )
- self._use_system_ffmpeg.setToolTip( '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)' )
+ self._use_system_ffmpeg.setToolTip( ClientGUIFunctions.WrapToolTip( '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)' ) )
self._load_images_with_pil = QW.QCheckBox( system_panel )
- self._load_images_with_pil.setToolTip( 'We are dropping CV and moving to PIL exclusively. If you want to help test, please turn this on and send hydev any images that render wrong!' )
+ self._load_images_with_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'We are dropping CV and moving to PIL exclusively. If you want to help test, please turn this on and send hydev any images that render wrong!' ) )
#
@@ -2411,7 +2411,7 @@ def __init__( self, parent ):
self._media_viewer_cursor_autohide_time_ms = ClientGUICommon.NoneableSpinCtrl( media_viewer_panel, none_phrase = 'do not autohide', min = 100, max = 100000, unit = 'ms' )
self._media_zooms = QW.QLineEdit( media_viewer_panel )
- self._media_zooms.setToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will be what the program steps through as you zoom a media up and down.' )
+ self._media_zooms.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will be what the program steps through as you zoom a media up and down.' ) )
self._media_zooms.textChanged.connect( self.EventZoomsChanged )
from hydrus.client.gui.canvas import ClientGUICanvasMedia
@@ -2425,16 +2425,16 @@ def __init__( self, parent ):
tt = 'When you zoom in or out, there is a centerpoint about which the image zooms. This point \'stays still\' while the image expands or shrinks around it. Different centerpoints give different feels, especially if you drag images around a bit before zooming.'
- self._media_viewer_zoom_center.setToolTip( tt )
+ self._media_viewer_zoom_center.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._draw_transparency_checkerboard_media_canvas = QW.QCheckBox( media_viewer_panel )
- self._draw_transparency_checkerboard_media_canvas.setToolTip( 'If unchecked, will fill in with the normal background colour. Does not apply to MPV.' )
+ self._draw_transparency_checkerboard_media_canvas.setToolTip( ClientGUIFunctions.WrapToolTip( 'If unchecked, will fill in with the normal background colour. Does not apply to MPV.' ) )
self._hide_uninteresting_local_import_time = QW.QCheckBox( media_viewer_panel )
- self._hide_uninteresting_local_import_time.setToolTip( 'If the file was imported at a similar time to when it was added to its current services (i.e. the number of seconds since both events differs by less than 10%), hide the import time in the top of the media viewer.' )
+ self._hide_uninteresting_local_import_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the file was imported at a similar time to when it was added to its current services (i.e. the number of seconds since both events differs by less than 10%), hide the import time in the top of the media viewer.' ) )
self._hide_uninteresting_modified_time = QW.QCheckBox( media_viewer_panel )
- self._hide_uninteresting_modified_time.setToolTip( 'If the file has a modified time similar to its import time (i.e. the number of seconds since both events differs by less than 10%), hide the modified time in the top of the media viewer.' )
+ self._hide_uninteresting_modified_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the file has a modified time similar to its import time (i.e. the number of seconds since both events differs by less than 10%), hide the modified time in the top of the media viewer.' ) )
self._anchor_and_hide_canvas_drags = QW.QCheckBox( media_viewer_panel )
self._touchscreen_canvas_drags_unanchor = QW.QCheckBox( media_viewer_panel )
@@ -2444,28 +2444,28 @@ def __init__( self, parent ):
slideshow_panel = ClientGUICommon.StaticBox( media_viewer_panel, 'slideshows' )
self._slideshow_durations = QW.QLineEdit( slideshow_panel )
- self._slideshow_durations.setToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will end up in the slideshow menu in the media viewer.' )
+ self._slideshow_durations.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will end up in the slideshow menu in the media viewer.' ) )
self._slideshow_durations.textChanged.connect( self.EventSlideshowChanged )
self._slideshow_always_play_duration_media_once_through = QW.QCheckBox( slideshow_panel )
- self._slideshow_always_play_duration_media_once_through.setToolTip( 'If this is on, then a slideshow will not move on until the current duration-having media has played once through.' )
+ self._slideshow_always_play_duration_media_once_through.setToolTip( ClientGUIFunctions.WrapToolTip( 'If this is on, then a slideshow will not move on until the current duration-having media has played once through.' ) )
self._slideshow_always_play_duration_media_once_through.clicked.connect( self.EventSlideshowChanged )
self._slideshow_short_duration_loop_seconds = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 86400, unit = 's' )
tt = '(Ensures very short loops play for a bit, but not five minutes) A slideshow will move on early if the current duration-having media has a duration less than this many seconds (and this is less than the overall slideshow period).'
- self._slideshow_short_duration_loop_seconds.setToolTip( tt )
+ self._slideshow_short_duration_loop_seconds.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._slideshow_short_duration_loop_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 99, unit = '%' )
tt = '(Ensures short videos play for a bit, but not twenty minutes) A slideshow will move on early if the current duration-having media has a duration less than this percentage of the overall slideshow period.'
- self._slideshow_short_duration_loop_percentage.setToolTip( tt )
+ self._slideshow_short_duration_loop_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._slideshow_short_duration_cutoff_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 99, unit = '%' )
tt = '(Ensures that slightly shorter videos move the slideshow cleanly along as soon as they are done) A slideshow will move on early if the current duration-having media will have played exactly once through between this many percent and 100% of the slideshow period.'
- self._slideshow_short_duration_cutoff_percentage.setToolTip( tt )
+ self._slideshow_short_duration_cutoff_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._slideshow_long_duration_overspill_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 500, unit = '%' )
tt = '(Ensures slightly longer videos will not get cut off right at the end) A slideshow will delay moving on if playing the current duration-having media would stretch the overall slideshow period less than this amount.'
- self._slideshow_long_duration_overspill_percentage.setToolTip( tt )
+ self._slideshow_long_duration_overspill_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -2923,7 +2923,7 @@ def __init__( self, parent, new_options ):
self._new_options = new_options
self._start_note_editing_at_end = QW.QCheckBox( self )
- self._start_note_editing_at_end.setToolTip( 'Otherwise, start the text cursor at the start of the document.' )
+ self._start_note_editing_at_end.setToolTip( ClientGUIFunctions.WrapToolTip( 'Otherwise, start the text cursor at the start of the document.' ) )
self._start_note_editing_at_end.setChecked( self._new_options.GetBoolean( 'start_note_editing_at_end' ) )
@@ -2963,13 +2963,13 @@ def __init__( self, parent, new_options ):
self._popup_message_force_min_width = QW.QCheckBox( self._popup_panel )
self._freeze_message_manager_when_mouse_on_other_monitor = QW.QCheckBox( self._popup_panel )
- self._freeze_message_manager_when_mouse_on_other_monitor.setToolTip( 'This is useful if you have a virtual desktop and find the popup manager restores strangely when you hop back to the hydrus display.' )
+ self._freeze_message_manager_when_mouse_on_other_monitor.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is useful if you have a virtual desktop and find the popup manager restores strangely when you hop back to the hydrus display.' ) )
self._freeze_message_manager_when_main_gui_minimised = QW.QCheckBox( self._popup_panel )
- self._freeze_message_manager_when_main_gui_minimised.setToolTip( 'This is useful if the popup toaster restores strangely after minimised changes.' )
+ self._freeze_message_manager_when_main_gui_minimised.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is useful if the popup toaster restores strangely after minimised changes.' ) )
self._notify_client_api_cookies = QW.QCheckBox( self._popup_panel )
- self._notify_client_api_cookies.setToolTip( 'This will make a short-lived popup message every time you get new cookie or http header information over the Client API.' )
+ self._notify_client_api_cookies.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will make a short-lived popup message every time you get new cookie or http header information over the Client API.' ) )
#
@@ -3057,7 +3057,7 @@ def __init__( self, parent, new_options ):
location_context = self._new_options.GetDefaultLocalLocationContext()
self._default_local_location_context = ClientGUILocation.LocationSearchContextButton( self._read_autocomplete_panel, location_context )
- self._default_local_location_context.setToolTip( 'This initialised into a bunch of dialogs across the program as a fallback. You can probably leave it alone forever, but if you delete or move away from \'my files\' as your main place to do work, please update it here.' )
+ self._default_local_location_context.setToolTip( ClientGUIFunctions.WrapToolTip( 'This initialised into a bunch of dialogs across the program as a fallback. You can probably leave it alone forever, but if you delete or move away from \'my files\' as your main place to do work, please update it here.' ) )
self._default_local_location_context.SetOnlyImportableDomainsAllowed( True )
@@ -3065,21 +3065,21 @@ def __init__( self, parent, new_options ):
self._default_search_synchronised = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'This refers to the button on the autocomplete dropdown that enables new searches to start. If this is on, then new search pages will search as soon as you enter the first search predicate. If off, no search will happen until you switch it back on.'
- self._default_search_synchronised.setToolTip( tt )
+ self._default_search_synchronised.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._autocomplete_float_main_gui = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'The autocomplete dropdown can either \'float\' on top of the main window, or if that does not work well for you, it can embed into the parent page panel.'
- self._autocomplete_float_main_gui.setToolTip( tt )
+ self._autocomplete_float_main_gui.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._ac_read_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._read_autocomplete_panel, min = 1, max = 128 )
self._always_show_system_everything = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'After users get some experience with the program and a larger collection, they tend to have less use for system:everything.'
- self._always_show_system_everything.setToolTip( tt )
+ self._always_show_system_everything.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._filter_inbox_and_archive_predicates = QW.QCheckBox( self._read_autocomplete_panel )
tt = 'If everything is current in the inbox (or archive), then there is no use listing it or its opposite--it either does not change the search or it produces nothing. If you find it jarring though, turn it off here!'
- self._filter_inbox_and_archive_predicates.setToolTip( tt )
+ self._filter_inbox_and_archive_predicates.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -3096,7 +3096,7 @@ def __init__( self, parent, new_options ):
misc_panel = ClientGUICommon.StaticBox( self, 'file search' )
self._forced_search_limit = ClientGUICommon.NoneableSpinCtrl( misc_panel, '', min = 1, max = 100000 )
- self._forced_search_limit.setToolTip( 'This is overruled if you set an explicit system:limit larger than it.' )
+ self._forced_search_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is overruled if you set an explicit system:limit larger than it.' ) )
#
@@ -3283,9 +3283,9 @@ def __init__( self, parent, new_options ):
#
sort_by_text = 'You can manage your namespace sorting schemes here.'
- sort_by_text += os.linesep
+ sort_by_text += '\n'
sort_by_text += 'The client will sort media by comparing their namespaces, moving from left to right until an inequality is found.'
- sort_by_text += os.linesep
+ sort_by_text += '\n'
sort_by_text += 'Any namespaces here will also appear in your collect-by dropdowns.'
namespace_sorting_box.Add( ClientGUICommon.BetterStaticText( namespace_sorting_box, sort_by_text ), CC.FLAGS_EXPAND_PERPENDICULAR )
@@ -3375,14 +3375,14 @@ def __init__( self, parent, new_options ):
thumbnail_cache_panel = ClientGUICommon.StaticBox( self, 'thumbnail cache' )
- self._thumbnail_cache_size = ClientGUIControls.BytesControl( thumbnail_cache_panel )
+ self._thumbnail_cache_size = ClientGUIBytes.BytesControl( thumbnail_cache_panel )
self._thumbnail_cache_size.valueChanged.connect( self.EventThumbnailsUpdate )
tt = 'When thumbnails are loaded from disk, their bitmaps are saved for a while in memory so near-future access is super fast. If the total store of thumbnails exceeds this size setting, the least-recent-to-be-accessed will be discarded until the total size is less than it again.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Most thumbnails are RGB, which means their size here is roughly [width x height x 3].'
- self._thumbnail_cache_size.setToolTip( tt )
+ self._thumbnail_cache_size.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._estimated_number_thumbnails = QW.QLabel( '', thumbnail_cache_panel )
@@ -3390,18 +3390,18 @@ def __init__( self, parent, new_options ):
tt = 'The amount of not-accessed time after which a thumbnail will naturally be removed from the cache.'
- self._thumbnail_cache_timeout.setToolTip( tt )
+ self._thumbnail_cache_timeout.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
image_cache_panel = ClientGUICommon.StaticBox( self, 'image cache' )
- self._image_cache_size = ClientGUIControls.BytesControl( image_cache_panel )
+ self._image_cache_size = ClientGUIBytes.BytesControl( image_cache_panel )
self._image_cache_size.valueChanged.connect( self.EventImageCacheUpdate )
tt = 'When images are loaded from disk, their 100% zoom renders are saved for a while in memory so near-future access is super fast. If the total store of images exceeds this size setting, the least-recent-to-be-accessed will be discarded until the total size is less than it again.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Most images are RGB, which means their size here is roughly [width x height x 3], with those dimensions being at 100% zoom.'
- self._image_cache_size.setToolTip( tt )
+ self._image_cache_size.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._estimated_number_fullscreens = QW.QLabel( '', image_cache_panel )
@@ -3409,76 +3409,76 @@ def __init__( self, parent, new_options ):
tt = 'The amount of not-accessed time after which a rendered image will naturally be removed from the cache.'
- self._image_cache_timeout.setToolTip( tt )
+ self._image_cache_timeout.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._image_cache_storage_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 20, max = 50 )
tt = 'This option sets how much of the cache can go towards one image. If an image\'s total size (usually width x height x 3) is too large compared to the cache, it should not be cached or it will just flush everything else out in one stroke.'
- self._image_cache_storage_limit_percentage.setToolTip( tt )
+ self._image_cache_storage_limit_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._image_cache_storage_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
tt = 'This represents the typical size we are talking about at this percentage level. Could be wider or taller, but overall should have the same number of pixels. Anything smaller will be saved in the cache after load, anything larger will be loaded on demand and forgotten as soon as you navigate away. If you want to have persistent fast access to images bigger than this, increase the total image cache size and/or the max % value permitted.'
- self._image_cache_storage_limit_percentage_st.setToolTip( tt )
+ self._image_cache_storage_limit_percentage_st.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._image_cache_prefetch_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 5, max = 20 )
tt = 'If you are browsing many big files, this option stops the prefetcher from overloading your cache by loading up seven or more gigantic images that each competitively flush each other out and need to be re-rendered over and over.'
- self._image_cache_prefetch_limit_percentage.setToolTip( tt )
+ self._image_cache_prefetch_limit_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._image_cache_prefetch_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
tt = 'This represents the typical size we are talking about at this percentage level. Could be wider or taller, but overall should have the same number of pixels. Anything smaller will be pre-fetched, anything larger will be loaded on demand. If you want images bigger than this to load fast as you browse, increase the total image cache size and/or the max % value permitted.'
- self._image_cache_prefetch_limit_percentage_st.setToolTip( tt )
+ self._image_cache_prefetch_limit_percentage_st.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._media_viewer_prefetch_delay_base_ms = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 2000 )
tt = 'How long to wait, after the current image is rendered, to start rendering neighbours. Does not matter so much any more, but if you have CPU lag, you can try boosting it a bit.'
- self._media_viewer_prefetch_delay_base_ms.setToolTip( tt )
+ self._media_viewer_prefetch_delay_base_ms.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._media_viewer_prefetch_num_previous = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 50 )
self._media_viewer_prefetch_num_next = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 50 )
self._prefetch_label_warning = ClientGUICommon.BetterStaticText( image_cache_panel )
- self._prefetch_label_warning.setToolTip( 'If you boost the prefetch numbers, make sure your image cache is big enough to handle it! Doubly so if you frequently load images that at 100% are far larger than your screen size. You really don\'t want to be prefetching more than your cache can hold!' )
+ self._prefetch_label_warning.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you boost the prefetch numbers, make sure your image cache is big enough to handle it! Doubly so if you frequently load images that at 100% are far larger than your screen size. You really don\'t want to be prefetching more than your cache can hold!' ) )
image_tile_cache_panel = ClientGUICommon.StaticBox( self, 'image tile cache' )
- self._image_tile_cache_size = ClientGUIControls.BytesControl( image_tile_cache_panel )
+ self._image_tile_cache_size = ClientGUIBytes.BytesControl( image_tile_cache_panel )
self._image_tile_cache_size.valueChanged.connect( self.EventImageTilesUpdate )
tt = 'Zooming and displaying an image is expensive. When an image is rendered to screen at a particular zoom, the client breaks the virtual canvas into tiles and only scales and draws the image onto the viewable ones. As you pan around, new tiles may be needed and old ones discarded. It is all cached so you can pan and zoom over the same areas quickly.'
- self._image_tile_cache_size.setToolTip( tt )
+ self._image_tile_cache_size.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._estimated_number_image_tiles = QW.QLabel( '', image_tile_cache_panel )
tt = 'You do not need to go crazy here unless you do a huge amount of zooming and really need multiple zoom levels cached for 10+ files you are comparing with each other.'
- self._estimated_number_image_tiles.setToolTip( tt )
+ self._estimated_number_image_tiles.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._image_tile_cache_timeout = ClientGUITime.TimeDeltaButton( image_tile_cache_panel, min = 300, hours = True, minutes = True )
tt = 'The amount of not-accessed time after which a rendered tile will naturally be removed from the cache.'
- self._image_tile_cache_timeout.setToolTip( tt )
+ self._image_tile_cache_timeout.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._ideal_tile_dimension = ClientGUICommon.BetterSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
tt = 'This is the screen-visible square size the system will aim for. Smaller tiles are more memory efficient but prone to warping and other artifacts. Extreme values may waste CPU.'
- self._ideal_tile_dimension.setToolTip( tt )
+ self._ideal_tile_dimension.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
buffer_panel = ClientGUICommon.StaticBox( self, 'video buffer' )
- self._video_buffer_size = ClientGUIControls.BytesControl( buffer_panel )
+ self._video_buffer_size = ClientGUIBytes.BytesControl( buffer_panel )
self._video_buffer_size.valueChanged.connect( self.EventVideoBufferUpdate )
self._estimated_number_video_frames = QW.QLabel( '', buffer_panel )
@@ -3568,7 +3568,7 @@ def __init__( self, parent, new_options ):
#
text = 'Important if you want smooth navigation between different images in the media viewer. If you deal with huge images, bump up cache size and max size that can be cached or prefetched, but be prepared to pay the memory price.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Allowing more prefetch is great, but it needs CPU.'
st = ClientGUICommon.BetterStaticText( image_cache_panel, text )
@@ -3617,13 +3617,13 @@ def __init__( self, parent, new_options ):
#
text = 'This old option does not apply to mpv! It only applies to the native hydrus animation renderer!'
- text += os.linesep
+ text += '\n'
text += 'Hydrus video rendering is CPU intensive.'
- text += os.linesep
+ text += '\n'
text += 'If you have a lot of memory, you can set a generous potential video buffer to compensate.'
- text += os.linesep
+ text += '\n'
text += 'If the video buffer can hold an entire video, it only needs to be rendered once and will play and loop very smoothly.'
- text += os.linesep
+ text += '\n'
text += 'PROTIP: Do not go crazy here.'
st = ClientGUICommon.BetterStaticText( buffer_panel, text )
@@ -3919,10 +3919,10 @@ def __init__( self, parent, new_options ):
tt = 'It sometimes takes a few seconds for your network adapter to reconnect after a wake. This adds a grace period after a detected wake-from-sleep to allow your OS to sort that out before Hydrus starts making requests.'
- self._wake_delay_period.setToolTip( tt )
+ self._wake_delay_period.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._file_system_waits_on_wakeup = QW.QCheckBox( sleep_panel )
- self._file_system_waits_on_wakeup.setToolTip( 'This is useful if your hydrus is stored on a NAS that takes a few seconds to get going after your machine resumes from sleep.' )
+ self._file_system_waits_on_wakeup.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is useful if your hydrus is stored on a NAS that takes a few seconds to get going after your machine resumes from sleep.' ) )
#
@@ -4058,7 +4058,7 @@ def __init__( self, parent, new_options ):
self._num_recent_petition_reasons = ClientGUICommon.BetterSpinBox( general_panel, initial = 5, min = 0, max = 100 )
tt = 'In manage tags, tag siblings, and tag parents, you may be asked to provide a reason with a petition you make to a hydrus repository. There are some fixed reasons, but the dialog can also remember what you recently typed. This controls how many recent reasons it will remember.'
- self._num_recent_petition_reasons.setToolTip( tt )
+ self._num_recent_petition_reasons.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._ac_select_first_with_count = QW.QCheckBox( general_panel )
@@ -4079,22 +4079,22 @@ def __init__( self, parent, new_options ):
#
self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
- self._expand_parents_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' )
+ self._expand_parents_on_storage_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' ) )
self._expand_parents_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
- self._expand_parents_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
+ self._expand_parents_on_storage_autocomplete_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects the autocomplete results taglist.' ) )
self._show_parent_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' ) )
- self._show_parent_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents either hang below tags or summarise in a suffix.' )
+ self._show_parent_decorators_on_storage_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents either hang below tags or summarise in a suffix.' ) )
self._show_parent_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
- self._show_parent_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
+ self._show_parent_decorators_on_storage_autocomplete_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects the autocomplete results taglist.' ) )
self._show_sibling_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' ) )
- self._show_sibling_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and siblings summarise in a suffix.' )
+ self._show_sibling_decorators_on_storage_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and siblings summarise in a suffix.' ) )
self._show_sibling_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
- self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
+ self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( ClientGUIFunctions.WrapToolTip( 'This affects the autocomplete results taglist.' ) )
self._num_recent_petition_reasons.setValue( self._new_options.GetInteger( 'num_recent_petition_reasons' ) )
@@ -4203,32 +4203,32 @@ def __init__( self, parent, new_options ):
self._show_namespaces = QW.QCheckBox( render_panel )
self._show_number_namespaces = QW.QCheckBox( render_panel )
- self._show_number_namespaces.setToolTip( 'This lets unnamespaced "16:9" show as that, not hiding the "16".' )
+ self._show_number_namespaces.setToolTip( ClientGUIFunctions.WrapToolTip( 'This lets unnamespaced "16:9" show as that, not hiding the "16".' ) )
self._show_subtag_number_namespaces = QW.QCheckBox( render_panel )
- self._show_subtag_number_namespaces.setToolTip( 'This lets unnamespaced "page:3" show as that, not hiding the "page" where it can get mixed with chapter etc...' )
+ self._show_subtag_number_namespaces.setToolTip( ClientGUIFunctions.WrapToolTip( 'This lets unnamespaced "page:3" show as that, not hiding the "page" where it can get mixed with chapter etc...' ) )
self._namespace_connector = QW.QLineEdit( render_panel )
self._sibling_connector = QW.QLineEdit( render_panel )
self._fade_sibling_connector = QW.QCheckBox( render_panel )
tt = 'If set, then if the sibling goes from one namespace to another, that colour will fade across the distance of the sibling connector. Just a bit of fun.'
- self._fade_sibling_connector.setToolTip( tt )
+ self._fade_sibling_connector.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._sibling_connector_custom_namespace_colour = ClientGUICommon.NoneableTextCtrl( render_panel, none_phrase = 'use ideal tag colour' )
tt = 'The sibling connector can use a particular namespace\'s colour.'
- self._sibling_connector_custom_namespace_colour.setToolTip( tt )
+ self._sibling_connector_custom_namespace_colour.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._or_connector = QW.QLineEdit( render_panel )
tt = 'When an OR predicate is rendered, it splits the components by this text.'
- self._or_connector.setToolTip( tt )
+ self._or_connector.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._or_connector_custom_namespace_colour = QW.QLineEdit( render_panel )
tt = 'The OR connector can use a particular namespace\'s colour.'
- self._or_connector_custom_namespace_colour.setToolTip( tt )
+ self._or_connector_custom_namespace_colour.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._replace_tag_underscores_with_spaces = QW.QCheckBox( render_panel )
self._replace_tag_emojis_with_boxes = QW.QCheckBox( render_panel )
- self._replace_tag_emojis_with_boxes.setToolTip( 'This will replace emojis and weird symbols with □ in front-facing user views, in case you are getting crazy rendering. It may break some CJK punctuation.' )
+ self._replace_tag_emojis_with_boxes.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will replace emojis and weird symbols with □ in front-facing user views, in case you are getting crazy rendering. It may break some CJK punctuation.' ) )
#
@@ -4482,7 +4482,7 @@ def __init__( self, parent, new_options ):
self._related_tags_concurrence_threshold_percent = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min = 1, max = 100 )
tt = 'The related tags system looks for tags that tend to be used on the same files. Here you can set how strict it is. How many percent of tag A\'s files must tag B on for tag B to be a good suggestion? Higher numbers will mean fewer but more relevant suggestions.'
- self._related_tags_concurrence_threshold_percent.setToolTip( tt )
+ self._related_tags_concurrence_threshold_percent.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -4492,7 +4492,7 @@ def __init__( self, parent, new_options ):
self._search_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( search_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditSearchTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteSearchTagSliceWeight )
tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by the tag type that searched for them. Set to 0 to not search with that type of tag.'
- self._search_tag_slices_weights.setToolTip( tt )
+ self._search_tag_slices_weights.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
search_tag_slices_weight_panel.SetListCtrl( self._search_tag_slices_weights )
@@ -4508,7 +4508,7 @@ def __init__( self, parent, new_options ):
self._result_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( result_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditResultTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteResultTagSliceWeight )
tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by their tag type. Set to 0 to not suggest that type of tag at all.'
- self._result_tag_slices_weights.setToolTip( tt )
+ self._result_tag_slices_weights.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
result_tag_slices_weight_panel.SetListCtrl( self._result_tag_slices_weights )
@@ -4939,22 +4939,22 @@ def __init__( self, parent, new_options ):
# I tried <100%, but Qt seems to cap it to 1.0. Sad!
self._thumbnail_dpr_percentage = ClientGUICommon.BetterSpinBox( self, min = 100, max = 800 )
tt = 'If your OS runs at an UI scale greater than 100%, mirror it here and your thumbnails will look crisp. If you have multiple monitors at different UI scales, or you change UI scale regularly, set it to the largest one you use.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'I believe the UI scale on the monitor this dialog opened on was {}'.format( HydrusData.ConvertFloatToPercentage( self.devicePixelRatio() ) )
- self._thumbnail_dpr_percentage.setToolTip( tt )
+ self._thumbnail_dpr_percentage.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._video_thumbnail_percentage_in = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
self._thumbnail_visibility_scroll_percent = ClientGUICommon.BetterSpinBox( self, min=1, max=99 )
- self._thumbnail_visibility_scroll_percent.setToolTip( 'Lower numbers will cause fewer scrolls, higher numbers more.' )
+ self._thumbnail_visibility_scroll_percent.setToolTip( ClientGUIFunctions.WrapToolTip( 'Lower numbers will cause fewer scrolls, higher numbers more.' ) )
self._allow_blurhash_fallback = QW.QCheckBox( self )
tt = 'If hydrus does not have a thumbnail for a file (e.g. you are looking at a deleted file, or one unexpectedly missing), but it does know its blurhash, it will generate a blurry thumbnail based off that blurhash. Turning this behaviour off here will make it always show the default "hydrus" thumbnail.'
- self._allow_blurhash_fallback.setToolTip( tt )
+ self._allow_blurhash_fallback.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._fade_thumbnails = QW.QCheckBox( self )
tt = 'Whenever thumbnails change (appearing on a page, selecting, an icon or tag banner changes), they normally fade from the old to the new. If you would rather they change instantly, in one frame, uncheck this.'
- self._fade_thumbnails.setToolTip( tt )
+ self._fade_thumbnails.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._focus_preview_on_ctrl_click = QW.QCheckBox( self )
self._focus_preview_on_ctrl_click_only_static = QW.QCheckBox( self )
@@ -5196,7 +5196,7 @@ def _Copy( self ):
urls = sorted( self._current_urls_count.keys() )
- text = os.linesep.join( urls )
+ text = '\n'.join( urls )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -5295,7 +5295,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of URLs', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of URLs', e )
@@ -5502,18 +5502,18 @@ def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPh
self._missing_subfolders_to_new_subfolders = {}
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 += '\n' * 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 be clicking _add a possibly correct location_ and then select the new folder where the subdirectories all went. You can repeat this if your folders are missing in multiple locations. Check everything reports _ok!_'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Although it is best if you can find everything, you only _have_ to fix the subdirectories starting with \'f\', which store your original files. Those starting \'t\' and \'r\' are for your thumbnails, which can be regenerated with a bit of work.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'Then hit \'apply\', and the client will launch. You should double-check all your locations under \'database->move media files\' immediately.'
- text += os.linesep * 2
+ text += '\n' * 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. Regardless of the situation, the document at "install_dir/db/help my media files are broke.txt" may be useful background reading.'
if self._only_thumbs:
- text += os.linesep * 2
+ text += '\n' * 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.'
@@ -5705,9 +5705,9 @@ def UserIsOKToOK( self ):
if thumb_problems:
message = 'Some or all of your incorrect paths have not been corrected, but they are all thumbnail paths.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Would you like instead to create new empty subdirectories at the previous (or corrected, if you have entered them) locations?'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You can run database->regenerate->thumbnails to fill them up again.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
index 87fa17cc2..7c33b7c92 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
@@ -53,7 +53,7 @@
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUILocation
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIBytes
from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.importing.options import FileImportOptions
from hydrus.client.metadata import ClientTags
@@ -820,7 +820,7 @@ def _SetMaxNumBytes( self ):
panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg, message = message )
- control = ClientGUIControls.NoneableBytesControl( panel, initial_value = 100 * ( 1024 ** 3 ) )
+ control = ClientGUIBytes.NoneableBytesControl( panel, initial_value = 100 * ( 1024 ** 3 ) )
control.SetValue( max_num_bytes )
@@ -889,7 +889,7 @@ def _Update( self ):
label = 'media is {}, thumbnails are estimated at {}-{}'.format( HydrusData.ToHumanBytes( approx_total_client_files ), HydrusData.ToHumanBytes( approx_total_thumbnails_min ), HydrusData.ToHumanBytes( approx_total_thumbnails_max ) )
self._current_media_paths_st.setText( label )
- self._current_media_paths_st.setToolTip( 'Precise thumbnail sizes are not tracked, so this is an estimate based on your current thumbnail dimensions.' )
+ self._current_media_paths_st.setToolTip( ClientGUIFunctions.WrapToolTip( 'Precise thumbnail sizes are not tracked, so this is an estimate based on your current thumbnail dimensions.' ) )
base_locations = self._GetListCtrlLocations()
@@ -988,7 +988,7 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_content_type.addItem( HC.content_type_string_lookup[ content_type ], content_type )
- self._migration_content_type.setToolTip( 'Sets what will be migrated.' )
+ self._migration_content_type.setToolTip( ClientGUIFunctions.WrapToolTip( 'Sets what will be migrated.' ) )
self._migration_source = ClientGUICommon.BetterChoice( self._migration_panel )
@@ -996,11 +996,11 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_source_hash_type_st = ClientGUICommon.BetterStaticText( self._migration_panel, 'hash type: unknown' )
- self._migration_source_hash_type_st.setToolTip( 'If this is something other than sha256, this will only work for files the client has ever previously imported.' )
+ self._migration_source_hash_type_st.setToolTip( ClientGUIFunctions.WrapToolTip( 'If this is something other than sha256, this will only work for files the client has ever previously imported.' ) )
self._migration_source_content_status_filter = ClientGUICommon.BetterChoice( self._migration_panel )
- self._migration_source_content_status_filter.setToolTip( 'This filters which status of tags will be migrated.' )
+ self._migration_source_content_status_filter.setToolTip( ClientGUIFunctions.WrapToolTip( 'This filters which status of tags will be migrated.' ) )
self._migration_source_file_filtering_type = ClientGUICommon.BetterChoice( self._migration_panel )
@@ -1013,15 +1013,15 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_source_file_filtering_type.SetValue( self.HASHES_LOCATION )
- self._migration_source_file_filtering_type.setToolTip( 'Choose whether to do this operation for the files you launched the dialog on, or a whole file domain.' )
+ self._migration_source_file_filtering_type.setToolTip( ClientGUIFunctions.WrapToolTip( 'Choose whether to do this operation for the files you launched the dialog on, or a whole file domain.' ) )
self._migration_source_location_context_button = ClientGUILocation.LocationSearchContextButton( self._migration_panel, location_context )
- self._migration_source_location_context_button.setToolTip( 'Set which files should be eligible. This can get as complicated as you like!' )
+ self._migration_source_location_context_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set which files should be eligible. This can get as complicated as you like!' ) )
self._migration_source_archive_path_button = ClientGUICommon.BetterButton( self._migration_panel, 'no path set', self._SetSourceArchivePath )
message = 'Tags that pass this filter will be applied to the destination with the chosen action.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'For instance, if you whitelist the \'series\' namespace, only series: tags from the source will be added to/deleted from the destination.'
tag_filter = HydrusTags.TagFilter()
@@ -1029,7 +1029,7 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_source_tag_filter = ClientGUITags.TagFilterButton( self._migration_panel, message, tag_filter, label_prefix = 'tags taken: ' )
message = 'The left side of a tag sibling/parent pair must pass this filter for the pair to be included in the migration.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'For instance, if you whitelist the \'character\' namespace, only pairs from the source with character: tags on the left will be added to/deleted from the destination.'
tag_filter = HydrusTags.TagFilter()
@@ -1037,7 +1037,7 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_source_left_tag_pair_filter = ClientGUITags.TagFilterButton( self._migration_panel, message, tag_filter, label_prefix = 'left: ' )
message = 'The right side of a tag sibling/parent pair must pass this filter for the pair to be included in the migration.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'For instance, if you whitelist the \'series\' namespace, only pairs from the source with series: tags on the right will be added to/deleted from the destination.'
tag_filter = HydrusTags.TagFilter()
@@ -1057,7 +1057,7 @@ def __init__( self, parent, service_key, hashes = None ):
self._migration_destination_hash_type_choice.addItem( hash_type, hash_type )
- self._migration_destination_hash_type_choice.setToolTip( 'If you set something other than sha256, this will only work for files the client has ever previously imported.' )
+ self._migration_destination_hash_type_choice.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you set something other than sha256, this will only work for files the client has ever previously imported.' ) )
self._migration_action = ClientGUICommon.BetterChoice( self._migration_panel )
@@ -1123,11 +1123,11 @@ def __init__( self, parent, service_key, hashes = None ):
#
message = 'The content from the SOURCE that the FILTER ALLOWS is applied using the ACTION to the DESTINATION.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'To delete content en masse from one location, select what you want to delete with the filter and set the source and destination the same.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'These migrations can be powerful, so be very careful that you understand what you are doing and choose what you want. Large jobs may have a significant initial setup time, during which case the client may hang briefly, but once they start they are pausable or cancellable. If you do want to perform a large action, it is a good idea to back up your database first, just in case you get a result you did not intend.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You may need to restart your client to see their effect.'
st = ClientGUICommon.BetterStaticText( self, message )
@@ -1307,9 +1307,9 @@ def _MigrationGo( self ):
title = 'taking {} {}{} from "{}" and {} "{}"'.format( source_content_statuses_strings[ content_statuses ], HC.content_type_string_lookup[ content_type ], extra_info, source.GetName(), destination_action_strings[ content_action ], destination.GetName() )
message = 'Migrations can make huge changes. They can be cancelled early, but any work they do cannot always be undone. Please check that this summary looks correct:'
- message += os.linesep * 2
+ message += '\n' * 2
message += title
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you plan to make a very big change (especially a mass delete), I recommend making a backup of your database before going ahead, just in case something unexpected happens.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'looks good', no_label = 'go back' )
@@ -1419,7 +1419,7 @@ def _SetDestinationArchivePath( self ):
self._migration_destination_archive_path_button.setText( filename )
- self._migration_destination_archive_path_button.setToolTip( path )
+ self._migration_destination_archive_path_button.setToolTip( ClientGUIFunctions.WrapToolTip( path ) )
@@ -1497,7 +1497,7 @@ def _SetSourceArchivePath( self ):
self._migration_source_archive_path_button.setText( filename )
- self._migration_source_archive_path_button.setToolTip( path )
+ self._migration_source_archive_path_button.setToolTip( ClientGUIFunctions.WrapToolTip( path ) )
@@ -1681,10 +1681,10 @@ def _UpdateMigrationControlsNewType( self ):
self._migration_destination_hash_type_choice.setEnabled( True )
self._migration_source_archive_path_button.setText( 'no path set' )
- self._migration_source_archive_path_button.setToolTip( '' )
+ self._migration_source_archive_path_button.setToolTip( ClientGUIFunctions.WrapToolTip( '' ) )
self._migration_destination_archive_path_button.setText( 'no path set' )
- self._migration_destination_archive_path_button.setToolTip( '' )
+ self._migration_destination_archive_path_button.setToolTip( ClientGUIFunctions.WrapToolTip( '' ) )
if content_type == HC.CONTENT_TYPE_MAPPINGS:
@@ -1738,7 +1738,7 @@ def __init__( self, parent, network_engine ):
self._repo_link = ClientGUICommon.BetterHyperLink( self, 'get user-made downloaders here', 'https://github.com/CuddleBear92/Hydrus-Presets-and-Scripts/tree/master/Downloaders' )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Or you can paste bitmaps from clipboard!' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Or you can paste bitmaps from clipboard!' ) )
st = ClientGUICommon.BetterStaticText( self, label = 'Drop downloader-encoded pngs onto Lain to import.' )
@@ -1829,7 +1829,7 @@ def _ImportPayloads( self, payloads ):
if len( payloads ) > 1:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If there are more unloadable objects in this import, they will be skipped silently.'
@@ -2116,7 +2116,7 @@ def _ImportPayloads( self, payloads ):
if len( new_domain_metadatas ) > TOO_MANY_DM:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'There are more than ' + HydrusData.ToHumanInt( TOO_MANY_DM ) + ' domain metadata objects. So I do not give you dozens of preview windows, I will only show you these first ' + HydrusData.ToHumanInt( TOO_MANY_DM ) + '.'
@@ -2135,16 +2135,16 @@ def _ImportPayloads( self, payloads ):
all_to_add.extend( new_domain_metadatas )
message = 'The client is about to add and link these objects:'
- message += os.linesep * 2
- message += os.linesep.join( ( obj.GetSafeSummary() for obj in all_to_add[:20] ) )
+ message += '\n' * 2
+ message += '\n'.join( ( obj.GetSafeSummary() for obj in all_to_add[:20] ) )
if len( all_to_add ) > 20:
- message += os.linesep
+ message += '\n'
message += '(and ' + HydrusData.ToHumanInt( len( all_to_add ) - 20 ) + ' others)'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Does that sound good?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -2914,7 +2914,7 @@ def _SeeDescription( self ):
job_type = self._action_selector.GetValue()
message = ClientFiles.regen_file_enum_to_description_lookup[ job_type ]
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This job has weight {}, where a normalised unit of file work has value {}.'.format( HydrusData.ToHumanInt( ClientFiles.regen_file_enum_to_job_weight_lookup[ job_type ] ), HydrusData.ToHumanInt( ClientFiles.NORMALISED_BIG_JOB_WEIGHT ) )
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -3615,7 +3615,7 @@ def __init__( self, parent, paths = None ):
self._tag_button = ClientGUICommon.BetterButton( self, 'add tags/urls with the import >>', self._AddTags )
self._tag_button.setObjectName( 'HydrusAccept' )
- self._tag_button.setToolTip( 'You can add specific tags to these files, import from sidecar files, or generate them based on filename. Don\'t be afraid to experiment!' )
+ self._tag_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can add specific tags to these files, import from sidecar files, or generate them based on filename. Don\'t be afraid to experiment!' ) )
gauge_sizer = QP.HBoxLayout()
diff --git a/hydrus/client/gui/ClientGUIShortcutControls.py b/hydrus/client/gui/ClientGUIShortcutControls.py
index 49c85e7a2..3f9bb9f01 100644
--- a/hydrus/client/gui/ClientGUIShortcutControls.py
+++ b/hydrus/client/gui/ClientGUIShortcutControls.py
@@ -14,6 +14,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@@ -331,15 +332,15 @@ def GetValue( self ):
if dupe_command is not None:
message = 'The shortcut:'
- message += os.linesep * 2
+ message += '\n' * 2
message += shortcut.ToString()
- message += os.linesep * 2
+ message += '\n' * 2
message += 'is mapped twice:'
- message += os.linesep * 2
+ message += '\n' * 2
message += command.ToString()
- message += os.linesep * 2
+ message += '\n' * 2
message += dupe_command.ToString()
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The system only supports one command per shortcut in a set for now, please remove one.'
raise HydrusExceptions.VetoException( message )
@@ -361,6 +362,7 @@ def RemoveShortcuts( self ):
+
class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merge_non_number_numpad, all_shortcuts ):
@@ -368,13 +370,13 @@ def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merg
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp )
- help_button.setToolTip( 'Show help regarding editing shortcuts.' )
+ help_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Show help regarding editing shortcuts.' ) )
self._call_mouse_buttons_primary_secondary = QW.QCheckBox( self )
- self._call_mouse_buttons_primary_secondary.setToolTip( 'Useful if you swap your buttons around.' )
+ self._call_mouse_buttons_primary_secondary.setToolTip( ClientGUIFunctions.WrapToolTip( 'Useful if you swap your buttons around.' ) )
self._shortcuts_merge_non_number_numpad = QW.QCheckBox( self )
- self._shortcuts_merge_non_number_numpad.setToolTip( 'This means a "numpad" variant of Return/Home/Arrow etc.. is just counted as a normal one. Helps clear up a bunch of annoying keyboard mappings.' )
+ self._shortcuts_merge_non_number_numpad.setToolTip( ClientGUIFunctions.WrapToolTip( 'This means a "numpad" variant of Return/Home/Arrow etc.. is just counted as a normal one. Helps clear up a bunch of annoying keyboard mappings.' ) )
reserved_panel = ClientGUICommon.StaticBox( self, 'built-in hydrus shortcut sets' )
@@ -633,17 +635,17 @@ def _RestoreDefaults( self ):
def _ShowHelp( self ):
message = 'I am in the process of converting the multiple old messy shortcut systems to this single unified engine. Many actions are not yet available here, and mouse support is very limited. I expect to overwrite the reserved shortcut sets back to (new and expanded) defaults at least once more, so don\'t remap everything yet unless you are ok with doing it again.'
- message += os.linesep * 2
+ message += '\n' * 2
message += '---'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'In hydrus, shortcuts are split into different sets that are active in different contexts. Depending on where the program focus is, multiple sets can be active at the same time. On a keyboard or mouse event, the active sets will be consulted one after another (typically from the smallest and most precise focus to the largest and broadest parent) until an action match is found.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'There are two kinds--ones built-in to hydrus, and custom sets that you turn on and off:'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The built-in shortcut sets are always active in their contexts--the \'main_gui\' one is always consulted when you hit a key on the main gui window, for instance. They have limited actions to choose from, appropriate to their context. If you would prefer to, say, open the manage tags dialog with Ctrl+F3, edit or add that entry in the \'media\' set and that new shortcut will apply anywhere you are focused on some particular media.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Custom shortcuts sets are those you can create and rename at will. They are only ever active in the media viewer window, and only when you set them so from the top hover-window\'s keyboard icon. They are primarily meant for setting tags and ratings with shortcuts, and are intended to be turned on and off as you perform different \'filtering\' jobs--for instance, you might like to set the 1-5 keys to the different values of a five-star rating system, or assign a few simple keystrokes to a number of common tags.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The built-in \'media\' set also supports tag and rating actions, if you would like some of those to always be active.'
ClientGUIDialogsMessage.ShowInformation( self, message )
diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py
index f8f483c2d..19263ced8 100644
--- a/hydrus/client/gui/ClientGUIShortcuts.py
+++ b/hydrus/client/gui/ClientGUIShortcuts.py
@@ -287,7 +287,10 @@ def SetMouseLabels( call_mouse_buttons_primary_secondary ):
CAC.SIMPLE_REMOVE_FILE_FROM_VIEW,
CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM,
CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER,
+ CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER,
CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE,
+ CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE,
+ CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES,
CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER,
CAC.SIMPLE_COPY_BMP,
CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE,
@@ -298,10 +301,6 @@ def SetMouseLabels( call_mouse_buttons_primary_secondary ):
CAC.SIMPLE_COPY_MD5_HASH,
CAC.SIMPLE_COPY_SHA1_HASH,
CAC.SIMPLE_COPY_SHA512_HASH,
- CAC.SIMPLE_GET_SIMILAR_TO_EXACT,
- CAC.SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR,
- CAC.SIMPLE_GET_SIMILAR_TO_SIMILAR,
- CAC.SIMPLE_GET_SIMILAR_TO_SPECULATIVE,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE_COLLECTIONS,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_CUSTOM,
@@ -420,8 +419,6 @@ def SetMouseLabels( call_mouse_buttons_primary_secondary ):
CAC.SIMPLE_PAUSE_MEDIA,
CAC.SIMPLE_PAUSE_PLAY_MEDIA,
CAC.SIMPLE_MEDIA_SEEK_DELTA,
- CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM,
- CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER,
CAC.SIMPLE_CLOSE_MEDIA_VIEWER
]
@@ -429,16 +426,12 @@ def SetMouseLabels( call_mouse_buttons_primary_secondary ):
CAC.SIMPLE_PAUSE_MEDIA,
CAC.SIMPLE_PAUSE_PLAY_MEDIA,
CAC.SIMPLE_MEDIA_SEEK_DELTA,
- CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM,
- CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER,
CAC.SIMPLE_LAUNCH_MEDIA_VIEWER
]
SHORTCUTS_THUMBNAILS_ACTIONS = [
CAC.SIMPLE_LAUNCH_MEDIA_VIEWER,
CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER,
- CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM,
- CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER,
CAC.SIMPLE_SELECT_FILES,
CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS,
CAC.SIMPLE_REARRANGE_THUMBNAILS,
diff --git a/hydrus/client/gui/ClientGUIStringControls.py b/hydrus/client/gui/ClientGUIStringControls.py
index 7a781e401..1d648176c 100644
--- a/hydrus/client/gui/ClientGUIStringControls.py
+++ b/hydrus/client/gui/ClientGUIStringControls.py
@@ -11,6 +11,7 @@
from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIStringPanels
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@@ -57,7 +58,7 @@ def _UpdateLabel( self ):
label = self._string_converter.ToString()
- self.setToolTip( label )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( label ) )
elided_label = HydrusText.ElideText( label, 64 )
@@ -177,7 +178,7 @@ def _UpdateLabel( self ):
statements = [ HydrusText.ElideText( statement, 64 ) for statement in statements ]
- label = os.linesep.join( statements )
+ label = '\n'.join( statements )
self.setText( label )
diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py
index 1c1dbe0b7..ae7b7181f 100644
--- a/hydrus/client/gui/ClientGUIStringPanels.py
+++ b/hydrus/client/gui/ClientGUIStringPanels.py
@@ -288,11 +288,11 @@ def SetStringProcessor( self, string_processor: ClientStrings.StringProcessor ):
if True in ( isinstance( processing_step, ClientStrings.StringSlicer ) for processing_step in self._string_processor.GetProcessingSteps() ):
- self.setToolTip( 'String Slicing is ignored here.' )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( 'String Slicing is ignored here.' ) )
else:
- self.setToolTip( '' )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( '' ) )
self._UpdateResults()
@@ -731,7 +731,7 @@ def __init__( self, parent, conversion_type, data, example_text ):
self._data_hash_function = ClientGUICommon.BetterChoice( self._control_panel )
tt = 'This hashes the string\'s UTF-8-decoded bytes to hexadecimal.'
- self._data_hash_function.setToolTip( tt )
+ self._data_hash_function.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
for e in ( 'hex', 'base64', 'url percent encoding', 'unicode escape characters', 'html entities' ):
@@ -1182,10 +1182,10 @@ def __init__( self, parent, string_joiner: ClientStrings.StringJoiner, test_data
self._controls_panel = ClientGUICommon.StaticBox( self, 'join text' )
self._joiner = QW.QLineEdit( self._controls_panel )
- self._joiner.setToolTip( 'The strings will be joined using this text. For instance, joining "A" "B" "C" with ", " will create "A, B, C". Entering "\\n" will convert to a newline, which means if you want to join by backslash, you need to enter two, "\\\\". You can enter the empty string, which simply concatenates.' )
+ self._joiner.setToolTip( ClientGUIFunctions.WrapToolTip( 'The strings will be joined using this text. For instance, joining "A" "B" "C" with ", " will create "A, B, C". Entering "\\n" will convert to a newline, which means if you want to join by backslash, you need to enter two, "\\\\". You can enter the empty string, which simply concatenates.' ) )
self._join_tuple_size = ClientGUICommon.NoneableSpinCtrl( self._controls_panel, none_phrase = 'merge all into one string', min = 2 )
- self._join_tuple_size.setToolTip( 'If you want to merge your strings in a 1-2, 1-2, 1-2 (e.g. you have domain-path pairs you want to joint into URLs), or 1-2-3, 1-2-3, 1-2-3 fashion, set the size of your groups here. If the remainder at the end of the list does not fit the group size, it is discarded.' )
+ self._join_tuple_size.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you want to merge your strings in a 1-2, 1-2, 1-2 (e.g. you have domain-path pairs you want to joint into URLs), or 1-2-3, 1-2-3, 1-2-3 fashion, set the size of your groups here. If the remainder at the end of the list does not fit the group size, it is discarded.' ) )
self._summary_st = ClientGUICommon.BetterStaticText( self._controls_panel )
@@ -1761,7 +1761,7 @@ def __init__( self, parent, string_sorter: ClientStrings.StringSorter, test_data
tt = 'If you want to sort by a substring, for instance a number in a longer string, you can place a regex here like \'\\d+\' just to capture and sort by that number. It does not affect the final strings, just what it compared for sort.'
- self._regex.setToolTip( tt )
+ self._regex.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1905,7 +1905,7 @@ def __init__( self, parent, string_splitter: ClientStrings.StringSplitter, examp
self._separator = QW.QLineEdit( self._controls_panel )
tt = 'The string will be split wherever it encounters these characters. Entering "\\n" will convert to a newline, which means if you want to split by backslash, you need to enter two, "\\\\".'
- self._separator.setToolTip( tt )
+ self._separator.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._max_splits = ClientGUICommon.NoneableSpinCtrl( self._controls_panel, min = 1, max = 65535, unit = 'splits', none_phrase = 'no limit' )
diff --git a/hydrus/client/gui/ClientGUIStyle.py b/hydrus/client/gui/ClientGUIStyle.py
index 695e94f19..29524ea27 100644
--- a/hydrus/client/gui/ClientGUIStyle.py
+++ b/hydrus/client/gui/ClientGUIStyle.py
@@ -20,12 +20,14 @@ def ClearStylesheet():
SetStyleSheet( ORIGINAL_STYLESHEET )
+
def GetAvailableStyles():
# so eventually expand this to do QStylePlugin or whatever we are doing to add more QStyles
- return list( QW.QStyleFactory.keys() )
+ return sorted( QW.QStyleFactory.keys(), key = HydrusData.HumanTextSortKey )
+
def GetAvailableStylesheets():
if not os.path.exists( STYLESHEET_DIR ) or not os.path.isdir( STYLESHEET_DIR ):
@@ -45,6 +47,8 @@ def GetAvailableStylesheets():
+ HydrusData.HumanTextSort( stylesheet_filenames )
+
return stylesheet_filenames
def InitialiseDefaults():
@@ -122,7 +126,7 @@ def SetStyleSheet( stylesheet, prepend_hydrus = True ):
global DEFAULT_HYDRUS_STYLESHEET
- stylesheet_to_use = DEFAULT_HYDRUS_STYLESHEET + os.linesep * 2 + stylesheet
+ stylesheet_to_use = DEFAULT_HYDRUS_STYLESHEET + '\n' * 2 + stylesheet
global CURRENT_STYLESHEET
diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py
index 5d5eed12b..549be06f0 100644
--- a/hydrus/client/gui/ClientGUISubscriptions.py
+++ b/hydrus/client/gui/ClientGUISubscriptions.py
@@ -199,20 +199,20 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.
self._initial_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
- self._initial_file_limit.setToolTip( 'The first sync will add no more than this many URLs.' )
+ self._initial_file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'The first sync will add no more than this many URLs.' ) )
self._periodic_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
- self._periodic_file_limit.setToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' )
+ self._periodic_file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' ) )
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
- self._this_is_a_random_sample_sub.setToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' )
+ self._this_is_a_random_sample_sub.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' ) )
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options )
self._file_presentation_panel = ClientGUICommon.StaticBox( self, 'file publication' )
self._show_a_popup_while_working = QW.QCheckBox( self._file_presentation_panel )
- self._show_a_popup_while_working.setToolTip( 'Careful with this! Leave it on to begin with, just in case it goes wrong!' )
+ self._show_a_popup_while_working.setToolTip( ClientGUIFunctions.WrapToolTip( 'Careful with this! Leave it on to begin with, just in case it goes wrong!' ) )
self._publish_files_to_popup_button = QW.QCheckBox( self._file_presentation_panel )
self._publish_files_to_page = QW.QCheckBox( self._file_presentation_panel )
@@ -221,11 +221,11 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.
tt = 'This is great to merge multiple subs to a combined location!'
- self._publish_label_override.setToolTip( tt )
+ self._publish_label_override.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
tt = 'If unchecked, each query will produce its own \'subscription_name: query\' button or page.'
- self._merge_query_publish_events.setToolTip( tt )
+ self._merge_query_publish_events.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -521,7 +521,7 @@ def _CopyQueries( self ):
query_texts.append( query_header.GetQueryText() )
- clipboard_text = os.linesep.join( query_texts )
+ clipboard_text = '\n'.join( query_texts )
if len( clipboard_text ) > 0:
@@ -694,7 +694,7 @@ def _CopyQualityInfo( self, data ):
data_strings.append( data_string )
- text = os.linesep.join( data_strings )
+ text = '\n'.join( data_strings )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -740,7 +740,7 @@ def _ShowQualityInfo( self, data ):
data_strings.append( data_string )
- message = os.linesep.join( data_strings )
+ message = '\n'.join( data_strings )
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -827,7 +827,7 @@ def _PasteQueries( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of Queries', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of Queries', e )
return
@@ -879,19 +879,19 @@ def _PasteQueries( self ):
else:
- aeqt_separator = os.linesep
+ aeqt_separator = '\n'
message += 'The queries:'
- message += os.linesep * 2
+ message += '\n' * 2
message += aeqt_separator.join( already_existing_query_texts )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Were already in the subscription.'
if len( DEAD_query_headers ) > 0:
- message += os.linesep * 2
+ message += '\n' * 2
if len( DEAD_query_headers ) > 50:
@@ -907,13 +907,13 @@ def _PasteQueries( self ):
else:
- aeqt_separator = os.linesep
+ aeqt_separator = '\n'
message += 'The DEAD queries:'
- message += os.linesep * 2
+ message += '\n' * 2
message += aeqt_separator.join( DEAD_query_texts )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Were revived.'
@@ -923,7 +923,7 @@ def _PasteQueries( self ):
if len( already_existing_query_texts ) > 0:
- message += os.linesep * 2
+ message += '\n' * 2
if len( new_query_texts ) > 50:
@@ -938,13 +938,13 @@ def _PasteQueries( self ):
else:
- nqt_separator = os.linesep
+ nqt_separator = '\n'
message += 'The queries:'
- message += os.linesep * 2
+ message += '\n' * 2
message += nqt_separator.join( new_query_texts )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Were added.'
@@ -2468,9 +2468,9 @@ def Merge( self ):
edited_datas = []
message = 'Are you sure you want to merge the selected subscriptions? This will combine all selected subscriptions that share the same downloader, wrapping all their different queries into one subscription.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is a big operation, so if it does not do what you expect, hit cancel afterwards!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please note that all other subscription settings settings (like paused status and file limits and tag options) will be merged as well, so double-check your merged subs\' settings afterwards.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
diff --git a/hydrus/client/gui/ClientGUISystemTray.py b/hydrus/client/gui/ClientGUISystemTray.py
index e7ff51647..40626f67d 100644
--- a/hydrus/client/gui/ClientGUISystemTray.py
+++ b/hydrus/client/gui/ClientGUISystemTray.py
@@ -5,9 +5,9 @@
from qtpy import QtGui as QG
from hydrus.core import HydrusConstants as HC
-from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientGlobals as CG
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import QtPorting as QP
@@ -159,21 +159,21 @@ def _UpdateTooltip( self ):
app_display_name = CG.client_controller.new_options.GetString( 'app_display_name' )
- tooltip = app_display_name
+ tt = app_display_name
if self._network_traffic_paused:
- tooltip = '{} - network traffic paused'.format( tooltip )
+ tt = '{} - network traffic paused'.format( tt )
if self._subscriptions_paused:
- tooltip = '{} - subscriptions paused'.format( tooltip )
+ tt = '{} - subscriptions paused'.format( tt )
- if self.toolTip != tooltip:
+ if self.toolTip != tt:
- self.setToolTip( tooltip )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
diff --git a/hydrus/client/gui/ClientGUITagSuggestions.py b/hydrus/client/gui/ClientGUITagSuggestions.py
index 805e67744..ee96a64eb 100644
--- a/hydrus/client/gui/ClientGUITagSuggestions.py
+++ b/hydrus/client/gui/ClientGUITagSuggestions.py
@@ -16,6 +16,7 @@
from hydrus.client import ClientParsing
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIDialogs
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListBoxesData
@@ -352,24 +353,24 @@ def __init__( self, parent, service_key, activate_callable ):
self._status_label = ClientGUICommon.BetterStaticText( self, label = 'ready' )
self._just_do_local_files = ClientGUICommon.OnOffButton( self, on_label = 'just for my files', off_label = 'for all known files', start_on = True )
- self._just_do_local_files.setToolTip( 'Select how big the search is. Searching across all known files on a repository produces high quality results but takes a long time.' )
+ self._just_do_local_files.setToolTip( ClientGUIFunctions.WrapToolTip( 'Select how big the search is. Searching across all known files on a repository produces high quality results but takes a long time.' ) )
tt = 'If you select some tags, this will search using only those as reference!'
self._button_1 = QW.QPushButton( 'quick', self )
self._button_1.clicked.connect( self.RefreshQuick )
self._button_1.setMinimumWidth( 30 )
- self._button_1.setToolTip( tt )
+ self._button_1.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._button_2 = QW.QPushButton( 'medium', self )
self._button_2.clicked.connect( self.RefreshMedium )
self._button_2.setMinimumWidth( 30 )
- self._button_2.setToolTip( tt )
+ self._button_2.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._button_3 = QW.QPushButton( 'thorough', self )
self._button_3.clicked.connect( self.RefreshThorough )
self._button_3.setMinimumWidth( 30 )
- self._button_3.setToolTip( tt )
+ self._button_3.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
if CG.client_controller.services_manager.GetServiceType( self._service_key ) == HC.LOCAL_TAG:
diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py
index 278749092..f8b3792b2 100644
--- a/hydrus/client/gui/ClientGUITags.py
+++ b/hydrus/client/gui/ClientGUITags.py
@@ -45,8 +45,9 @@
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUILocation
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIColourPicker
from hydrus.client.gui.widgets import ClientGUIMenuButton
+from hydrus.client.gui.widgets import ClientGUITextInput
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
@@ -65,7 +66,7 @@ def EditNamespaceSort( win: QW.QWidget, sort_data ):
edit_string = '-'.join( escaped_namespaces )
message = 'Write the namespaces you would like to sort by here, separated by hyphens. Any namespace in any of your sort definitions will be added to the collect-by menu.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If the namespace you want to add has a hyphen, like \'creator-id\', instead type it with a backslash escape, like \'creator\\-id-page\'.'
with ClientGUIDialogs.DialogTextEntry( win, message, allow_blank = False, default = edit_string ) as dlg:
@@ -129,7 +130,7 @@ def __init__( self, parent: QW.QWidget, tag_autocomplete_options: ClientTagsHand
#
self._write_autocomplete_tag_domain = ClientGUICommon.BetterChoice( self )
- self._write_autocomplete_tag_domain.setToolTip( 'A manage tags autocomplete will start with this domain. Typically only useful with this service or "all known tags".' )
+ self._write_autocomplete_tag_domain.setToolTip( ClientGUIFunctions.WrapToolTip( 'A manage tags autocomplete will start with this domain. Typically only useful with this service or "all known tags".' ) )
self._write_autocomplete_tag_domain.addItem( services_manager.GetName( CC.COMBINED_TAG_SERVICE_KEY ), CC.COMBINED_TAG_SERVICE_KEY )
@@ -139,33 +140,33 @@ def __init__( self, parent: QW.QWidget, tag_autocomplete_options: ClientTagsHand
self._override_write_autocomplete_location_context = QW.QCheckBox( self )
- self._override_write_autocomplete_location_context.setToolTip( 'If set, a manage tags dialog autocomplete will start with a different file domain than the one that launched the dialog.' )
+ self._override_write_autocomplete_location_context.setToolTip( ClientGUIFunctions.WrapToolTip( 'If set, a manage tags dialog autocomplete will start with a different file domain than the one that launched the dialog.' ) )
self._write_autocomplete_location_context = ClientGUILocation.LocationSearchContextButton( self, tag_autocomplete_options.GetWriteAutocompleteLocationContext() )
- self._write_autocomplete_location_context.setToolTip( 'A manage tags autocomplete will start with this domain. Normally only useful for "all known files" or "my files".' )
+ self._write_autocomplete_location_context.setToolTip( ClientGUIFunctions.WrapToolTip( 'A manage tags autocomplete will start with this domain. Normally only useful for "all known files" or "my files".' ) )
self._write_autocomplete_location_context.SetAllKnownFilesAllowed( True, False )
self._search_namespaces_into_full_tags = QW.QCheckBox( self )
- self._search_namespaces_into_full_tags.setToolTip( 'If on, a search for "ser" will return all "series:" results such as "series:metroid". On large tag services, these searches are extremely slow.' )
+ self._search_namespaces_into_full_tags.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, a search for "ser" will return all "series:" results such as "series:metroid". On large tag services, these searches are extremely slow.' ) )
self._unnamespaced_search_gives_any_namespace_wildcards = QW.QCheckBox( self )
- self._unnamespaced_search_gives_any_namespace_wildcards.setToolTip( 'If on, an unnamespaced search like "sam" will return special wildcards for "sam* (any namespace)" and "sam (any namespace)", just as if you had typed "*:sam".' )
+ self._unnamespaced_search_gives_any_namespace_wildcards.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, an unnamespaced search like "sam" will return special wildcards for "sam* (any namespace)" and "sam (any namespace)", just as if you had typed "*:sam".' ) )
self._namespace_bare_fetch_all_allowed = QW.QCheckBox( self )
- self._namespace_bare_fetch_all_allowed.setToolTip( 'If on, a search for "series:" will return all "series:" results. On large tag services, these searches are extremely slow.' )
+ self._namespace_bare_fetch_all_allowed.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, a search for "series:" will return all "series:" results. On large tag services, these searches are extremely slow.' ) )
self._namespace_fetch_all_allowed = QW.QCheckBox( self )
- self._namespace_fetch_all_allowed.setToolTip( 'If on, a search for "series:*" will return all "series:" results. On large tag services, these searches are extremely slow.' )
+ self._namespace_fetch_all_allowed.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, a search for "series:*" will return all "series:" results. On large tag services, these searches are extremely slow.' ) )
self._fetch_all_allowed = QW.QCheckBox( self )
- self._fetch_all_allowed.setToolTip( 'If on, a search for "*" will return all tags. On large tag services, these searches are extremely slow.' )
+ self._fetch_all_allowed.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, a search for "*" will return all tags. On large tag services, these searches are extremely slow.' ) )
self._fetch_results_automatically = QW.QCheckBox( self )
- self._fetch_results_automatically.setToolTip( 'If on, results will load as you type. If off, you will have to hit a shortcut (default Ctrl+Space) to load results.' )
+ self._fetch_results_automatically.setToolTip( ClientGUIFunctions.WrapToolTip( 'If on, results will load as you type. If off, you will have to hit a shortcut (default Ctrl+Space) to load results.' ) )
self._exact_match_character_threshold = ClientGUICommon.NoneableSpinCtrl( self, none_phrase = 'always autocomplete (only appropriate for small tag services)', min = 1, max = 256, unit = 'characters' )
- self._exact_match_character_threshold.setToolTip( 'When the search text has <= this many characters, autocomplete will not occur and you will only get results that exactly match the input. Increasing this value makes autocomplete snappier but reduces the number of results.' )
+ self._exact_match_character_threshold.setToolTip( ClientGUIFunctions.WrapToolTip( 'When the search text has <= this many characters, autocomplete will not occur and you will only get results that exactly match the input. Increasing this value makes autocomplete snappier but reduces the number of results.' ) )
#
@@ -366,9 +367,9 @@ def __init__( self, parent, master_service_keys_to_sibling_applicable_service_ke
self._warning.setObjectName( 'HydrusWarning' )
message = 'While a tag service normally only applies its own siblings and parents to itself, it does not have to. You can have other services\' rules apply (e.g. putting the PTR\'s siblings on your "my tags"), or no siblings/parents at all.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you apply multiple services and there are conflicts (e.g. disagreements on where siblings go, or loops), the services at the top of the list have precedence. If you want to overwrite some PTR rules, then make what you want on a local service and then put it above the PTR here. Also, siblings apply first, then parents.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you make big changes here, it will take a long time for the client to recalculate everything. Check the sync progress panel under _tags->sibling/parent sync_ to see how it is going. If your client gets laggy doing the recalc, turn it off during "normal time".'
self._message = ClientGUICommon.BetterStaticText( self, label = message )
@@ -636,7 +637,7 @@ def __init__( self, parent: QW.QWidget, tag_display_manager: ClientTagsHandling.
if self._service_key == CC.COMBINED_TAG_SERVICE_KEY:
message = 'These options apply to all tag services, or to where the tag domain is "all known tags".'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This tag domain is the union of all other services, so it can be more computationally expensive. You most often see it on new search pages.'
else:
@@ -700,7 +701,7 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
#
self._show_all_panels_button = ClientGUICommon.BetterButton( self, 'show other panels', self._ShowAllPanels )
- self._show_all_panels_button.setToolTip( 'This shows the whitelist and advanced panels, in case you want to craft a clever blacklist with \'except\' rules.' )
+ self._show_all_panels_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'This shows the whitelist and advanced panels, in case you want to craft a clever blacklist with \'except\' rules.' ) )
show_the_button = self._only_show_blacklist and CG.client_controller.new_options.GetBoolean( 'advanced_mode' )
@@ -1121,7 +1122,7 @@ def _ImportFavourite( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Tag Filter object', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Tag Filter object', e )
return
@@ -1176,7 +1177,7 @@ def _InitAdvancedPanel( self ):
self._advanced_blacklist = ClientGUIListBoxes.ListBoxTagsFilter( self._advanced_blacklist_panel, read_only = self._read_only )
- self._advanced_blacklist_input = ClientGUIControls.TextAndPasteCtrl( self._advanced_blacklist_panel, self._AdvancedAddBlacklistMultiple, allow_empty_input = True )
+ self._advanced_blacklist_input = ClientGUITextInput.TextAndPasteCtrl( self._advanced_blacklist_panel, self._AdvancedAddBlacklistMultiple, allow_empty_input = True )
delete_blacklist_button = ClientGUICommon.BetterButton( self._advanced_blacklist_panel, 'delete', self._AdvancedDeleteBlacklistButton )
blacklist_everything_button = ClientGUICommon.BetterButton( self._advanced_blacklist_panel, 'block everything', self._AdvancedBlacklistEverything )
@@ -1187,7 +1188,7 @@ def _InitAdvancedPanel( self ):
self._advanced_whitelist = ClientGUIListBoxes.ListBoxTagsFilter( self._advanced_whitelist_panel, read_only = self._read_only )
- self._advanced_whitelist_input = ClientGUIControls.TextAndPasteCtrl( self._advanced_whitelist_panel, self._AdvancedAddWhitelistMultiple, allow_empty_input = True )
+ self._advanced_whitelist_input = ClientGUITextInput.TextAndPasteCtrl( self._advanced_whitelist_panel, self._AdvancedAddWhitelistMultiple, allow_empty_input = True )
delete_whitelist_button = ClientGUICommon.BetterButton( self._advanced_whitelist_panel, 'delete', self._AdvancedDeleteWhitelistButton )
@@ -1270,7 +1271,7 @@ def _InitBlacklistPanel( self ):
self._simple_blacklist = ClientGUIListBoxes.ListBoxTagsFilter( self._simple_whitelist_panel, read_only = self._read_only )
- self._simple_blacklist_input = ClientGUIControls.TextAndPasteCtrl( self._simple_whitelist_panel, self._SimpleAddBlacklistMultiple, allow_empty_input = True )
+ self._simple_blacklist_input = ClientGUITextInput.TextAndPasteCtrl( self._simple_whitelist_panel, self._SimpleAddBlacklistMultiple, allow_empty_input = True )
delete_blacklist_button = ClientGUICommon.BetterButton( self._simple_whitelist_panel, 'remove', self._SimpleDeleteBlacklistButton )
blacklist_everything_button = ClientGUICommon.BetterButton( self._simple_whitelist_panel, 'block everything', self._AdvancedBlacklistEverything )
@@ -1361,7 +1362,7 @@ def _InitWhitelistPanel( self ):
self._simple_whitelist = ClientGUIListBoxes.ListBoxTagsFilter( self._simple_whitelist_panel, read_only = self._read_only )
- self._simple_whitelist_input = ClientGUIControls.TextAndPasteCtrl( self._simple_whitelist_panel, self._SimpleAddWhitelistMultiple, allow_empty_input = True )
+ self._simple_whitelist_input = ClientGUITextInput.TextAndPasteCtrl( self._simple_whitelist_panel, self._SimpleAddWhitelistMultiple, allow_empty_input = True )
delete_whitelist_button = ClientGUICommon.BetterButton( self._simple_whitelist_panel, 'remove', self._SimpleDeleteWhitelistButton )
@@ -1475,9 +1476,9 @@ def _ShowAllPanels( self ):
def _ShowHelp( self ):
help = 'Here you can set rules to filter tags for one purpose or another. The default is typically to permit all tags. Check the current filter summary text at the bottom-left of the panel to ensure you have your logic correct.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'The whitelist/blacklist/advanced tabs are different ways of looking at the same filter, so you can choose which works best for you. Sometimes it is more useful to think about a filter as a whitelist (where only the listed contents are kept) or a blacklist (where everything _except_ the listed contents are kept), while the advanced tab lets you do a more complicated combination of the two.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'As well as selecting entire namespaces with the checkboxes, you can type or paste the individual tags directly--just hit enter to add each one. Double-click an existing entry in a list to remove it.'
ClientGUIDialogsMessage.ShowInformation( self, help )
@@ -1978,17 +1979,17 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, medias: typing.List[
self._tag_in_reverse = QW.QCheckBox( self )
tt = 'Tag the last file first and work backwards, e.g. for start=1, step=1 on five files, set 5, 4, 3, 2, 1.'
- self._tag_in_reverse.setToolTip( tt )
+ self._tag_in_reverse.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
initial_start = self._GetInitialStart()
self._start = ClientGUICommon.BetterSpinBox( self, initial = initial_start, min = -10000000, max = 10000000 )
tt = 'If you initialise this dialog and the first file already has that namespace, this widget will start with that version! A little overlap/prep may help here!'
- self._start.setToolTip( tt )
+ self._start.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._step = ClientGUICommon.BetterSpinBox( self, initial = 1, min = -10000, max = 10000 )
tt = 'This sets how much the numerical tag should increment with each iteration. Negative values are fine and will decrement.'
- self._step.setToolTip( tt )
+ self._step.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
label = 'initialising\n\ninitialising'
self._summary_st = ClientGUICommon.BetterStaticText( self, label = label )
@@ -2613,10 +2614,10 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
self._remove_tags = ClientGUICommon.BetterButton( self._tags_box_sorter, text, self._RemoveTagsButton )
self._copy_button = ClientGUICommon.BetterBitmapButton( self._tags_box_sorter, CC.global_pixmaps().copy, self._Copy )
- self._copy_button.setToolTip( 'Copy selected tags to the clipboard. If none are selected, copies all.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy selected tags to the clipboard. If none are selected, copies all.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self._tags_box_sorter, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste newline-separated tags from the clipboard into here.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste newline-separated tags from the clipboard into here.' ) )
self._show_deleted = False
@@ -2650,7 +2651,7 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
self._incremental_tagging_button = ClientGUICommon.BetterButton( self._tags_box_sorter, HC.UNICODE_PLUS_OR_MINUS, self._DoIncrementalTagging )
- self._incremental_tagging_button.setToolTip( 'Incremental Tagging' )
+ self._incremental_tagging_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Incremental Tagging' ) )
self._incremental_tagging_button.setVisible( len( media ) > 1 )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._incremental_tagging_button, 5 )
@@ -2882,7 +2883,7 @@ def _EnterTags( self, tags, only_add = False, only_remove = False, forced_reason
t_c_lines.append( 'and {} others'.format( HydrusData.ToHumanInt( len( tag_counts ) - 25 ) ) )
- tooltip = os.linesep.join( t_c_lines )
+ tooltip = '\n'.join( t_c_lines )
bdc_choices.append( ( text, data, tooltip ) )
@@ -3066,7 +3067,7 @@ def _Copy( self ):
tags = HydrusTags.SortNumericTags( tags )
- text = os.linesep.join( tags )
+ text = '\n'.join( tags )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -3192,7 +3193,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
@@ -3345,8 +3346,8 @@ def RemoveTags( self, tags ):
if len( tags ) < 10:
message = 'Are you sure you want to remove these tags:'
- message += os.linesep * 2
- message += os.linesep.join( ( HydrusText.ElideText( tag, 64 ) for tag in tags ) )
+ message += '\n' * 2
+ message += '\n'.join( ( HydrusText.ElideText( tag, 64 ) for tag in tags ) )
else:
@@ -3505,10 +3506,10 @@ def __init__( self, parent, service_key, tags = None ):
self._pursue_whole_chain = QW.QCheckBox( self )
tt = 'When you enter tags in the bottom boxes, the upper list is filtered to pertinent related relationships.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'With this off, it will show all (grand)children and (grand)parents. With it on, it shows the full chain, including cousins. This can be overwhelming!'
- self._pursue_whole_chain.setToolTip( tt )
+ self._pursue_whole_chain.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
# leave up here since other things have updates based on them
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL )
@@ -3558,7 +3559,7 @@ def __init__( self, parent, service_key, tags = None ):
#
- self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising' + HC.UNICODE_ELLIPSIS + os.linesep + '.' )
+ self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising' + HC.UNICODE_ELLIPSIS + '\n' + '.' )
self._sync_status_st = ClientGUICommon.BetterStaticText( self, '' )
self._sync_status_st.setWordWrap( True )
self._count_st = ClientGUICommon.BetterStaticText( self, '' )
@@ -3689,10 +3690,10 @@ def _AddPairs( self, pairs, add_only = False ):
else:
- pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in new_pairs ) )
+ pair_strings = '\n'.join( ( child + '->' + parent for ( child, parent ) in new_pairs ) )
- message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your request.'
+ message = 'Enter a reason for:' + '\n' * 2 + pair_strings + '\n' * 2 + 'To be added. A janitor will review your request.'
fixed_suggestions = [
'obvious by definition (a sword is a weapon)',
@@ -3734,157 +3735,154 @@ def _AddPairs( self, pairs, add_only = False ):
affected_pairs.extend( new_pairs )
- else:
+
+ if len( current_pairs ) > 0:
- if len( current_pairs ) > 0:
+ do_it = True
+
+ if self._i_am_local_tag_service:
- do_it = True
+ reason = 'removed by user'
- if self._i_am_local_tag_service:
+ else:
+
+ if len( current_pairs ) > 10:
- reason = 'removed by user'
+ pair_strings = 'The many pairs you entered.'
else:
- if len( current_pairs ) > 10:
+ pair_strings = '\n'.join( ( child + '->' + parent for ( child, parent ) in current_pairs ) )
+
+
+ if len( current_pairs ) > 1:
+
+ message = 'The pairs:' + '\n' * 2 + pair_strings + '\n' * 2 + 'Already exist.'
+
+ else:
+
+ message = 'The pair ' + pair_strings + ' already exists.'
+
+
+ result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'petition to remove', no_label = 'do nothing' )
+
+ if result == QW.QDialog.Accepted:
+
+ if self._service.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_MODERATE ):
- pair_strings = 'The many pairs you entered.'
+ reason = 'admin'
else:
- pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in current_pairs ) )
-
-
- if len( current_pairs ) > 1:
-
- message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Already exist.'
+ message = 'Enter a reason for:'
+ message += '\n' * 2
+ message += pair_strings
+ message += '\n' * 2
+ message += 'to be removed. A janitor will review your petition.'
- else:
+ fixed_suggestions = [
+ 'obvious typo/mistake'
+ ]
- message = 'The pair ' + pair_strings + ' already exists.'
+ suggestions = CG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE )
-
- result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'petition to remove', no_label = 'do nothing' )
-
- if result == QW.QDialog.Accepted:
+ suggestions.extend( fixed_suggestions )
- if self._service.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_MODERATE ):
-
- reason = 'admin'
-
- else:
-
- message = 'Enter a reason for:'
- message += os.linesep * 2
- message += pair_strings
- message += os.linesep * 2
- message += 'to be removed. A janitor will review your petition.'
-
- fixed_suggestions = [
- 'obvious typo/mistake'
- ]
-
- suggestions = CG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE )
-
- suggestions.extend( fixed_suggestions )
+ with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
- with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
+ if dlg.exec() == QW.QDialog.Accepted:
- if dlg.exec() == QW.QDialog.Accepted:
-
- reason = dlg.GetValue()
-
-
- if reason not in fixed_suggestions:
-
- CG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE, reason )
-
-
- else:
+ reason = dlg.GetValue()
+
+
+ if reason not in fixed_suggestions:
- do_it = False
+ CG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE, reason )
+ else:
+
+ do_it = False
+
- else:
-
- do_it = False
-
-
-
- if do_it:
+ else:
- for pair in current_pairs:
-
- self._pairs_to_reasons[ pair ] = reason
-
+ do_it = False
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
+
+
+ if do_it:
+
+ for pair in current_pairs:
- affected_pairs.extend( current_pairs )
+ self._pairs_to_reasons[ pair ] = reason
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
+
+ affected_pairs.extend( current_pairs )
+
- if len( pending_pairs ) > 0:
+
+ if len( pending_pairs ) > 0:
- if len( pending_pairs ) > 10:
-
- pair_strings = 'The many pairs you entered.'
-
- else:
-
- pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in pending_pairs ) )
-
+ if len( pending_pairs ) > 10:
- if len( pending_pairs ) > 1:
-
- message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are pending.'
-
- else:
-
- message = 'The pair ' + pair_strings + ' is pending.'
-
+ pair_strings = 'The many pairs you entered.'
- result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
+ else:
- if result == QW.QDialog.Accepted:
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
-
- affected_pairs.extend( pending_pairs )
-
+ pair_strings = '\n'.join( ( child + '->' + parent for ( child, parent ) in pending_pairs ) )
- if len( petitioned_pairs ) > 0:
+ if len( pending_pairs ) > 1:
+
+ message = 'The pairs:' + '\n' * 2 + pair_strings + '\n' * 2 + 'Are pending.'
+
+ else:
+
+ message = 'The pair ' + pair_strings + ' is pending.'
+
- if len( petitioned_pairs ) > 10:
-
- pair_strings = 'The many pairs you entered.'
-
- else:
-
- pair_strings = os.linesep.join( ( child + '->' + parent for ( child, parent ) in petitioned_pairs ) )
-
+ result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
+
+ if result == QW.QDialog.Accepted:
- if len( petitioned_pairs ) > 1:
-
- message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are petitioned.'
-
- else:
-
- message = 'The pair ' + pair_strings + ' is petitioned.'
-
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
- result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
+ affected_pairs.extend( pending_pairs )
- if result == QW.QDialog.Accepted:
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
-
- affected_pairs.extend( petitioned_pairs )
-
+
+
+ if len( petitioned_pairs ) > 0:
+
+ if len( petitioned_pairs ) > 10:
+
+ pair_strings = 'The many pairs you entered.'
+
+ else:
+
+ pair_strings = '\n'.join( ( child + '->' + parent for ( child, parent ) in petitioned_pairs ) )
+
+
+ if len( petitioned_pairs ) > 1:
+
+ message = 'The pairs:' + '\n' * 2 + pair_strings + '\n' * 2 + 'Are petitioned.'
+
+ else:
+
+ message = 'The pair ' + pair_strings + ' is petitioned.'
+
+
+ result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
+
+ if result == QW.QDialog.Accepted:
+
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
+
+ affected_pairs.extend( petitioned_pairs )
@@ -4024,6 +4022,8 @@ def _DeserialiseImportString( self, import_string ):
pairs.append( pair )
+ pairs = HydrusData.DedupeList( pairs )
+
return pairs
@@ -4062,7 +4062,7 @@ def _GetExportString( self ):
tags.append( b )
- export_string = os.linesep.join( tags )
+ export_string = '\n'.join( tags )
return export_string
@@ -4092,7 +4092,7 @@ def _ImportFromClipboard( self, add_only = False ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of child-parent line-pairs', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of child-parent line-pairs', e )
@@ -4383,7 +4383,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
self._current_statuses_to_pairs = current_statuses_to_pairs
simple_status_text = 'Files with a tag on the left will also be given the tag on the right.'
- simple_status_text += os.linesep
+ simple_status_text += '\n'
simple_status_text += 'As an experiment, this panel will only display the \'current\' pairs for those tags entered below.'
self._status_st.setText( simple_status_text )
@@ -4451,7 +4451,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
- s = os.linesep * 2
+ s = '\n' * 2
status_text = s.join( ( service_part, maintenance_part, changes_part ) )
@@ -4465,7 +4465,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
s = 'The account for this service is currently unsynced! It is uncertain if you have permission to upload parents! Please try to refresh the account in _review services_.'
- status_text = '{}{}{}'.format( s, os.linesep * 2, status_text )
+ status_text = '{}{}{}'.format( s, '\n' * 2, status_text )
elif not account.HasPermission( HC.CONTENT_TYPE_TAG_PARENTS, HC.PERMISSION_ACTION_PETITION ):
@@ -4473,7 +4473,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
s = 'The account for this service does not seem to have permission to upload parents! You can edit them here for now, but the pending menu will not try to upload any changes you make.'
- status_text = '{}{}{}'.format( s, os.linesep * 2, status_text )
+ status_text = '{}{}{}'.format( s, '\n' * 2, status_text )
@@ -4861,7 +4861,7 @@ def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reaso
else:
- pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in new_pairs ) )
+ pair_strings = '\n'.join( ( old + '->' + new for ( old, new ) in new_pairs ) )
fixed_suggestions = [
@@ -4873,7 +4873,7 @@ def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reaso
suggestions.extend( fixed_suggestions )
- message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your petition.'
+ message = 'Enter a reason for:' + '\n' * 2 + pair_strings + '\n' * 2 + 'To be added. A janitor will review your petition.'
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
@@ -4932,170 +4932,168 @@ def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reaso
- else:
+
+ if len( current_pairs ) > 0:
+
+ do_it = True
- if len( current_pairs ) > 0:
+ if default_reason is not None:
- do_it = True
+ reason = default_reason
- if default_reason is not None:
-
- reason = default_reason
-
- elif self._i_am_local_tag_service:
+ elif self._i_am_local_tag_service:
+
+ reason = 'removed by user'
+
+ else:
+
+ if self._service.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_MODERATE ):
- reason = 'removed by user'
+ reason = 'admin'
else:
- if self._service.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_MODERATE ):
+ if len( current_pairs ) > 10:
- reason = 'admin'
+ pair_strings = 'The many pairs you entered.'
else:
- if len( current_pairs ) > 10:
-
- pair_strings = 'The many pairs you entered.'
-
- else:
-
- pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in current_pairs ) )
-
-
- message = 'Enter a reason for:'
- message += os.linesep * 2
- message += pair_strings
- message += os.linesep * 2
- message += 'to be removed. You will see the delete as soon as you upload, but a janitor will review your petition to decide if all users should receive it as well.'
-
- fixed_suggestions = [
- 'obvious typo/mistake',
- 'disambiguation',
- 'correcting to repository standard'
- ]
-
- suggestions = CG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE )
-
- suggestions.extend( fixed_suggestions )
-
- with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- reason = dlg.GetValue()
-
- if reason not in fixed_suggestions:
-
- CG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE, reason )
-
-
- else:
-
- do_it = False
-
-
+ pair_strings = '\n'.join( ( old + '->' + new for ( old, new ) in current_pairs ) )
-
- if do_it:
+ message = 'Enter a reason for:'
+ message += '\n' * 2
+ message += pair_strings
+ message += '\n' * 2
+ message += 'to be removed. You will see the delete as soon as you upload, but a janitor will review your petition to decide if all users should receive it as well.'
+
+ fixed_suggestions = [
+ 'obvious typo/mistake',
+ 'disambiguation',
+ 'correcting to repository standard'
+ ]
+
+ suggestions = CG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE )
- we_are_autopetitioning = self.AUTO_PETITION_REASON in self._pairs_to_reasons.values()
+ suggestions.extend( fixed_suggestions )
- if we_are_autopetitioning:
+ with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
- if self._i_am_local_tag_service:
+ if dlg.exec() == QW.QDialog.Accepted:
- reason = 'REPLACEMENT: by user'
+ reason = dlg.GetValue()
+
+ if reason not in fixed_suggestions:
+
+ CG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE, reason )
+
else:
- reason = 'REPLACEMENT: {}'.format( reason )
+ do_it = False
- for pair in current_pairs:
-
- self._pairs_to_reasons[ pair ] = reason
-
+
+
+ if do_it:
+
+ we_are_autopetitioning = self.AUTO_PETITION_REASON in self._pairs_to_reasons.values()
+
+ if we_are_autopetitioning:
- if we_are_autopetitioning:
+ if self._i_am_local_tag_service:
- for ( p, r ) in list( self._pairs_to_reasons.items() ):
-
- if r == self.AUTO_PETITION_REASON:
-
- self._pairs_to_reasons[ p ] = reason
-
-
+ reason = 'REPLACEMENT: by user'
-
- with self._current_pairs_lock:
+ else:
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
+ reason = 'REPLACEMENT: {}'.format( reason )
-
- if len( pending_pairs ) > 0:
-
- if len( pending_pairs ) > 10:
-
- pair_strings = 'The many pairs you entered.'
+ for pair in current_pairs:
- else:
-
- pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in pending_pairs ) )
+ self._pairs_to_reasons[ pair ] = reason
- if len( pending_pairs ) > 1:
+ if we_are_autopetitioning:
- message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are pending.'
+ for ( p, r ) in list( self._pairs_to_reasons.items() ):
+
+ if r == self.AUTO_PETITION_REASON:
+
+ self._pairs_to_reasons[ p ] = reason
+
+
- else:
+
+ with self._current_pairs_lock:
- message = 'The pair ' + pair_strings + ' is pending.'
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( current_pairs )
- result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
+
+
+ if len( pending_pairs ) > 0:
+
+ if len( pending_pairs ) > 10:
- if result == QW.QDialog.Accepted:
-
- with self._current_pairs_lock:
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
-
-
+ pair_strings = 'The many pairs you entered.'
+
+ else:
+
+ pair_strings = '\n'.join( ( old + '->' + new for ( old, new ) in pending_pairs ) )
+
+
+ if len( pending_pairs ) > 1:
+
+ message = 'The pairs:' + '\n' * 2 + pair_strings + '\n' * 2 + 'Are pending.'
+
+ else:
+
+ message = 'The pair ' + pair_strings + ' is pending.'
- if len( petitioned_pairs ) > 0:
+ result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the pend', no_label = 'do nothing' )
- if len( petitioned_pairs ) > 10:
-
- pair_strings = 'The many pairs you entered.'
-
- else:
-
- pair_strings = ', '.join( ( old + '->' + new for ( old, new ) in petitioned_pairs ) )
-
+ if result == QW.QDialog.Accepted:
- if len( petitioned_pairs ) > 1:
-
- message = 'The pairs:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'Are petitioned.'
-
- else:
+ with self._current_pairs_lock:
- message = 'The pair ' + pair_strings + ' is petitioned.'
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pending_pairs )
- result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
+
+
+ if len( petitioned_pairs ) > 0:
+
+ if len( petitioned_pairs ) > 10:
- if result == QW.QDialog.Accepted:
+ pair_strings = 'The many pairs you entered.'
+
+ else:
+
+ pair_strings = ', '.join( ( old + '->' + new for ( old, new ) in petitioned_pairs ) )
+
+
+ if len( petitioned_pairs ) > 1:
+
+ message = 'The pairs:' + '\n' * 2 + pair_strings + '\n' * 2 + 'Are petitioned.'
+
+ else:
+
+ message = 'The pair ' + pair_strings + ' is petitioned.'
+
+
+ result = ClientGUIDialogsQuick.GetYesNo( self, message, title = 'Choose what to do.', yes_label = 'rescind the petition', no_label = 'do nothing' )
+
+ if result == QW.QDialog.Accepted:
+
+ with self._current_pairs_lock:
- with self._current_pairs_lock:
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
-
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( petitioned_pairs )
@@ -5163,7 +5161,7 @@ def _AutoPetitionLoops( self, pairs ):
if next_new in seen_tags:
message = 'The pair you mean to add seems to connect to a sibling loop already in your database! Please undo this loop manually. The tags involved in the loop are:'
- message += os.linesep * 2
+ message += '\n' * 2
message += ', '.join( seen_tags )
ClientGUIDialogsMessage.ShowCritical( self, 'Loop problem!', message )
@@ -5236,7 +5234,7 @@ def _CanAdd( self, potential_pair ):
if next_new in seen_tags:
message = 'The pair you mean to add seems to connect to a sibling loop already in your database! Please undo this loop first. The tags involved in the loop are:'
- message += os.linesep * 2
+ message += '\n' * 2
message += ', '.join( seen_tags )
ClientGUIDialogsMessage.ShowWarning( self, message )
@@ -5363,6 +5361,8 @@ def _DeserialiseImportString( self, import_string ):
pairs.append( pair )
+ pairs = HydrusData.DedupeList( pairs )
+
return pairs
@@ -5401,7 +5401,7 @@ def _GetExportString( self ):
tags.append( b )
- export_string = os.linesep.join( tags )
+ export_string = '\n'.join( tags )
return export_string
@@ -5435,7 +5435,7 @@ def _ImportFromClipboard( self, add_only = False ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of lesser-ideal sibling line-pairs', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of lesser-ideal sibling line-pairs', e )
@@ -5836,7 +5836,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
- s = os.linesep * 2
+ s = '\n' * 2
status_text = s.join( ( service_part, maintenance_part, changes_part ) )
@@ -5850,7 +5850,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
s = 'The account for this service is currently unsynced! It is uncertain if you have permission to upload parents! Please try to refresh the account in _review services_.'
- status_text = '{}{}{}'.format( s, os.linesep * 2, status_text )
+ status_text = '{}{}{}'.format( s, '\n' * 2, status_text )
elif not account.HasPermission( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.PERMISSION_ACTION_PETITION ):
@@ -5858,7 +5858,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
s = 'The account for this service does not seem to have permission to upload parents! You can edit them here for now, but the pending menu will not try to upload any changes you make.'
- status_text = '{}{}{}'.format( s, os.linesep * 2, status_text )
+ status_text = '{}{}{}'.format( s, '\n' * 2, status_text )
@@ -6101,8 +6101,8 @@ def publish_callable( result ):
if len( status[ 'waiting_on_tag_repos' ] ) > 0:
- message += os.linesep * 2
- message += os.linesep.join( status[ 'waiting_on_tag_repos' ] )
+ message += '\n' * 2
+ message += '\n'.join( status[ 'waiting_on_tag_repos' ] )
sync_halted = True
@@ -6267,7 +6267,7 @@ def _UpdateLabel( self ):
self.setText( button_text )
- self.setToolTip( tt )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
def GetValue( self ):
@@ -6475,8 +6475,8 @@ def __init__( self, parent: QW.QWidget, tag_summary_generator: TagSummaryGenerat
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
- self._background_colour = ClientGUICommon.AlphaColourControl( edit_panel )
- self._text_colour = ClientGUICommon.AlphaColourControl( edit_panel )
+ self._background_colour = ClientGUIColourPicker.AlphaColourControl( edit_panel )
+ self._text_colour = ClientGUIColourPicker.AlphaColourControl( edit_panel )
self._namespaces_listbox = ClientGUIListBoxes.QueueListBox( edit_panel, 8, self._ConvertNamespaceToListBoxString, self._AddNamespaceInfo, self._EditNamespaceInfo )
@@ -6499,7 +6499,7 @@ def __init__( self, parent: QW.QWidget, tag_summary_generator: TagSummaryGenerat
self._text_colour.SetValue( text_colour )
self._namespaces_listbox.AddDatas( namespace_info )
self._separator.setText( separator )
- self._example_tags.setPlainText( os.linesep.join( example_tags ) )
+ self._example_tags.setPlainText( '\n'.join( example_tags ) )
self._UpdateTest()
diff --git a/hydrus/client/gui/ClientGUITime.py b/hydrus/client/gui/ClientGUITime.py
index 200836e3d..b9ac62f87 100644
--- a/hydrus/client/gui/ClientGUITime.py
+++ b/hydrus/client/gui/ClientGUITime.py
@@ -8,19 +8,19 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientTime
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUINumberTest
from hydrus.client.importing.options import ClientImportOptions
def QDateTimeToPrettyString( dt: typing.Optional[ QC.QDateTime ], include_milliseconds = False ):
@@ -52,7 +52,7 @@ def __init__( self, parent, checker_options ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp )
- help_button.setToolTip( 'Show help regarding these checker options.' )
+ help_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Show help regarding these checker options.' ) )
help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', object_name = 'HydrusIndeterminate' )
@@ -103,20 +103,20 @@ def __init__( self, parent, checker_options ):
self._reactive_check_panel = ClientGUICommon.StaticBox( self, 'reactive checking' )
self._intended_files_per_check = ClientGUICommon.BetterSpinBox( self._reactive_check_panel, min=1, max=1000 )
- self._intended_files_per_check.setToolTip( 'How many new files you want the checker to find on each check. If a source is producing about 2 files a day, and this is set to 6, you will probably get a check every three days. You probably want this to be a low number, like 1-4.' )
+ self._intended_files_per_check.setToolTip( ClientGUIFunctions.WrapToolTip( 'How many new files you want the checker to find on each check. If a source is producing about 2 files a day, and this is set to 6, you will probably get a check every three days. You probably want this to be a low number, like 1-4.' ) )
self._never_faster_than = TimeDeltaCtrl( self._reactive_check_panel, min = never_faster_than_min, days = True, hours = True, minutes = True, seconds = True )
- self._never_faster_than.setToolTip( 'Even if the download source produces many new files, the checker will never ask for a check more often than this. This is a safety measure.' )
+ self._never_faster_than.setToolTip( ClientGUIFunctions.WrapToolTip( 'Even if the download source produces many new files, the checker will never ask for a check more often than this. This is a safety measure.' ) )
self._never_slower_than = TimeDeltaCtrl( self._reactive_check_panel, min = never_slower_than_min, days = True, hours = True, minutes = True, seconds = True )
- self._never_slower_than.setToolTip( 'Even if the download source slows down significantly, the checker will make sure it checks at least this often anyway, just to catch a future wave in time.' )
+ self._never_slower_than.setToolTip( ClientGUIFunctions.WrapToolTip( 'Even if the download source slows down significantly, the checker will make sure it checks at least this often anyway, just to catch a future wave in time.' ) )
#
self._static_check_panel = ClientGUICommon.StaticBox( self, 'static checking' )
self._flat_check_period = TimeDeltaCtrl( self._static_check_panel, min = flat_check_period_min, days = True, hours = True, minutes = True, seconds = True )
- self._flat_check_period.setToolTip( 'Always use the same check delay. It is based on the time the last check completed, not the time the last check was due. If you want once a day with no skips, try setting this to 23 hours.' )
+ self._flat_check_period.setToolTip( ClientGUIFunctions.WrapToolTip( 'Always use the same check delay. It is based on the time the last check completed, not the time the last check was due. If you want once a day with no skips, try setting this to 23 hours.' ) )
#
@@ -215,19 +215,19 @@ def __init__( self, parent, checker_options ):
def _ShowHelp( self ):
help = 'The intention of this object is to govern how frequently the watcher or subscription checks for new files--and when it should stop completely.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'PROTIP: Do not change anything here unless you understand what it means!'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'In general, checkers can and should be set up to check faster or slower based on how fast new files are coming in. This is polite to the server you are talking to and saves you CPU and bandwidth. The rate of new files is called the \'file velocity\' and is based on how many files appeared in a certain period before the _most recent check time_.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'Once the first check is done and an initial file velocity is established, the time to the next check will be based on what you set for the \'intended files per check\'. If the current file velocity is 10 files per 24 hours, and you set the intended files per check to 5 files, the checker will set the next check time to be 12 hours after the previous check time.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'After a check is completed, the new file velocity and next check time is calculated, so when files are being posted frequently, it will check more often. When things are slow, it will slow down as well. There are also minimum and maximum check periods to smooth out the bumps.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'But if you would rather just check at a fixed rate, check the checkbox and you will get a simpler \'static checking\' panel.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'If the \'file velocity\' drops below a certain amount, the checker considers the source of files dead and will stop checking. If it falls into this state but you think there might have since been a rush of new files, hit the watcher or subscription\'s \'check now\' button in an attempt to revive the checker. If there are new files, it will start checking again until they drop off once more.'
- help += os.linesep * 2
+ help += '\n' * 2
help += 'If you are still not comfortable with how this system works, the \'reasonable defaults\' are good fallbacks. Most of the time, setting some reasonable rules and leaving checkers to do their work is the best way to deal with this stuff, rather than obsessing over the exact perfect values you want for each situation.'
ClientGUIDialogsMessage.ShowInformation( self, help )
@@ -701,13 +701,13 @@ def __init__( self, parent, time_allowed = True, seconds_allowed = False, millis
tt += 'For instance, if you have a manga chapter in order in a search page, but their import times are scattered, set this to 5 milliseconds and their import times will be set to cascade nicely.'
tt += '\n' * 2
tt += 'Negative values are allowed!'
- self._step.setToolTip( tt )
+ self._step.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._copy_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().copy, self._Copy )
- self._copy_button.setToolTip( 'Copy timestamp to the clipboard.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy timestamp to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste timestamp from another datetime widget.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste timestamp from another datetime widget.' ) )
#
@@ -866,7 +866,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'A simple integer timestamp', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'A simple integer timestamp', e )
return
@@ -1486,7 +1486,7 @@ def SetValue( self, timestamp_data_stub: ClientTime.TimestampData ):
-class NumberTestWidgetDuration( ClientGUIControls.NumberTestWidget ):
+class NumberTestWidgetDuration( ClientGUINumberTest.NumberTestWidget ):
def _GenerateAbsoluteValueWidget( self, max: int ):
diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py
index b325d7034..0d0920e44 100644
--- a/hydrus/client/gui/QtPorting.py
+++ b/hydrus/client/gui/QtPorting.py
@@ -22,6 +22,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import QtInit
isValid = QtInit.isValid
@@ -1558,7 +1559,7 @@ def SetStatusText( self, text, index, tooltip = None ):
if cell.toolTip() != tooltip:
- cell.setToolTip( tooltip )
+ cell.setToolTip( ClientGUIFunctions.WrapToolTip( tooltip ) )
@@ -1856,7 +1857,7 @@ def paintEvent( self, event ):
fontMetrics = painter.fontMetrics()
- text_lines = self.text().split( '\n' )
+ text_lines = self.text().splitlines()
line_spacing = fontMetrics.lineSpacing()
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index 9c3e98aef..cafde66fd 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -7,9 +7,7 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusLists
-from hydrus.core import HydrusPaths
from hydrus.core import HydrusTags
from hydrus.core import HydrusTime
from hydrus.core.files.images import HydrusImageHandling
@@ -20,7 +18,6 @@
from hydrus.client import ClientDuplicates
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
-from hydrus.client import ClientPaths
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsManage
@@ -28,10 +25,6 @@
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIDuplicates
from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIMedia
-from hydrus.client.gui import ClientGUIMediaActions
-from hydrus.client.gui import ClientGUIMediaControls
-from hydrus.client.gui import ClientGUIMediaMenus
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIRatings
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
@@ -42,6 +35,10 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.canvas import ClientGUICanvasHoverFrames
from hydrus.client.gui.canvas import ClientGUICanvasMedia
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
+from hydrus.client.gui.media import ClientGUIMediaModalActions
+from hydrus.client.gui.media import ClientGUIMediaControls
+from hydrus.client.gui.media import ClientGUIMediaMenus
from hydrus.client.media import ClientMedia
from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.metadata import ClientContentUpdates
@@ -368,7 +365,6 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext ):
CG.client_controller.sub( self, 'ZoomIn', 'canvas_zoom_in' )
CG.client_controller.sub( self, 'ZoomOut', 'canvas_zoom_out' )
CG.client_controller.sub( self, 'ZoomSwitch', 'canvas_zoom_switch' )
- CG.client_controller.sub( self, 'OpenExternally', 'canvas_open_externally' )
CG.client_controller.sub( self, 'ManageTags', 'canvas_manage_tags' )
CG.client_controller.sub( self, 'update', 'notify_new_colourset' )
@@ -405,7 +401,7 @@ def _CopyHashToClipboard( self, hash_type ):
return
- ClientGUIMedia.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
+ ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
def _CopyFileToClipboard( self ):
@@ -529,7 +525,7 @@ def _ManageNotes( self, name_to_start_on = None ):
return
- ClientGUIMediaActions.EditFileNotes( self, self._current_media, name_to_start_on = name_to_start_on )
+ ClientGUIMediaModalActions.EditFileNotes( self, self._current_media, name_to_start_on = name_to_start_on )
def _ManageRatings( self ):
@@ -594,7 +590,7 @@ def _ManageTimestamps( self ):
return
- ClientGUIMediaActions.EditFileTimestamps( self, [ self._current_media ] )
+ ClientGUIMediaModalActions.EditFileTimestamps( self, [ self._current_media ] )
def _ManageURLs( self ):
@@ -623,67 +619,17 @@ def _MediaFocusWentToExternalProgram( self ):
return
- mime = self._current_media.GetMime()
-
if self._current_media.HasDuration():
self._media_container.Pause()
- def _OpenExternally( self ):
-
- if self._current_media is None:
-
- return
-
-
- hash = self._current_media.GetHash()
- mime = self._current_media.GetMime()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- launch_path = self._new_options.GetMimeLaunch( mime )
-
- HydrusPaths.LaunchFile( path, launch_path )
-
- self._MediaFocusWentToExternalProgram()
-
-
- def _OpenFileInWebBrowser( self ):
-
- if self._current_media is not None:
-
- hash = self._current_media.GetHash()
- mime = self._current_media.GetMime()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- ClientPaths.LaunchPathInWebBrowser( path )
-
- self._MediaFocusWentToExternalProgram()
-
-
-
- def _OpenFileLocation( self ):
-
- if self._current_media is not None:
-
- ClientGUIMedia.OpenFileLocation( self._current_media )
-
- self._MediaFocusWentToExternalProgram()
-
-
-
def _OpenKnownURL( self ):
if self._current_media is not None:
- ClientGUIMedia.DoOpenKnownURLFromShortcut( self, self._current_media )
+ ClientGUIMediaModalActions.DoOpenKnownURLFromShortcut( self, self._current_media )
@@ -743,7 +689,7 @@ def _Undelete( self ):
return
- ClientGUIMediaActions.UndeleteMedia( self, ( self._current_media, ) )
+ ClientGUIMediaModalActions.UndeleteMedia( self, (self._current_media,) )
def CleanBeforeDestroy( self ):
@@ -844,14 +790,6 @@ def MouseIsOverMedia( self ):
- def OpenExternally( self, canvas_key ):
-
- if self._canvas_key == canvas_key:
-
- self._OpenExternally()
-
-
-
def paintEvent( self, event ):
painter = QG.QPainter( self )
@@ -960,7 +898,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
if self._current_media is not None:
- ClientGUIMedia.CopyMediaURLs( [ self._current_media ] )
+ ClientGUIMediaSimpleActions.CopyMediaURLs( [ self._current_media ] )
elif action == CAC.SIMPLE_DELETE_FILE:
@@ -977,11 +915,61 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
- self._OpenExternally()
+ it_worked = ClientGUIMediaSimpleActions.OpenExternally( self._current_media )
+
+ if it_worked:
+
+ self._MediaFocusWentToExternalProgram()
+
elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
- self._OpenFileLocation()
+ it_worked = ClientGUIMediaSimpleActions.OpenFileLocation( self._current_media )
+
+ if it_worked:
+
+ self._MediaFocusWentToExternalProgram()
+
+
+ elif action == CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER:
+
+ it_worked = ClientGUIMediaSimpleActions.OpenInWebBrowser( self._current_media )
+
+ if it_worked:
+
+ self._MediaFocusWentToExternalProgram()
+
+
+ elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE:
+
+ if self._current_media is not None:
+
+ hash = self._current_media.GetHash()
+
+ ClientGUIMediaSimpleActions.ShowFilesInNewPage( [ hash ], self._location_context )
+
+ self._MediaFocusWentToExternalProgram()
+
+
+ elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE:
+
+ if self._current_media is not None:
+
+ hash = self._current_media.GetHash()
+
+ ClientGUIMediaSimpleActions.ShowFilesInNewDuplicatesFilterPage( [ hash ], self._location_context )
+
+ self._MediaFocusWentToExternalProgram()
+
+
+ elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
+
+ if self._current_media is not None:
+
+ hamming_distance = command.GetSimpleData()
+
+ ClientGUIMediaSimpleActions.ShowSimilarFilesInNewPage( [ self._current_media ], self._location_context, hamming_distance )
+
elif action == CAC.SIMPLE_PAN_UP:
@@ -1019,7 +1007,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
duplicate_type = command.GetSimpleData()
- ClientGUIMedia.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type )
+ ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type )
elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FOCUSED_FALSE_POSITIVES:
@@ -1207,7 +1195,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
return
- command_processed = ClientGUIMediaActions.ApplyContentApplicationCommandToMedia( self, command, ( self._current_media, ) )
+ command_processed = ClientGUIMediaModalActions.ApplyContentApplicationCommandToMedia( self, command, (self._current_media,) )
else:
@@ -1463,7 +1451,7 @@ def ShowMenu( self ):
ClientGUIMediaMenus.AddPrettyInfoLines( info_menu, info_lines )
- ClientGUIMediaMenus.AddFileViewingStatsMenu( info_menu, ( self._current_media, ) )
+ ClientGUIMediaMenus.AddFileViewingStatsMenu( info_menu, (self._current_media,) )
ClientGUIMenus.AppendMenu( menu, info_menu, top_line )
@@ -1533,7 +1521,7 @@ def ShowMenu( self ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
ClientGUIMenus.AppendMenuItem( manage_menu, 'times', 'Edit the timestamps for your files.', self._ManageTimestamps )
- ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaActions.SetFilesForcedFiletypes, self, [ self._current_media ] )
+ ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaModalActions.SetFilesForcedFiletypes, self, [ self._current_media ] )
ClientGUIMediaMenus.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
@@ -1541,20 +1529,7 @@ def ShowMenu( self ):
ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
- open_menu = ClientGUIMenus.GenerateMenu( menu )
-
- ClientGUIMenus.AppendMenuItem( open_menu, 'in external program', 'Open this file in your OS\'s default program.', self._OpenExternally )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Show your current media in a simple new page.', self._ShowMediaInNewPage )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in web browser', 'Show this file in your OS\'s web browser.', self._OpenFileInWebBrowser )
-
- show_open_in_explorer = advanced_mode and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
-
- if show_open_in_explorer:
-
- ClientGUIMenus.AppendMenuItem( open_menu, 'in file browser', 'Show this file in your OS\'s file browser.', self._OpenFileLocation )
-
-
- ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
+ ClientGUIMediaMenus.AddOpenMenu( self, menu, self._current_media, [ self._current_media ] )
share_menu = ClientGUIMenus.GenerateMenu( menu )
@@ -4539,7 +4514,7 @@ def ShowMenu( self ):
ClientGUIMenus.AppendSeparator( info_menu )
- ClientGUIMediaMenus.AddFileViewingStatsMenu( info_menu, ( self._current_media, ) )
+ ClientGUIMediaMenus.AddFileViewingStatsMenu( info_menu, (self._current_media,) )
filetype_summary = ClientMedia.GetMediasFiletypeSummaryString( [ self._current_media ] )
size_summary = HydrusData.ToHumanBytes( self._current_media.GetSize() )
@@ -4683,39 +4658,33 @@ def ShowMenu( self ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage this file\'s notes.', self._ManageNotes )
ClientGUIMenus.AppendMenuItem( manage_menu, 'times', 'Edit the timestamps for your files.', self._ManageTimestamps )
- ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaActions.SetFilesForcedFiletypes, self, [ self._current_media ] )
+ ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaModalActions.SetFilesForcedFiletypes, self, [ self._current_media ] )
ClientGUIMediaMenus.AddManageFileViewingStatsMenu( self, manage_menu, [ self._current_media ] )
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
- ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaActions.GetLocalFileActionServiceKeys( ( self._current_media, ) )
+ ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( (self._current_media,) )
multiple_selected = False
- ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
-
- ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
-
- open_menu = ClientGUIMenus.GenerateMenu( menu )
-
- ClientGUIMenus.AppendMenuItem( open_menu, 'in external program', 'Open this file in the default external program.', self._OpenExternally )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Show your current media in a simple new page.', self._ShowMediaInNewPage )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in web browser', 'Show this file in your OS\'s web browser.', self._OpenFileInWebBrowser )
-
- show_open_in_explorer = advanced_mode and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
-
- if show_open_in_explorer:
+ if len( local_duplicable_to_file_service_keys ) > 0 or len( local_moveable_from_and_to_file_service_keys ) > 0:
+
+ files_menu = ClientGUIMenus.GenerateMenu( menu )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in file browser', 'Show this file in your OS\'s file browser.', self._OpenFileLocation )
+ ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, files_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
+ ClientGUIMenus.AppendMenu( menu, files_menu, 'files' )
+
+
+ ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
- ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
+ ClientGUIMediaMenus.AddOpenMenu( self, menu, self._current_media, [ self._current_media ] )
share_menu = ClientGUIMenus.GenerateMenu( menu )
copy_menu = ClientGUIMenus.GenerateMenu( share_menu )
-
+
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
copy_hash_menu = ClientGUIMenus.GenerateMenu( copy_menu )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasFrame.py b/hydrus/client/gui/canvas/ClientGUICanvasFrame.py
index 58f9b8c42..f52f4d05b 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasFrame.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasFrame.py
@@ -1,17 +1,14 @@
-import typing
-
from qtpy import QtCore as QC
from hydrus.core import HydrusConstants as HC
-from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientGlobals as CG
-from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import ClientGUITopLevelWindows
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.canvas import ClientGUICanvas
+from hydrus.client.gui.media import ClientGUIMediaControls
class CanvasFrame( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.FrameThatResizesWithHovers ):
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
index a2a4cd5bf..999302d38 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
@@ -17,8 +17,6 @@
from hydrus.client.gui import ClientGUIDragDrop
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIMediaActions
-from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIRatings
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
@@ -28,6 +26,8 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.canvas import ClientGUIMPV
from hydrus.client.gui.lists import ClientGUIListBoxes
+from hydrus.client.gui.media import ClientGUIMediaModalActions
+from hydrus.client.gui.media import ClientGUIMediaControls
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.metadata import ClientContentUpdates
@@ -630,7 +630,7 @@ def get_logic_report_string():
tuples.append( ( 'focus is good: ', focus_is_good ) )
tuples.append( ( 'current focus tlw: ', current_focus_tlw ) )
- message = os.linesep * 2 + os.linesep.join( ( a + str( b ) for ( a, b ) in tuples ) )
+ message = '\n' * 2 + '\n'.join( ( a + str( b ) for ( a, b ) in tuples ) )
return message
@@ -742,15 +742,15 @@ def _PopulateCenterButtons( self ):
self._archive_button.setFocusPolicy( QC.Qt.TabFocus )
self._trash_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().delete, CG.client_controller.pub, 'canvas_delete', self._canvas_key )
- self._trash_button.setToolTip( 'send to trash' )
+ self._trash_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'send to trash' ) )
self._trash_button.setFocusPolicy( QC.Qt.TabFocus )
self._delete_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().trash_delete, CG.client_controller.pub, 'canvas_delete', self._canvas_key )
- self._delete_button.setToolTip( 'delete completely' )
+ self._delete_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'delete completely' ) )
self._delete_button.setFocusPolicy( QC.Qt.TabFocus )
self._undelete_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().undelete, CG.client_controller.pub, 'canvas_undelete', self._canvas_key )
- self._undelete_button.setToolTip( 'undelete' )
+ self._undelete_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'undelete' ) )
self._undelete_button.setFocusPolicy( QC.Qt.TabFocus )
QP.AddToLayout( self._top_hbox, self._archive_button, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -790,14 +790,14 @@ def _PopulateRightButtons( self ):
shortcuts = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().keyboard, self._ShowShortcutMenu )
- shortcuts.setToolTip( 'shortcuts' )
+ shortcuts.setToolTip( ClientGUIFunctions.WrapToolTip( 'shortcuts' ) )
shortcuts.setFocusPolicy( QC.Qt.TabFocus )
self._show_embedded_metadata_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().listctrl, self._ShowFileEmbeddedMetadata )
self._show_embedded_metadata_button.setFocusPolicy( QC.Qt.TabFocus )
fullscreen_switch = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().fullscreen_switch, CG.client_controller.pub, 'canvas_fullscreen_switch', self._canvas_key )
- fullscreen_switch.setToolTip( 'fullscreen switch' )
+ fullscreen_switch.setToolTip( ClientGUIFunctions.WrapToolTip( 'fullscreen switch' ) )
fullscreen_switch.setFocusPolicy( QC.Qt.TabFocus )
if HC.PLATFORM_MACOS:
@@ -812,12 +812,12 @@ def _PopulateRightButtons( self ):
drag_button = QW.QPushButton( self )
drag_button.setIcon( QG.QIcon( CC.global_pixmaps().drag ) )
drag_button.setIconSize( CC.global_pixmaps().drag.size() )
- drag_button.setToolTip( 'drag from here to export file' )
+ drag_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'drag from here to export file' ) )
drag_button.pressed.connect( self.DragButtonHit )
drag_button.setFocusPolicy( QC.Qt.TabFocus )
close = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().stop, CG.client_controller.pub, 'canvas_close', self._canvas_key )
- close.setToolTip( 'close' )
+ close.setToolTip( ClientGUIFunctions.WrapToolTip( 'close' ) )
close.setFocusPolicy( QC.Qt.TabFocus )
QP.AddToLayout( self._top_hbox, self._zoom_text, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -838,13 +838,13 @@ def _ResetArchiveButton( self ):
if self._current_media.HasInbox():
ClientGUIFunctions.SetBitmapButtonBitmap( self._archive_button, CC.global_pixmaps().archive )
- self._archive_button.setToolTip( 'archive' )
+ self._archive_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'archive' ) )
else:
ClientGUIFunctions.SetBitmapButtonBitmap( self._archive_button, CC.global_pixmaps().to_inbox )
- self._archive_button.setToolTip( 'return to inbox' )
+ self._archive_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'return to inbox' ) )
@@ -903,7 +903,7 @@ def _ResetButtons( self ):
tt = 'show {}'.format( ' and '.join( tt_components ) )
- self._show_embedded_metadata_button.setToolTip( tt )
+ self._show_embedded_metadata_button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
# enabled, not visible, so it doesn't bounce the others around on scroll
@@ -970,7 +970,7 @@ def _ShowFileEmbeddedMetadata( self ):
return
- ClientGUIMediaActions.ShowFileEmbeddedMetadata( self, self._current_media )
+ ClientGUIMediaModalActions.ShowFileEmbeddedMetadata( self, self._current_media )
def _ShowShortcutMenu( self ):
@@ -1128,7 +1128,7 @@ def _PopulateLeftButtons( self ):
def _ResetArchiveButton( self ):
ClientGUIFunctions.SetBitmapButtonBitmap( self._archive_button, CC.global_pixmaps().archive )
- self._archive_button.setToolTip( 'archive' )
+ self._archive_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'archive' ) )
class CanvasHoverFrameTopNavigable( CanvasHoverFrameTop ):
@@ -1399,7 +1399,7 @@ def _ResetData( self ):
else:
- remote_string = os.linesep.join( remote_strings )
+ remote_string = '\n'.join( remote_strings )
self._file_repos.setText( remote_string )
@@ -1618,7 +1618,7 @@ def __init__( self, parent, my_canvas, top_right_hover: CanvasHoverFrameTopRight
def _EditNotes( self, name ):
- ClientGUIMediaActions.EditFileNotes( self, self._current_media, name_to_start_on = name )
+ ClientGUIMediaModalActions.EditFileNotes( self, self._current_media, name_to_start_on = name )
def _GetIdealSizeAndPosition( self ):
@@ -1808,11 +1808,11 @@ def __init__( self, parent: QW.QWidget, my_canvas: QW.QWidget, canvas_key: bytes
self._comparison_media = None
self._show_in_a_page_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().fullscreen_switch, self.showPairInPage.emit )
- self._show_in_a_page_button.setToolTip( 'send pair to the duplicates media page, for later processing' )
+ self._show_in_a_page_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'send pair to the duplicates media page, for later processing' ) )
self._show_in_a_page_button.setFocusPolicy( QC.Qt.TabFocus )
self._trash_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().delete, CG.client_controller.pub, 'canvas_delete', self._canvas_key )
- self._trash_button.setToolTip( 'send to trash' )
+ self._trash_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'send to trash' ) )
self._trash_button.setFocusPolicy( QC.Qt.TabFocus )
menu_items = []
@@ -1832,7 +1832,7 @@ def __init__( self, parent: QW.QWidget, my_canvas: QW.QWidget, canvas_key: bytes
self._cog_button.setFocusPolicy( QC.Qt.TabFocus )
close_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().stop, CG.client_controller.pub, 'canvas_close', self._canvas_key )
- close_button.setToolTip( 'close filter' )
+ close_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'close filter' ) )
close_button.setFocusPolicy( QC.Qt.TabFocus )
self._back_a_pair = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().first, self.sendApplicationCommand.emit, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_DUPLICATE_FILTER_BACK ) )
@@ -1965,7 +1965,7 @@ def _EditBackgroundSwitchIntensity( self ):
panel = ClientGUIScrolledPanelsEdit.EditNoneableIntegerPanel( dlg, value, message = message, none_phrase = 'do not change', min = 1, max = 9 )
- panel.setToolTip( tooltip )
+ panel.setToolTip( ClientGUIFunctions.WrapToolTip( tooltip ) )
dlg.SetPanel( panel )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
index 6042c6502..c8c344a7c 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
@@ -30,13 +30,12 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientRendering
from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIMedia
-from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtInit
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.canvas import ClientGUIMPV
-from hydrus.client.gui.canvas import ClientGUIMediaVolume
+from hydrus.client.gui.media import ClientGUIMediaControls
+from hydrus.client.gui.media import ClientGUIMediaVolume
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.media import ClientMedia
@@ -688,24 +687,6 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
self.SeekDelta( direction, duration_ms )
- elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenExternally( self._media )
-
-
- elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenFileLocation( self._media )
-
-
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
@@ -3048,24 +3029,6 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
self.SeekDelta( direction, duration_ms )
- elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenExternally( self._media )
-
-
- elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenFileLocation( self._media )
-
-
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
@@ -3640,21 +3603,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
action = command.GetSimpleAction()
- if action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
-
- if self._media is not None:
-
- ClientGUIMedia.OpenExternally( self._media )
-
-
- elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
-
- if self._media is not None:
-
- ClientGUIMedia.OpenFileLocation( self._media )
-
-
- elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
+ if action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
diff --git a/hydrus/client/gui/canvas/ClientGUIMPV.py b/hydrus/client/gui/canvas/ClientGUIMPV.py
index 2701c6da0..e4f68365a 100644
--- a/hydrus/client/gui/canvas/ClientGUIMPV.py
+++ b/hydrus/client/gui/canvas/ClientGUIMPV.py
@@ -1,6 +1,5 @@
import locale
import os
-import time
import traceback
import typing
@@ -18,11 +17,10 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIDialogsMessage
-from hydrus.client.gui import ClientGUIMedia
-from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
-from hydrus.client.gui.canvas import ClientGUIMediaVolume
+from hydrus.client.gui.media import ClientGUIMediaControls
+from hydrus.client.gui.media import ClientGUIMediaVolume
from hydrus.client.media import ClientMedia
mpv_failed_reason = 'MPV seems ok!'
@@ -739,24 +737,6 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
self.SeekDelta( direction, duration_ms )
- elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenExternally( self._media )
-
-
- elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
-
- if self._media is not None:
-
- self.Pause()
-
- ClientGUIMedia.OpenFileLocation( self._media )
-
-
elif action == CAC.SIMPLE_CLOSE_MEDIA_VIEWER and self._canvas_type in CC.CANVAS_MEDIA_VIEWER_TYPES:
self.window().close()
diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py
index dd248f60d..e43aadbca 100644
--- a/hydrus/client/gui/exporting/ClientGUIExport.py
+++ b/hydrus/client/gui/exporting/ClientGUIExport.py
@@ -91,9 +91,9 @@ def _AddFolder( self ):
if len( metadata_routers ) > 0:
message = 'You have some default metadata sidecar settings, most likely from a previous file export. They look like this:'
- message += os.linesep * 2
- message += os.linesep.join( [ router.ToString( pretty = True ) for router in metadata_routers ] )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( [ router.ToString( pretty = True ) for router in metadata_routers ] )
+ message += '\n' * 2
message += 'Do you want these in the new export folder?'
( result, cancelled ) = ClientGUIDialogsQuick.GetYesNo( self, message, no_label = 'no, I want an empty sidecar list', check_for_cancelled = True )
@@ -264,7 +264,7 @@ def __init__( self, parent, export_folder: ClientExportingFiles.ExportFolder ):
if HC.PLATFORM_WINDOWS:
- self._export_symlinks.setToolTip( 'You probably need to run hydrus as Admin for this to work on Windows.')
+ self._export_symlinks.setToolTip( ClientGUIFunctions.WrapToolTip( 'You probably need to run hydrus as Admin for this to work on Windows.' ) )
#
@@ -569,7 +569,7 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
if HC.PLATFORM_WINDOWS:
- self._export_symlinks.setToolTip( 'You probably need to run hydrus as Admin for this to work on Windows.')
+ self._export_symlinks.setToolTip( ClientGUIFunctions.WrapToolTip( 'You probably need to run hydrus as Admin for this to work on Windows.' ) )
metadata_routers = new_options.GetDefaultExportFilesMetadataRouters()
@@ -723,7 +723,7 @@ def _DoExport( self, quit_afterwards = False ):
if delete_afterwards:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'THE FILES WILL BE DELETED FROM THE CLIENT AFTERWARDS'
diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py
index fb48e5011..f9ac01d83 100644
--- a/hydrus/client/gui/importing/ClientGUIImport.py
+++ b/hydrus/client/gui/importing/ClientGUIImport.py
@@ -75,7 +75,7 @@ def _EditOptions( self ):
def _SetToolTip( self ):
- self.setToolTip( self._checker_options.GetSummary() )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( self._checker_options.GetSummary() ) )
def _SetValue( self, checker_options ):
@@ -367,7 +367,7 @@ def AddRegex( self ):
except Exception as e:
text = 'That regex would not compile!'
- text += os.linesep * 2
+ text += '\n' * 2
text += str( e )
ClientGUIDialogsMessage.ShowWarning( self, text )
@@ -611,7 +611,7 @@ def _GetTagsFromClipboard( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
raise
@@ -1261,14 +1261,14 @@ def __init__( self, parent, page_key, name = 'gallery query' ):
self._file_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._import_queue_panel )
self._files_pause_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.PauseFiles )
- self._files_pause_button.setToolTip( 'pause/play files' )
+ self._files_pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play files' ) )
self._gallery_panel = ClientGUICommon.StaticBox( self, 'search' )
self._gallery_status = ClientGUICommon.BetterStaticText( self._gallery_panel, ellipsize_end = True )
self._gallery_pause_button = ClientGUICommon.BetterBitmapButton( self._gallery_panel, CC.global_pixmaps().gallery_pause, self.PauseGallery )
- self._gallery_pause_button.setToolTip( 'pause/play search' )
+ self._gallery_pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play search' ) )
self._gallery_seed_log_control = ClientGUIGallerySeedLog.GallerySeedLogStatusControl( self._gallery_panel, CG.client_controller, False, True, 'search', page_key = self._page_key )
@@ -1276,7 +1276,7 @@ def __init__( self, parent, page_key, name = 'gallery query' ):
self._file_limit = ClientGUICommon.NoneableSpinCtrl( self, 'stop after this many files', min = 1, none_phrase = 'no limit' )
self._file_limit.valueChanged.connect( self.EventFileLimit )
- self._file_limit.setToolTip( 'stop searching the gallery once this many files has been reached' )
+ self._file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'stop searching the gallery once this many files has been reached' ) )
file_import_options = FileImportOptions.FileImportOptions()
file_import_options.SetIsDefault( True )
@@ -1681,7 +1681,7 @@ def __init__( self, parent, page_key, name = 'watcher' ):
imports_panel = ClientGUICommon.StaticBox( self._options_panel, 'imports' )
self._files_pause_button = ClientGUICommon.BetterBitmapButton( imports_panel, CC.global_pixmaps().file_pause, self.PauseFiles )
- self._files_pause_button.setToolTip( 'pause/play files' )
+ self._files_pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play files' ) )
self._file_status = ClientGUICommon.BetterStaticText( imports_panel, ellipsize_end = True )
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( imports_panel, CG.client_controller, self._page_key )
@@ -1694,7 +1694,7 @@ def __init__( self, parent, page_key, name = 'watcher' ):
self._file_velocity_status = ClientGUICommon.BetterStaticText( checker_panel, ellipsize_end = True )
self._checking_pause_button = ClientGUICommon.BetterBitmapButton( checker_panel, CC.global_pixmaps().gallery_pause, self.PauseChecking )
- self._checking_pause_button.setToolTip( 'pause/play checking' )
+ self._checking_pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play checking' ) )
self._watcher_status = ClientGUICommon.BetterStaticText( checker_panel, ellipsize_end = True )
diff --git a/hydrus/client/gui/importing/ClientGUIImportFolders.py b/hydrus/client/gui/importing/ClientGUIImportFolders.py
index 526efc91f..d77e23369 100644
--- a/hydrus/client/gui/importing/ClientGUIImportFolders.py
+++ b/hydrus/client/gui/importing/ClientGUIImportFolders.py
@@ -11,6 +11,7 @@
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFileSeedCache
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUITime
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@@ -195,7 +196,7 @@ def __init__( self, parent, import_folder: ClientImportLocal.ImportFolder ):
self._last_modified_time_skip_period = ClientGUITime.TimeDeltaButton( self._folder_box, min = 1, days = True, hours = True, minutes = True, seconds = True )
tt = 'If a file has a modified time more recent than this long ago, it will not be imported in the current check. Helps to avoid importing files that are in the process of downloading/copying (usually on a NAS where other "already in use" checks may fail).'
- self._last_modified_time_skip_period.setToolTip( tt )
+ self._last_modified_time_skip_period.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._paused = QW.QCheckBox( self._folder_box )
diff --git a/hydrus/client/gui/importing/ClientGUIImportOptions.py b/hydrus/client/gui/importing/ClientGUIImportOptions.py
index 5edaa5fae..9b1a9fc00 100644
--- a/hydrus/client/gui/importing/ClientGUIImportOptions.py
+++ b/hydrus/client/gui/importing/ClientGUIImportOptions.py
@@ -25,7 +25,7 @@
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.search import ClientGUILocation
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIBytes
from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.importing.options import FileImportOptions
from hydrus.client.importing.options import NoteImportOptions
@@ -64,12 +64,12 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
self._use_default_dropdown.addItem( 'set custom file import options just for this importer', False )
tt = 'Normally, the client will refer to the defaults (as set under "options->importing") at the time of import.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'It is easier to work this way, since you can change a single default setting and update all current and future downloaders that refer to those defaults, whereas having specific options for every subscription or downloader means you have to update every single one just to make a little change somewhere.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'But if you are doing a one-time import that has some unusual file rules, set them here.'
- self._use_default_dropdown.setToolTip( tt )
+ self._use_default_dropdown.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -86,10 +86,10 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
self._exclude_deleted = QW.QCheckBox( pre_import_panel )
tt = 'By default, the client will not try to reimport files that it knows were deleted before. This is a good setting and should be left on in general.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'However, you might like to turn it off for a one-time job where you want to force an import of previously deleted files.'
- self._exclude_deleted.setToolTip( tt )
+ self._exclude_deleted.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -109,25 +109,25 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
tt = 'DO NOT SET THESE AS THE EXPENSIVE "DO NOT CHECK" UNLESS YOU KNOW YOU NEED THEM FOR THIS ONE JOB'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If hydrus recognises a file\'s URL or hash, it can determine that it is "already in db" or "previously deleted" and skip the download entirely, saving a huge amount of time and bandwidth. The logic behind this can get quite complicated, and it is usually best to let it work normally.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If the checking is set to "dispositive", then if a match is found, that match will be trusted and the other match type is not consulted. Note that, for now, SHA256 hashes your client has never seen before will never count as "matches", just like an MD5 it has not seen before, so in all cases the import will defer to any set url check that says "already in db/previously deleted". (This is to deal with some cloud-storage in-transfer optimisation hash-changing. Novel SHA256 hashes are not always trustworthy.)'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If you believe your clientside parser or url mappings are completely broken, and these logical tests are producing false positive "deleted" or "already in db" results, then set one or both of these to "do not check". Only ever do this for one-time manually fired jobs. Do not turn this on for a normal download or a subscription! You do not need to switch off checking for a file maintenance job that is filling in missing files, as missing files are automatically detected in the logic.'
- self._preimport_hash_check_type.setToolTip( tt )
- self._preimport_url_check_type.setToolTip( tt )
+ self._preimport_hash_check_type.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+ self._preimport_url_check_type.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._preimport_url_check_looks_for_neighbours = QW.QCheckBox( pre_import_panel )
tt = 'When a file-url mapping is found, and additional check can be performed to see if it is trustworthy.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If the URL has a Post URL Class, and the file has multiple other URLs with the same domain & URL Class (basically the file has multiple URLs on the same site), then the mapping is assumed to be some parse spam and not trustworthy (leading to more "this file looks new" results in the pre-check).'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'This test is best left on unless you are doing a single job that is messed up by the logic.'
- self._preimport_url_check_looks_for_neighbours.setToolTip( tt )
+ self._preimport_url_check_looks_for_neighbours.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -135,22 +135,22 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
tt = 'This is an old setting, it basically just rejects all jpegs and pngs with more than a 1GB bitmap, or about 250-350 Megapixels. In can be useful if you have an older computer that will die at a 16,000x22,000 png.'
- self._allow_decompression_bombs.setToolTip( tt )
+ self._allow_decompression_bombs.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._mimes = ClientGUIOptionsPanels.OptionsPanelMimesTree( pre_import_panel, HC.ALLOWED_MIMES )
- self._min_size = ClientGUIControls.NoneableBytesControl( pre_import_panel )
+ self._min_size = ClientGUIBytes.NoneableBytesControl( pre_import_panel )
self._min_size.SetValue( 5 * 1024 )
- self._max_size = ClientGUIControls.NoneableBytesControl( pre_import_panel )
+ self._max_size = ClientGUIBytes.NoneableBytesControl( pre_import_panel )
self._max_size.SetValue( 100 * 1024 * 1024 )
- self._max_gif_size = ClientGUIControls.NoneableBytesControl( pre_import_panel )
+ self._max_gif_size = ClientGUIBytes.NoneableBytesControl( pre_import_panel )
self._max_gif_size.SetValue( 32 * 1024 * 1024 )
tt = 'This catches most of those gif conversions of webms. These files are low quality but huge and mostly a waste of storage and bandwidth.'
- self._max_gif_size.setToolTip( tt )
+ self._max_gif_size.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._min_resolution = ClientGUICommon.NoneableSpinCtrl( pre_import_panel, num_dimensions = 2 )
self._min_resolution.SetValue( ( 50, 50 ) )
@@ -160,8 +160,8 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
tt = 'If either width or height is violated, the file will fail this test and be ignored. It does not have to be both.'
- self._min_resolution.setToolTip( tt )
- self._max_resolution.setToolTip( tt )
+ self._min_resolution.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+ self._max_resolution.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -190,16 +190,16 @@ def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.F
self._associate_source_urls = QW.QCheckBox( post_import_panel )
tt = 'Any URL in the \'chain\' to the file will be linked to it as a \'known url\' unless that URL has a matching URL Class that is set otherwise. Normally, since Gallery URL Classes are by default set not to associate, this means the file will get a visible Post URL and a less prominent direct File URL.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If you are doing a one-off job and do not want to associate these URLs, disable it here. Do not unset this unless you have a reason to!'
- self._associate_primary_urls.setToolTip( tt )
+ self._associate_primary_urls.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
tt = 'If the parser discovers and additional source URL for another site (e.g. "This file on wewbooru was originally posted to Bixiv [here]."), should that URL be associated with the final URL? Should it be trusted to make \'already in db/previously deleted\' determinations?'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'You should turn this off if the site supplies bad (incorrect or imprecise or malformed) source urls.'
- self._associate_source_urls.setToolTip( tt )
+ self._associate_source_urls.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -568,12 +568,12 @@ def __init__( self, parent: QW.QWidget, note_import_options: NoteImportOptions.N
self._use_default_dropdown.addItem( 'set custom note import options just for this importer', False )
tt = 'Normally, the client will refer to the defaults (as set under "network->downloaders->manage default import options") at the time of import.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'It is easier to work this way, since you can change a single default setting and update all current and future downloaders that refer to those defaults, whereas having specific options for every subscription or downloader means you have to update every single one just to make a little change somewhere.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'But if you are doing a one-time import that has some unusual note merge rules, set them here.'
- self._use_default_dropdown.setToolTip( tt )
+ self._use_default_dropdown.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -589,13 +589,13 @@ def __init__( self, parent: QW.QWidget, note_import_options: NoteImportOptions.N
tt = 'Check this to get notes. Uncheck to disable it and get nothing.'
- self._get_notes.setToolTip( tt )
+ self._get_notes.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._extend_existing_note_if_possible = QW.QCheckBox( self._specific_options_panel )
tt = 'If a note with the same name already exists on the file, but the new note text is just the same as what exists but with something new appended, should we just replace the existing note with the new extended one?'
- self._extend_existing_note_if_possible.setToolTip( tt )
+ self._extend_existing_note_if_possible.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._conflict_resolution = ClientGUICommon.BetterChoice( self._specific_options_panel )
@@ -611,27 +611,27 @@ def __init__( self, parent: QW.QWidget, note_import_options: NoteImportOptions.N
tt = 'If a note with the same name already exists on the file and the above \'extend\' rule does not apply, what should we do?'
- self._conflict_resolution.setToolTip( tt )
+ self._conflict_resolution.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._name_whitelist = ClientGUIListBoxes.AddEditDeleteListBox( self._specific_options_panel, 6, str, self._AddWhitelistItem, self._EditWhitelistItem )
tt = 'If you only want some of the notes the parser provides, state them here. Leave this box blank to allow all notes.'
- self._name_whitelist.setToolTip( tt )
+ self._name_whitelist.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._names_to_name_overrides = ClientGUIStringControls.StringToStringDictControl( self._specific_options_panel, dict(), min_height = 6, key_name = 'parser name', value_name = 'saved name' )
tt = 'If you want to rename any of the notes the parser provides, set it up here.'
- self._names_to_name_overrides.setToolTip( tt )
+ self._names_to_name_overrides.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._all_name_override = ClientGUICommon.NoneableTextCtrl( self._specific_options_panel, none_phrase = 'do not mass-rename' )
tt = 'If you want a hacky way to rename one note that is not caught by the above rename rules, whatever it is originally called, set this.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'If multiple notes get renamed this way, then the note conflict rules will apply as they conflict with each other. New notes are processed in original name alphabetical order.'
- self._all_name_override.setToolTip( tt )
+ self._all_name_override.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -680,7 +680,7 @@ def __init__( self, parent: QW.QWidget, note_import_options: NoteImportOptions.N
rows.append( ( 'get notes: ', self._get_notes ) )
rows.append( ( 'if possible, extend existing notes: ', self._extend_existing_note_if_possible ) )
rows.append( ( 'if existing note conflict, what to do: ', self._conflict_resolution ) )
- rows.append( ( 'only allow these note names' + os.linesep + '(leave blank for \'get all\'): ', self._name_whitelist ) )
+ rows.append( ( 'only allow these note names' + '\n' + '(leave blank for \'get all\'): ', self._name_whitelist ) )
rows.append( ( 'rename these notes as they come in: ', self._names_to_name_overrides ) )
rows.append( ( 'rename spare note(s) to this: ', self._all_name_override ) )
@@ -886,28 +886,28 @@ def __init__( self, parent: QW.QWidget, presentation_import_options: Presentatio
tt = 'All files means \'successful\' and \'already in db\'.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'New means only \'successful\'.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'None means this is a silent importer. This is rarely useful.'
- self._presentation_status.setToolTip( tt )
+ self._presentation_status.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._presentation_inbox = ClientGUICommon.BetterChoice( self )
tt = 'Inbox or archive means all files.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Must be in inbox means only inbox files _at the time of the presentation_. This can be neat as you process and revisit currently watched threads.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'Or in inbox (which only shows if you are set to only see new files) allows already in db results if they are currently in the inbox. Essentially you are just excluding already-in-archive files.'
- self._presentation_inbox.setToolTip( tt )
+ self._presentation_inbox.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._presentation_location = ClientGUILocation.LocationSearchContextButton( self, presentation_import_options.GetLocationContext() )
tt = 'This is mostly for technical purposes on hydev\'s end, but if you want, you can filter the presented files based on a location context.'
- self._presentation_location.setToolTip( tt )
+ self._presentation_location.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1058,9 +1058,9 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, service_tag_import_o
else:
message = 'Here you can filter which tags are applied to the files being imported in this context. This typically means those tags on a booru file page beside the file, but other contexts provide tags from different locations and quality.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The namespace checkboxes on the left are compiled from what all your current parsers say they can do and are simply for convenience. It is worth doing some smaller tests with a new download source to make sure you know what it can provide and what you actually want.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Once you are happy, you might want to say \'only "character:", "creator:" and "series:" tags\', or \'everything _except_ "species:" tags\'. This tag filter can get complicated if you want it to--check the help button in the top-right for more information.'
@@ -1131,11 +1131,11 @@ def _EditOnlyAddExistingTagsFilter( self ):
namespaces = CG.client_controller.network_engine.domain_manager.GetParserNamespaces()
message = 'If you do not want the \'only add tags that already exist\' option to apply to all tags coming in, set a filter here for the tags you _want_ to be exposed to this test.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'For instance, if you only want the wash of messy unnamespaced tags to be exposed to the test, then set a simple whitelist for only \'unnamespaced\'.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is obviously a complicated idea, so make sure you test it on a small scale before you try anything big.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Clicking ok on this dialog will automatically turn on the already-exists filter if it is off.'
panel = ClientGUITags.EditTagFilterPanel( dlg, self._only_add_existing_tags_filter, namespaces = namespaces, message = message )
@@ -1249,7 +1249,7 @@ def __init__( self, parent: QW.QWidget, tag_import_options: TagImportOptions.Tag
#
help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp )
- help_button.setToolTip( 'Show help regarding these tag options.' )
+ help_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Show help regarding these tag options.' ) )
#
@@ -1261,12 +1261,12 @@ def __init__( self, parent: QW.QWidget, tag_import_options: TagImportOptions.Tag
self._use_default_dropdown.addItem( 'set custom tag import options just for this importer', False )
tt = 'Normally, the client will refer to the defaults (as set under "network->downloaders->manage default import options") for the appropriate tag import options at the time of import.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'It is easier to work this way, since you can change a single default setting and update all current and future downloaders that refer to those defaults, whereas having specific options for every subscription or downloader means you have to update every single one just to make a little change somewhere.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'But if you are doing a one-time import that has some unusual tag rules, set them here.'
- self._use_default_dropdown.setToolTip( tt )
+ self._use_default_dropdown.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1284,36 +1284,36 @@ def __init__( self, parent: QW.QWidget, tag_import_options: TagImportOptions.Tag
self._fetch_tags_even_if_hash_recognised_and_file_already_in_db = QW.QCheckBox( downloader_options_panel )
tt = 'I strongly recommend you uncheck this for normal use. When it is on, downloaders are inefficent!'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'This will force the client to download the metadata for a file even if it thinks it has visited its page before. Normally, hydrus will skip an URL in this case. It is useful to turn this on if you want to force a recheck of the tags in that page.'
- self._fetch_tags_even_if_url_recognised_and_file_already_in_db.setToolTip( tt )
+ self._fetch_tags_even_if_url_recognised_and_file_already_in_db.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
tt = 'I strongly recommend you uncheck this for normal use. When it is on, downloaders could be inefficent!'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'This will force the client to download the metadata for a file even if the gallery step has given a hash that the client thinks it recognises. Normally, hydrus will skip an URL in this case (although the hash-from-gallery case is rare, so this option rarely matters). This is mostly a debug complement to the url check option.'
- self._fetch_tags_even_if_hash_recognised_and_file_already_in_db.setToolTip( tt )
+ self._fetch_tags_even_if_hash_recognised_and_file_already_in_db.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
tag_blacklist = tag_import_options.GetTagBlacklist()
message = 'If a file about to be downloaded has a tag on the site that this blacklist blocks, the file will not be downloaded and imported. If you want to stop \'scat\' or \'gore\', just type them into the list.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This system tests the all tags that are parsed from the site, not any other tags the files may have in different places. Siblings of all those tags will also be tested. If none of your tag services have excellent siblings, it is worth adding multiple versions of your tag, just to catch different sites terms. Link up \'gore\', \'guro\', \'violence\', etc...'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Additionally, unnamespaced rules will apply to namespaced tags. \'metroid\' in the blacklist will catch \'series:metroid\' as parsed from a site.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It is worth doing a small test here, just to make sure it is all set up how you want.'
self._tag_blacklist_button = ClientGUITags.TagFilterButton( downloader_options_panel, message, tag_blacklist, only_show_blacklist = True )
- self._tag_blacklist_button.setToolTip( 'A blacklist will ignore files if they have any of a certain list of tags.' )
+ self._tag_blacklist_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'A blacklist will ignore files if they have any of a certain list of tags.' ) )
self._tag_whitelist = list( tag_import_options.GetTagWhitelist() )
self._tag_whitelist_button = ClientGUICommon.BetterButton( downloader_options_panel, 'whitelist', self._EditWhitelist )
- self._tag_blacklist_button.setToolTip( 'A whitelist will ignore files if they do not have any of a certain list of tags.' )
+ self._tag_blacklist_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'A whitelist will ignore files if they do not have any of a certain list of tags.' ) )
self._UpdateTagWhitelistLabel()
@@ -1404,7 +1404,7 @@ def __init__( self, parent: QW.QWidget, tag_import_options: TagImportOptions.Tag
def _EditWhitelist( self ):
message = 'If you add tags here, then any file importing with these options must have at least one of these tags from the download source. You can mix it with a blacklist--both will apply in turn.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is usually easier and faster to do just by adding tags to the downloader query (e.g. "artistname desired_tag"), so reserve this for downloaders that do not work on tags or where you want to whitelist multiple tags.'
with ClientGUIDialogs.DialogInputTags( self, CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, list( self._tag_whitelist ), message = message ) as dlg:
@@ -1779,7 +1779,7 @@ def __init__( self, parent, show_downloader_options: bool, allow_default_selecti
action = QW.QAction()
action.setText( 'import options' )
- action.setToolTip( 'edit the different options for this importer' )
+ action.setToolTip( ClientGUIFunctions.WrapToolTip( 'edit the different options for this importer' ) )
action.triggered.connect( self._EditOptions )
@@ -1885,7 +1885,7 @@ def _PasteFileImportOptions( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised File Import Options', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised File Import Options', e )
return
@@ -1926,7 +1926,7 @@ def _PasteNoteImportOptions( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Note Import Options', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Note Import Options', e )
return
@@ -1967,7 +1967,7 @@ def _PasteTagImportOptions( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Tag Import Options', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Tag Import Options', e )
return
@@ -2237,11 +2237,11 @@ def _SetLabelAndToolTip( self ):
my_action.setText( label )
- s = os.linesep * 2
+ s = '\n' * 2
summary = s.join( summaries )
- my_action.setToolTip( summary )
+ my_action.setToolTip( ClientGUIFunctions.WrapToolTip( summary ) )
def _SetFileImportOptions( self, file_import_options: FileImportOptions.FileImportOptions ):
diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py
index 3a2e306e2..4785cd120 100644
--- a/hydrus/client/gui/lists/ClientGUIListBoxes.py
+++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py
@@ -23,6 +23,7 @@
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIShortcuts
@@ -520,7 +521,7 @@ def _ImportFromClipboard( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e )
@@ -616,12 +617,12 @@ def _ImportObject( self, obj, can_present_messages = True ):
if len( bad_object_type_names ) > 0:
message = 'The imported objects included these types:'
- message += os.linesep * 2
- message += os.linesep.join( bad_object_type_names )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( bad_object_type_names )
+ message += '\n' * 2
message += 'Whereas this control only allows:'
- message += os.linesep * 2
- message += os.linesep.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
+ message += '\n' * 2
+ message += '\n'.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
ClientGUIDialogsMessage.ShowWarning( self, message )
@@ -629,8 +630,8 @@ def _ImportObject( self, obj, can_present_messages = True ):
if len( other_bad_errors ) > 0:
message = 'The imported objects were wrong for this control:'
- message += os.linesep * 2
- message += os.linesep.join( other_bad_errors )
+ message += '\n' * 2
+ message += '\n'.join( other_bad_errors )
ClientGUIDialogsMessage.ShowWarning( self, message )
@@ -2555,7 +2556,7 @@ def _ProcessMenuCopyEvent( self, command ):
if len( texts ) > 0:
- text = os.linesep.join( texts )
+ text = '\n'.join( texts )
CG.client_controller.pub( 'clipboard', 'text', text )
diff --git a/hydrus/client/gui/lists/ClientGUIListCtrl.py b/hydrus/client/gui/lists/ClientGUIListCtrl.py
index 3e3c51081..84cbed0d1 100644
--- a/hydrus/client/gui/lists/ClientGUIListCtrl.py
+++ b/hydrus/client/gui/lists/ClientGUIListCtrl.py
@@ -8,6 +8,7 @@
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -16,6 +17,7 @@
from hydrus.client.gui import ClientGUIDragDrop
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIShortcuts
@@ -145,7 +147,7 @@ def __init__( self, parent, column_list_type, height_num_chars, data_to_tuples_f
self.headerItem().setText( i, name )
- self.headerItem().setToolTip( i, name )
+ self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) )
if i == last_column_index:
@@ -237,13 +239,8 @@ def _AddDataInfo( self, data_info ):
text = display_tuple[i]
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- append_item.setText( i, text )
- append_item.setToolTip( i, text )
+ append_item.setText( i, HydrusText.GetFirstLine( text ) )
+ append_item.setToolTip( i, ClientGUIFunctions.WrapToolTip( text ) )
self.addTopLevelItem( append_item )
@@ -422,7 +419,7 @@ def _RefreshHeaderNames( self ):
self.headerItem().setText( i, name_for_title )
- self.headerItem().setToolTip( i, name )
+ self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) )
@@ -523,19 +520,15 @@ def _UpdateRow( self, index, display_tuple ):
for ( column_index, value ) in enumerate( display_tuple ):
- if len( value ) > 0:
-
- value = value.splitlines()[0]
-
-
tree_widget_item = self.topLevelItem( index )
+ first_line = HydrusText.GetFirstLine( value )
existing_value = tree_widget_item.text( column_index )
- if existing_value != value:
+ if existing_value != first_line:
- tree_widget_item.setText( column_index, value )
- tree_widget_item.setToolTip( column_index, value )
+ tree_widget_item.setText( column_index, first_line )
+ tree_widget_item.setToolTip( column_index, ClientGUIFunctions.WrapToolTip( value ) )
@@ -1403,7 +1396,7 @@ def _ImportFromClipboard( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e )
return
@@ -1483,12 +1476,12 @@ def _ImportObject( self, obj, can_present_messages = True ):
if can_present_messages and len( bad_object_type_names ) > 0:
message = 'The imported objects included these types:'
- message += os.linesep * 2
- message += os.linesep.join( bad_object_type_names )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( bad_object_type_names )
+ message += '\n' * 2
message += 'Whereas this control only allows:'
- message += os.linesep * 2
- message += os.linesep.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
+ message += '\n' * 2
+ message += '\n'.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
ClientGUIDialogsMessage.ShowWarning( self, message )
@@ -1541,7 +1534,7 @@ def _ImportJSONs( self, paths ):
if len( paths ) > 1:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If there are more objects in this import with similar load problems, they will now be skipped silently.'
@@ -1596,7 +1589,7 @@ def _ImportPNGs( self, paths ):
if len( paths ) > 1:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If there are more objects in this import with similar load problems, they will now be skipped silently.'
@@ -1637,7 +1630,7 @@ def AddBitmapButton( self, bitmap, clicked_func, tooltip = None, enabled_only_on
if tooltip is not None:
- button.setToolTip( tooltip )
+ button.setToolTip( ClientGUIFunctions.WrapToolTip( tooltip ) )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_only_on_single_selection = enabled_only_on_single_selection, enabled_check_func = enabled_check_func )
@@ -1651,7 +1644,7 @@ def AddButton( self, label, clicked_func, enabled_only_on_selection = False, ena
if tooltip is not None:
- button.setToolTip( tooltip )
+ button.setToolTip( ClientGUIFunctions.WrapToolTip( tooltip ) )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_only_on_single_selection = enabled_only_on_single_selection, enabled_check_func = enabled_check_func )
diff --git a/hydrus/client/gui/ClientGUIMediaControls.py b/hydrus/client/gui/media/ClientGUIMediaControls.py
similarity index 97%
rename from hydrus/client/gui/ClientGUIMediaControls.py
rename to hydrus/client/gui/media/ClientGUIMediaControls.py
index 00271a017..02e7f0dd7 100644
--- a/hydrus/client/gui/ClientGUIMediaControls.py
+++ b/hydrus/client/gui/media/ClientGUIMediaControls.py
@@ -93,7 +93,7 @@ def __init__( self, parent, canvas_type, direction = 'down' ):
self._global_mute = AudioMuteButton( self, AUDIO_GLOBAL )
- self._global_mute.setToolTip( 'Global mute/unmute' )
+ self._global_mute.setToolTip( ClientGUIFunctions.WrapToolTip( 'Global mute/unmute' ) )
self._global_mute.setFocusPolicy( QC.Qt.NoFocus )
vbox = QP.VBoxLayout( margin = 0, spacing = 0 )
@@ -161,7 +161,7 @@ def __init__( self, parent, canvas_type, direction = 'down' ):
self._specific_mute = AudioMuteButton( self, volume_type )
- self._specific_mute.setToolTip( 'Mute/unmute: {}'.format( CC.canvas_type_str_lookup[ self._canvas_type ] ) )
+ self._specific_mute.setToolTip( ClientGUIFunctions.WrapToolTip( 'Mute/unmute: {}'.format( CC.canvas_type_str_lookup[ self._canvas_type ] ) ) )
if CG.client_controller.new_options.GetBoolean( option_to_use ):
diff --git a/hydrus/client/gui/ClientGUIMediaMenus.py b/hydrus/client/gui/media/ClientGUIMediaMenus.py
similarity index 86%
rename from hydrus/client/gui/ClientGUIMediaMenus.py
rename to hydrus/client/gui/media/ClientGUIMediaMenus.py
index c089798c0..361f143b0 100644
--- a/hydrus/client/gui/ClientGUIMediaMenus.py
+++ b/hydrus/client/gui/media/ClientGUIMediaMenus.py
@@ -7,15 +7,15 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusData
-from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client import ClientPaths
-from hydrus.client.gui import ClientGUIMedia
from hydrus.client.gui import ClientGUIMenus
+from hydrus.client.gui.media import ClientGUIMediaModalActions
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
from hydrus.client.media import ClientMedia
from hydrus.client.media import ClientMediaManagers
from hydrus.client.networking import ClientNetworkingFunctions
@@ -105,7 +105,7 @@ def AddDuplicatesMenu( win: QW.QWidget, menu: QW.QMenu, location_context: Client
else:
- ClientGUIMenus.AppendMenuItem( duplicates_menu, 'show the best quality file of this file\'s group', 'Load up a new search with this file\'s best quality duplicate.', ClientGUIMedia.ShowDuplicatesInNewPage, job_location_context, focused_hash, HC.DUPLICATE_KING )
+ ClientGUIMenus.AppendMenuItem( duplicates_menu, 'show the best quality file of this file\'s group', 'Load up a new search with this file\'s best quality duplicate.', ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage, job_location_context, focused_hash, HC.DUPLICATE_KING )
@@ -122,7 +122,7 @@ def AddDuplicatesMenu( win: QW.QWidget, menu: QW.QMenu, location_context: Client
label = 'view {} {}'.format( HydrusData.ToHumanInt( count ), HC.duplicate_type_string_lookup[ duplicate_type ] )
- ClientGUIMenus.AppendMenuItem( duplicates_menu, label, 'Show these duplicates in a new page.', ClientGUIMedia.ShowDuplicatesInNewPage, job_location_context, focused_hash, duplicate_type )
+ ClientGUIMenus.AppendMenuItem( duplicates_menu, label, 'Show these duplicates in a new page.', ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage, job_location_context, focused_hash, duplicate_type )
if duplicate_type == HC.DUPLICATE_MEMBER:
@@ -205,12 +205,12 @@ def AddDuplicatesMenu( win: QW.QWidget, menu: QW.QMenu, location_context: Client
for duplicate_type in ( HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ):
- ClientGUIMenus.AppendMenuItem( duplicates_edit_action_submenu, 'for ' + HC.duplicate_type_string_lookup[duplicate_type], 'Edit what happens when you set this status.', ClientGUIMedia.EditDuplicateContentMergeOptions, win, duplicate_type )
+ ClientGUIMenus.AppendMenuItem( duplicates_edit_action_submenu, 'for ' + HC.duplicate_type_string_lookup[duplicate_type], 'Edit what happens when you set this status.', ClientGUIMediaModalActions.EditDuplicateContentMergeOptions, win, duplicate_type )
if CG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
- ClientGUIMenus.AppendMenuItem( duplicates_edit_action_submenu, 'for ' + HC.duplicate_type_string_lookup[HC.DUPLICATE_ALTERNATE] + ' (advanced!)', 'Edit what happens when you set this status.', ClientGUIMedia.EditDuplicateContentMergeOptions, win, HC.DUPLICATE_ALTERNATE )
+ ClientGUIMenus.AppendMenuItem( duplicates_edit_action_submenu, 'for ' + HC.duplicate_type_string_lookup[HC.DUPLICATE_ALTERNATE] + ' (advanced!)', 'Edit what happens when you set this status.', ClientGUIMediaModalActions.EditDuplicateContentMergeOptions, win, HC.DUPLICATE_ALTERNATE )
ClientGUIMenus.AppendMenu( duplicates_action_submenu, duplicates_edit_action_submenu, 'edit default duplicate metadata merge options' )
@@ -485,9 +485,9 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
label = 'open this file\'s ' + HydrusData.ToHumanInt( len( urls ) ) + ' recognised urls in your web browser'
- ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open these urls in your web browser.', ClientGUIMedia.OpenURLs, urls )
+ ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open these urls in your web browser.', ClientGUIMediaModalActions.OpenURLs, win, urls )
- urls_string = os.linesep.join( urls )
+ urls_string = '\n'.join( urls )
label = 'copy this file\'s ' + HydrusData.ToHumanInt( len( urls ) ) + ' recognised urls to your clipboard'
@@ -500,9 +500,9 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
label = 'open this file\'s ' + HydrusData.ToHumanInt( len( urls ) ) + ' urls in your web browser'
- ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open these urls in your web browser.', ClientGUIMedia.OpenURLs, urls )
+ ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open these urls in your web browser.', ClientGUIMediaModalActions.OpenURLs, win, urls )
- urls_string = os.linesep.join( urls )
+ urls_string = '\n'.join( urls )
label = 'copy this file\'s ' + HydrusData.ToHumanInt( len( urls ) ) + ' urls to your clipboard'
@@ -529,11 +529,11 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
label = 'open files\' ' + url_class.GetName() + ' urls in your web browser'
- ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open this url class in your web browser for all files.', ClientGUIMedia.OpenMediaURLClassURLs, selected_media, url_class )
+ ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open this url class in your web browser for all files.', ClientGUIMediaModalActions.OpenMediaURLClassURLs, win, selected_media, url_class )
label = 'copy files\' ' + url_class.GetName() + ' urls'
- ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy this url class for all files.', ClientGUIMedia.CopyMediaURLClassURLs, selected_media, url_class )
+ ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy this url class for all files.', ClientGUIMediaSimpleActions.CopyMediaURLClassURLs, selected_media, url_class )
@@ -543,11 +543,11 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
label = 'open all files\' urls'
- ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open urls in your web browser for all files.', ClientGUIMedia.OpenMediaURLs, selected_media )
+ ClientGUIMenus.AppendMenuItem( urls_visit_menu, label, 'Open urls in your web browser for all files.', ClientGUIMediaModalActions.OpenMediaURLs, win, selected_media )
label = 'copy all files\' urls'
- ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy urls for all files.', ClientGUIMedia.CopyMediaURLs, selected_media )
+ ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy urls for all files.', ClientGUIMediaSimpleActions.CopyMediaURLs, selected_media )
#
@@ -566,8 +566,6 @@ def AddLocalFilesMoveAddToMenu( win: QW.QWidget, menu: QW.QMenu, local_duplicabl
return
- local_action_menu = ClientGUIMenus.GenerateMenu( menu )
-
if len( local_duplicable_to_file_service_keys ) > 0:
menu_tuples = []
@@ -595,7 +593,7 @@ def AddLocalFilesMoveAddToMenu( win: QW.QWidget, menu: QW.QMenu, local_duplicabl
submenu_name = 'add to'
- ClientGUIMenus.AppendMenuOrItem( local_action_menu, submenu_name, menu_tuples )
+ ClientGUIMenus.AppendMenuOrItem( menu, submenu_name, menu_tuples )
if len( local_moveable_from_and_to_file_service_keys ) > 0:
@@ -625,11 +623,9 @@ def AddLocalFilesMoveAddToMenu( win: QW.QWidget, menu: QW.QMenu, local_duplicabl
submenu_name = 'move'
- ClientGUIMenus.AppendMenuOrItem( local_action_menu, submenu_name, menu_tuples )
+ ClientGUIMenus.AppendMenuOrItem( menu, submenu_name, menu_tuples )
- ClientGUIMenus.AppendMenu( menu, local_action_menu, 'local services' )
-
def AddManageFileViewingStatsMenu( win: QW.QWidget, menu: QW.QMenu, flat_medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
@@ -637,11 +633,67 @@ def AddManageFileViewingStatsMenu( win: QW.QWidget, menu: QW.QMenu, flat_medias:
submenu = ClientGUIMenus.GenerateMenu( menu )
- ClientGUIMenus.AppendMenuItem( submenu, 'clear', 'Clear all the recorded file viewing stats for the selected files.', ClientGUIMedia.DoClearFileViewingStats, win, flat_medias )
+ ClientGUIMenus.AppendMenuItem( submenu, 'clear', 'Clear all the recorded file viewing stats for the selected files.', ClientGUIMediaModalActions.DoClearFileViewingStats, win, flat_medias )
ClientGUIMenus.AppendMenu( menu, submenu, 'viewing stats' )
+def AddOpenMenu( win: QW.QWidget, menu: QW.QMenu, focused_media: typing.Optional[ ClientMedia.Media ], selected_media: typing.Collection[ ClientMedia.Media ] ):
+
+ if len( selected_media ) == 0:
+
+ return
+
+
+ open_menu = ClientGUIMenus.GenerateMenu( menu )
+
+ ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Copy your current selection into a simple new page.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE ) )
+ ClientGUIMenus.AppendMenuItem( open_menu, 'in a new duplicate filter page', 'Make a new duplicate filter page that searches for these files specifically.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE ) )
+
+ similar_menu = ClientGUIMenus.GenerateMenu( open_menu )
+
+ if focused_media is not None:
+
+ if focused_media.HasStaticImages():
+
+ ClientGUIMenus.AppendSeparator( similar_menu )
+
+ ClientGUIMenus.AppendMenuItem( similar_menu, 'exact match', 'Search the database for files that look precisely like those selected.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES, simple_data = CC.HAMMING_EXACT_MATCH ) )
+ ClientGUIMenus.AppendMenuItem( similar_menu, 'very similar', 'Search the database for files that look just like those selected.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES, simple_data = CC.HAMMING_VERY_SIMILAR ) )
+ ClientGUIMenus.AppendMenuItem( similar_menu, 'similar', 'Search the database for files that look generally like those selected.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES, simple_data = CC.HAMMING_SIMILAR ) )
+ ClientGUIMenus.AppendMenuItem( similar_menu, 'speculative', 'Search the database for files that probably look like those selected. This is sometimes useful for symbols with sharp edges or lines.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES, simple_data = CC.HAMMING_SPECULATIVE ) )
+
+ ClientGUIMenus.AppendMenu( open_menu, similar_menu, 'similar files in a new page' )
+
+
+ ClientGUIMenus.AppendSeparator( open_menu )
+
+ if len( selected_media ) > 1:
+
+ prefix = 'focused file '
+
+ else:
+
+ prefix = ''
+
+
+ ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}in external program', 'Launch this file with your OS\'s default program for it.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM ) )
+ ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}in web browser', 'Show this file in your OS\'s web browser.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER ) )
+
+ if focused_media.GetLocationsManager().IsLocal():
+
+ show_open_in_explorer = CG.client_controller.new_options.GetBoolean( 'advanced_mode' ) and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
+
+ if show_open_in_explorer:
+
+ ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}in file browser', 'Show this file in your OS\'s file browser.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER ) )
+
+
+
+
+ ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
+
+
def AddPrettyInfoLines( menu, pretty_info_lines ):
def add_pretty_info_str( m, line ):
diff --git a/hydrus/client/gui/ClientGUIMediaActions.py b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
similarity index 76%
rename from hydrus/client/gui/ClientGUIMediaActions.py
rename to hydrus/client/gui/media/ClientGUIMediaModalActions.py
index 25da4b39b..22341cfdc 100644
--- a/hydrus/client/gui/ClientGUIMediaActions.py
+++ b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
@@ -1,5 +1,6 @@
import collections
-import itertools
+import os
+import time
import typing
from qtpy import QtWidgets as QW
@@ -7,7 +8,6 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusLists
from hydrus.core import HydrusThreading
from hydrus.core import HydrusTime
@@ -18,6 +18,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientFiles
+from hydrus.client import ClientPaths
from hydrus.client import ClientPDFHandling
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIAsync
@@ -26,11 +27,12 @@
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIScrolledPanelsReview
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
-def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.ApplicationCommand, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
+def ApplyContentApplicationCommandToMedia( win: QW.QWidget, command: CAC.ApplicationCommand, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
if not command.IsContentCommand():
@@ -65,7 +67,7 @@ def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.Appl
source_service_key = None
- MoveOrDuplicateLocalFiles( parent, service_key, action, media, source_service_key = source_service_key )
+ MoveOrDuplicateLocalFiles( win, service_key, action, media, source_service_key = source_service_key )
else:
@@ -75,7 +77,7 @@ def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.Appl
tag = value
- content_updates = GetContentUpdatesForAppliedContentApplicationCommandTags( parent, service_key, service_type, action, media, tag )
+ content_updates = GetContentUpdatesForAppliedContentApplicationCommandTags( win, service_key, service_type, action, media, tag )
elif service_type in HC.RATINGS_SERVICES:
@@ -150,6 +152,216 @@ def ClearDeleteRecord( win, media ):
+
+def CopyHashesToClipboard( win: QW.QWidget, hash_type: str, medias: typing.Sequence[ ClientMedia.Media ] ):
+
+ hex_it = True
+
+ desired_hashes = []
+
+ flat_media = ClientMedia.FlattenMedia( medias )
+
+ sha256_hashes = [ media.GetHash() for media in flat_media ]
+
+ if hash_type in ( 'pixel_hash', 'blurhash' ):
+
+ file_info_managers = [ media.GetFileInfoManager() for media in flat_media ]
+
+ if hash_type == 'pixel_hash':
+
+ desired_hashes = [ fim.pixel_hash for fim in file_info_managers if fim.pixel_hash is not None ]
+
+ elif hash_type == 'blurhash':
+
+ desired_hashes = [ fim.blurhash for fim in file_info_managers if fim.blurhash is not None ]
+
+ hex_it = False
+
+
+ elif hash_type == 'sha256':
+
+ desired_hashes = sha256_hashes
+
+ else:
+
+ num_hashes = len( sha256_hashes )
+ num_remote_medias = len( [ not media.GetLocationsManager().IsLocal() for media in flat_media ] )
+
+ source_to_desired = CG.client_controller.Read( 'file_hashes', sha256_hashes, 'sha256', hash_type )
+
+ desired_hashes = [ source_to_desired[ source_hash ] for source_hash in sha256_hashes if source_hash in source_to_desired ]
+
+ num_missing = num_hashes - len( desired_hashes )
+
+ if num_missing > 0:
+
+ if num_missing == num_hashes:
+
+ message = 'Unfortunately, none of the {} hashes could be found.'.format( hash_type )
+
+ else:
+
+ message = 'Unfortunately, {} of the {} hashes could not be found.'.format( HydrusData.ToHumanInt( num_missing ), hash_type )
+
+
+ if num_remote_medias > 0:
+
+ message += ' {} of the files you wanted are not currently in this client. If they have never visited this client, the lookup is impossible.'.format( HydrusData.ToHumanInt( num_remote_medias ) )
+
+
+ if num_remote_medias < num_hashes:
+
+ message += ' It could be that some of the local files are currently missing this information in the hydrus database. A file maintenance job (under the database menu) can repopulate this data.'
+
+
+ ClientGUIDialogsMessage.ShowWarning( win, message )
+
+
+
+ if len( desired_hashes ) > 0:
+
+ if hex_it:
+
+ text_lines = [ desired_hash.hex() for desired_hash in desired_hashes ]
+
+ else:
+
+ text_lines = desired_hashes
+
+
+ if CG.client_controller.new_options.GetBoolean( 'prefix_hash_when_copying' ):
+
+ text_lines = [ '{}:{}'.format( hash_type, hex_hash ) for hex_hash in text_lines ]
+
+
+ hex_hashes_text = '\n'.join( text_lines )
+
+ CG.client_controller.pub( 'clipboard', 'text', hex_hashes_text )
+
+ job_status = ClientThreading.JobStatus()
+
+ job_status.SetStatusText( '{} {} hashes copied'.format( HydrusData.ToHumanInt( len( desired_hashes ) ), hash_type ) )
+
+ CG.client_controller.pub( 'message', job_status )
+
+ job_status.FinishAndDismiss( 2 )
+
+
+
+def DoClearFileViewingStats( win: QW.QWidget, flat_medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
+
+ if len( flat_medias ) == 0:
+
+ return
+
+
+ if len( flat_medias ) == 1:
+
+ insert = 'this file'
+
+ else:
+
+ insert = 'these {} files'.format( HydrusData.ToHumanInt( len( flat_medias ) ) )
+
+
+ message = 'Clear the file viewing count/duration and \'last viewed time\' for {}?'.format( insert )
+
+ result = ClientGUIDialogsQuick.GetYesNo( win, message )
+
+ if result == QW.QDialog.Accepted:
+
+ hashes = { m.GetHash() for m in flat_medias }
+
+ content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILE_VIEWING_STATS, HC.CONTENT_UPDATE_DELETE, hashes )
+
+ CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update ) )
+
+
+
+def DoOpenKnownURLFromShortcut( win, media ):
+
+ urls = media.GetLocationsManager().GetURLs()
+
+ matched_labels_and_urls = []
+ unmatched_urls = []
+
+ if len( urls ) > 0:
+
+ for url in urls:
+
+ try:
+
+ url_class = CG.client_controller.network_engine.domain_manager.GetURLClass( url )
+
+ except HydrusExceptions.URLClassException:
+
+ continue
+
+
+ if url_class is None:
+
+ unmatched_urls.append( url )
+
+ else:
+
+ label = url_class.GetName() + ': ' + url
+
+ matched_labels_and_urls.append( ( label, url ) )
+
+
+
+ matched_labels_and_urls.sort()
+ unmatched_urls.sort()
+
+
+ if len( matched_labels_and_urls ) == 0:
+
+ return
+
+ elif len( matched_labels_and_urls ) == 1:
+
+ url = matched_labels_and_urls[0][1]
+
+ else:
+
+ matched_labels_and_urls.extend( ( url, url ) for url in unmatched_urls )
+
+ try:
+
+ url = ClientGUIDialogsQuick.SelectFromList( win, 'Select which URL', matched_labels_and_urls, sort_tuples = False )
+
+ except HydrusExceptions.CancelledException:
+
+ return
+
+
+
+ ClientPaths.LaunchURLInWebBrowser( url )
+
+
+# this isn't really a 'media' guy, and it edits the options in place, so maybe move/edit/whatever!
+def EditDuplicateContentMergeOptions( win: QW.QWidget, duplicate_type: int ):
+
+ new_options = CG.client_controller.new_options
+
+ duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type )
+
+ with ClientGUITopLevelWindowsPanels.DialogEdit( win, 'edit duplicate merge options' ) as dlg:
+
+ panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options )
+
+ dlg.SetPanel( panel )
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ duplicate_content_merge_options = panel.GetValue()
+
+ new_options.SetDuplicateContentMergeOptions( duplicate_type, duplicate_content_merge_options )
+
+
+
+
+
def EditFileNotes( win: QW.QWidget, media: ClientMedia.MediaSingleton, name_to_start_on = typing.Optional[ str ] ):
names_to_notes = media.GetNotesManager().GetNamesToNotes()
@@ -375,7 +587,7 @@ def GetContentUpdatesForAppliedContentApplicationCommandRatingsNumericalIncDec(
return content_updates
-def GetContentUpdatesForAppliedContentApplicationCommandTags( parent: QW.QWidget, service_key: bytes, service_type: int, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], tag: str ):
+def GetContentUpdatesForAppliedContentApplicationCommandTags( win: QW.QWidget, service_key: bytes, service_type: int, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], tag: str ):
hashes = set()
@@ -469,7 +681,7 @@ def GetContentUpdatesForAppliedContentApplicationCommandTags( parent: QW.QWidget
from hydrus.client.gui import ClientGUIDialogs
- with ClientGUIDialogs.DialogTextEntry( parent, message ) as dlg:
+ with ClientGUIDialogs.DialogTextEntry( win, message ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
@@ -493,40 +705,6 @@ def GetContentUpdatesForAppliedContentApplicationCommandTags( parent: QW.QWidget
return content_updates
-def GetLocalFileActionServiceKeys( media: typing.Collection[ ClientMedia.MediaSingleton ] ):
-
- local_media_file_service_keys = set( CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )
-
- local_duplicable_to_file_service_keys = set()
- local_moveable_from_and_to_file_service_keys = set()
-
- for m in media:
-
- locations_manager = m.GetLocationsManager()
-
- current = locations_manager.GetCurrent()
-
- if locations_manager.IsLocal():
-
- can_send_to = local_media_file_service_keys.difference( current )
- can_send_from = local_media_file_service_keys.intersection( current )
-
- if len( can_send_to ) > 0:
-
- local_duplicable_to_file_service_keys.update( can_send_to )
-
- if len( can_send_from ) > 0:
-
- # can_send_from does not include trash. we won't say 'move from trash to blah' since that's a little complex. we'll just say 'add to blah' in that case I think
-
- local_moveable_from_and_to_file_service_keys.update( list( itertools.product( can_send_from, can_send_to ) ) )
-
-
-
-
-
- return ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys )
-
def MoveOrDuplicateLocalFiles( win: QW.QWidget, dest_service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], source_service_key: typing.Optional[ bytes ] = None ):
@@ -539,7 +717,7 @@ def MoveOrDuplicateLocalFiles( win: QW.QWidget, dest_service_key: bytes, action:
return
- ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = GetLocalFileActionServiceKeys( media )
+ ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( media )
do_yes_no = do_yes_no = CG.client_controller.new_options.GetBoolean( 'confirm_multiple_local_file_services_copy' )
yes_no_text = 'Add {} files to {}?'.format( HydrusData.ToHumanInt( len( applicable_media ) ), dest_service_name )
@@ -707,6 +885,111 @@ def publish_callable( result ):
job.start()
+def OpenURLs( win: QW.QWidget, urls ):
+
+ urls = sorted( urls )
+
+ if len( urls ) > 1:
+
+ message = 'Open the {} URLs in your web browser?'.format( len( urls ) )
+
+ if len( urls ) > 10:
+
+ message += ' This will take some time.'
+
+
+ result = ClientGUIDialogsQuick.GetYesNo( win, message )
+
+ if result != QW.QDialog.Accepted:
+
+ return
+
+
+
+ def do_it( urls ):
+
+ job_status = None
+
+ num_urls = len( urls )
+
+ if num_urls > 5:
+
+ job_status = ClientThreading.JobStatus( pausable = True, cancellable = True )
+
+ job_status.SetStatusTitle( 'Opening URLs' )
+
+ CG.client_controller.pub( 'message', job_status )
+
+
+ try:
+
+ for ( i, url ) in enumerate( urls ):
+
+ if job_status is not None:
+
+ ( i_paused, should_quit ) = job_status.WaitIfNeeded()
+
+ if should_quit:
+
+ return
+
+
+ job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( i + 1, num_urls ) )
+ job_status.SetVariable( 'popup_gauge_1', ( i + 1, num_urls ) )
+
+
+ ClientPaths.LaunchURLInWebBrowser( url )
+
+ time.sleep( 1 )
+
+
+ finally:
+
+ if job_status is not None:
+
+ job_status.FinishAndDismiss( 1 )
+
+
+
+
+ CG.client_controller.CallToThread( do_it, urls )
+
+
+def OpenMediaURLs( win: QW.QWidget, medias ):
+
+ urls = set()
+
+ for media in medias:
+
+ media_urls = media.GetLocationsManager().GetURLs()
+
+ urls.update( media_urls )
+
+
+ OpenURLs( win, urls )
+
+
+def OpenMediaURLClassURLs( win: QW.QWidget, medias, url_class ):
+
+ urls = set()
+
+ for media in medias:
+
+ media_urls = media.GetLocationsManager().GetURLs()
+
+ for url in media_urls:
+
+ # can't do 'url_class.matches', as it will match too many
+ if CG.client_controller.network_engine.domain_manager.GetURLClass( url ) == url_class:
+
+ urls.add( url )
+
+
+
+
+ OpenURLs( win, urls )
+
+
def SetFilesForcedFiletypes( win: QW.QWidget, medias: typing.Collection[ ClientMedia.Media ] ):
# boot a panel, it shows the user what current mimes are, what forced mimes are, and they have the choice to set all to x
@@ -871,44 +1154,6 @@ def ShowFileEmbeddedMetadata( win: QW.QWidget, media: ClientMedia.MediaSingleton
frame.SetPanel( panel )
-def UndeleteFiles( hashes ):
-
- local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
-
- for chunk_of_hashes in HydrusData.SplitIteratorIntoChunks( hashes, 64 ):
-
- media_results = CG.client_controller.Read( 'media_results', chunk_of_hashes )
-
- service_keys_to_hashes = collections.defaultdict( list )
-
- for media_result in media_results:
-
- locations_manager = media_result.GetLocationsManager()
-
- if CC.TRASH_SERVICE_KEY not in locations_manager.GetCurrent():
-
- continue
-
-
- hash = media_result.GetHash()
-
- for service_key in locations_manager.GetDeleted().intersection( local_file_service_keys ):
-
- service_keys_to_hashes[ service_key ].append( hash )
-
-
-
- for ( service_key, service_hashes ) in service_keys_to_hashes.items():
-
- content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, service_hashes )
-
- content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( service_key, content_update )
-
- CG.client_controller.WriteSynchronous( 'content_updates', content_update_package )
-
-
-
-
def UndeleteMedia( win, media ):
undeletable_media = [ m for m in media if m.GetLocationsManager().IsLocal() ]
diff --git a/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py b/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py
new file mode 100644
index 000000000..5533734cf
--- /dev/null
+++ b/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py
@@ -0,0 +1,265 @@
+import collections
+import itertools
+import os
+import typing
+
+from hydrus.core import HydrusConstants as HC
+from hydrus.core import HydrusPaths
+from hydrus.core import HydrusData
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
+from hydrus.client import ClientLocation
+from hydrus.client import ClientPaths
+from hydrus.client.media import ClientMedia
+from hydrus.client.metadata import ClientContentUpdates
+from hydrus.client.search import ClientSearch
+
+def CopyMediaURLs( medias ):
+
+ urls = set()
+
+ for media in medias:
+
+ media_urls = media.GetLocationsManager().GetURLs()
+
+ urls.update( media_urls )
+
+
+ urls = sorted( urls )
+
+ urls_string = '\n'.join( urls )
+
+ CG.client_controller.pub( 'clipboard', 'text', urls_string )
+
+
+def CopyMediaURLClassURLs( medias, url_class ):
+
+ urls = set()
+
+ for media in medias:
+
+ media_urls = media.GetLocationsManager().GetURLs()
+
+ for url in media_urls:
+
+ # can't do 'url_class.matches', as it will match too many
+ if CG.client_controller.network_engine.domain_manager.GetURLClass( url ) == url_class:
+
+ urls.add( url )
+
+
+
+
+ urls = sorted( urls )
+
+ urls_string = '\n'.join( urls )
+
+ CG.client_controller.pub( 'clipboard', 'text', urls_string )
+
+
+def GetLocalFileActionServiceKeys( media: typing.Collection[ ClientMedia.MediaSingleton ] ):
+
+ local_media_file_service_keys = set( CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )
+
+ local_duplicable_to_file_service_keys = set()
+ local_moveable_from_and_to_file_service_keys = set()
+
+ for m in media:
+
+ locations_manager = m.GetLocationsManager()
+
+ current = locations_manager.GetCurrent()
+
+ if locations_manager.IsLocal():
+
+ can_send_to = local_media_file_service_keys.difference( current )
+ can_send_from = local_media_file_service_keys.intersection( current )
+
+ if len( can_send_to ) > 0:
+
+ local_duplicable_to_file_service_keys.update( can_send_to )
+
+ if len( can_send_from ) > 0:
+
+ # can_send_from does not include trash. we won't say 'move from trash to blah' since that's a little complex. we'll just say 'add to blah' in that case I think
+
+ local_moveable_from_and_to_file_service_keys.update( list( itertools.product( can_send_from, can_send_to ) ) )
+
+
+
+
+
+ return ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys )
+
+
+def OpenExternally( media: typing.Optional[ ClientMedia.MediaSingleton ] ) -> bool:
+
+ if media is None:
+
+ return False
+
+
+ if not media.GetLocationsManager().IsLocal():
+
+ return False
+
+
+ hash = media.GetHash()
+ mime = media.GetMime()
+
+ path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
+
+ launch_path = CG.client_controller.new_options.GetMimeLaunch( mime )
+
+ HydrusPaths.LaunchFile( path, launch_path )
+
+ return True
+
+
+def OpenFileLocation( media: typing.Optional[ ClientMedia.MediaSingleton ] ) -> bool:
+
+ if media is None:
+
+ return False
+
+
+ if not media.GetLocationsManager().IsLocal():
+
+ return False
+
+
+ hash = media.GetHash()
+ mime = media.GetMime()
+
+ path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
+
+ HydrusPaths.OpenFileLocation( path )
+
+ return True
+
+
+def OpenInWebBrowser( media: typing.Optional[ ClientMedia.MediaSingleton ] ) -> bool:
+
+ if media is None:
+
+ return False
+
+
+ if not media.GetLocationsManager().IsLocal():
+
+ return False
+
+
+ hash = media.GetHash()
+ mime = media.GetMime()
+
+ path = CG.client_controller.client_files_manager.GetFilePath( hash, mime )
+
+ ClientPaths.LaunchPathInWebBrowser( path )
+
+ return True
+
+
+def ShowDuplicatesInNewPage( location_context: ClientLocation.LocationContext, hash, duplicate_type ):
+
+ # TODO: this can be replaced by a call to the MediaResult when it holds these hashes
+ # don't forget to return itself in position 0!
+ hashes = CG.client_controller.Read( 'file_duplicate_hashes', location_context, hash, duplicate_type )
+
+ if hashes is not None and len( hashes ) > 1:
+
+ CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes )
+
+ else:
+
+ location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_FILE_SERVICE_KEY )
+
+ hashes = CG.client_controller.Read( 'file_duplicate_hashes', location_context, hash, duplicate_type )
+
+ if hashes is not None and len( hashes ) > 1:
+
+ HydrusData.ShowText( 'Could not find the members of this group in this location, so searched all known files and found more.' )
+
+ CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes )
+
+ else:
+
+ HydrusData.ShowText( 'Sorry, could not find the members of this group either at the given location or in all known files. There may be a problem here, so let hydev know.' )
+
+
+
+
+def ShowFilesInNewDuplicatesFilterPage( hashes: typing.Collection[ bytes ], location_context: ClientLocation.LocationContext ):
+
+ activate_window = CG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
+
+ predicates = [ ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, value = ( tuple( hashes ), 'sha256' ) ) ]
+
+ page_name = 'duplicates'
+
+ CG.client_controller.pub( 'new_page_duplicates', location_context, initial_predicates = predicates, page_name = page_name, activate_window = activate_window )
+
+
+def ShowFilesInNewPage( hashes: typing.Collection[ bytes ], location_context: ClientLocation.LocationContext, media_sort = None, media_collect = None ):
+
+ CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes, initial_sort = media_sort, initial_collect = media_collect )
+
+
+def ShowSimilarFilesInNewPage( media: typing.Collection[ ClientMedia.MediaSingleton ], location_context: ClientLocation.LocationContext, max_hamming: int ):
+
+ hashes = set()
+
+ for m in media:
+
+ if m.GetMime() in HC.FILES_THAT_HAVE_PERCEPTUAL_HASH:
+
+ hashes.add( m.GetHash() )
+
+
+
+ if len( hashes ) > 0:
+
+ initial_predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO_FILES, ( tuple( hashes ), max_hamming ) ) ]
+
+ CG.client_controller.pub( 'new_page_query', location_context, initial_predicates = initial_predicates )
+
+
+
+def UndeleteFiles( hashes ):
+
+ local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
+
+ for chunk_of_hashes in HydrusData.SplitIteratorIntoChunks( hashes, 64 ):
+
+ media_results = CG.client_controller.Read( 'media_results', chunk_of_hashes )
+
+ service_keys_to_hashes = collections.defaultdict( list )
+
+ for media_result in media_results:
+
+ locations_manager = media_result.GetLocationsManager()
+
+ if CC.TRASH_SERVICE_KEY not in locations_manager.GetCurrent():
+
+ continue
+
+
+ hash = media_result.GetHash()
+
+ for service_key in locations_manager.GetDeleted().intersection( local_file_service_keys ):
+
+ service_keys_to_hashes[ service_key ].append( hash )
+
+
+
+ for ( service_key, service_hashes ) in service_keys_to_hashes.items():
+
+ content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, service_hashes )
+
+ content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( service_key, content_update )
+
+ CG.client_controller.WriteSynchronous( 'content_updates', content_update_package )
+
+
+
diff --git a/hydrus/client/gui/canvas/ClientGUIMediaVolume.py b/hydrus/client/gui/media/ClientGUIMediaVolume.py
similarity index 96%
rename from hydrus/client/gui/canvas/ClientGUIMediaVolume.py
rename to hydrus/client/gui/media/ClientGUIMediaVolume.py
index 251cc5b39..63fb1737b 100644
--- a/hydrus/client/gui/canvas/ClientGUIMediaVolume.py
+++ b/hydrus/client/gui/media/ClientGUIMediaVolume.py
@@ -1,6 +1,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
-from hydrus.client.gui import ClientGUIMediaControls
+
+from hydrus.client.gui.media import ClientGUIMediaControls
def GetCorrectCurrentMute( canvas_type: int ):
diff --git a/hydrus/client/gui/media/__init__.py b/hydrus/client/gui/media/__init__.py
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/hydrus/client/gui/media/__init__.py
@@ -0,0 +1 @@
+
diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
index 2031fbf24..0f8f8264d 100644
--- a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
+++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
@@ -272,7 +272,7 @@ def _RefreshLabel( self ):
elided_text = HydrusText.ElideText( text, 64 )
self.setText( elided_text )
- self.setToolTip( text )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( text ) )
def GetValue( self ):
diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py
index 7d26bd384..2b715ac53 100644
--- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py
+++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py
@@ -2,6 +2,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientStrings
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.metadata import ClientMetadataMigrationCore
@@ -16,11 +17,11 @@ def __init__( self, parent: QW.QWidget ):
self._remove_actual_filename_ext = QW.QCheckBox( self )
tt = 'If you set this, the actual filename\'s extension will not be used in the sidecar. For a txt sidecar, \'my_image.jpg\' will be matched with \'my_image.txt\'.'
- self._remove_actual_filename_ext.setToolTip( tt )
+ self._remove_actual_filename_ext.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._suffix = QW.QLineEdit( self )
tt = 'If you set this, the sidecar will include this extra suffix. For a txt sidecar, \'my_image.jpg\' will be matched with \'my_image.jpg.tags.txt\'.'
- self._suffix.setToolTip( tt )
+ self._suffix.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
string_converter = ClientStrings.StringConverter()
@@ -146,7 +147,7 @@ def __init__( self, parent: QW.QWidget ):
tt = 'You can separate the "rows" of tags by something other than newlines if you like. If you are parsing multiple multi-line notes, try separating them by four pipes, ||||. If you have/want a CSV list, try a separator of "," or ", ".'
- self._choice.setToolTip( tt )
+ self._choice.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._custom_input = QW.QLineEdit( self )
diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py
index 2d2c311bb..7be5430d6 100644
--- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py
+++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py
@@ -11,6 +11,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientParsing
from hydrus.client.gui import ClientGUIDialogs
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIScrolledPanels
@@ -83,7 +84,7 @@ def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporte
self._forced_note_name = ClientGUICommon.NoneableTextCtrl( self._forced_note_name_panel, message = 'Forced Note Name: ', none_phrase = 'use "name: text" format' )
tt = 'Normally, the sidecar exporter is at this stage expecting notes in the format "name: text". If you only have the text, you can force the name here. This is only useful if you are parsing one note through here, or you will get all sorts of renaming conflicts.'
- self._forced_note_name.setToolTip( tt )
+ self._forced_note_name.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._forced_note_name.SetValue( None )
@@ -105,7 +106,7 @@ def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporte
self._nested_object_names_list = ClientGUIListBoxes.QueueListBox( self, 4, str, self._AddObjectName, self._EditObjectName )
tt = 'If you leave this empty, the strings will be exported as a simple list. If you set it as [files,tags], the exported string list will be placed under nested objects with keys "files"->"tags". Note that this will also update an existing file, so, if you are feeling clever, you can have multiple routers writing tags and URLs to different destinations in the same file!'
- self._nested_object_names_list.setToolTip( tt )
+ self._nested_object_names_list.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
vbox = QP.VBoxLayout()
@@ -413,9 +414,9 @@ def _SetValue( self, exporter: ClientMetadataMigrationExporters.SingleFileMetada
def _ShowSidecarHelp( self ):
message = 'Sidecars are typically named just as their associated file but with the additional extension. \'image.jpg\' makes \'image.jpg.txt\', and so on.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Sidecar exporters will overwrite whatever is at their set destination, so be careful if you intend to set up multiple simultaneous exports, or the second will overwrite the first. You can safely export to two or more different locations in the same .json file, but if you export to .txt, use the \'suffix\' control to export to different files.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If there is no content to write, no new file will be created.'
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -485,7 +486,7 @@ def _RefreshLabel( self ):
elided_text = HydrusText.ElideText( text, 64 )
self.setText( elided_text )
- self.setToolTip( text )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( text ) )
def GetValue( self ):
diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py
index ba312b492..67302a559 100644
--- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py
+++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py
@@ -12,6 +12,7 @@
from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import ClientGUITime
@@ -92,7 +93,7 @@ def __init__( self, parent: QW.QWidget, importer: ClientMetadataMigrationImporte
tt += '\n'
tt += '"display" = with sibling replacements and implied parents (what you see in normal views, good for export to other programs)'
- self._tag_display_type_button.setToolTip( tt )
+ self._tag_display_type_button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
rows = []
@@ -135,7 +136,7 @@ def __init__( self, parent: QW.QWidget, importer: ClientMetadataMigrationImporte
self._string_processor_button = ClientGUIStringControls.StringProcessorButton( self, string_processor, self._GetExampleTestData )
tt = 'You can alter the texts that come in through this source here.'
- self._string_processor_button.setToolTip( tt )
+ self._string_processor_button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
vbox = QP.VBoxLayout()
diff --git a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
index c95d31fe7..5a6cbe68f 100644
--- a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
+++ b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
@@ -1,4 +1,3 @@
-import os
import typing
from qtpy import QtCore as QC
@@ -7,7 +6,6 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTime
from hydrus.core.networking import HydrusNetwork
from hydrus.core.networking import HydrusNetworking
@@ -25,8 +23,8 @@
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.widgets import ClientGUIBandwidth
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
class EditAccountTypePanel( ClientGUIScrolledPanels.EditPanel ):
@@ -88,7 +86,7 @@ def __init__( self, parent: QW.QWidget, service_type: int, account_type: HydrusN
gridbox = ClientGUICommon.WrapInGrid( self._permissions_panel, gridbox_rows )
- self._bandwidth_rules_control = ClientGUIControls.BandwidthRulesCtrl( self, bandwidth_rules )
+ self._bandwidth_rules_control = ClientGUIBandwidth.BandwidthRulesCtrl( self, bandwidth_rules )
self._auto_creation_box = ClientGUICommon.StaticBox( self, 'automatic account creation' )
@@ -109,7 +107,7 @@ def __init__( self, parent: QW.QWidget, service_type: int, account_type: HydrusN
#
intro = 'If you wish, you can allow new users to create their own accounts. They will be limited to a certain number over a particular time.'
- intro += os.linesep * 2
+ intro += '\n' * 2
intro += 'Set to 0 to disable auto-creation.'
st = ClientGUICommon.BetterStaticText( self._auto_creation_box, label = intro )
@@ -587,7 +585,7 @@ def _AccountClicked( self ):
keys_in_order = sorted( account_info.keys() )
- account_info_components.append( os.linesep.join( ( '{}: {}'.format( key, account_info[ key ] ) for key in keys_in_order ) ) )
+ account_info_components.append( '\n'.join( ( '{}: {}'.format( key, account_info[ key ] ) for key in keys_in_order ) ) )
else:
@@ -604,7 +602,7 @@ def _AccountClicked( self ):
account_info_components.append( 'Could not find this account!' )
- joiner = os.linesep * 2
+ joiner = '\n' * 2
account_info = joiner.join( account_info_components )
@@ -622,7 +620,7 @@ def _CopyCheckedAccountKeys( self ):
if len( checked_account_keys ) > 0:
- account_keys_text = os.linesep.join( ( account_key.hex() for account_key in checked_account_keys ) )
+ account_keys_text = '\n'.join( ( account_key.hex() for account_key in checked_account_keys ) )
CG.client_controller.pub( 'clipboard', 'text', account_keys_text )
@@ -700,7 +698,7 @@ def publish_callable( result ):
account_errors = sorted( account_errors )
- ClientGUIDialogsMessage.ShowInformation( self, 'Errors were encountered during account fetch:{}{}'.format( os.linesep * 2, os.linesep.join( account_errors ) ) )
+ ClientGUIDialogsMessage.ShowInformation( self, 'Errors were encountered during account fetch:{}{}'.format( '\n' * 2, '\n'.join( account_errors ) ) )
if not self._done_first_fetch:
@@ -1233,7 +1231,7 @@ def _DoDeleteAllAccountContent( self ):
if self._service.GetServiceType() == HC.TAG_REPOSITORY:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Note that if the user never had permission to add siblings and parents on their own (i.e. they could only ever _petition_ to add them), then their petitioned siblings and parents will not be deleted (janitor accounts take ownership of siblings and parents when they approve them).'
diff --git a/hydrus/client/gui/networking/ClientGUINetwork.py b/hydrus/client/gui/networking/ClientGUINetwork.py
index 53a520777..6fcb52e06 100644
--- a/hydrus/client/gui/networking/ClientGUINetwork.py
+++ b/hydrus/client/gui/networking/ClientGUINetwork.py
@@ -27,8 +27,8 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.widgets import ClientGUIBandwidth
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
from hydrus.client.networking import ClientNetworking
from hydrus.client.networking import ClientNetworkingDomain
from hydrus.client.networking import ClientNetworkingContexts
@@ -39,14 +39,14 @@ def __init__( self, parent: QW.QWidget, bandwidth_rules: HydrusNetworking.Bandwi
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
- self._bandwidth_rules_ctrl = ClientGUIControls.BandwidthRulesCtrl( self, bandwidth_rules )
+ self._bandwidth_rules_ctrl = ClientGUIBandwidth.BandwidthRulesCtrl( self, bandwidth_rules )
vbox = QP.VBoxLayout()
intro = 'A network job exists in several contexts. It must wait for all those contexts to have free bandwidth before it can work.'
- intro += os.linesep * 2
+ intro += '\n' * 2
intro += 'You are currently editing:'
- intro += os.linesep * 2
+ intro += '\n' * 2
intro += summary
st = ClientGUICommon.BetterStaticText( self, intro )
@@ -578,7 +578,7 @@ def __init__( self, parent, controller ):
self._reset_default_bandwidth_rules_button = ClientGUICommon.BetterButton( self, 'reset default bandwidth rules', self._ResetDefaultBandwidthRules )
default_rules_help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowDefaultRulesHelp )
- default_rules_help_button.setToolTip( 'Show help regarding default bandwidth rules.' )
+ default_rules_help_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Show help regarding default bandwidth rules.' ) )
self._delete_record_button = ClientGUICommon.BetterButton( self, 'delete selected history', self._DeleteNetworkContexts )
@@ -772,15 +772,15 @@ def _ResetDefaultBandwidthRules( self ):
def _ShowDefaultRulesHelp( self ):
help_text = 'Network requests act in multiple contexts. Most use the \'global\' and \'web domain\' network contexts, but a downloader page or subscription will also add a special label for itself. Each context can have its own set of bandwidth rules.'
- help_text += os.linesep * 2
+ help_text += '\n' * 2
help_text += 'If a network context does not have some specific rules set up, it will fall back to its respective default. It is possible for a default to not have any rules. If you want to set general policy, like "Never download more than 1GB/day from any individual website," or "Limit the entire client to 2MB/s," do it through \'global\' and the defaults.'
- help_text += os.linesep * 2
+ help_text += '\n' * 2
help_text += 'All contexts\' rules are consulted and have to pass before a request can do work. If you set a 2MB/s limit on a website domain and a 64KB/s limit on global, your download will only ever run at 64KB/s (and it fact it will probably run much slower, since everything shares the global context!). To make sense, network contexts with broader scope should have more lenient rules.'
- help_text += os.linesep * 2
+ help_text += '\n' * 2
help_text += 'There are two special ephemeral \'instance\' contexts, for downloaders and thread watchers. These represent individual queries, either a single gallery search or a single watched thread. It can be useful to set default rules for these so your searches will gather a fast initial sample of results in the first few minutes--so you can make sure you are happy with them--but otherwise trickle the rest in over time. This keeps your CPU and other bandwidth limits less hammered and helps to avoid accidental downloads of many thousands of small bad files or a few hundred gigantic files all in one go.'
- help_text += os.linesep * 2
+ help_text += '\n' * 2
help_text += 'Please note that this system bases its calendar dates on UTC time (it helps servers and clients around the world stay in sync a bit easier). This has no bearing on what, for instance, the \'past 24 hours\' means, but monthly transitions may occur a few hours off whatever your midnight is.'
- help_text += os.linesep * 2
+ help_text += '\n' * 2
help_text += 'If you do not understand what is going on here, you can safely leave it alone. The default settings make for a _reasonable_ and polite profile that will not accidentally cause you to download way too much in one go or piss off servers by being too aggressive. If you want to throttle your client, the simplest way is to add a simple rule like \'500MB per day\' to the global context.'
ClientGUIDialogsMessage.ShowInformation( self, help_text )
diff --git a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py
index 7b5bc4cc9..0995518a4 100644
--- a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py
+++ b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py
@@ -47,7 +47,7 @@ def __init__( self, parent ):
self._cancel_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().stop, self.Cancel )
self._error_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().dump_fail, self._ShowErrorMenu )
- self._error_button.setToolTip( 'Click here to see the last job\'s error.' )
+ self._error_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Click here to see the last job\'s error.' ) )
self._error_button.hide()
@@ -343,7 +343,7 @@ def ClearNetworkJob( self ):
self._network_job = None
- self._gauge.setToolTip( '' )
+ self._gauge.setToolTip( ClientGUIFunctions.WrapToolTip( '' ) )
self._Update()
@@ -367,7 +367,7 @@ def SetNetworkJob( self, network_job: ClientNetworkingJobs.NetworkJob ):
self._network_job = network_job
- self._gauge.setToolTip( self._network_job.GetURL() )
+ self._gauge.setToolTip( ClientGUIFunctions.WrapToolTip( self._network_job.GetURL() ) )
self._Update()
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index fc2054357..4abd59e60 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -53,8 +53,8 @@
from hydrus.client.gui.parsing import ClientGUIParsingFormulae
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
from hydrus.client.gui.widgets import ClientGUIMenuButton
+from hydrus.client.gui.widgets import ClientGUITextInput
from hydrus.client.importing import ClientImporting
from hydrus.client.importing import ClientImportWatchers
from hydrus.client.importing import ClientImportLocal
@@ -842,7 +842,7 @@ def thread_do_it( file_search_context_1, file_search_context_2, dupe_search_type
def _ResetUnknown( self ):
text = 'ADVANCED TOOL: This will delete all the current potential duplicate pairs. All files that may be similar will be queued for search again.'
- text += os.linesep * 2
+ text += '\n' * 2
text += 'This can be useful if you know you have database damage and need to reset and re-search everything, or if you have accidentally searched too broadly and are now swamped with too many false positives. It is not useful for much else.'
result = ClientGUIDialogsQuick.GetYesNo( self, text )
@@ -1176,7 +1176,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self._import_queue_panel, self._controller, self._page_key )
self._pause_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.Pause )
- self._pause_button.setToolTip( 'pause/play imports' )
+ self._pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play imports' ) )
self._hdd_import: ClientImportLocal.HDDImport = self._management_controller.GetVariable( 'hdd_import' )
@@ -1311,7 +1311,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
#
- self._query_input = ClientGUIControls.TextAndPasteCtrl( self._gallery_downloader_panel, self._PendQueries )
+ self._query_input = ClientGUITextInput.TextAndPasteCtrl( self._gallery_downloader_panel, self._PendQueries )
self._cog_button = ClientGUICommon.BetterBitmapButton( self._gallery_downloader_panel, CC.global_pixmaps().cog, self._ShowCogMenu )
@@ -1319,7 +1319,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._file_limit = ClientGUICommon.NoneableSpinCtrl( self._gallery_downloader_panel, 'stop after this many files', min = 1, none_phrase = 'no limit' )
self._file_limit.valueChanged.connect( self.EventFileLimit )
- self._file_limit.setToolTip( 'per query, stop searching the gallery once this many files has been reached' )
+ self._file_limit.setToolTip( ClientGUIFunctions.WrapToolTip( 'per query, stop searching the gallery once this many files has been reached' ) )
file_import_options = self._multiple_gallery_import.GetFileImportOptions()
tag_import_options = self._multiple_gallery_import.GetTagImportOptions()
@@ -1568,7 +1568,7 @@ def _CopySelectedQueries( self ):
if len( gallery_importers ) > 0:
- text = os.linesep.join( ( gallery_importer.GetQueryText() for gallery_importer in gallery_importers ) )
+ text = '\n'.join( ( gallery_importer.GetQueryText() for gallery_importer in gallery_importers ) )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -1840,13 +1840,13 @@ def _RemoveGalleryImports( self ):
if num_working > 0:
- message += os.linesep * 2
+ message += '\n' * 2
message += HydrusData.ToHumanInt( num_working ) + ' are still working.'
if self._highlighted_gallery_import is not None and self._highlighted_gallery_import in removees:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The currently highlighted query will be removed, and the media panel cleared.'
@@ -2250,7 +2250,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._watchers_listctrl.Sort()
- self._watcher_url_input = ClientGUIControls.TextAndPasteCtrl( self._watchers_panel, self._AddURLs )
+ self._watcher_url_input = ClientGUITextInput.TextAndPasteCtrl( self._watchers_panel, self._AddURLs )
self._watcher_url_input.setPlaceholderText( 'watcher url' )
@@ -2535,7 +2535,7 @@ def _CopySelectedSubjects( self ):
if len( watchers ) > 0:
- text = os.linesep.join( ( watcher.GetSubject() for watcher in watchers ) )
+ text = '\n'.join( ( watcher.GetSubject() for watcher in watchers ) )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -2547,7 +2547,7 @@ def _CopySelectedURLs( self ):
if len( watchers ) > 0:
- text = os.linesep.join( ( watcher.GetURL() for watcher in watchers ) )
+ text = '\n'.join( ( watcher.GetURL() for watcher in watchers ) )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -2864,19 +2864,19 @@ def _RemoveWatchers( self ):
if num_working > 0:
- message += os.linesep * 2
+ message += '\n' * 2
message += HydrusData.ToHumanInt( num_working ) + ' are still working.'
if num_alive > 0:
- message += os.linesep * 2
+ message += '\n' * 2
message += HydrusData.ToHumanInt( num_alive ) + ' are not yet DEAD.'
if self._highlighted_watcher is not None and self._highlighted_watcher in removees:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The currently highlighted watcher will be removed, and the media panel cleared.'
@@ -3214,7 +3214,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._import_queue_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'imports' )
self._pause_files_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.PauseFiles )
- self._pause_files_button.setToolTip( 'pause/play files' )
+ self._pause_files_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play files' ) )
self._current_action = ClientGUICommon.BetterStaticText( self._import_queue_panel, ellipsize_end = True )
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self._import_queue_panel, self._controller, self._page_key )
self._file_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._import_queue_panel )
@@ -3226,7 +3226,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._simple_parsing_jobs_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'parsing' )
self._pause_queue_button = ClientGUICommon.BetterBitmapButton( self._simple_parsing_jobs_panel, CC.global_pixmaps().gallery_pause, self.PauseQueue )
- self._pause_queue_button.setToolTip( 'pause/play queue' )
+ self._pause_queue_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play queue' ) )
self._parser_status = ClientGUICommon.BetterStaticText( self._simple_parsing_jobs_panel, ellipsize_end = True )
@@ -3247,7 +3247,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._delay_button = QW.QPushButton( '\u2193', self._simple_parsing_jobs_panel )
self._delay_button.clicked.connect( self.EventDelay )
- self._page_url_input = ClientGUIControls.TextAndPasteCtrl( self._simple_parsing_jobs_panel, self._PendPageURLs )
+ self._page_url_input = ClientGUITextInput.TextAndPasteCtrl( self._simple_parsing_jobs_panel, self._PendPageURLs )
self._page_url_input.setPlaceholderText( 'url to be parsed by the selected formula' )
@@ -3446,7 +3446,7 @@ def add_callable():
def _PendPageURLs( self, unclean_urls ):
- urls = [ ClientNetworkingFunctions.WashURL( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
+ urls = [ ClientNetworkingFunctions.EnsureURLIsEncoded( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
simple_downloader_formula = self._formulae.GetValue()
@@ -3676,7 +3676,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._import_queue_panel = ClientGUICommon.StaticBox( self._url_panel, 'imports' )
self._pause_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.Pause )
- self._pause_button.setToolTip( 'pause/play files' )
+ self._pause_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play files' ) )
self._file_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._import_queue_panel )
@@ -3694,7 +3694,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
#
- self._url_input = ClientGUIControls.TextAndPasteCtrl( self._url_panel, self._PendURLs )
+ self._url_input = ClientGUITextInput.TextAndPasteCtrl( self._url_panel, self._PendURLs )
self._url_input.setPlaceholderText( 'any url hydrus recognises, or a raw file url' )
@@ -3770,7 +3770,7 @@ def _PendURLs( self, unclean_urls, filterable_tags = None, additional_service_ke
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
- urls = [ ClientNetworkingFunctions.WashURL( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
+ urls = [ ClientNetworkingFunctions.EnsureURLIsEncoded( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
self._urls_import.PendURLs( urls, filterable_tags = filterable_tags, additional_service_keys_to_tags = additional_service_keys_to_tags )
@@ -5110,7 +5110,7 @@ def EventContentsRightClick( self, contents ):
else:
- text = os.linesep.join( copyable_items )
+ text = '\n'.join( copyable_items )
ClientGUIMenus.AppendMenuItem( menu, 'copy {} tags'.format( HydrusData.ToHumanInt( len( copyable_items ) ) ), 'Copy this tag.', CG.client_controller.pub, 'clipboard', 'text', text )
diff --git a/hydrus/client/gui/pages/ClientGUIPages.py b/hydrus/client/gui/pages/ClientGUIPages.py
index d20638eeb..ec1b772c4 100644
--- a/hydrus/client/gui/pages/ClientGUIPages.py
+++ b/hydrus/client/gui/pages/ClientGUIPages.py
@@ -1619,7 +1619,7 @@ def _RefreshPageName( self, index ):
full_page_name = page.GetName()
- full_page_name = full_page_name.replace( os.linesep, '' )
+ full_page_name = full_page_name.replace( '\n', '' )
page_name = HydrusText.ElideText( full_page_name, max_page_name_chars )
@@ -2690,7 +2690,7 @@ def GetTestAbleToCloseStatement( self ):
message = HydrusData.ToHumanInt( c ) + ' pages say:' + reason
- message += os.linesep
+ message += '\n'
return message
@@ -3571,7 +3571,7 @@ def TestAbleToClose( self ):
if statement is not None:
message = 'Are you sure you want to close this page of pages?'
- message += os.linesep * 2
+ message += '\n' * 2
message += statement
result = ClientGUIDialogsQuick.GetYesNo( self, message )
diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py
index 4557ae7d0..f4c5a1b1b 100644
--- a/hydrus/client/gui/pages/ClientGUIResults.py
+++ b/hydrus/client/gui/pages/ClientGUIResults.py
@@ -33,9 +33,6 @@
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIDuplicates
from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIMedia
-from hydrus.client.gui import ClientGUIMediaActions
-from hydrus.client.gui import ClientGUIMediaMenus
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIScrolledPanelsManagement
@@ -46,13 +43,15 @@
from hydrus.client.gui.canvas import ClientGUICanvas
from hydrus.client.gui.canvas import ClientGUICanvasFrame
from hydrus.client.gui.exporting import ClientGUIExport
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
+from hydrus.client.gui.media import ClientGUIMediaModalActions
+from hydrus.client.gui.media import ClientGUIMediaMenus
from hydrus.client.gui.networking import ClientGUIHydrusNetwork
from hydrus.client.gui.pages import ClientGUIManagementController
from hydrus.client.media import ClientMedia
from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
-from hydrus.client.search import ClientSearch
MAC_QUARTZ_OK = True
@@ -270,7 +269,7 @@ def _ClearDeleteRecord( self ):
media = self._GetSelectedFlatMedia()
- ClientGUIMediaActions.ClearDeleteRecord( self, media )
+ ClientGUIMediaModalActions.ClearDeleteRecord( self, media )
def _CopyBMPToClipboard( self, resolution = None ):
@@ -325,7 +324,7 @@ def _CopyHashToClipboard( self, hash_type ):
media = self._GetFocusSingleton()
- ClientGUIMedia.CopyHashesToClipboard( self, hash_type, [ media ] )
+ ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, [ media ] )
@@ -333,7 +332,7 @@ def _CopyHashesToClipboard( self, hash_type ):
medias = self._GetSelectedMediaOrdered()
- ClientGUIMedia.CopyHashesToClipboard( self, hash_type, medias )
+ ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, medias )
def _CopyPathToClipboard( self ):
@@ -365,7 +364,7 @@ def _CopyPathsToClipboard( self ):
if len( paths ) > 0:
- text = os.linesep.join( paths )
+ text = '\n'.join( paths )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -420,7 +419,7 @@ def _CopyServiceFilenamesToClipboard( self, service_key ):
if len( filenames ) > 0:
- copy_string = os.linesep.join( filenames )
+ copy_string = '\n'.join( filenames )
CG.client_controller.pub( 'clipboard', 'text', copy_string )
@@ -785,28 +784,6 @@ def _GetSelectedMediaOrdered( self ):
return sorted( self._selected_media, key = lambda m: self._sorted_media.index( m ) )
- def _GetSimilarTo( self, max_hamming ):
-
- hashes = set()
-
- media = self._GetSelectedFlatMedia()
-
- for m in media:
-
- if m.GetMime() in HC.FILES_THAT_HAVE_PERCEPTUAL_HASH:
-
- hashes.add( m.GetHash() )
-
-
-
- if len( hashes ) > 0:
-
- initial_predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO_FILES, ( tuple( hashes ), max_hamming ) ) ]
-
- CG.client_controller.pub( 'new_page_query', self._location_context, initial_predicates = initial_predicates )
-
-
-
def _GetSortedSelectedMimeDescriptors( self ):
def GetDescriptor( plural, classes, num_collections ):
@@ -1140,7 +1117,7 @@ def _ManageNotes( self ):
media = self._GetFocusSingleton()
- ClientGUIMediaActions.EditFileNotes( self, media )
+ ClientGUIMediaModalActions.EditFileNotes( self, media )
self.setFocus( QC.Qt.OtherFocusReason )
@@ -1196,7 +1173,7 @@ def _ManageTimestamps( self ):
if len( ordered_selected_flat_media ) > 0:
- ClientGUIMediaActions.EditFileTimestamps( self, ordered_selected_flat_media )
+ ClientGUIMediaModalActions.EditFileTimestamps( self, ordered_selected_flat_media )
self.setFocus( QC.Qt.OtherFocusReason )
@@ -1248,32 +1225,6 @@ def _ModifyUploaders( self, file_service_key ):
- def _OpenExternally( self ):
-
- if self._HasFocusSingleton():
-
- media = self._GetFocusSingleton()
-
- if media.GetLocationsManager().IsLocal():
-
- self.focusMediaPaused.emit()
-
- hash = media.GetHash()
- mime = media.GetMime()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- new_options = CG.client_controller.new_options
-
- launch_path = new_options.GetMimeLaunch( mime )
-
- HydrusPaths.LaunchFile( path, launch_path )
-
-
-
-
def _OpenFileInWebBrowser( self ):
if self._HasFocusSingleton():
@@ -1296,27 +1247,6 @@ def _OpenFileInWebBrowser( self ):
- def _OpenFileLocation( self ):
-
- if self._HasFocusSingleton():
-
- focused_singleton = self._GetFocusSingleton()
-
- if focused_singleton.GetLocationsManager().IsLocal():
-
- hash = focused_singleton.GetHash()
- mime = focused_singleton.GetMime()
-
- client_files_manager = CG.client_controller.client_files_manager
-
- path = client_files_manager.GetFilePath( hash, mime )
-
- self.focusMediaPaused.emit()
-
- HydrusPaths.OpenFileLocation( path )
-
-
-
def _MacQuicklook( self ):
if HC.PLATFORM_MACOS and self._HasFocusSingleton():
@@ -1350,7 +1280,7 @@ def _OpenKnownURL( self ):
focused_singleton = self._GetFocusSingleton()
- ClientGUIMedia.DoOpenKnownURLFromShortcut( self, focused_singleton )
+ ClientGUIMediaModalActions.DoOpenKnownURLFromShortcut( self, focused_singleton )
@@ -1481,7 +1411,7 @@ def _RegenerateFileData( self, job_type ):
if job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_METADATA:
message = 'This will reparse the {} selected files\' metadata.'.format( HydrusData.ToHumanInt( num_files ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If the files were imported before some more recent improvement in the parsing code (such as EXIF rotation or bad video resolution or duration or frame count calculation), this will update them.'
elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL:
@@ -1501,7 +1431,7 @@ def _RegenerateFileData( self, job_type ):
if num_files > 50:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You have selected {} files, so this job may take some time. You can run it all now or schedule it to the overall file maintenance queue for later spread-out processing.'.format( HydrusData.ToHumanInt( num_files ) )
yes_tuples = []
@@ -1630,7 +1560,7 @@ def _SetCollectionsAsAlternate( self ):
if len( collections ) > 0:
message = 'Are you sure you want to set files in the selected collections as alternates? Each collection will be considered a separate group of alternates.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Be careful applying this to large groups--any more than a few dozen files, and the client could hang a long time.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -1728,7 +1658,7 @@ def _SetDuplicates( self, duplicate_type, media_pairs = None, media_group = None
if duplicate_type == HC.DUPLICATE_FALSE_POSITIVE:
message = 'False positive records are complicated, and setting that relationship for {} files ({} pairs) at once is likely a mistake.'.format( num_files_str, media_pairs_str )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Are you sure all of these files are all potential duplicates and that they are all false positive matches with each other? If not, I recommend you step back for now.'
yes_label = 'I know what I am doing'
@@ -1737,7 +1667,7 @@ def _SetDuplicates( self, duplicate_type, media_pairs = None, media_group = None
elif duplicate_type == HC.DUPLICATE_ALTERNATE:
message = 'Are you certain all these {} files are alternates with every other member of the selection, and that none are duplicates?'.format( num_files_str )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If some of them may be duplicates, I recommend you either deselect the possible duplicates and try again, or just leave this group to be processed in the normal duplicate filter.'
yes_label = 'they are all alternates'
@@ -2069,24 +1999,11 @@ def _ShareOnLocalBooru( self ):
- def _ShowSelectionInNewDuplicateFilterPage( self ):
-
- hashes = self._GetSelectedHashes( ordered = True )
-
- activate_window = CG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
-
- predicates = [ ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, value = ( tuple( hashes ), 'sha256' ) ) ]
-
- page_name = 'duplicates'
-
- CG.client_controller.pub( 'new_page_duplicates', self._location_context, initial_predicates = predicates, page_name = page_name, activate_window = activate_window )
-
-
def _ShowSelectionInNewPage( self ):
hashes = self._GetSelectedHashes( ordered = True )
- if hashes is not None and len( hashes ) > 0:
+ if len( hashes ) > 0:
media_sort = self._management_controller.GetVariable( 'media_sort' )
@@ -2099,7 +2016,7 @@ def _ShowSelectionInNewPage( self ):
media_collect = ClientMedia.MediaCollect()
- CG.client_controller.pub( 'new_page_query', self._location_context, initial_hashes = hashes, initial_sort = media_sort, initial_collect = media_collect )
+ ClientGUIMediaSimpleActions.ShowFilesInNewPage( hashes, self._location_context, media_sort = media_sort, media_collect = media_collect )
@@ -2113,7 +2030,7 @@ def _Undelete( self ):
media = self._GetSelectedFlatMedia()
- ClientGUIMediaActions.UndeleteMedia( self, media )
+ ClientGUIMediaModalActions.UndeleteMedia( self, media )
def _UpdateBackgroundColour( self ):
@@ -2282,7 +2199,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
if len( ordered_selected_media ) > 0:
- ClientGUIMedia.CopyMediaURLs( ordered_selected_media )
+ ClientGUIMediaSimpleActions.CopyMediaURLs( ordered_selected_media )
elif action == CAC.SIMPLE_REARRANGE_THUMBNAILS:
@@ -2361,7 +2278,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
duplicate_type = command.GetSimpleData()
- ClientGUIMedia.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type )
+ ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type )
elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FOCUSED_FALSE_POSITIVES:
@@ -2566,37 +2483,69 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
self._Remove( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) )
- elif action == CAC.SIMPLE_GET_SIMILAR_TO_EXACT:
+ elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER:
- self._GetSimilarTo( CC.HAMMING_EXACT_MATCH )
+ self._LaunchMediaViewer()
- elif action == CAC.SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR:
+ elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
- self._GetSimilarTo( CC.HAMMING_VERY_SIMILAR )
+ if self._HasFocusSingleton():
+
+ focused_singleton = self._GetFocusSingleton()
+
+ it_worked = ClientGUIMediaSimpleActions.OpenExternally( focused_singleton )
+
+ if it_worked:
+
+ self.focusMediaPaused.emit()
+
+
- elif action == CAC.SIMPLE_GET_SIMILAR_TO_SIMILAR:
+ elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
- self._GetSimilarTo( CC.HAMMING_SIMILAR )
+ if self._HasFocusSingleton():
+
+ focused_singleton = self._GetFocusSingleton()
+
+ it_worked = ClientGUIMediaSimpleActions.OpenFileLocation( focused_singleton )
+
+ if it_worked:
+
+ self.focusMediaPaused.emit()
+
+
- elif action == CAC.SIMPLE_GET_SIMILAR_TO_SPECULATIVE:
+ elif action == CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER:
- self._GetSimilarTo( CC.HAMMING_SPECULATIVE )
+ if self._HasFocusSingleton():
+
+ focused_singleton = self._GetFocusSingleton()
+
+ it_worked = ClientGUIMediaSimpleActions.OpenInWebBrowser( focused_singleton )
+
+ if it_worked:
+
+ self.focusMediaPaused.emit()
+
+
- elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER:
+ elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE:
- self._LaunchMediaViewer()
+ self._ShowSelectionInNewPage()
- elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM:
+ elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE:
- self._OpenExternally()
+ hashes = self._GetSelectedHashes( ordered = True )
- elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER:
+ ClientGUIMediaSimpleActions.ShowFilesInNewDuplicatesFilterPage( hashes, self._location_context )
- self._OpenFileLocation()
+ elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
- elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE:
+ media = self._GetSelectedFlatMedia()
- self._ShowSelectionInNewPage()
+ hamming_distance = command.GetSimpleData()
+
+ ClientGUIMediaSimpleActions.ShowSimilarFilesInNewPage( media, self._location_context, hamming_distance )
elif action == CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER:
@@ -2613,7 +2562,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
elif command.IsContentCommand():
- command_processed = ClientGUIMediaActions.ApplyContentApplicationCommandToMedia( self, command, self._GetSelectedFlatMedia() )
+ command_processed = ClientGUIMediaModalActions.ApplyContentApplicationCommandToMedia( self, command, self._GetSelectedFlatMedia() )
else:
@@ -3886,11 +3835,10 @@ def ShowMenu( self, do_not_show_just_return = False ):
all_file_domains = HydrusData.MassUnion( locations_manager.GetCurrent() for locations_manager in all_locations_managers )
all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } )
- all_local_file_domains = services_manager.Filter( all_specific_file_domains, ( HC.LOCAL_FILE_DOMAIN, ) )
- all_local_file_domains_sorted = sorted( all_local_file_domains, key = CG.client_controller.services_manager.GetName )
+ some_downloading = True in ( locations_manager.IsDownloading() for locations_manager in selected_locations_managers )
- all_file_repos = services_manager.Filter( all_specific_file_domains, ( HC.FILE_REPOSITORY, ) )
+ focused_is_local = False
has_local = True in ( locations_manager.IsLocal() for locations_manager in all_locations_managers )
has_remote = True in ( locations_manager.IsRemote() for locations_manager in all_locations_managers )
@@ -4021,8 +3969,6 @@ def ShowMenu( self, do_not_show_just_return = False ):
disparate_petitioned_remote_service_keys = petitioned_remote_service_keys - common_petitioned_remote_service_keys
disparate_deleted_remote_service_keys = deleted_remote_service_keys - common_deleted_remote_service_keys
- some_downloading = True in ( locations_manager.IsDownloading() for locations_manager in selected_locations_managers )
-
pending_file_service_keys = pending_remote_service_keys.intersection( file_repository_service_keys )
petitioned_file_service_keys = petitioned_remote_service_keys.intersection( file_repository_service_keys )
@@ -4361,7 +4307,7 @@ def ShowMenu( self, do_not_show_just_return = False ):
ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage notes for the focused file.', self._ManageNotes )
ClientGUIMenus.AppendMenuItem( manage_menu, 'times', 'Edit the timestamps for your files.', self._ManageTimestamps )
- ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaActions.SetFilesForcedFiletypes, self, self._selected_media )
+ ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaModalActions.SetFilesForcedFiletypes, self, self._selected_media )
ClientGUIMediaMenus.AddDuplicatesMenu( self, manage_menu, self._location_context, focus_singleton, num_selected, collections_selected )
@@ -4378,7 +4324,7 @@ def ShowMenu( self, do_not_show_just_return = False ):
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
- ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaActions.GetLocalFileActionServiceKeys( flat_selected_medias )
+ ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( flat_selected_medias )
len_interesting_local_service_keys = 0
@@ -4406,93 +4352,87 @@ def ShowMenu( self, do_not_show_just_return = False ):
len_interesting_remote_service_keys += len( ipfs_service_keys )
- if len_interesting_local_service_keys > 0 and len_interesting_remote_service_keys > 0:
-
- files_parent_menu = ClientGUIMenus.GenerateMenu( menu )
-
- ClientGUIMenus.AppendMenu( menu, files_parent_menu, 'files' )
-
- else:
+ if len_interesting_local_service_keys > 0 or len_interesting_remote_service_keys > 0:
- files_parent_menu = menu
-
-
- if len_interesting_local_service_keys > 0:
-
- ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, files_parent_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
-
-
- if len_interesting_remote_service_keys > 0:
+ files_menu = ClientGUIMenus.GenerateMenu( menu )
- remote_action_menu = ClientGUIMenus.GenerateMenu( files_parent_menu )
+ ClientGUIMenus.AppendMenu( menu, files_menu, 'files' )
- if len( downloadable_file_service_keys ) > 0:
+ if len_interesting_local_service_keys > 0:
- ClientGUIMenus.AppendMenuItem( remote_action_menu, download_phrase, 'Download all possible selected files.', self._DownloadSelected )
+ ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, files_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
- if some_downloading:
+ if len_interesting_remote_service_keys > 0:
- ClientGUIMenus.AppendMenuItem( remote_action_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self._RescindDownloadSelected )
+ ClientGUIMenus.AppendSeparator( files_menu )
-
- if len( uploadable_file_service_keys ) > 0:
-
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles )
-
-
- if len( pending_file_service_keys ) > 0:
-
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles )
-
-
- if len( petitionable_file_service_keys ) > 0:
-
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles )
-
-
- if len( petitioned_file_service_keys ) > 0:
-
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles )
-
-
- if len( deletable_file_service_keys ) > 0:
-
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete )
+ if len( downloadable_file_service_keys ) > 0:
+
+ ClientGUIMenus.AppendMenuItem( files_menu, download_phrase, 'Download all possible selected files.', self._DownloadSelected )
+
-
- if len( modifyable_file_service_keys ) > 0:
+ if some_downloading:
+
+ ClientGUIMenus.AppendMenuItem( files_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self._RescindDownloadSelected )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders )
+ if len( uploadable_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles )
+
-
- if len( pinnable_ipfs_service_keys ) > 0:
+ if len( pending_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles )
+ if len( petitionable_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles )
+
-
- if len( pending_ipfs_service_keys ) > 0:
+ if len( petitioned_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles )
+ if len( deletable_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete )
+
-
- if len( unpinnable_ipfs_service_keys ) > 0:
+ if len( modifyable_file_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles )
+ if len( pinnable_ipfs_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles )
+
-
- if len( petitioned_ipfs_service_keys ) > 0:
+ if len( pending_ipfs_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles )
+ if len( unpinnable_ipfs_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles )
+
-
- if multiple_selected and len( ipfs_service_keys ) > 0:
+ if len( petitioned_ipfs_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles )
+
- ClientGUIMediaMenus.AddServiceKeysToMenu( remote_action_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory )
+ if multiple_selected and len( ipfs_service_keys ) > 0:
+
+ ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory )
+
- ClientGUIMenus.AppendMenu( files_parent_menu, remote_action_menu, 'remote services' )
-
#
@@ -4500,42 +4440,7 @@ def ShowMenu( self, do_not_show_just_return = False ):
#
- open_menu = ClientGUIMenus.GenerateMenu( menu )
-
- ClientGUIMenus.AppendMenuItem( open_menu, 'in a new page', 'Copy your current selection into a simple new page.', self._ShowSelectionInNewPage )
-
- if self._focused_media.HasStaticImages():
-
- similar_menu = ClientGUIMenus.GenerateMenu( open_menu )
-
- ClientGUIMenus.AppendMenuItem( similar_menu, 'in a new duplicate filter page', 'Make a new duplicate filter page that searches for these files specifically.', self._ShowSelectionInNewDuplicateFilterPage )
-
- ClientGUIMenus.AppendSeparator( similar_menu )
-
- ClientGUIMenus.AppendMenuLabel( similar_menu, 'search for similar-looking files:' )
- ClientGUIMenus.AppendMenuItem( similar_menu, 'exact match', 'Search the database for files that look precisely like those selected.', self._GetSimilarTo, CC.HAMMING_EXACT_MATCH )
- ClientGUIMenus.AppendMenuItem( similar_menu, 'very similar', 'Search the database for files that look just like those selected.', self._GetSimilarTo, CC.HAMMING_VERY_SIMILAR )
- ClientGUIMenus.AppendMenuItem( similar_menu, 'similar', 'Search the database for files that look generally like those selected.', self._GetSimilarTo, CC.HAMMING_SIMILAR )
- ClientGUIMenus.AppendMenuItem( similar_menu, 'speculative', 'Search the database for files that probably look like those selected. This is sometimes useful for symbols with sharp edges or lines.', self._GetSimilarTo, CC.HAMMING_SPECULATIVE )
-
- ClientGUIMenus.AppendMenu( open_menu, similar_menu, 'similar files' )
-
-
- ClientGUIMenus.AppendSeparator( open_menu )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in external program', 'Launch this file with your OS\'s default program for it.', self._OpenExternally )
- ClientGUIMenus.AppendMenuItem( open_menu, 'in web browser', 'Show this file in your OS\'s web browser.', self._OpenFileInWebBrowser )
-
- if focused_is_local:
-
- show_open_in_explorer = advanced_mode and ( HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS )
-
- if show_open_in_explorer:
-
- ClientGUIMenus.AppendMenuItem( open_menu, 'in file browser', 'Show this file in your OS\'s file browser.', self._OpenFileLocation )
-
-
-
- ClientGUIMenus.AppendMenu( menu, open_menu, 'open' )
+ ClientGUIMediaMenus.AddOpenMenu( self, menu, self._focused_media, self._selected_media )
# share
@@ -4669,9 +4574,11 @@ def ShowMenu( self, do_not_show_just_return = False ):
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
+
if not do_not_show_just_return:
CGC.core().PopupMenu( self, menu )
+
else:
diff --git a/hydrus/client/gui/parsing/ClientGUIParsing.py b/hydrus/client/gui/parsing/ClientGUIParsing.py
index 3799fe64b..743bd7aa8 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsing.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsing.py
@@ -20,6 +20,7 @@
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUISerialisable
from hydrus.client.gui import ClientGUIStringControls
@@ -246,16 +247,16 @@ def _Export( self ):
export_object = HydrusSerialisable.SerialisableList( self._listctrl.GetData() )
message = 'The end-user will see this sort of summary:'
- message += os.linesep * 2
- message += os.linesep.join( ( obj.GetSafeSummary() for obj in export_object[:20] ) )
+ message += '\n' * 2
+ message += '\n'.join( ( obj.GetSafeSummary() for obj in export_object[:20] ) )
if len( export_object ) > 20:
- message += os.linesep
+ message += '\n'
message += '(and ' + HydrusData.ToHumanInt( len( export_object ) - 20 ) + ' others)'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Does that look good? (Ideally, every object should have correct and sane domains listed here)'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -560,7 +561,7 @@ def __init__( self, parent: QW.QWidget, content_parser: ClientParsing.ContentPar
self._namespace = ClientGUICommon.NoneableTextCtrl( self._mappings_panel, none_phrase = 'any namespace' )
tt = 'The difference between "any namespace" and setting an empty input for "unnamespaced" is "unnamespaced" will force unnamespaced, even if the parsed tag includes a colon. If you are parsing hydrus content and expect to see "namespace:subtag", hit "any namespace", and if you are parsing normal boorus that might have a colon in for weird reasons, try "unnamespaced".'
- self._namespace.setToolTip( tt )
+ self._namespace.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -607,7 +608,7 @@ def __init__( self, parent: QW.QWidget, content_parser: ClientParsing.ContentPar
self._header_name = QW.QLineEdit( self._header_panel )
tt = 'Any header you parse here will be passed on to subsequent jobs/objects created by this same parse. Next gallery pages, file downloads from post urls, post pages spawned from multi-file posts. And the headers will be passed on to their children. Should help with tokenised searches or weird guest-login issues.'
- self._header_name.setToolTip( tt )
+ self._header_name.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1569,7 +1570,7 @@ def qt_tidy_up( example_data, example_bytes, error ):
stuff_read = 'no response'
- example_data = 'fetch failed: {}'.format( e ) + os.linesep * 2 + stuff_read
+ example_data = 'fetch failed: {}'.format( e ) + '\n' * 2 + stuff_read
QP.CallAfter( qt_tidy_up, example_data, example_bytes, error )
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
index 00c967c56..8133bfa62 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
@@ -601,7 +601,7 @@ def __init__( self, parent, tag_rule ):
self._tag_attributes = ClientGUIStringControls.StringToStringDictControl( self, tag_attributes, min_height = 4 )
self._tag_index = ClientGUICommon.NoneableSpinCtrl( self, 'index to fetch', none_phrase = 'get all', min = -65536, max = 65535 )
- self._tag_index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
+ self._tag_index.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' ) )
self._tag_depth = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
@@ -1049,7 +1049,7 @@ def __init__( self, parent: QW.QWidget, rule: ClientParsing.ParseRuleHTML ):
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self, string_match )
self._index = ClientGUICommon.BetterSpinBox( self, min=-65536, max=65535 )
- self._index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
+ self._index.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' ) )
#
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
index 77c8ead1f..d93a803e8 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
@@ -284,7 +284,7 @@ def Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Nodes', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Nodes', e )
@@ -471,7 +471,7 @@ def qt_code( parsed_urls ):
result_lines.append( '*** RESULTS END ***' )
- results_text = os.linesep.join( result_lines )
+ results_text = '\n'.join( result_lines )
self._results.setPlainText( results_text )
@@ -764,7 +764,7 @@ def EventFetchData( self ):
HydrusData.ShowException( e )
message = 'Could not fetch data!'
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
ClientGUIDialogsMessage.ShowCritical( self, 'Could not fetch!', message )
@@ -790,7 +790,7 @@ def qt_code( results ):
result_lines.append( '*** RESULTS END ***' )
- results_text = os.linesep.join( result_lines )
+ results_text = '\n'.join( result_lines )
self._results.setPlainText( results_text )
@@ -1142,7 +1142,7 @@ def ImportFromClipboard( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'JSON-serialised Parsing Scripts', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Parsing Scripts', e )
@@ -1203,7 +1203,7 @@ def __init__( self, parent ):
self._status.setWordWrap( True )
self._link_button = ClientGUICommon.BetterBitmapButton( main_panel, CC.global_pixmaps().link, self.LinkButton )
- self._link_button.setToolTip( 'urls found by the script' )
+ self._link_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'urls found by the script' ) )
self._cancel_button = ClientGUICommon.BetterBitmapButton( main_panel, CC.global_pixmaps().stop, self.CancelButton )
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingTest.py b/hydrus/client/gui/parsing/ClientGUIParsingTest.py
index b20cb6589..614c55eaa 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingTest.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingTest.py
@@ -20,6 +20,7 @@
from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import QtPorting as QP
@@ -50,13 +51,13 @@ def __init__( self, parent, object_callable, test_data: typing.Optional[ ClientP
self._example_data_raw_description = ClientGUICommon.BetterStaticText( raw_data_panel )
self._copy_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().copy, self._Copy )
- self._copy_button.setToolTip( 'Copy the current example data to the clipboard.' )
+ self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy the current example data to the clipboard.' ) )
self._fetch_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().link, self._FetchFromURL )
- self._fetch_button.setToolTip( 'Fetch data from a URL.' )
+ self._fetch_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Fetch data from a URL.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste the current clipboard data into here.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste the current clipboard data into here.' ) )
self._example_data_raw_preview = QW.QPlainTextEdit( raw_data_panel )
self._example_data_raw_preview.setReadOnly( True )
@@ -167,7 +168,7 @@ def do_it( url ):
except Exception as e:
- example_data = 'fetch failed:' + os.linesep * 2 + str( e )
+ example_data = 'fetch failed:' + '\n' * 2 + str( e )
HydrusData.ShowException( e )
@@ -218,7 +219,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'UTF-8 text', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'UTF-8 text', e )
@@ -303,7 +304,7 @@ def _SetExampleData( self, example_data, example_bytes = None ):
if len( example_data_to_show ) > MAX_CHARS_IN_PREVIEW:
- preview = 'PREVIEW:' + os.linesep + str( example_data_to_show[:MAX_CHARS_IN_PREVIEW] )
+ preview = 'PREVIEW:' + '\n' + str( example_data_to_show[:MAX_CHARS_IN_PREVIEW] )
else:
@@ -326,7 +327,7 @@ def _SetExampleData( self, example_data, example_bytes = None ):
if len( example_data ) > MAX_CHARS_IN_PREVIEW:
- preview = 'PREVIEW:' + os.linesep + repr( example_data[:MAX_CHARS_IN_PREVIEW] )
+ preview = 'PREVIEW:' + '\n' + repr( example_data[:MAX_CHARS_IN_PREVIEW] )
else:
@@ -422,7 +423,7 @@ def TestParse( self ):
trace = ''.join( traceback.format_exception( etype, value, tb ) )
- message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
+ message = 'Exception:' + '\n' + str( etype.__name__ ) + ': ' + str( e ) + '\n' + trace
self._results.setPlainText( message )
@@ -464,7 +465,7 @@ def __init__( self, parent, object_callable, pre_parsing_converter_callable, tes
self._example_data_post_conversion_description = ClientGUICommon.BetterStaticText( post_conversion_panel )
self._copy_button_post_conversion = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().copy, self._CopyPostConversion )
- self._copy_button_post_conversion.setToolTip( 'Copy the current post conversion data to the clipboard.' )
+ self._copy_button_post_conversion.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy the current post conversion data to the clipboard.' ) )
self._refresh_post_conversion_button = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
self._example_data_post_conversion_preview = QW.QPlainTextEdit( post_conversion_panel )
@@ -518,7 +519,7 @@ def _SetExampleData( self, example_data, example_bytes = None ):
if len( post_conversion_example_data ) > 1024:
- preview = 'PREVIEW:' + os.linesep + str( post_conversion_example_data[:1024] )
+ preview = 'PREVIEW:' + '\n' + str( post_conversion_example_data[:1024] )
else:
@@ -551,7 +552,7 @@ def _SetExampleData( self, example_data, example_bytes = None ):
trace = ''.join( traceback.format_exception( etype, value, tb ) )
- message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
+ message = 'Exception:' + '\n' + str( etype.__name__ ) + ': ' + str( e ) + '\n' + trace
preview = message
@@ -594,7 +595,7 @@ def __init__( self, parent, object_callable, pre_parsing_converter_callable, for
self._example_data_post_separation_description = ClientGUICommon.BetterStaticText( post_separation_panel )
self._copy_button_post_separation = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().copy, self._CopyPostSeparation )
- self._copy_button_post_separation.setToolTip( 'Copy the current post separation data to the clipboard.' )
+ self._copy_button_post_separation.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy the current post separation data to the clipboard.' ) )
self._refresh_post_separation_button = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
self._example_data_post_separation_preview = QW.QPlainTextEdit( post_separation_panel )
@@ -626,7 +627,7 @@ def __init__( self, parent, object_callable, pre_parsing_converter_callable, for
def _CopyPostSeparation( self ):
- joiner = os.linesep * 2
+ joiner = '\n' * 2
CG.client_controller.pub( 'clipboard', 'text', joiner.join( self._example_data_post_separation ) )
@@ -651,13 +652,13 @@ def _SetExampleData( self, example_data, example_bytes = None ):
separation_example_data = formula.Parse( example_parsing_context, self._example_data_post_conversion, self._collapse_newlines )
- joiner = os.linesep * 2
+ joiner = '\n' * 2
preview = joiner.join( separation_example_data )
if len( preview ) > 1024:
- preview = 'PREVIEW:' + os.linesep + str( preview[:1024] )
+ preview = 'PREVIEW:' + '\n' + str( preview[:1024] )
description = HydrusData.ToHumanInt( len( separation_example_data ) ) + ' subsidiary posts parsed'
@@ -672,7 +673,7 @@ def _SetExampleData( self, example_data, example_bytes = None ):
trace = ''.join( traceback.format_exception( etype, value, tb ) )
- message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
+ message = 'Exception:' + '\n' + str( etype.__name__ ) + ': ' + str( e ) + '\n' + trace
preview = message
@@ -731,7 +732,7 @@ def TestParse( self ):
pretty_texts.append( pretty_text )
- separator = os.linesep * 2
+ separator = '\n' * 2
end_pretty_text = separator.join( pretty_texts )
@@ -745,7 +746,7 @@ def TestParse( self ):
trace = ''.join( traceback.format_exception( etype, value, tb ) )
- message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
+ message = 'Exception:' + '\n' + str( etype.__name__ ) + ': ' + str( e ) + '\n' + trace
self._results.setPlainText( message )
diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py
index 99f4b1fe4..8af179c15 100644
--- a/hydrus/client/gui/search/ClientGUIACDropdown.py
+++ b/hydrus/client/gui/search/ClientGUIACDropdown.py
@@ -22,6 +22,7 @@
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIScrolledPanels
@@ -1837,10 +1838,10 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
#
self._paste_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'You can paste a newline-separated list of regular tags and/or system predicates.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can paste a newline-separated list of regular tags and/or system predicates.' ) )
self._favourite_searches_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().star, self._FavouriteSearchesMenu )
- self._favourite_searches_button.setToolTip( 'Load or save a favourite search.' )
+ self._favourite_searches_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Load or save a favourite search.' ) )
self._cancel_search_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().stop, self.searchCancelled.emit )
@@ -1853,15 +1854,15 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
#
self._include_current_tags = ClientGUICommon.OnOffButton( self._dropdown_window, on_label = 'include current tags', off_label = 'exclude current tags', start_on = tag_context.include_current_tags )
- self._include_current_tags.setToolTip( 'select whether to include current tags in the search' )
+ self._include_current_tags.setToolTip( ClientGUIFunctions.WrapToolTip( 'select whether to include current tags in the search' ) )
self._include_pending_tags = ClientGUICommon.OnOffButton( self._dropdown_window, on_label = 'include pending tags', off_label = 'exclude pending tags', start_on = tag_context.include_pending_tags )
- self._include_pending_tags.setToolTip( 'select whether to include pending tags in the search' )
+ self._include_pending_tags.setToolTip( ClientGUIFunctions.WrapToolTip( 'select whether to include pending tags in the search' ) )
self._search_pause_play = ClientGUICommon.OnOffButton( self._dropdown_window, on_label = 'searching immediately', off_label = 'search paused', start_on = synchronised )
- self._search_pause_play.setToolTip( 'select whether to renew the search as soon as a new predicate is entered' )
+ self._search_pause_play.setToolTip( ClientGUIFunctions.WrapToolTip( 'select whether to renew the search as soon as a new predicate is entered' ) )
self._or_basic = ClientGUICommon.BetterButton( self._dropdown_window, 'OR', self._CreateNewOR )
- self._or_basic.setToolTip( 'Create a new empty OR predicate in the dialog.' )
+ self._or_basic.setToolTip( ClientGUIFunctions.WrapToolTip( 'Create a new empty OR predicate in the dialog.' ) )
if not CG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
@@ -1869,7 +1870,7 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
self._or_advanced = ClientGUICommon.BetterButton( self._dropdown_window, 'advanced', self._AdvancedORInput )
- self._or_advanced.setToolTip( 'You can paste complicated predicate strings in here and it will parse into proper logic.' )
+ self._or_advanced.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can paste complicated predicate strings in here and it will parse into proper logic.' ) )
if not CG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
@@ -1877,11 +1878,11 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
self._or_cancel = ClientGUICommon.BetterBitmapButton( self._dropdown_window, CC.global_pixmaps().delete, self._CancelORConstruction )
- self._or_cancel.setToolTip( 'Cancel OR Predicate construction.' )
+ self._or_cancel.setToolTip( ClientGUIFunctions.WrapToolTip( 'Cancel OR Predicate construction.' ) )
self._or_cancel.hide()
self._or_rewind = ClientGUICommon.BetterBitmapButton( self._dropdown_window, CC.global_pixmaps().previous, self._RewindORConstruction )
- self._or_rewind.setToolTip( 'Rewind OR Predicate construction.' )
+ self._or_rewind.setToolTip( ClientGUIFunctions.WrapToolTip( 'Rewind OR Predicate construction.' ) )
self._or_rewind.hide()
button_hbox_1 = QP.HBoxLayout()
@@ -2284,7 +2285,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
@@ -2862,7 +2863,7 @@ def __init__( self, parent, chosen_tag_callable, location_context, tag_service_k
self._location_context_button.SetAllKnownFilesAllowed( True, False )
self._paste_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste from the clipboard and quick-enter as if you had typed. This can take multiple newline-separated tags.' )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste from the clipboard and quick-enter as if you had typed. This can take multiple newline-separated tags.' ) )
if not show_paste_button:
@@ -2982,7 +2983,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of tags', e )
raise
@@ -3089,9 +3090,9 @@ def __init__( self, parent, initial_string = None ):
vbox = QP.VBoxLayout()
summary = 'Enter a complicated tag search here as text, such as \'( blue eyes and blonde hair ) or ( green eyes and red hair )\', and this should turn it into hydrus-compatible search predicates.'
- summary += os.linesep * 2
+ summary += '\n' * 2
summary += 'Accepted operators: not (!, -), and (&&), or (||), implies (=>), xor, xnor (iff, <=>), nand, nor. Many system predicates are also supported.'
- summary += os.linesep * 2
+ summary += '\n' * 2
summary += 'Parentheses work the usual way. \\ can be used to escape characters (e.g. to search for tags including parentheses)'
st = ClientGUICommon.BetterStaticText( self, summary )
@@ -3222,7 +3223,7 @@ def _UpdateText( self ):
- output = os.linesep.join( ( pred.ToString() for pred in self._current_predicates ) )
+ output = '\n'.join( ( pred.ToString() for pred in self._current_predicates ) )
object_name = 'HydrusValid'
except ValueError as e:
diff --git a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
index b9b5a6c84..ed7c7aaec 100644
--- a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
+++ b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py
@@ -8,6 +8,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
+from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIRatings
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUIPredicatesSingle
@@ -109,7 +110,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
QW.QWidget.__init__( self, parent )
- self.setToolTip( 'Set "is" and leave rating null to search for "unrated".' )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set "is" and leave rating null to search for "unrated".' ) )
self._service_key = service_key
@@ -245,7 +246,7 @@ def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Op
QW.QWidget.__init__( self, parent )
- self.setToolTip( 'Set "is" and leave rating null to search for "unrated".' )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set "is" and leave rating null to search for "unrated".' ) )
self._service_key = service_key
diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
index f43e176f6..0ed0f7963 100644
--- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
+++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
@@ -21,7 +21,8 @@
from hydrus.client.gui import ClientGUITime
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIBytes
+from hydrus.client.gui.widgets import ClientGUINumberTest
from hydrus.client.search import ClientSearch
class StaticSystemPredicateButton( QW.QWidget ):
@@ -1079,7 +1080,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, unit_string = 'fps', appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 5 )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, unit_string = 'fps', appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 5 )
#
@@ -1145,7 +1146,7 @@ def __init__( self, parent, predicate ):
self._sign.SetValue( predicate.IsInclusive() )
- hashes_text = os.linesep.join( [ hash.hex() for hash in hashes ] )
+ hashes_text = '\n'.join( [ hash.hex() for hash in hashes ] )
self._hashes.setPlainText( hashes_text )
@@ -1260,7 +1261,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
#
@@ -1761,7 +1762,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, appropriate_absolute_plus_or_minus_default = 300 )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators, max = 1000000, appropriate_absolute_plus_or_minus_default = 300 )
#
@@ -1807,7 +1808,7 @@ def __init__( self, parent, predicate ):
self._namespace = QW.QLineEdit( self )
self._namespace.setPlaceholderText( 'Leave empty for unnamespaced, \'*\' for all namespaces' )
- self._namespace.setToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' )
+ self._namespace.setToolTip( ClientGUIFunctions.WrapToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' ) )
self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=','>'] )
@@ -1897,7 +1898,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators )
#
@@ -1949,7 +1950,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators )
#
@@ -2003,7 +2004,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, max = 100000000, appropriate_absolute_plus_or_minus_default = 5000 )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators, max = 100000000, appropriate_absolute_plus_or_minus_default = 5000 )
#
@@ -2132,11 +2133,11 @@ def __init__( self, parent, predicate ):
( pixel_hashes, perceptual_hashes, hamming_distance ) = predicate.GetValue()
- hashes_text = os.linesep.join( [ hash.hex() for hash in pixel_hashes ] )
+ hashes_text = '\n'.join( [ hash.hex() for hash in pixel_hashes ] )
self._pixel_hashes.setPlainText( hashes_text )
- hashes_text = os.linesep.join( [ hash.hex() for hash in perceptual_hashes ] )
+ hashes_text = '\n'.join( [ hash.hex() for hash in perceptual_hashes ] )
self._perceptual_hashes.setPlainText( hashes_text )
@@ -2325,7 +2326,7 @@ def __init__( self, parent, predicate ):
( hashes, hamming_distance ) = predicate.GetValue()
- hashes_text = os.linesep.join( [ hash.hex() for hash in hashes ] )
+ hashes_text = '\n'.join( [ hash.hex() for hash in hashes ] )
self._hashes.setPlainText( hashes_text )
@@ -2385,7 +2386,7 @@ def __init__( self, parent, predicate ):
self._sign = QP.RadioBox( self, choices=['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] )
- self._bytes = ClientGUIControls.BytesControl( self )
+ self._bytes = ClientGUIBytes.BytesControl( self )
#
@@ -2450,7 +2451,7 @@ def __init__( self, parent, predicate ):
self._namespace.setText( namespace )
self._namespace.setPlaceholderText( 'Leave empty for unnamespaced, \'*\' for all namespaces' )
- self._namespace.setToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' )
+ self._namespace.setToolTip( ClientGUIFunctions.WrapToolTip( 'Leave empty for unnamespaced, \'*\' for all namespaces. Other wildcards also supported.' ) )
self._sign.SetStringSelection( sign )
self._num.setValue( num )
@@ -2498,7 +2499,7 @@ def __init__( self, parent, predicate ):
ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
]
- self._number_test = ClientGUIControls.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
+ self._number_test = ClientGUINumberTest.NumberTestWidget( self, allowed_operators = allowed_operators, unit_string = 'px', appropriate_absolute_plus_or_minus_default = 200 )
#
diff --git a/hydrus/client/gui/search/ClientGUISearch.py b/hydrus/client/gui/search/ClientGUISearch.py
index 3cfb5ce80..7f0bd09e7 100644
--- a/hydrus/client/gui/search/ClientGUISearch.py
+++ b/hydrus/client/gui/search/ClientGUISearch.py
@@ -663,7 +663,7 @@ def __init__( self, parent, predicate: ClientSearch.Predicate ):
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT:
label = 'system:limit clips a large search result down to the given number of files. It is very useful for processing in smaller batches.'
- label += os.linesep * 2
+ label += '\n' * 2
label += 'For all the simpler sorts (filesize, duration, etc...), it will select the n largest/smallest in the result set appropriate for that sort. For complicated sorts like tags, it will sample randomly.'
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 64 ), ), show_remove_button = False ) )
@@ -892,7 +892,7 @@ def __init__( self, parent, predicate_panel_class, predicate ):
QW.QWidget.__init__( self, parent )
self._defaults_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().star, self._DefaultsMenu )
- self._defaults_button.setToolTip( 'Set a new default.' )
+ self._defaults_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set a new default.' ) )
self._predicate_panel = predicate_panel_class( self, predicate )
self._parent = parent
@@ -1032,7 +1032,7 @@ def SetValue( self, tag_context: ClientSearch.TagContext ):
self.setText( label )
- self.setToolTip( label )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( label ) )
if self._tag_context != original_tag_context:
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index 8bb8ae06a..790b7510b 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -9,7 +9,6 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTime
@@ -38,9 +37,10 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.media import ClientGUIMediaSimpleActions
+from hydrus.client.gui.widgets import ClientGUIBandwidth
from hydrus.client.gui.widgets import ClientGUIColourPicker
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientRatings
@@ -297,14 +297,14 @@ def UserIsOKToOK( self ):
if len( deletee_service_names ) > 0:
message = 'You are about to delete the following services:'
- message += os.linesep * 2
- message += os.linesep.join( deletee_service_names )
- message += os.linesep * 2
+ message += '\n' * 2
+ message += '\n'.join( deletee_service_names )
+ message += '\n' * 2
message += 'Are you absolutely sure this is correct?'
if tag_service_in_deletes:
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If the tag service you are deleting is very large, this operation may take a very very long time. You client will lock up until it is done.'
@@ -933,10 +933,10 @@ def _SelectAccountTypeForAutoAccountCreation( self, account_types: typing.List[
unavailable_texts.append( text )
- unavailable_text = os.linesep * 2
+ unavailable_text = '\n' * 2
unavailable_text += 'These other account types are currently in short supply and will be available after a delay:'
- unavailable_text += os.linesep * 2
- unavailable_text += os.linesep.join( unavailable_texts )
+ unavailable_text += '\n' * 2
+ unavailable_text += '\n'.join( unavailable_texts )
if len( available_account_types ) == 1:
@@ -1121,19 +1121,19 @@ def __init__( self, parent, service_type, dictionary ):
self._port = ClientGUICommon.BetterSpinBox( self._client_server_options_panel, min = 1, max = 65535 )
self._allow_non_local_connections = QW.QCheckBox( self._client_server_options_panel )
- self._allow_non_local_connections.setToolTip( 'Allow other computers on the network to talk to use service. If unchecked, only localhost can talk to it. On Windows, the first time you start a local service that allows non-local connections, you will get the Windows firewall popup dialog when you ok the main services dialog.' )
+ self._allow_non_local_connections.setToolTip( ClientGUIFunctions.WrapToolTip( 'Allow other computers on the network to talk to use service. If unchecked, only localhost can talk to it. On Windows, the first time you start a local service that allows non-local connections, you will get the Windows firewall popup dialog when you ok the main services dialog.' ) )
self._use_https = QW.QCheckBox( self._client_server_options_panel )
- self._use_https.setToolTip( 'Host the server using https instead of http. This uses a self-signed certificate, stored in your db folder, which is imperfect but better than straight http. Your software (e.g. web browser testing the Client API welcome page) may need to go through a manual \'approve this ssl certificate\' process before it can work. If you host your client on a real DNS domain and acquire your own signed certificate, you can replace the cert+key file pair with that.' )
+ self._use_https.setToolTip( ClientGUIFunctions.WrapToolTip( 'Host the server using https instead of http. This uses a self-signed certificate, stored in your db folder, which is imperfect but better than straight http. Your software (e.g. web browser testing the Client API welcome page) may need to go through a manual \'approve this ssl certificate\' process before it can work. If you host your client on a real DNS domain and acquire your own signed certificate, you can replace the cert+key file pair with that.' ) )
self._support_cors = QW.QCheckBox( self._client_server_options_panel )
- self._support_cors.setToolTip( 'Have this server support Cross-Origin Resource Sharing, which allows web browsers to access it off other domains. Turn this on if you want to access this service through a web-based wrapper (e.g. a booru wrapper) hosted on another domain.' )
+ self._support_cors.setToolTip( ClientGUIFunctions.WrapToolTip( 'Have this server support Cross-Origin Resource Sharing, which allows web browsers to access it off other domains. Turn this on if you want to access this service through a web-based wrapper (e.g. a booru wrapper) hosted on another domain.' ) )
self._log_requests = QW.QCheckBox( self._client_server_options_panel )
- self._log_requests.setToolTip( 'Hydrus server services will write a brief anonymous line to the log for every request made, but for the client services this tends to be a bit spammy. You probably want this off unless you are testing something.' )
+ self._log_requests.setToolTip( ClientGUIFunctions.WrapToolTip( 'Hydrus server services will write a brief anonymous line to the log for every request made, but for the client services this tends to be a bit spammy. You probably want this off unless you are testing something.' ) )
self._use_normie_eris = QW.QCheckBox( self._client_server_options_panel )
- self._use_normie_eris.setToolTip( 'Use alternate ASCII art on the root page of the server.' )
+ self._use_normie_eris.setToolTip( ClientGUIFunctions.WrapToolTip( 'Use alternate ASCII art on the root page of the server.' ) )
self._upnp = ClientGUICommon.NoneableSpinCtrl( self._client_server_options_panel, none_phrase = 'do not forward port', max = 65535 )
@@ -1141,9 +1141,9 @@ def __init__( self, parent, service_type, dictionary ):
self._external_host_override = ClientGUICommon.NoneableTextCtrl( self._client_server_options_panel )
self._external_port_override = ClientGUICommon.NoneableTextCtrl( self._client_server_options_panel )
- self._external_port_override.setToolTip( 'Setting this to a non-none empty string will forego the \':\' in the URL.' )
+ self._external_port_override.setToolTip( ClientGUIFunctions.WrapToolTip( 'Setting this to a non-none empty string will forego the \':\' in the URL.' ) )
- self._bandwidth_rules = ClientGUIControls.BandwidthRulesCtrl( self._client_server_options_panel, dictionary[ 'bandwidth_rules' ] )
+ self._bandwidth_rules = ClientGUIBandwidth.BandwidthRulesCtrl( self._client_server_options_panel, dictionary[ 'bandwidth_rules' ] )
#
@@ -1487,13 +1487,13 @@ def __init__( self, parent, dictionary ):
interaction_panel = ClientGUIPanels.IPFSDaemonStatusAndInteractionPanel( self, self.parentWidget().GetValue )
- tts = 'This is an *experimental* IPFS filestore that will not copy files when they are pinned. IPFS will refer to files using their original location (i.e. your hydrus client\'s file folder(s)).'
- tts += os.linesep * 2
- tts += 'Only turn this on if you know what it is.'
+ tt = 'This is an *experimental* IPFS filestore that will not copy files when they are pinned. IPFS will refer to files using their original location (i.e. your hydrus client\'s file folder(s)).'
+ tt += '\n' * 2
+ tt += 'Only turn this on if you know what it is.'
self._use_nocopy = QW.QCheckBox( self )
- self._use_nocopy.setToolTip( tts )
+ self._use_nocopy.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
portable_initial_dict = dict( dictionary[ 'nocopy_abs_path_translations' ] )
@@ -1529,17 +1529,17 @@ def __init__( self, parent, dictionary ):
self._multihash_prefix = QW.QLineEdit( self )
- tts = 'When you tell the client to copy a ipfs multihash to your clipboard, it will prefix it with whatever is set here.'
- tts += os.linesep * 2
- tts += 'Use this if you want to copy a full gateway url. For instance, you could put here:'
- tts += os.linesep * 2
- tts += 'http://127.0.0.1:8080/ipfs/'
- tts += os.linesep
- tts += '-or-'
- tts += os.linesep
- tts += 'http://ipfs.io/ipfs/'
+ tt = 'When you tell the client to copy a ipfs multihash to your clipboard, it will prefix it with whatever is set here.'
+ tt += '\n' * 2
+ tt += 'Use this if you want to copy a full gateway url. For instance, you could put here:'
+ tt += '\n' * 2
+ tt += 'http://127.0.0.1:8080/ipfs/'
+ tt += '\n'
+ tt += '-or-'
+ tt += '\n'
+ tt += 'http://ipfs.io/ipfs/'
- self._multihash_prefix.setToolTip( tts )
+ self._multihash_prefix.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
#
@@ -1568,11 +1568,11 @@ def __init__( self, parent, dictionary ):
def _ShowHelp( self ):
message = '\'nocopy\' is experimental and advanced!'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'In order to add a file through \'nocopy\', IPFS needs to be given a path that is beneath the directory in which its datastore is. Usually this is your USERDIR (default IPFS location is ~/.ipfs). Also, if your IPFS daemon runs on another computer, that path needs to be according to that machine\'s filesystem (and, perhaps, pointing to a shared folder that can stores your hydrus files).'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If your hydrus client_files directory is not already in your USERDIR, you will need to make some symlinks and then put these paths in the control so hydrus knows how to translate the paths when it pins.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'e.g. If you symlink E:\\hydrus\\files to C:\\users\\you\\ipfs_maps\\e_media, then put that same C:\\users\\you\\ipfs_maps\\e_media in the right column for that hydrus file location, and you _should_ be good.'
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -1623,7 +1623,7 @@ def __init__( self, parent, service ):
self._service = service
self._id_button = ClientGUICommon.BetterButton( self, 'id', self._GetAndShowID )
- self._id_button.setToolTip( 'Click to fetch your service\'s database id.' )
+ self._id_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Click to fetch your service\'s database id.' ) )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._id_button, 4 )
@@ -2468,7 +2468,7 @@ def __init__( self, parent, service ):
self._rule_widgets = []
self._network_sync_paused_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().pause, self._PausePlayNetworkSync )
- self._network_sync_paused_button.setToolTip( 'pause/play account sync' )
+ self._network_sync_paused_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play account sync' ) )
self._refresh_account_button = ClientGUICommon.BetterButton( self, 'refresh account', self._RefreshAccount )
self._copy_account_key_button = ClientGUICommon.BetterButton( self, 'copy account id', self._CopyAccountKey )
@@ -2718,10 +2718,10 @@ def __init__( self, parent, service ):
self._repo_options_st = ClientGUICommon.BetterStaticText( self._network_panel )
tt = 'The update period is how often the repository bundles its recent uploads into a package for users to download. Anything you upload may take this long for other people to see.'
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'The anonymisation period is how long it takes for account information to be scrubbed from content. After this time, server admins/janitors cannot tell which account uploaded something.'
- self._repo_options_st.setToolTip( tt )
+ self._repo_options_st.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._metadata_st = ClientGUICommon.BetterStaticText( self._network_panel )
@@ -2731,7 +2731,7 @@ def __init__( self, parent, service ):
self._download_progress = ClientGUICommon.TextAndGauge( self._network_panel )
self._update_downloading_paused_button = ClientGUICommon.BetterBitmapButton( self._network_panel, CC.global_pixmaps().pause, self._PausePlayUpdateDownloading )
- self._update_downloading_paused_button.setToolTip( 'pause/play update downloading' )
+ self._update_downloading_paused_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play update downloading' ) )
self._service_info_button = ClientGUICommon.BetterButton( self._network_panel, 'fetch service info', self._FetchServiceInfo )
@@ -2750,7 +2750,7 @@ def __init__( self, parent, service ):
self._processing_panel = ClientGUICommon.StaticBox( self, 'processing sync' )
self._update_processing_paused_button = ClientGUICommon.BetterBitmapButton( self._processing_panel, CC.global_pixmaps().pause, self._PausePlayUpdateProcessing )
- self._update_processing_paused_button.setToolTip( 'pause/play all update processing' )
+ self._update_processing_paused_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play all update processing' ) )
self._processing_definitions_progress = ClientGUICommon.TextAndGauge( self._processing_panel )
@@ -2765,7 +2765,7 @@ def __init__( self, parent, service ):
processing_progress = ClientGUICommon.TextAndGauge( self._processing_panel )
processing_paused_button = ClientGUICommon.BetterBitmapButton( self._processing_panel, CC.global_pixmaps().pause, self._PausePlayUpdateProcessing, content_type )
- processing_paused_button.setToolTip( 'pause/play update processing for {}'.format( HC.content_type_string_lookup[ content_type ] ) )
+ processing_paused_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'pause/play update processing for {}'.format( HC.content_type_string_lookup[ content_type ] ) ) )
self._content_types_to_gauges_and_buttons[ content_type ] = ( processing_progress, processing_paused_button )
@@ -2875,7 +2875,7 @@ def _DoAFullMetadataResync( self ):
name = self._service.GetName()
message = 'This will flag the client to resync the information about which update files it should download. It will occur on the next download sync.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is useful if the metadata archive has become unsynced, either due to a bug or a service switch. If it is not needed, it will not make any changes.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3007,12 +3007,12 @@ def publish_callable( service_info_dict ):
message = 'Note that num file hashes and tags here include deleted content so will likely not line up with your review services value, which is only for current content.'
- message += os.linesep * 2
+ message += '\n' * 2
tuples = [ ( HC.service_info_enum_str_lookup[ info_type ], HydrusData.ToHumanInt( service_info_dict[ info_type ] ) ) for info_type in service_info_types if info_type in service_info_dict ]
string_rows = [ '{}: {}'.format( info_type, info ) for ( info_type, info ) in tuples ]
- message += os.linesep.join( string_rows )
+ message += '\n'.join( string_rows )
ClientGUIDialogsMessage.ShowInformation( self, message )
@@ -3145,14 +3145,14 @@ def _Refresh( self ):
self._tag_filter_button.setEnabled( True )
- tt = 'See which tags this repository accepts. Summary:{}{}'.format( os.linesep * 2, tag_filter.ToPermittedString() )
+ tt = 'See which tags this repository accepts. Summary:{}{}'.format( '\n' * 2, tag_filter.ToPermittedString() )
- self._tag_filter_button.setToolTip( tt )
+ self._tag_filter_button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
except HydrusExceptions.DataMissing:
self._tag_filter_button.setEnabled( False )
- self._tag_filter_button.setToolTip( 'Do not have a tag filter for this repository. Try refreshing your account, or, if your client is old, update it.' )
+ self._tag_filter_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Do not have a tag filter for this repository. Try refreshing your account, or, if your client is old, update it.' ) )
@@ -3175,7 +3175,7 @@ def do_it( service, my_updater ):
name = self._service.GetName()
message = 'This will command the client to reprocess all definition updates for {}. It will not delete anything.'.format( name )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is a only useful as a debug tool for filling in \'gaps\'. If you do not understand what this does, turn back now.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3207,7 +3207,7 @@ def do_it( service, my_updater, content_types_to_reset ):
name = self._service.GetName()
message = 'This will command the client to reprocess ({}) for {}. It will not delete anything.'.format( ', '.join( ( HC.content_type_string_lookup[ content_type ] for content_type in content_types ) ), name )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'This is a only useful as a debug tool for filling in \'gaps\' caused by processing bugs or database damage. If you do not understand what this does, turn back now.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3222,7 +3222,7 @@ def _Reset( self ):
name = self._service.GetName()
- message = 'This will delete all the processed information for ' + name + ' from the database, including definitions.' + os.linesep * 2 + 'Once the service is reset, you will have to reprocess everything from your downloaded update files. The client will naturally do this in its idle time as before, just starting over from the beginning.' + os.linesep * 2 + 'This is a severe maintenance task that is only appropriate after trying to recover from critical database error. If you do not understand what this does, click no!'
+ message = 'This will delete all the processed information for ' + name + ' from the database, including definitions.' + '\n' * 2 + 'Once the service is reset, you will have to reprocess everything from your downloaded update files. The client will naturally do this in its idle time as before, just starting over from the beginning.' + '\n' * 2 + 'This is a severe maintenance task that is only appropriate after trying to recover from critical database error. If you do not understand what this does, click no!'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3260,7 +3260,7 @@ def do_it( service, my_updater, content_types_to_reset ):
name = self._service.GetName()
message = 'You are about to delete and reprocess ({}) for {}.'.format( ', '.join( ( HC.content_type_string_lookup[ content_type ] for content_type in content_types ) ), name )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'It may take some time to delete it all, and then future idle time to reprocess. It is only worth doing this if you believe there are logical problems in the initial process. If you just want to fill in gaps, use that simpler maintenance task, which runs much faster.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3328,7 +3328,7 @@ def do_it( service, my_updater ):
def _SyncProcessingNow( self ):
message = 'This will tell the database to process any possible outstanding update files right now.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'You can still use the client while it runs, but it may make some things like autocomplete lookup a bit juddery.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -3513,7 +3513,7 @@ def _CopyMultihashes( self ):
multihash_prefix = self._service.GetMultihashPrefix()
- text = os.linesep.join( ( multihash_prefix + multihash for multihash in multihashes ) )
+ text = '\n'.join( ( multihash_prefix + multihash for multihash in multihashes ) )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -3778,7 +3778,7 @@ def _CopyExternalShareURL( self ):
urls.append( url )
- text = os.linesep.join( urls )
+ text = '\n'.join( urls )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -3801,7 +3801,7 @@ def _CopyInternalShareURL( self ):
urls.append( url )
- text = os.linesep.join( urls )
+ text = '\n'.join( urls )
CG.client_controller.pub( 'clipboard', 'text', text )
@@ -3970,7 +3970,7 @@ def __init__( self, parent, service ):
def _ClearRatings( self, advanced_action, action_description ):
message = 'Delete any ratings on this service for {}? THIS CANNOT BE UNDONE'.format( action_description )
- message += os.linesep * 2
+ message += '\n' * 2
message += 'Please note a client restart is needed to see the ratings disappear in media views.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -4142,7 +4142,7 @@ def __init__( self, parent, service ):
def _ClearTrash( self ):
message = 'This will completely clear your trash of all its files, deleting them permanently from the client. This operation cannot be undone.'
- message += os.linesep * 2
+ message += '\n' * 2
message += 'If you have many files in your trash, it will take some time to complete and for all the files to eventually be deleted.'
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = 'do it', no_label = 'forget it' )
@@ -4190,9 +4190,7 @@ def do_it( service ):
hashes = CG.client_controller.Read( 'trash_hashes' )
- from hydrus.client.gui import ClientGUIMediaActions
-
- ClientGUIMediaActions.UndeleteFiles( hashes )
+ ClientGUIMediaSimpleActions.UndeleteFiles( hashes )
CG.client_controller.pub( 'service_updated', service )
diff --git a/hydrus/client/gui/services/ClientGUIServersideServices.py b/hydrus/client/gui/services/ClientGUIServersideServices.py
index 49f6d8b81..406156d51 100644
--- a/hydrus/client/gui/services/ClientGUIServersideServices.py
+++ b/hydrus/client/gui/services/ClientGUIServersideServices.py
@@ -16,8 +16,9 @@
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.widgets import ClientGUIBandwidth
from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.gui.widgets import ClientGUIControls
+from hydrus.client.gui.widgets import ClientGUIBytes
from hydrus.client.gui.widgets import ClientGUIMenuButton
class EditServersideService( ClientGUIScrolledPanels.EditPanel ):
@@ -145,7 +146,7 @@ def __init__( self, parent: QW.QWidget, dictionary ):
bandwidth_rules = dictionary[ 'bandwidth_rules' ]
- self._bandwidth_rules = ClientGUIControls.BandwidthRulesCtrl( self, bandwidth_rules )
+ self._bandwidth_rules = ClientGUIBandwidth.BandwidthRulesCtrl( self, bandwidth_rules )
#
@@ -173,7 +174,7 @@ def __init__( self, parent: QW.QWidget, dictionary ):
ClientGUICommon.StaticBox.__init__( self, parent, 'file repository' )
self._log_uploader_ips = QW.QCheckBox( self )
- self._max_storage = ClientGUIControls.NoneableBytesControl( self, initial_value = 5 * 1024 * 1024 * 1024 )
+ self._max_storage = ClientGUIBytes.NoneableBytesControl( self, initial_value = 5 * 1024 * 1024 * 1024 )
#
@@ -219,7 +220,7 @@ def __init__( self, parent: QW.QWidget, dictionary ):
bandwidth_rules = dictionary[ 'server_bandwidth_rules' ]
- self._bandwidth_rules = ClientGUIControls.BandwidthRulesCtrl( self, bandwidth_rules )
+ self._bandwidth_rules = ClientGUIBandwidth.BandwidthRulesCtrl( self, bandwidth_rules )
#
diff --git a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
index 063f2cfd2..c5b776d87 100644
--- a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
+++ b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
@@ -541,6 +541,12 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
self._file_filter.addItem( file_filter.ToString(), file_filter )
+ #
+
+ self._hamming_distance_panel = QW.QWidget( self )
+
+ self._hamming_distance = ClientGUICommon.BetterSpinBox( self._hamming_distance_panel, min = 0, max = 64 )
+
#
hbox = QP.HBoxLayout()
@@ -551,6 +557,16 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
#
+ rows = []
+
+ rows.append( ( 'Search distance:', self._hamming_distance ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( self._hamming_distance_panel, rows )
+
+ self._hamming_distance_panel.setLayout( gridbox )
+
+ #
+
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._simple_actions, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
@@ -558,6 +574,7 @@ def __init__( self, parent: QW.QWidget, shortcuts_name: str ):
QP.AddToLayout( vbox, self._seek_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._thumbnail_move_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._file_filter_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+ QP.AddToLayout( vbox, self._hamming_distance_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._thumbnail_rearrange_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.setLayout( vbox )
@@ -576,6 +593,7 @@ def _UpdateControls( self ):
self._seek_panel.setVisible( action == CAC.SIMPLE_MEDIA_SEEK_DELTA )
self._thumbnail_move_panel.setVisible( action == CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS )
self._file_filter_panel.setVisible( action == CAC.SIMPLE_SELECT_FILES )
+ self._hamming_distance_panel.setVisible( action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES )
def GetValue( self ):
@@ -616,6 +634,12 @@ def GetValue( self ):
simple_data = file_filter
+ elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
+
+ hamming_distance = self._hamming_distance.value()
+
+ simple_data = hamming_distance
+
elif action == CAC.SIMPLE_REARRANGE_THUMBNAILS:
rearrange_type = self._thumbnail_rearrange_type.GetValue()
@@ -669,6 +693,12 @@ def SetValue( self, command: CAC.ApplicationCommand ):
self._file_filter.SetValue( file_filter )
+ elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
+
+ hamming_distance = command.GetSimpleData()
+
+ self._hamming_distance.setValue( hamming_distance )
+
elif action == CAC.SIMPLE_REARRANGE_THUMBNAILS:
( rearrange_type, rearrange_data ) = command.GetSimpleData()
diff --git a/hydrus/client/gui/widgets/ClientGUIBandwidth.py b/hydrus/client/gui/widgets/ClientGUIBandwidth.py
new file mode 100644
index 000000000..6c987e1a2
--- /dev/null
+++ b/hydrus/client/gui/widgets/ClientGUIBandwidth.py
@@ -0,0 +1,264 @@
+import typing
+
+from qtpy import QtWidgets as QW
+
+from hydrus.core import HydrusConstants as HC
+from hydrus.core import HydrusData
+from hydrus.core import HydrusTime
+from hydrus.core.networking import HydrusNetworking
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client.gui import ClientGUIScrolledPanels
+from hydrus.client.gui import ClientGUITime
+from hydrus.client.gui import ClientGUITopLevelWindowsPanels
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
+from hydrus.client.gui.lists import ClientGUIListCtrl
+from hydrus.client.gui.widgets import ClientGUIBytes
+from hydrus.client.gui.widgets import ClientGUICommon
+
+class BandwidthRulesCtrl( ClientGUICommon.StaticBox ):
+
+ def __init__( self, parent, bandwidth_rules ):
+
+ ClientGUICommon.StaticBox.__init__( self, parent, 'bandwidth rules' )
+
+ listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
+
+ # example for later:
+ '''
+ def sort_call( desired_columns, rule ):
+
+ ( bandwidth_type, time_delta, max_allowed ) = rule
+
+ sort_time_delta = SafeNoneInt( time_delta )
+
+ result = {}
+
+ result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.MAX_ALLOWED ] = max_allowed
+ result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.EVERY ] = sort_time_delta
+
+ return result
+
+
+ def display_call( desired_columns, rule ):
+
+ ( bandwidth_type, time_delta, max_allowed ) = rule
+
+ if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
+
+ pretty_max_allowed = HydrusData.ToHumanBytes( max_allowed )
+
+ elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
+
+ pretty_max_allowed = '{} requests'.format( HydrusData.ToHumanInt( max_allowed ) )
+
+
+ pretty_time_delta = HydrusTime.TimeDeltaToPrettyTimeDelta( time_delta )
+
+ result = {}
+
+ result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.MAX_ALLOWED ] = pretty_max_allowed
+ result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.EVERY ] = pretty_time_delta
+
+ return result
+
+'''
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_BANDWIDTH_RULES.ID, 8, self._ConvertRuleToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+
+ listctrl_panel.SetListCtrl( self._listctrl )
+
+ listctrl_panel.AddButton( 'add', self._Add )
+ listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
+ listctrl_panel.AddDeleteButton()
+
+ #
+
+ self._listctrl.AddDatas( bandwidth_rules.GetRules() )
+
+ self._listctrl.Sort()
+
+ #
+
+ self.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+
+ def _Add( self ):
+
+ rule = ( HC.BANDWIDTH_TYPE_DATA, None, 1024 * 1024 * 100 )
+
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit rule' ) as dlg:
+
+ panel = self._EditPanel( dlg, rule )
+
+ dlg.SetPanel( panel )
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ new_rule = panel.GetValue()
+
+ self._listctrl.AddDatas( ( new_rule, ) )
+
+ self._listctrl.Sort()
+
+
+
+
+ def _ConvertRuleToListCtrlTuples( self, rule ):
+
+ ( bandwidth_type, time_delta, max_allowed ) = rule
+
+ pretty_time_delta = HydrusTime.TimeDeltaToPrettyTimeDelta( time_delta )
+
+ if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
+
+ pretty_max_allowed = HydrusData.ToHumanBytes( max_allowed )
+
+ elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
+
+ pretty_max_allowed = HydrusData.ToHumanInt( max_allowed ) + ' requests'
+
+
+ sort_time_delta = ClientGUIListCtrl.SafeNoneInt( time_delta )
+
+ sort_tuple = ( max_allowed, sort_time_delta )
+ display_tuple = ( pretty_max_allowed, pretty_time_delta )
+
+ return ( display_tuple, sort_tuple )
+
+
+ def _Edit( self ):
+
+ selected_rules = self._listctrl.GetData( only_selected = True )
+
+ edited_datas = []
+
+ for rule in selected_rules:
+
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit rule' ) as dlg:
+
+ panel = self._EditPanel( dlg, rule )
+
+ dlg.SetPanel( panel )
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ edited_rule = panel.GetValue()
+
+ self._listctrl.DeleteDatas( ( rule, ) )
+
+ self._listctrl.AddDatas( ( edited_rule, ) )
+
+ edited_datas.append( edited_rule )
+
+ else:
+
+ break
+
+
+
+
+ self._listctrl.SelectDatas( edited_datas )
+
+ self._listctrl.Sort()
+
+
+ def GetValue( self ):
+
+ bandwidth_rules = HydrusNetworking.BandwidthRules()
+
+ for rule in self._listctrl.GetData():
+
+ ( bandwidth_type, time_delta, max_allowed ) = rule
+
+ bandwidth_rules.AddRule( bandwidth_type, time_delta, max_allowed )
+
+
+ return bandwidth_rules
+
+
+ class _EditPanel( ClientGUIScrolledPanels.EditPanel ):
+
+ def __init__( self, parent, rule ):
+
+ ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
+
+ self._bandwidth_type = ClientGUICommon.BetterChoice( self )
+
+ self._bandwidth_type.addItem( 'data', HC.BANDWIDTH_TYPE_DATA )
+ self._bandwidth_type.addItem( 'requests', HC.BANDWIDTH_TYPE_REQUESTS )
+
+ self._bandwidth_type.currentIndexChanged.connect( self._UpdateEnabled )
+
+ self._max_allowed_bytes = ClientGUIBytes.BytesControl( self )
+ self._max_allowed_requests = ClientGUICommon.BetterSpinBox( self, min=1, max=1048576 )
+
+ self._time_delta = ClientGUITime.TimeDeltaButton( self, min = 1, days = True, hours = True, minutes = True, seconds = True, monthly_allowed = True )
+
+ #
+
+ ( bandwidth_type, time_delta, max_allowed ) = rule
+
+ self._bandwidth_type.SetValue( bandwidth_type )
+
+ self._time_delta.SetValue( time_delta )
+
+ if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
+
+ self._max_allowed_bytes.SetValue( max_allowed )
+
+ else:
+
+ self._max_allowed_requests.setValue( max_allowed )
+
+
+ self._UpdateEnabled()
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._max_allowed_bytes, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._max_allowed_requests, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._bandwidth_type, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,' every '), CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._time_delta, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self.widget().setLayout( hbox )
+
+
+ def _UpdateEnabled( self ):
+
+ bandwidth_type = self._bandwidth_type.GetValue()
+
+ if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
+
+ self._max_allowed_bytes.show()
+ self._max_allowed_requests.hide()
+
+ elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
+
+ self._max_allowed_bytes.hide()
+ self._max_allowed_requests.show()
+
+
+ def GetValue( self ):
+
+ bandwidth_type = self._bandwidth_type.GetValue()
+
+ time_delta = self._time_delta.GetValue()
+
+ if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
+
+ max_allowed = self._max_allowed_bytes.GetValue()
+
+ elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
+
+ max_allowed = self._max_allowed_requests.value()
+
+
+ return ( bandwidth_type, time_delta, max_allowed )
+
+
+
diff --git a/hydrus/client/gui/widgets/ClientGUIBytes.py b/hydrus/client/gui/widgets/ClientGUIBytes.py
new file mode 100644
index 000000000..e91bedf74
--- /dev/null
+++ b/hydrus/client/gui/widgets/ClientGUIBytes.py
@@ -0,0 +1,177 @@
+import typing
+
+from qtpy import QtCore as QC
+from qtpy import QtWidgets as QW
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client.gui import ClientGUIFunctions
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.widgets import ClientGUICommon
+
+class BytesControl( QW.QWidget ):
+
+ valueChanged = QC.Signal()
+
+ def __init__( self, parent, initial_value = 65536 ):
+
+ QW.QWidget.__init__( self, parent )
+
+ self._spin = ClientGUICommon.BetterSpinBox( self, min=0, max=1048576 )
+
+ self._unit = ClientGUICommon.BetterChoice( self )
+
+ self._unit.addItem( 'B', 1 )
+ self._unit.addItem( 'KB', 1024 )
+ self._unit.addItem( 'MB', 1024 ** 2 )
+ self._unit.addItem( 'GB', 1024 ** 3 )
+ self._unit.addItem( 'TB', 1024 ** 4 )
+
+ #
+
+ self.SetValue( initial_value )
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._spin, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._unit, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self.setLayout( hbox )
+
+ min_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._unit, 8 )
+
+ self._unit.setMinimumWidth( min_width )
+
+ self._spin.valueChanged.connect( self._HandleValueChanged )
+ self._unit.currentIndexChanged.connect( self._HandleValueChanged )
+
+
+ def _HandleValueChanged( self, val ):
+
+ self.valueChanged.emit()
+
+
+ def GetSeparatedValue( self ):
+
+ return ( self._spin.value(), self._unit.GetValue() )
+
+
+ def GetValue( self ):
+
+ return self._spin.value() * self._unit.GetValue()
+
+
+ def SetSeparatedValue( self, value, unit ):
+
+ return ( self._spin.setValue( value ), self._unit.SetValue( unit ) )
+
+
+ def SetValue( self, value: int ):
+
+ max_unit = 1024 ** 4
+
+ unit = 1
+
+ while value % 1024 == 0 and unit < max_unit:
+
+ value //= 1024
+
+ unit *= 1024
+
+
+ self._spin.setValue( value )
+ self._unit.SetValue( unit )
+
+
+
+class NoneableBytesControl( QW.QWidget ):
+
+ valueChanged = QC.Signal()
+
+ def __init__( self, parent, initial_value = 65536, none_label = 'no limit' ):
+
+ QW.QWidget.__init__( self, parent )
+
+ self._bytes = BytesControl( self )
+
+ self._none_checkbox = QW.QCheckBox( none_label, self )
+
+ #
+
+ self.SetValue( initial_value )
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._bytes, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._none_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self.setLayout( hbox )
+
+ #
+
+ self._none_checkbox.clicked.connect( self._UpdateEnabled )
+
+ self._bytes.valueChanged.connect( self._HandleValueChanged )
+ self._none_checkbox.clicked.connect( self._HandleValueChanged )
+
+
+ def _UpdateEnabled( self ):
+
+ if self._none_checkbox.isChecked():
+
+ self._bytes.setEnabled( False )
+
+ else:
+
+ self._bytes.setEnabled( True )
+
+
+
+ def _HandleValueChanged( self ):
+
+ self.valueChanged.emit()
+
+
+ def GetValue( self ):
+
+ if self._none_checkbox.isChecked():
+
+ return None
+
+ else:
+
+ return self._bytes.GetValue()
+
+
+
+ def setToolTip( self, text ):
+
+ QW.QWidget.setToolTip( self, text )
+
+ for c in self.children():
+
+ if isinstance( c, QW.QWidget ):
+
+ c.setToolTip( text )
+
+
+
+ def SetValue( self, value ):
+
+ if value is None:
+
+ self._none_checkbox.setChecked( True )
+
+ else:
+
+ self._none_checkbox.setChecked( False )
+
+ self._bytes.SetValue( value )
+
+
+ self._UpdateEnabled()
+
+
diff --git a/hydrus/client/gui/widgets/ClientGUIColourPicker.py b/hydrus/client/gui/widgets/ClientGUIColourPicker.py
index 26c0d7f5a..e2e965bf7 100644
--- a/hydrus/client/gui/widgets/ClientGUIColourPicker.py
+++ b/hydrus/client/gui/widgets/ClientGUIColourPicker.py
@@ -5,15 +5,59 @@
from qtpy import QtGui as QG
from hydrus.core import HydrusData
-from hydrus.core import HydrusGlobals as HG
+from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogsMessage
-from hydrus.client.gui import ClientGUIFunctions
+from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIStyle
from hydrus.client.gui import QtInit
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.widgets import ClientGUICommon
+
+class AlphaColourControl( QW.QWidget ):
+
+ def __init__( self, parent ):
+
+ QW.QWidget.__init__( self, parent )
+
+ self._colour_picker = ColourPickerButton( self )
+
+ self._alpha_selector = ClientGUICommon.BetterSpinBox( self, min=0, max=255 )
+
+ hbox = QP.HBoxLayout( spacing = 5 )
+
+ QP.AddToLayout( hbox, self._colour_picker, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'alpha:'), CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._alpha_selector, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ hbox.addStretch( 1 )
+
+ self.setLayout( hbox )
+
+
+ def GetValue( self ):
+
+ colour = self._colour_picker.GetColour()
+
+ a = self._alpha_selector.value()
+
+ colour.setAlpha( a )
+
+ return colour
+
+
+ def SetValue( self, colour: QG.QColor ):
+
+ picker_colour = QG.QColor( colour.rgb() )
+
+ self._colour_picker.SetColour( picker_colour )
+
+ self._alpha_selector.setValue( colour.alpha() )
+
+
def EditColour( win: QW.QWidget, colour: QG.QColor ):
@@ -159,7 +203,7 @@ def _ImportHexFromClipboard( self ):
except Exception as e:
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'A hex colour like #FF0050', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'A hex colour like #FF0050', e )
return
diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py
index 0e429ea40..ec4c7f320 100644
--- a/hydrus/client/gui/widgets/ClientGUICommon.py
+++ b/hydrus/client/gui/widgets/ClientGUICommon.py
@@ -10,7 +10,6 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@@ -21,7 +20,6 @@
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
-from hydrus.client.gui.widgets import ClientGUIColourPicker
from hydrus.client.networking import ClientNetworkingFunctions
def AddGridboxStretchSpacer( win: QW.QWidget, layout: QW.QGridLayout ):
@@ -183,7 +181,7 @@ def _RefreshToolTip( self ):
if self._simple_shortcut_command is not None:
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += '----------'
names_to_shortcuts = ClientGUIShortcuts.shortcuts_manager().GetNamesToShortcuts( self._simple_shortcut_command )
@@ -207,19 +205,19 @@ def _RefreshToolTip( self ):
pretty_name = name
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += ', '.join( shortcut_strings )
- tt += os.linesep
+ tt += '\n'
tt += '({}->{})'.format( pretty_name, CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] )
else:
- tt += os.linesep * 2
+ tt += '\n' * 2
tt += 'no shortcuts set'
- tt += os.linesep
+ tt += '\n'
tt += '({})'.format( CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] )
@@ -701,7 +699,7 @@ def setText( self, text ):
if self._tooltip_label:
- self.setToolTip( text )
+ self.setToolTip( ClientGUIFunctions.WrapToolTip( text ) )
@@ -935,47 +933,6 @@ def Invert( self ):
-class AlphaColourControl( QW.QWidget ):
-
- def __init__( self, parent ):
-
- QW.QWidget.__init__( self, parent )
-
- self._colour_picker = ClientGUIColourPicker.ColourPickerButton( self )
-
- self._alpha_selector = BetterSpinBox( self, min=0, max=255 )
-
- hbox = QP.HBoxLayout( spacing = 5 )
-
- QP.AddToLayout( hbox, self._colour_picker, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, BetterStaticText(self,'alpha:'), CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._alpha_selector, CC.FLAGS_CENTER_PERPENDICULAR )
-
- hbox.addStretch( 1 )
-
- self.setLayout( hbox )
-
-
- def GetValue( self ):
-
- colour = self._colour_picker.GetColour()
-
- a = self._alpha_selector.value()
-
- colour.setAlpha( a )
-
- return colour
-
-
- def SetValue( self, colour: QG.QColor ):
-
- picker_colour = QG.QColor( colour.rgb() )
-
- self._colour_picker.SetColour( picker_colour )
-
- self._alpha_selector.setValue( colour.alpha() )
-
-
class ExportPatternButton( BetterButton ):
def __init__( self, parent ):
@@ -1008,6 +965,7 @@ def _Hit( self ):
CGC.core().PopupMenu( self, menu )
+
class Gauge( QW.QProgressBar ):
def __init__( self, *args, **kwargs ):
@@ -1687,6 +1645,7 @@ def setReadOnly( self, value: bool ):
self._text.setReadOnly( value )
self._checkbox.setEnabled( not value )
+
def setToolTip( self, text ):
QW.QWidget.setToolTip( self, text )
diff --git a/hydrus/client/gui/widgets/ClientGUIControls.py b/hydrus/client/gui/widgets/ClientGUIControls.py
deleted file mode 100644
index a9a28ad2a..000000000
--- a/hydrus/client/gui/widgets/ClientGUIControls.py
+++ /dev/null
@@ -1,705 +0,0 @@
-import typing
-
-from qtpy import QtCore as QC
-from qtpy import QtWidgets as QW
-
-from hydrus.core import HydrusConstants as HC
-from hydrus.core import HydrusData
-from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
-from hydrus.core import HydrusText
-from hydrus.core import HydrusTime
-from hydrus.core.networking import HydrusNetworking
-
-from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientGlobals as CG
-from hydrus.client.gui import ClientGUIDialogsMessage
-from hydrus.client.gui import ClientGUIFunctions
-from hydrus.client.gui import ClientGUIScrolledPanels
-from hydrus.client.gui import ClientGUITime
-from hydrus.client.gui import ClientGUITopLevelWindowsPanels
-from hydrus.client.gui import QtPorting as QP
-from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
-from hydrus.client.gui.lists import ClientGUIListCtrl
-from hydrus.client.gui.widgets import ClientGUICommon
-from hydrus.client.search import ClientSearch
-
-class BandwidthRulesCtrl( ClientGUICommon.StaticBox ):
-
- def __init__( self, parent, bandwidth_rules ):
-
- ClientGUICommon.StaticBox.__init__( self, parent, 'bandwidth rules' )
-
- listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
-
- # example for later:
- '''
- def sort_call( desired_columns, rule ):
-
- ( bandwidth_type, time_delta, max_allowed ) = rule
-
- sort_time_delta = SafeNoneInt( time_delta )
-
- result = {}
-
- result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.MAX_ALLOWED ] = max_allowed
- result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.EVERY ] = sort_time_delta
-
- return result
-
-
- def display_call( desired_columns, rule ):
-
- ( bandwidth_type, time_delta, max_allowed ) = rule
-
- if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
-
- pretty_max_allowed = HydrusData.ToHumanBytes( max_allowed )
-
- elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
-
- pretty_max_allowed = '{} requests'.format( HydrusData.ToHumanInt( max_allowed ) )
-
-
- pretty_time_delta = HydrusTime.TimeDeltaToPrettyTimeDelta( time_delta )
-
- result = {}
-
- result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.MAX_ALLOWED ] = pretty_max_allowed
- result[ CGLC.COLUMN_LIST_BANDWIDTH_RULES.EVERY ] = pretty_time_delta
-
- return result
-
-'''
-
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_BANDWIDTH_RULES.ID, 8, self._ConvertRuleToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
-
- listctrl_panel.SetListCtrl( self._listctrl )
-
- listctrl_panel.AddButton( 'add', self._Add )
- listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
- listctrl_panel.AddDeleteButton()
-
- #
-
- self._listctrl.AddDatas( bandwidth_rules.GetRules() )
-
- self._listctrl.Sort()
-
- #
-
- self.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
-
-
- def _Add( self ):
-
- rule = ( HC.BANDWIDTH_TYPE_DATA, None, 1024 * 1024 * 100 )
-
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit rule' ) as dlg:
-
- panel = self._EditPanel( dlg, rule )
-
- dlg.SetPanel( panel )
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- new_rule = panel.GetValue()
-
- self._listctrl.AddDatas( ( new_rule, ) )
-
- self._listctrl.Sort()
-
-
-
-
- def _ConvertRuleToListCtrlTuples( self, rule ):
-
- ( bandwidth_type, time_delta, max_allowed ) = rule
-
- pretty_time_delta = HydrusTime.TimeDeltaToPrettyTimeDelta( time_delta )
-
- if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
-
- pretty_max_allowed = HydrusData.ToHumanBytes( max_allowed )
-
- elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
-
- pretty_max_allowed = HydrusData.ToHumanInt( max_allowed ) + ' requests'
-
-
- sort_time_delta = ClientGUIListCtrl.SafeNoneInt( time_delta )
-
- sort_tuple = ( max_allowed, sort_time_delta )
- display_tuple = ( pretty_max_allowed, pretty_time_delta )
-
- return ( display_tuple, sort_tuple )
-
-
- def _Edit( self ):
-
- selected_rules = self._listctrl.GetData( only_selected = True )
-
- edited_datas = []
-
- for rule in selected_rules:
-
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit rule' ) as dlg:
-
- panel = self._EditPanel( dlg, rule )
-
- dlg.SetPanel( panel )
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- edited_rule = panel.GetValue()
-
- self._listctrl.DeleteDatas( ( rule, ) )
-
- self._listctrl.AddDatas( ( edited_rule, ) )
-
- edited_datas.append( edited_rule )
-
- else:
-
- break
-
-
-
-
- self._listctrl.SelectDatas( edited_datas )
-
- self._listctrl.Sort()
-
-
- def GetValue( self ):
-
- bandwidth_rules = HydrusNetworking.BandwidthRules()
-
- for rule in self._listctrl.GetData():
-
- ( bandwidth_type, time_delta, max_allowed ) = rule
-
- bandwidth_rules.AddRule( bandwidth_type, time_delta, max_allowed )
-
-
- return bandwidth_rules
-
-
- class _EditPanel( ClientGUIScrolledPanels.EditPanel ):
-
- def __init__( self, parent, rule ):
-
- ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
-
- self._bandwidth_type = ClientGUICommon.BetterChoice( self )
-
- self._bandwidth_type.addItem( 'data', HC.BANDWIDTH_TYPE_DATA )
- self._bandwidth_type.addItem( 'requests', HC.BANDWIDTH_TYPE_REQUESTS )
-
- self._bandwidth_type.currentIndexChanged.connect( self._UpdateEnabled )
-
- self._max_allowed_bytes = BytesControl( self )
- self._max_allowed_requests = ClientGUICommon.BetterSpinBox( self, min=1, max=1048576 )
-
- self._time_delta = ClientGUITime.TimeDeltaButton( self, min = 1, days = True, hours = True, minutes = True, seconds = True, monthly_allowed = True )
-
- #
-
- ( bandwidth_type, time_delta, max_allowed ) = rule
-
- self._bandwidth_type.SetValue( bandwidth_type )
-
- self._time_delta.SetValue( time_delta )
-
- if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
-
- self._max_allowed_bytes.SetValue( max_allowed )
-
- else:
-
- self._max_allowed_requests.setValue( max_allowed )
-
-
- self._UpdateEnabled()
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._max_allowed_bytes, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._max_allowed_requests, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._bandwidth_type, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,' every '), CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._time_delta, CC.FLAGS_CENTER_PERPENDICULAR )
-
- self.widget().setLayout( hbox )
-
-
- def _UpdateEnabled( self ):
-
- bandwidth_type = self._bandwidth_type.GetValue()
-
- if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
-
- self._max_allowed_bytes.show()
- self._max_allowed_requests.hide()
-
- elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
-
- self._max_allowed_bytes.hide()
- self._max_allowed_requests.show()
-
-
- def GetValue( self ):
-
- bandwidth_type = self._bandwidth_type.GetValue()
-
- time_delta = self._time_delta.GetValue()
-
- if bandwidth_type == HC.BANDWIDTH_TYPE_DATA:
-
- max_allowed = self._max_allowed_bytes.GetValue()
-
- elif bandwidth_type == HC.BANDWIDTH_TYPE_REQUESTS:
-
- max_allowed = self._max_allowed_requests.value()
-
-
- return ( bandwidth_type, time_delta, max_allowed )
-
-
-
-class BytesControl( QW.QWidget ):
-
- valueChanged = QC.Signal()
-
- def __init__( self, parent, initial_value = 65536 ):
-
- QW.QWidget.__init__( self, parent )
-
- self._spin = ClientGUICommon.BetterSpinBox( self, min=0, max=1048576 )
-
- self._unit = ClientGUICommon.BetterChoice( self )
-
- self._unit.addItem( 'B', 1 )
- self._unit.addItem( 'KB', 1024 )
- self._unit.addItem( 'MB', 1024 ** 2 )
- self._unit.addItem( 'GB', 1024 ** 3 )
- self._unit.addItem( 'TB', 1024 ** 4 )
-
- #
-
- self.SetValue( initial_value )
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._spin, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._unit, CC.FLAGS_CENTER_PERPENDICULAR )
-
- self.setLayout( hbox )
-
- min_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._unit, 8 )
-
- self._unit.setMinimumWidth( min_width )
-
- self._spin.valueChanged.connect( self._HandleValueChanged )
- self._unit.currentIndexChanged.connect( self._HandleValueChanged )
-
-
- def _HandleValueChanged( self, val ):
-
- self.valueChanged.emit()
-
-
- def GetSeparatedValue( self ):
-
- return ( self._spin.value(), self._unit.GetValue() )
-
-
- def GetValue( self ):
-
- return self._spin.value() * self._unit.GetValue()
-
-
- def SetSeparatedValue( self, value, unit ):
-
- return ( self._spin.setValue( value ), self._unit.SetValue( unit ) )
-
-
- def SetValue( self, value: int ):
-
- max_unit = 1024 ** 4
-
- unit = 1
-
- while value % 1024 == 0 and unit < max_unit:
-
- value //= 1024
-
- unit *= 1024
-
-
- self._spin.setValue( value )
- self._unit.SetValue( unit )
-
-
-
-class NoneableBytesControl( QW.QWidget ):
-
- valueChanged = QC.Signal()
-
- def __init__( self, parent, initial_value = 65536, none_label = 'no limit' ):
-
- QW.QWidget.__init__( self, parent )
-
- self._bytes = BytesControl( self )
-
- self._none_checkbox = QW.QCheckBox( none_label, self )
-
- #
-
- self.SetValue( initial_value )
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._bytes, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._none_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
-
- self.setLayout( hbox )
-
- #
-
- self._none_checkbox.clicked.connect( self._UpdateEnabled )
-
- self._bytes.valueChanged.connect( self._HandleValueChanged )
- self._none_checkbox.clicked.connect( self._HandleValueChanged )
-
-
- def _UpdateEnabled( self ):
-
- if self._none_checkbox.isChecked():
-
- self._bytes.setEnabled( False )
-
- else:
-
- self._bytes.setEnabled( True )
-
-
-
- def _HandleValueChanged( self ):
-
- self.valueChanged.emit()
-
-
- def GetValue( self ):
-
- if self._none_checkbox.isChecked():
-
- return None
-
- else:
-
- return self._bytes.GetValue()
-
-
-
- def setToolTip( self, text ):
-
- QW.QWidget.setToolTip( self, text )
-
- for c in self.children():
-
- if isinstance( c, QW.QWidget ):
-
- c.setToolTip( text )
-
-
-
- def SetValue( self, value ):
-
- if value is None:
-
- self._none_checkbox.setChecked( True )
-
- else:
-
- self._none_checkbox.setChecked( False )
-
- self._bytes.SetValue( value )
-
-
- self._UpdateEnabled()
-
-
-
-class NumberTestWidget( QW.QWidget ):
-
- def __init__( self, parent, allowed_operators = None, max = 200000, unit_string = None, appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 15 ):
-
- QW.QWidget.__init__( self, parent )
-
- choice_tuples = []
-
- for possible_operator in [
- ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
- ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
- ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
- ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
- ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
- ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
- ]:
-
- if possible_operator in allowed_operators:
-
- text = ClientSearch.number_test_operator_to_str_lookup[ possible_operator ]
-
- if possible_operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
-
- text += '%'
-
-
- choice_tuples.append( ( text, possible_operator ) )
-
-
-
- self._operator = QP.DataRadioBox( self, choice_tuples = choice_tuples )
-
- self._value = self._GenerateValueWidget( max )
-
- #
-
- self._absolute_plus_or_minus_panel = QW.QWidget( self )
-
- self._absolute_plus_or_minus = self._GenerateAbsoluteValueWidget( max )
-
- self._SetAbsoluteValue( appropriate_absolute_plus_or_minus_default )
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
-
- QP.AddToLayout( hbox, self._absolute_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
-
- if unit_string is not None:
-
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
-
-
- self._absolute_plus_or_minus_panel.setLayout( hbox )
-
- #
-
- self._percent_plus_or_minus_panel = QW.QWidget( self )
-
- self._percent_plus_or_minus = ClientGUICommon.BetterSpinBox( self._percent_plus_or_minus_panel, min = 0, max = 10000, width = 60 )
-
- self._percent_plus_or_minus.setValue( appropriate_percentage_plus_or_minus_default )
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
-
- QP.AddToLayout( hbox, self._percent_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
-
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = '%' ), CC.FLAGS_CENTER_PERPENDICULAR )
-
- self._percent_plus_or_minus_panel.setLayout( hbox )
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._operator, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._value, CC.FLAGS_CENTER_PERPENDICULAR )
-
- if unit_string is not None:
-
- QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
-
-
- QP.AddToLayout( hbox, self._absolute_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( hbox, self._percent_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
-
- self.setLayout( hbox )
-
- self._operator.radioBoxChanged.connect( self._UpdateVisibility )
-
- self._UpdateVisibility()
-
-
-
- def _GenerateAbsoluteValueWidget( self, max: int ):
-
- return ClientGUICommon.BetterSpinBox( self._absolute_plus_or_minus_panel, min = 0, max = int( max / 2 ), width = 60 )
-
-
- def _GenerateValueWidget( self, max: int ):
-
- return ClientGUICommon.BetterSpinBox( self, max = max, width = 60 )
-
-
- def _GetSubValue( self ):
-
- return self._value.value()
-
-
- def _SetSubValue( self, value ):
-
- return self._value.setValue( value )
-
-
- def _GetAbsoluteValue( self ):
-
- return self._absolute_plus_or_minus.value()
-
-
- def _SetAbsoluteValue( self, value ):
-
- return self._absolute_plus_or_minus.setValue( value )
-
-
- def _UpdateVisibility( self ):
-
- operator = self._operator.GetValue()
-
- self._absolute_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE )
- self._percent_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT )
-
-
- def GetValue( self ) -> ClientSearch.NumberTest:
-
- operator = self._operator.GetValue()
- value = self._GetSubValue()
-
- if operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
-
- extra_value = self._GetAbsoluteValue()
-
- elif operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
-
- extra_value = self._percent_plus_or_minus.value() / 100
-
- else:
-
- extra_value = None
-
-
- return ClientSearch.NumberTest( operator = operator, value = value, extra_value = extra_value )
-
-
- def SetValue( self, number_test: ClientSearch.NumberTest ):
-
- self._operator.SetValue( number_test.operator )
- self._SetSubValue( number_test.value )
-
- if number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
-
- self._SetAbsoluteValue( number_test.extra_value )
-
- elif number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
-
- self._percent_plus_or_minus.setValue( int( number_test.extra_value * 100 ) )
-
-
- self._UpdateVisibility()
-
-
-
-class TextAndPasteCtrl( QW.QWidget ):
-
- def __init__( self, parent, add_callable, allow_empty_input = False ):
-
- self._add_callable = add_callable
- self._allow_empty_input = allow_empty_input
-
- QW.QWidget.__init__( self, parent )
-
- self._text_input = QW.QLineEdit( self )
- self._text_input.installEventFilter( ClientGUICommon.TextCatchEnterEventFilter( self._text_input, self.EnterText ) )
-
- self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( 'Paste multiple inputs from the clipboard. Assumes the texts are newline-separated.' )
-
- self.setFocusProxy( self._text_input )
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._text_input, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR )
-
- self.setLayout( hbox )
-
-
- def _Paste( self ):
-
- try:
-
- raw_text = CG.client_controller.GetClipboardText()
-
- except HydrusExceptions.DataMissing as e:
-
- HydrusData.PrintException( e )
-
- ClientGUIDialogsMessage.ShowCritical( self, 'Problem pasting!', str(e) )
-
- return
-
-
- try:
-
- texts = [ text for text in HydrusText.DeserialiseNewlinedTexts( raw_text ) ]
-
- if not self._allow_empty_input:
-
- texts = [ text for text in texts if text != '' ]
-
-
- if len( texts ) > 0:
-
- self._add_callable( texts )
-
-
- except Exception as e:
-
- ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of text', e )
-
-
-
- def EnterText( self ):
-
- text = self._text_input.text()
-
- text = HydrusText.StripIOInputLine( text )
-
- if text == '' and not self._allow_empty_input:
-
- return
-
-
- self._add_callable( ( text, ) )
-
- self._text_input.clear()
-
-
- def GetValue( self ):
-
- return self._text_input.text()
-
-
- def setPlaceholderText( self, text ):
-
- self._text_input.setPlaceholderText( text )
-
-
- def SetValue( self, text ):
-
- self._text_input.setText( text )
-
-
diff --git a/hydrus/client/gui/widgets/ClientGUINumberTest.py b/hydrus/client/gui/widgets/ClientGUINumberTest.py
new file mode 100644
index 000000000..fb038c94d
--- /dev/null
+++ b/hydrus/client/gui/widgets/ClientGUINumberTest.py
@@ -0,0 +1,183 @@
+import typing
+
+from qtpy import QtWidgets as QW
+
+from hydrus.core import HydrusConstants as HC
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.widgets import ClientGUICommon
+from hydrus.client.search import ClientSearch
+
+class NumberTestWidget( QW.QWidget ):
+
+ def __init__( self, parent, allowed_operators = None, max = 200000, unit_string = None, appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 15 ):
+
+ QW.QWidget.__init__( self, parent )
+
+ choice_tuples = []
+
+ for possible_operator in [
+ ClientSearch.NUMBER_TEST_OPERATOR_LESS_THAN,
+ ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE,
+ ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT,
+ ClientSearch.NUMBER_TEST_OPERATOR_EQUAL,
+ ClientSearch.NUMBER_TEST_OPERATOR_NOT_EQUAL,
+ ClientSearch.NUMBER_TEST_OPERATOR_GREATER_THAN
+ ]:
+
+ if possible_operator in allowed_operators:
+
+ text = ClientSearch.number_test_operator_to_str_lookup[ possible_operator ]
+
+ if possible_operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
+
+ text += '%'
+
+
+ choice_tuples.append( ( text, possible_operator ) )
+
+
+
+ self._operator = QP.DataRadioBox( self, choice_tuples = choice_tuples )
+
+ self._value = self._GenerateValueWidget( max )
+
+ #
+
+ self._absolute_plus_or_minus_panel = QW.QWidget( self )
+
+ self._absolute_plus_or_minus = self._GenerateAbsoluteValueWidget( max )
+
+ self._SetAbsoluteValue( appropriate_absolute_plus_or_minus_default )
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
+
+ QP.AddToLayout( hbox, self._absolute_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ if unit_string is not None:
+
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._absolute_plus_or_minus_panel, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
+
+
+ self._absolute_plus_or_minus_panel.setLayout( hbox )
+
+ #
+
+ self._percent_plus_or_minus_panel = QW.QWidget( self )
+
+ self._percent_plus_or_minus = ClientGUICommon.BetterSpinBox( self._percent_plus_or_minus_panel, min = 0, max = 10000, width = 60 )
+
+ self._percent_plus_or_minus.setValue( appropriate_percentage_plus_or_minus_default )
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = HC.UNICODE_PLUS_OR_MINUS ), CC.FLAGS_CENTER_PERPENDICULAR )
+
+ QP.AddToLayout( hbox, self._percent_plus_or_minus, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._percent_plus_or_minus_panel, label = '%' ), CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self._percent_plus_or_minus_panel.setLayout( hbox )
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._operator, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._value, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ if unit_string is not None:
+
+ QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self, label = unit_string ), CC.FLAGS_CENTER_PERPENDICULAR )
+
+
+ QP.AddToLayout( hbox, self._absolute_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( hbox, self._percent_plus_or_minus_panel, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self.setLayout( hbox )
+
+ self._operator.radioBoxChanged.connect( self._UpdateVisibility )
+
+ self._UpdateVisibility()
+
+
+
+ def _GenerateAbsoluteValueWidget( self, max: int ):
+
+ return ClientGUICommon.BetterSpinBox( self._absolute_plus_or_minus_panel, min = 0, max = int( max / 2 ), width = 60 )
+
+
+ def _GenerateValueWidget( self, max: int ):
+
+ return ClientGUICommon.BetterSpinBox( self, max = max, width = 60 )
+
+
+ def _GetSubValue( self ):
+
+ return self._value.value()
+
+
+ def _SetSubValue( self, value ):
+
+ return self._value.setValue( value )
+
+
+ def _GetAbsoluteValue( self ):
+
+ return self._absolute_plus_or_minus.value()
+
+
+ def _SetAbsoluteValue( self, value ):
+
+ return self._absolute_plus_or_minus.setValue( value )
+
+
+ def _UpdateVisibility( self ):
+
+ operator = self._operator.GetValue()
+
+ self._absolute_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE )
+ self._percent_plus_or_minus_panel.setVisible( operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT )
+
+
+ def GetValue( self ) -> ClientSearch.NumberTest:
+
+ operator = self._operator.GetValue()
+ value = self._GetSubValue()
+
+ if operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
+
+ extra_value = self._GetAbsoluteValue()
+
+ elif operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
+
+ extra_value = self._percent_plus_or_minus.value() / 100
+
+ else:
+
+ extra_value = None
+
+
+ return ClientSearch.NumberTest( operator = operator, value = value, extra_value = extra_value )
+
+
+ def SetValue( self, number_test: ClientSearch.NumberTest ):
+
+ self._operator.SetValue( number_test.operator )
+ self._SetSubValue( number_test.value )
+
+ if number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_ABSOLUTE:
+
+ self._SetAbsoluteValue( number_test.extra_value )
+
+ elif number_test.operator == ClientSearch.NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT:
+
+ self._percent_plus_or_minus.setValue( int( number_test.extra_value * 100 ) )
+
+
+ self._UpdateVisibility()
+
+
diff --git a/hydrus/client/gui/widgets/ClientGUITextInput.py b/hydrus/client/gui/widgets/ClientGUITextInput.py
new file mode 100644
index 000000000..d1d494575
--- /dev/null
+++ b/hydrus/client/gui/widgets/ClientGUITextInput.py
@@ -0,0 +1,109 @@
+import typing
+
+from qtpy import QtWidgets as QW
+
+from hydrus.core import HydrusData
+from hydrus.core import HydrusExceptions
+from hydrus.core import HydrusText
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
+from hydrus.client.gui import ClientGUIDialogsMessage
+from hydrus.client.gui import ClientGUIDialogsQuick
+from hydrus.client.gui import ClientGUIFunctions
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.widgets import ClientGUICommon
+
+class TextAndPasteCtrl( QW.QWidget ):
+
+ def __init__( self, parent, add_callable, allow_empty_input = False ):
+
+ self._add_callable = add_callable
+ self._allow_empty_input = allow_empty_input
+
+ QW.QWidget.__init__( self, parent )
+
+ self._text_input = QW.QLineEdit( self )
+ self._text_input.installEventFilter( ClientGUICommon.TextCatchEnterEventFilter( self._text_input, self.EnterText ) )
+
+ self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste multiple inputs from the clipboard. Assumes the texts are newline-separated.' ) )
+
+ self.setFocusProxy( self._text_input )
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._text_input, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR )
+
+ self.setLayout( hbox )
+
+
+ def _Paste( self ):
+
+ try:
+
+ raw_text = CG.client_controller.GetClipboardText()
+
+ except HydrusExceptions.DataMissing as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem pasting!', str(e) )
+
+ return
+
+
+ try:
+
+ texts = [ text for text in HydrusText.DeserialiseNewlinedTexts( raw_text ) ]
+
+ if not self._allow_empty_input:
+
+ texts = [ text for text in texts if text != '' ]
+
+
+ if len( texts ) > 0:
+
+ self._add_callable( texts )
+
+
+ except Exception as e:
+
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'Lines of text', e )
+
+
+
+ def EnterText( self ):
+
+ text = self._text_input.text()
+
+ text = HydrusText.StripIOInputLine( text )
+
+ if text == '' and not self._allow_empty_input:
+
+ return
+
+
+ self._add_callable( ( text, ) )
+
+ self._text_input.clear()
+
+
+ def GetValue( self ):
+
+ return self._text_input.text()
+
+
+ def setPlaceholderText( self, text ):
+
+ self._text_input.setPlaceholderText( text )
+
+
+ def SetValue( self, text ):
+
+ self._text_input.setText( text )
+
+
diff --git a/hydrus/client/importing/ClientImportControl.py b/hydrus/client/importing/ClientImportControl.py
index 3fc0c67b4..9dfeb5e19 100644
--- a/hydrus/client/importing/ClientImportControl.py
+++ b/hydrus/client/importing/ClientImportControl.py
@@ -116,16 +116,6 @@ def GenerateLiveStatusText( text: str, paused: bool, currently_working: bool, no
return text
-def NeatenStatusText( text: str ) -> str:
-
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- return text
-
-
def PageImporterShouldStopWorking( page_key: bytes ):
return HG.started_shutdown or not CG.client_controller.PageAlive( page_key )
diff --git a/hydrus/client/importing/ClientImportFileSeeds.py b/hydrus/client/importing/ClientImportFileSeeds.py
index 1ccfcda85..62b904907 100644
--- a/hydrus/client/importing/ClientImportFileSeeds.py
+++ b/hydrus/client/importing/ClientImportFileSeeds.py
@@ -12,11 +12,11 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTags
from hydrus.core import HydrusTemp
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.core.files import HydrusFileHandling
@@ -1275,10 +1275,10 @@ def SetStatus( self, status: int, note: str = '', exception = None ):
if exception is not None:
- first_line = repr( exception ).splitlines()[0]
+ first_line = HydrusText.GetFirstLine( repr( exception ) )
note = f'{first_line}{HC.UNICODE_ELLIPSIS} (Copy note to see full error)'
- note += os.linesep
+ note += '\n'
note += traceback.format_exc()
HydrusData.Print( 'Error when processing {}!'.format( self.file_seed_data ) )
diff --git a/hydrus/client/importing/ClientImportGallery.py b/hydrus/client/importing/ClientImportGallery.py
index 852c020de..304a1bf29 100644
--- a/hydrus/client/importing/ClientImportGallery.py
+++ b/hydrus/client/importing/ClientImportGallery.py
@@ -7,6 +7,7 @@
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -124,10 +125,7 @@ def _AmOverFileLimit( self ):
def _DelayWork( self, time_delta, reason ):
- if len( reason ) > 0:
-
- reason = reason.splitlines()[0]
-
+ reason = HydrusText.GetFirstLine( reason )
self._no_work_until = HydrusTime.GetNow() + time_delta
self._no_work_until_reason = reason
@@ -262,7 +260,7 @@ def status_hook( text ):
with self._lock:
- self._files_status = ClientImportControl.NeatenStatusText( text )
+ self._files_status = HydrusText.GetFirstLine( text )
@@ -343,7 +341,7 @@ def status_hook( text ):
with self._lock:
- self._gallery_status = ClientImportControl.NeatenStatusText( text )
+ self._gallery_status = HydrusText.GetFirstLine( text )
diff --git a/hydrus/client/importing/ClientImportGallerySeeds.py b/hydrus/client/importing/ClientImportGallerySeeds.py
index c3a981f5e..04813d359 100644
--- a/hydrus/client/importing/ClientImportGallerySeeds.py
+++ b/hydrus/client/importing/ClientImportGallerySeeds.py
@@ -12,6 +12,7 @@
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -345,10 +346,10 @@ def SetStatus( self, status, note = '', exception = None ):
if exception is not None:
- first_line = repr( exception ).splitlines()[0]
+ first_line = HydrusText.GetFirstLine( repr( exception ) )
note = f'{first_line}{HC.UNICODE_ELLIPSIS} (Copy note to see full error)'
- note += os.linesep
+ note += '\n'
note += traceback.format_exc()
HydrusData.Print( 'Error when processing ' + self.url + ' !' )
@@ -617,7 +618,7 @@ def WorkOnURL( self, gallery_token_name, gallery_seed_log: "GallerySeedLog", fil
except Exception as e:
note += ' - Attempted to generate a next gallery page url, but failed!'
- note += os.linesep
+ note += '\n'
note += traceback.format_exc()
diff --git a/hydrus/client/importing/ClientImportLocal.py b/hydrus/client/importing/ClientImportLocal.py
index d6470fd5d..9471fb1fb 100644
--- a/hydrus/client/importing/ClientImportLocal.py
+++ b/hydrus/client/importing/ClientImportLocal.py
@@ -10,6 +10,7 @@
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusThreading
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.core.files import HydrusFileHandling
@@ -28,7 +29,6 @@
from hydrus.client.metadata import ClientMetadataMigrationExporters
from hydrus.client.metadata import ClientMetadataMigrationImporters
from hydrus.client.metadata import ClientTags
-from hydrus.client.search import ClientSearch
class HDDImport( HydrusSerialisable.SerialisableBase ):
@@ -194,7 +194,7 @@ def status_hook( text ):
with self._lock:
- self._files_status = ClientImportControl.NeatenStatusText( text )
+ self._files_status = HydrusText.GetFirstLine( text )
diff --git a/hydrus/client/importing/ClientImportSimpleURLs.py b/hydrus/client/importing/ClientImportSimpleURLs.py
index e8a8727e0..7fe01e74f 100644
--- a/hydrus/client/importing/ClientImportSimpleURLs.py
+++ b/hydrus/client/importing/ClientImportSimpleURLs.py
@@ -8,6 +8,7 @@
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -69,10 +70,7 @@ def __init__( self ):
def _DelayWork( self, time_delta, reason ):
- if len( reason ) > 0:
-
- reason = reason.splitlines()[0]
-
+ reason = HydrusText.GetFirstLine( reason )
self._no_work_until = HydrusTime.GetNow() + time_delta
self._no_work_until_reason = reason
@@ -221,7 +219,7 @@ def status_hook( text ):
with self._lock:
- self._files_status = ClientImportControl.NeatenStatusText( text )
+ self._files_status = HydrusText.GetFirstLine( text )
@@ -401,7 +399,7 @@ def _WorkOnGallery( self ):
with self._lock:
- self._gallery_status = ClientImportControl.NeatenStatusText( parser_status )
+ self._gallery_status = HydrusText.GetFirstLine( parser_status )
if error_occurred:
@@ -882,10 +880,7 @@ def __init__( self ):
def _DelayWork( self, time_delta, reason ):
- if len( reason ) > 0:
-
- reason = reason.splitlines()[0]
-
+ reason = HydrusText.GetFirstLine( reason )
self._no_work_until = HydrusTime.GetNow() + time_delta
self._no_work_until_reason = reason
diff --git a/hydrus/client/importing/ClientImportSubscriptionLegacy.py b/hydrus/client/importing/ClientImportSubscriptionLegacy.py
index 1ae7d12ef..83d662917 100644
--- a/hydrus/client/importing/ClientImportSubscriptionLegacy.py
+++ b/hydrus/client/importing/ClientImportSubscriptionLegacy.py
@@ -525,11 +525,11 @@ def _CanDoWorkNow( self ):
if HG.subscription_report_mode:
message = 'Subscription "{}" CanDoWork check.'.format( self._name )
- message += os.linesep
+ message += '\n'
message += 'Paused/Global/Network Pause: {}/{}/{}'.format( self._paused, CG.client_controller.new_options.GetBoolean( 'pause_subs_sync' ), CG.client_controller.new_options.GetBoolean( 'pause_all_new_network_traffic' ) )
- message += os.linesep
+ message += '\n'
message += 'Started/Thread shutdown: {}/{}'.format( HG.started_shutdown, HydrusThreading.IsThreadShuttingDown() )
- message += os.linesep
+ message += '\n'
message += 'No delays: {}'.format( self._NoDelays() )
HydrusData.ShowText( message )
@@ -669,9 +669,9 @@ def _QueryFileLoginOK( self, query ):
login_fail_reason = str( e )
message = 'Query "' + query.GetHumanName() + '" for subscription "' + self._name + '" seemed to have an invalid login for one of its file imports. The reason was:'
- message += os.linesep * 2
+ message += '\n' * 2
message += login_fail_reason
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The subscription has paused. Please see if you can fix the problem and then unpause. If the login script stopped because of missing cookies or similar, it may be broken. Please check out Hydrus Companion for a better login solution.'
HydrusData.ShowText( message )
@@ -729,9 +729,9 @@ def _QuerySyncLoginOK( self, query ):
login_fail_reason = str( e )
message = 'Query "' + query.GetHumanName() + '" for subscription "' + self._name + '" seemed to have an invalid login. The reason was:'
- message += os.linesep * 2
+ message += '\n' * 2
message += login_fail_reason
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The subscription has paused. Please see if you can fix the problem and then unpause. If the login script stopped because of missing cookies or similar, it may be broken. Please check out Hydrus Companion for a better login solution.'
HydrusData.ShowText( message )
diff --git a/hydrus/client/importing/ClientImportSubscriptions.py b/hydrus/client/importing/ClientImportSubscriptions.py
index 8b9ac2ab5..49bb3d748 100644
--- a/hydrus/client/importing/ClientImportSubscriptions.py
+++ b/hydrus/client/importing/ClientImportSubscriptions.py
@@ -10,6 +10,7 @@
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusThreading
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -96,11 +97,11 @@ def _CanDoWorkNow( self ):
if HG.subscription_report_mode:
message = 'Subscription "{}" CanDoWork check.'.format( self._name )
- message += os.linesep
+ message += '\n'
message += 'Paused/Global/Network/Dialog Pause: {}/{}/{}/{}'.format( self._paused, CG.client_controller.new_options.GetBoolean( 'pause_subs_sync' ), CG.client_controller.new_options.GetBoolean( 'pause_all_new_network_traffic' ), CG.client_controller.subscriptions_manager.SubscriptionsArePausedForEditing() )
- message += os.linesep
+ message += '\n'
message += 'Started/Sub shutdown: {}/{}'.format( HG.started_shutdown, self._stop_work_for_shutdown )
- message += os.linesep
+ message += '\n'
message += 'No delays: {}'.format( self._NoDelays() )
HydrusData.ShowText( message )
@@ -120,10 +121,7 @@ def _DealWithMissingQueryLogContainerError( self, query_header: ClientImportSubs
def _DelayWork( self, time_delta, reason ):
- if len( reason ) > 0:
-
- reason = reason.splitlines()[0]
-
+ reason = HydrusText.GetFirstLine( reason )
self._no_work_until = HydrusTime.GetNow() + time_delta
self._no_work_until_reason = reason
@@ -241,7 +239,7 @@ def _NoDelays( self ):
def _ShowHitPeriodicFileLimitMessage( self, query_name: int, query_text: int, file_limit: int ):
message = 'The query "{}" for subscription "{}" found {} new URLs without running into any it had seen before.'.format( query_name, self._name, file_limit )
- message += os.linesep
+ message += '\n'
message += 'Either a user uploaded a lot of files to that query in a short period, in which case there is a gap in your subscription you may wish to fill, or the site has just changed its URL format, in which case you may see several of these messages for this site over the coming weeks, and you should ignore them.'
call = HydrusData.Call( CG.client_controller.pub, 'make_new_subscription_gap_downloader', self._gug_key_and_name, query_text, self._file_import_options.Duplicate(), self._tag_import_options.Duplicate(), self._note_import_options, file_limit * 5 )
@@ -280,7 +278,7 @@ def _SyncQueries( self, job_status ):
self._paused = True
message = 'The subscription "{}"\'s Gallery URL Generator, "{}" seems not to be functional! The sub has paused! The given reason was:'.format( self._name, self._gug_key_and_name[1] )
- message += os.linesep * 2
+ message += '\n' * 2
message += str( e )
HydrusData.ShowText( message )
@@ -421,9 +419,9 @@ def _SyncQuery(
if not self._paused:
message = 'Query "{}" for subscription "{}" seemed to have an invalid login. The reason was:'.format( query_header.GetHumanName(), self._name )
- message += os.linesep * 2
+ message += '\n' * 2
message += login_reason
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The subscription has paused. Please see if you can fix the problem and then unpause. If the login script stopped because of missing cookies or similar, it may be broken. Please check out Hydrus Companion for a better login solution.'
HydrusData.ShowText( message )
@@ -457,12 +455,7 @@ def _SyncQuery(
def status_hook( text ):
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- job_status.SetStatusText( status_prefix + ': ' + text )
+ job_status.SetStatusText( status_prefix + ': ' + HydrusText.GetFirstLine( text ) )
def title_hook( text ):
@@ -1033,9 +1026,9 @@ def _WorkOnQueryFiles(
if not self._paused:
message = 'Query "{}" for subscription "{}" seemed to have an invalid login for one of its file imports. The reason was:'.format( query_header.GetHumanName(), self._name )
- message += os.linesep * 2
+ message += '\n' * 2
message += login_reason
- message += os.linesep * 2
+ message += '\n' * 2
message += 'The subscription has paused. Please see if you can fix the problem and then unpause. If the login script stopped because of missing cookies or similar, it may be broken. Please check out Hydrus Companion for a better login solution.'
HydrusData.ShowText( message )
@@ -1066,12 +1059,7 @@ def _WorkOnQueryFiles(
def status_hook( text ):
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- job_status.SetStatusText( x_out_of_y + text, 2 )
+ job_status.SetStatusText( x_out_of_y + HydrusText.GetFirstLine( text ), 2 )
file_seed.WorkOnURL( file_seed_cache, status_hook, query_header.GenerateNetworkJobFactory( self._name ), ClientImporting.GenerateMultiplePopupNetworkJobPresentationContextFactory( job_status ), self._file_import_options, FileImportOptions.IMPORT_TYPE_QUIET, self._tag_import_options, self._note_import_options )
@@ -2103,11 +2091,11 @@ def ShowSnapshot( self ):
next_times = sorted( self._names_to_next_work_time.items(), key = lambda n_nwt_tuple: n_nwt_tuple[1] )
message = '{} subs: {}'.format( HydrusData.ToHumanInt( len( self._names_to_subscriptions ) ), ', '.join( sub_names ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += '{} running: {}'.format( HydrusData.ToHumanInt( len( self._names_to_running_subscription_info ) ), ', '.join( running ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += '{} not runnable: {}'.format( HydrusData.ToHumanInt( len( self._names_that_cannot_run ) ), ', '.join( cannot_run ) )
- message += os.linesep * 2
+ message += '\n' * 2
message += '{} next times: {}'.format( HydrusData.ToHumanInt( len( self._names_to_next_work_time ) ), ', '.join( ( '{}: {}'.format( name, ClientTime.TimestampToPrettyTimeDelta( next_work_time ) ) for ( name, next_work_time ) in next_times ) ) )
HydrusData.ShowText( message )
diff --git a/hydrus/client/importing/ClientImportWatchers.py b/hydrus/client/importing/ClientImportWatchers.py
index cdbda7cb6..fd42e8583 100644
--- a/hydrus/client/importing/ClientImportWatchers.py
+++ b/hydrus/client/importing/ClientImportWatchers.py
@@ -6,6 +6,7 @@
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
@@ -782,7 +783,7 @@ def status_hook( text ):
with self._lock:
- self._watcher_status = ClientImportControl.NeatenStatusText( text )
+ self._watcher_status = HydrusText.GetFirstLine( text )
@@ -790,12 +791,7 @@ def title_hook( text ):
with self._lock:
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- self._subject = text
+ self._subject = HydrusText.GetFirstLine( text )
@@ -889,13 +885,8 @@ def _Compact( self ):
def _DelayWork( self, time_delta, reason ):
- if len( reason ) > 0:
-
- reason = reason.splitlines()[0]
-
-
self._no_work_until = HydrusTime.GetNow() + time_delta
- self._no_work_until_reason = reason
+ self._no_work_until_reason = HydrusText.GetFirstLine( reason )
def _FileNetworkJobPresentationContextFactory( self, network_job ):
@@ -1161,7 +1152,7 @@ def status_hook( text ):
with self._lock:
- self._files_status = ClientImportControl.NeatenStatusText( text )
+ self._files_status = HydrusText.GetFirstLine( text )
diff --git a/hydrus/client/importing/ClientImporting.py b/hydrus/client/importing/ClientImporting.py
index f9d8ad8c8..76c5acf7a 100644
--- a/hydrus/client/importing/ClientImporting.py
+++ b/hydrus/client/importing/ClientImporting.py
@@ -4,6 +4,7 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
+from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
@@ -157,12 +158,7 @@ def network_job_factory( *args, **kwargs ):
def status_hook( text ):
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- job_status.SetStatusText( text )
+ job_status.SetStatusText( HydrusText.GetFirstLine( text ) )
network_job_presentation_context_factory = GenerateSinglePopupNetworkJobPresentationContextFactory( job_status )
@@ -232,12 +228,7 @@ def network_job_factory( *args, **kwargs ):
def status_hook( text ):
- if len( text ) > 0:
-
- text = text.splitlines()[0]
-
-
- job_status.SetStatusText( text, 2 )
+ job_status.SetStatusText( HydrusText.GetFirstLine( text ), 2 )
network_job_presentation_context_factory = GenerateMultiplePopupNetworkJobPresentationContextFactory( job_status )
diff --git a/hydrus/client/importing/options/ClientImportOptions.py b/hydrus/client/importing/options/ClientImportOptions.py
index 3ce45b2fc..e48445773 100644
--- a/hydrus/client/importing/options/ClientImportOptions.py
+++ b/hydrus/client/importing/options/ClientImportOptions.py
@@ -226,7 +226,7 @@ def GetSummary( self ):
death_statement = 'Stopping if file velocity falls below ' + HydrusData.ToHumanInt( death_files_found ) + ' files per ' + HydrusTime.TimeDeltaToPrettyTimeDelta( death_time_delta ) + '.'
- return timing_statement + os.linesep * 2 + death_statement
+ return timing_statement + '\n' * 2 + death_statement
def HasStaticCheckTime( self ):
diff --git a/hydrus/client/importing/options/FileImportOptions.py b/hydrus/client/importing/options/FileImportOptions.py
index 81bfd250a..9d40564d9 100644
--- a/hydrus/client/importing/options/FileImportOptions.py
+++ b/hydrus/client/importing/options/FileImportOptions.py
@@ -508,7 +508,7 @@ def GetSummary( self ):
#
- summary = os.linesep.join( statements )
+ summary = '\n'.join( statements )
return summary
diff --git a/hydrus/client/importing/options/TagImportOptions.py b/hydrus/client/importing/options/TagImportOptions.py
index c3874631f..7e0944347 100644
--- a/hydrus/client/importing/options/TagImportOptions.py
+++ b/hydrus/client/importing/options/TagImportOptions.py
@@ -564,7 +564,7 @@ def GetSummary( self, show_downloader_options ):
continue
- service_statement = name + ':' + os.linesep * 2 + os.linesep.join( sub_statements )
+ service_statement = name + ':' + '\n' * 2 + '\n'.join( sub_statements )
statements.append( service_statement )
@@ -603,7 +603,7 @@ def GetSummary( self, show_downloader_options ):
statements = pre_statements + [ '---' ] + statements
- separator = os.linesep * 2
+ separator = '\n' * 2
summary = separator.join( statements )
diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py
index f5c95c18b..f351f1b5f 100644
--- a/hydrus/client/media/ClientMedia.py
+++ b/hydrus/client/media/ClientMedia.py
@@ -838,6 +838,27 @@ def AddMedia( self, new_media ):
return new_media
+ def AddMediaResults( self, media_results ):
+
+ new_media = []
+
+ for media_result in media_results:
+
+ hash = media_result.GetHash()
+
+ if hash in self._hashes:
+
+ continue
+
+
+ new_media.append( self._GenerateMediaSingleton( media_result ) )
+
+
+ self.AddMedia( new_media )
+
+ return new_media
+
+
def Clear( self ):
self._singleton_media = set()
@@ -1337,27 +1358,6 @@ def __init__( self, location_context: ClientLocation.LocationContext, media_resu
CG.client_controller.sub( self, 'ProcessServiceUpdates', 'service_updates_gui' )
- def AddMediaResults( self, media_results ):
-
- new_media = []
-
- for media_result in media_results:
-
- hash = media_result.GetHash()
-
- if hash in self._hashes:
-
- continue
-
-
- new_media.append( self._GenerateMediaSingleton( media_result ) )
-
-
- self.AddMedia( new_media )
-
- return new_media
-
-
class MediaCollection( MediaList, Media ):
diff --git a/hydrus/client/metadata/ClientMetadataMigration.py b/hydrus/client/metadata/ClientMetadataMigration.py
index 3728d918d..86e2066cc 100644
--- a/hydrus/client/metadata/ClientMetadataMigration.py
+++ b/hydrus/client/metadata/ClientMetadataMigration.py
@@ -115,7 +115,12 @@ def GetPossibleImporterSidecarPaths( self, path ):
sidecar_importers = [ importer for importer in self._importers if isinstance( importer, ClientMetadataMigrationImporters.SingleFileMetadataImporterSidecar ) ]
- possible_sidecar_paths = { importer.GetExpectedSidecarPath( path ) for importer in sidecar_importers }
+ possible_sidecar_paths = set()
+
+ for importer in sidecar_importers:
+
+ possible_sidecar_paths.update( importer.GetPossibleSidecarPaths( path ) )
+
return possible_sidecar_paths
diff --git a/hydrus/client/metadata/ClientMetadataMigrationExporters.py b/hydrus/client/metadata/ClientMetadataMigrationExporters.py
index 24208f085..04312958f 100644
--- a/hydrus/client/metadata/ClientMetadataMigrationExporters.py
+++ b/hydrus/client/metadata/ClientMetadataMigrationExporters.py
@@ -407,7 +407,7 @@ def Export( self, hash: bytes, rows: typing.Collection[ str ] ):
try:
- url = ClientNetworkingFunctions.WashURL( row )
+ url = ClientNetworkingFunctions.EnsureURLIsEncoded( row )
url = CG.client_controller.network_engine.domain_manager.NormaliseURL( url )
@@ -543,7 +543,7 @@ def Export( self, actual_file_path: str, rows: typing.Collection[ str ] ):
except Exception as e:
# TODO: we probably want custom importer/exporter exceptions here
- raise Exception( 'Could not read the existing JSON at {}!{}{}'.format( path, os.linesep, e ) )
+ raise Exception( 'Could not read the existing JSON at {}!{}{}'.format( path, '\n', e ) )
else:
diff --git a/hydrus/client/metadata/ClientMetadataMigrationImporters.py b/hydrus/client/metadata/ClientMetadataMigrationImporters.py
index 4ceefdc12..ae2f31bd1 100644
--- a/hydrus/client/metadata/ClientMetadataMigrationImporters.py
+++ b/hydrus/client/metadata/ClientMetadataMigrationImporters.py
@@ -30,7 +30,7 @@ def GetStringProcessor( self ) -> ClientStrings.StringProcessor:
return self._string_processor
- def Import( self, *args, **kwargs ):
+ def Import( self, *args, **kwargs ) -> typing.Collection[ str ]:
raise NotImplementedError()
@@ -43,31 +43,7 @@ def ToString( self ) -> str:
class SingleFileMetadataImporterMedia( SingleFileMetadataImporter ):
- def Import( self, media_result: ClientMediaResult.MediaResult ):
-
- raise NotImplementedError()
-
-
- def ToString( self ) -> str:
-
- raise NotImplementedError()
-
-
-
-class SingleFileMetadataImporterSidecar( SingleFileMetadataImporter, ClientMetadataMigrationCore.SidecarNode ):
-
- def __init__( self, string_processor: ClientStrings.StringProcessor, remove_actual_filename_ext: bool, suffix: str, filename_string_converter: ClientStrings.StringConverter ):
-
- ClientMetadataMigrationCore.SidecarNode.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter )
- SingleFileMetadataImporter.__init__( self, string_processor )
-
-
- def GetExpectedSidecarPath( self, path: str ):
-
- raise NotImplementedError()
-
-
- def Import( self, actual_file_path: str ):
+ def Import( self, media_result: ClientMediaResult.MediaResult ) -> typing.Collection[ str ]:
raise NotImplementedError()
@@ -478,6 +454,45 @@ def ToString( self ) -> str:
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_MEDIA_URLS ] = SingleFileMetadataImporterMediaURLs
+class SingleFileMetadataImporterSidecar( SingleFileMetadataImporter, ClientMetadataMigrationCore.SidecarNode ):
+
+ def __init__( self, string_processor: ClientStrings.StringProcessor, remove_actual_filename_ext: bool, suffix: str, filename_string_converter: ClientStrings.StringConverter ):
+
+ ClientMetadataMigrationCore.SidecarNode.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter )
+ SingleFileMetadataImporter.__init__( self, string_processor )
+
+
+ def GetPossibleSidecarPaths( self, path: str ) -> typing.Collection[ str ]:
+
+ raise NotImplementedError()
+
+
+ def GetSidecarPath( self, actual_file_path ) -> typing.Optional[ str ]:
+
+ possible_paths = self.GetPossibleSidecarPaths( actual_file_path )
+
+ for path in possible_paths:
+
+ if os.path.exists( path ):
+
+ return path
+
+
+
+ return None
+
+
+ def Import( self, actual_file_path: str ) -> typing.Collection[ str ]:
+
+ raise NotImplementedError()
+
+
+ def ToString( self ) -> str:
+
+ raise NotImplementedError()
+
+
+
class SingleFileMetadataImporterJSON( SingleFileMetadataImporterSidecar, HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_METADATA_SINGLE_FILE_IMPORTER_JSON
@@ -567,9 +582,12 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
- def GetExpectedSidecarPath( self, actual_file_path: str ):
+ def GetPossibleSidecarPaths( self, actual_file_path: str ) -> typing.Collection[ str ]:
- return ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'json' )
+ return [
+ ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'json' ),
+ ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'JSON' )
+ ]
def GetJSONParsingFormula( self ) -> ClientParsing.ParseFormulaJSON:
@@ -579,9 +597,9 @@ def GetJSONParsingFormula( self ) -> ClientParsing.ParseFormulaJSON:
def Import( self, actual_file_path: str ) -> typing.Collection[ str ]:
- path = self.GetExpectedSidecarPath( actual_file_path )
+ path = self.GetSidecarPath( actual_file_path )
- if not os.path.exists( path ):
+ if path is None:
return []
@@ -729,9 +747,12 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
- def GetExpectedSidecarPath( self, actual_file_path: str ):
+ def GetPossibleSidecarPaths( self, actual_file_path: str ) -> typing.Collection[ str ]:
- return ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'txt' )
+ return [
+ ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'txt' ),
+ ClientMetadataMigrationCore.GetSidecarPath( actual_file_path, self._remove_actual_filename_ext, self._suffix, self._filename_string_converter, 'TXT' )
+ ]
def GetSeparator( self ) -> str:
@@ -741,9 +762,9 @@ def GetSeparator( self ) -> str:
def Import( self, actual_file_path: str ) -> typing.Collection[ str ]:
- path = self.GetExpectedSidecarPath( actual_file_path )
+ path = self.GetSidecarPath( actual_file_path )
- if not os.path.exists( path ):
+ if path is None:
return []
diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py
index a1bea2652..1cf7350ae 100644
--- a/hydrus/client/networking/ClientLocalServerResources.py
+++ b/hydrus/client/networking/ClientLocalServerResources.py
@@ -28,6 +28,7 @@
from hydrus.core import HydrusPaths
from hydrus.core import HydrusTags
from hydrus.core import HydrusTemp
+from hydrus.core import HydrusText
from hydrus.core import HydrusTime
from hydrus.core.files import HydrusFileHandling
from hydrus.core.files.images import HydrusImageHandling
@@ -1088,7 +1089,7 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
''' body += ''' -
''' + text.replace( os.linesep, newline ).replace( '\n', newline ) + '''
''' +''' + text.replace( '\n', newline ).replace( '\n', newline ) + '''
''' body+= '''''' @@ -1163,7 +1164,7 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):''' body += ''' -
''' + text.replace( os.linesep, newline ).replace( '\n', newline ) + '''
''' +''' + text.replace( '\n', newline ).replace( '\n', newline ) + '''
''' body+= '''''' @@ -1745,7 +1746,7 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): else: - note = repr( e ).splitlines()[0] + note = HydrusText.GetFirstLine( repr( e ) ) file_import_status = ClientImportFiles.FileImportStatus( CC.STATUS_ERROR, file_import_job.GetHash(), note = note ) @@ -4065,7 +4066,7 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): message_lines.extend( [ 'Altered: {}'.format( key ) for key in sorted( headers_altered ) ] ) - message = os.linesep.join( message_lines ) + message = '\n'.join( message_lines ) job_status = ClientThreading.JobStatus() diff --git a/hydrus/client/networking/ClientNetworkingContexts.py b/hydrus/client/networking/ClientNetworkingContexts.py index 4bdfe0d6e..cbd937731 100644 --- a/hydrus/client/networking/ClientNetworkingContexts.py +++ b/hydrus/client/networking/ClientNetworkingContexts.py @@ -122,12 +122,12 @@ def GetDefault( self ): def GetSummary( self ): summary = self.ToString() - summary += os.linesep * 2 + summary += '\n' * 2 summary += CC.network_context_type_description_lookup[ self.context_type ] if self.IsDefault(): - summary += os.linesep * 2 + summary += '\n' * 2 summary += 'This is the \'default\' version of this context. It stands in when a domain or subscription has no specific rules set.' diff --git a/hydrus/client/networking/ClientNetworkingDomain.py b/hydrus/client/networking/ClientNetworkingDomain.py index d022cf86c..dbcb9a601 100644 --- a/hydrus/client/networking/ClientNetworkingDomain.py +++ b/hydrus/client/networking/ClientNetworkingDomain.py @@ -339,7 +339,7 @@ def _GetURLToFetch( self, url: str ): except HydrusExceptions.URLClassException as e: - raise HydrusExceptions.URLClassException( 'Could not find a URL class for ' + url + '!' + os.linesep * 2 + str( e ) ) + raise HydrusExceptions.URLClassException( 'Could not find a URL class for ' + url + '!' + '\n' * 2 + str( e ) ) return url_to_fetch @@ -353,7 +353,7 @@ def _GetURLToFetchAndParser( self, url ): except HydrusExceptions.URLClassException as e: - raise HydrusExceptions.URLClassException( 'Could not find a URL class for ' + url + '!' + os.linesep * 2 + str( e ) ) + raise HydrusExceptions.URLClassException( 'Could not find a URL class for ' + url + '!' + '\n' * 2 + str( e ) ) url_class_key = parser_url_class.GetClassKey() @@ -1519,7 +1519,7 @@ def NormaliseURL( self, url, for_server = False ): if url_class is None: # this is less about washing as it is about stripping the fragment - normalised_url = ClientNetworkingFunctions.WashURL( url, keep_fragment = False ) + normalised_url = ClientNetworkingFunctions.EnsureURLIsEncoded( url, keep_fragment = False ) else: @@ -2278,8 +2278,8 @@ def GetDetailedSafeSummary( self ): if self.HasBandwidthRules(): m = 'Bandwidth rules: ' - m += os.linesep - m += os.linesep.join( [ HydrusNetworking.ConvertBandwidthRuleToString( rule ) for rule in self._bandwidth_rules.GetRules() ] ) + m += '\n' + m += '\n'.join( [ HydrusNetworking.ConvertBandwidthRuleToString( rule ) for rule in self._bandwidth_rules.GetRules() ] ) components.append( m ) @@ -2287,13 +2287,13 @@ def GetDetailedSafeSummary( self ): if self.HasHeaders(): m = 'Headers: ' - m += os.linesep - m += os.linesep.join( [ key + ' : ' + value + ' - ' + reason for ( key, value, reason ) in self._headers_list ] ) + m += '\n' + m += '\n'.join( [ key + ' : ' + value + ' - ' + reason for ( key, value, reason ) in self._headers_list ] ) components.append( m ) - joiner = os.linesep * 2 + joiner = '\n' * 2 s = joiner.join( components ) @@ -2368,9 +2368,9 @@ def Start( self ): # generate question question = 'For the network context ' + network_context.ToString() + ', can the client set this header?' - question += os.linesep * 2 + question += '\n' * 2 question += key + ': ' + value - question += os.linesep * 2 + question += '\n' * 2 question += reason job_status.SetVariable( 'popup_yes_no_question', question ) diff --git a/hydrus/client/networking/ClientNetworkingFunctions.py b/hydrus/client/networking/ClientNetworkingFunctions.py index 80a875de0..1a1c7d0ac 100644 --- a/hydrus/client/networking/ClientNetworkingFunctions.py +++ b/hydrus/client/networking/ClientNetworkingFunctions.py @@ -5,10 +5,66 @@ import unicodedata import urllib.parse -from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusExceptions from hydrus.client import ClientGlobals as CG + +percent_encoding_re = re.compile( r'%[0-9A-Fa-f]{2}' ) +double_hex_re = re.compile( r'[0-9A-Fa-f]{2}' ) +PARAM_EXCEPTION_CHARS = "!$&'()*+,;=@:/?" # https://www.rfc-editor.org/rfc/rfc3986#section-3.4 +PATH_EXCEPTION_CHARS = "!$&'()*+,;=@:" # https://www.rfc-editor.org/rfc/rfc3986#section-3.3 + +def ensure_component_is_encoded( mixed_encoding_string: str, safe_chars: str ) -> str: + + # this guy is supposed to be idempotent! + + # we do not want to double-encode %40 to %2540 + # we do want to encode a % sign on its own + # so let's split by % and then join it up again somewhat cleverly + + # this function fails when called to examine a query text for "120%120%hello", the hit new anime series, but I think that's it + + parts_of_mixed_encoding_string = mixed_encoding_string.split( '%' ) + + encoded_parts = [] + + for ( i, part ) in enumerate( parts_of_mixed_encoding_string ): + + if i > 0: + + encoded_parts.append( '%' ) # we add the % back in + + if double_hex_re.match( part ) is None: + + # this part does not start with two hex chars, hence the preceding % character was not encoded, so we make the joiner '%25' + encoded_parts.append( '25' ) + + + + encoded_parts.append( urllib.parse.quote( part, safe = safe_chars ) ) + + + encoded_string = ''.join( encoded_parts ) + + return encoded_string + + +def ensure_param_component_is_encoded( param_component: str ) -> str: + """ + Either the key or the value. It can include a mix of encoded and non-encoded characters, it will be returned all encoded. + """ + + return ensure_component_is_encoded( param_component, PARAM_EXCEPTION_CHARS ) + + +def ensure_path_component_is_encoded( path_component: str ) -> str: + """ + A single path component, no slashes. It can include a mix of encoded and non-encoded characters, it will be returned all encoded. + """ + + return ensure_component_is_encoded( path_component, PATH_EXCEPTION_CHARS ) + + def AddCookieToSession( session, name, value, domain, path, expires, secure = False, rest = None ): version = 0 @@ -30,6 +86,7 @@ def AddCookieToSession( session, name, value, domain, path, expires, secure = Fa session.cookies.set_cookie( cookie ) + def ConvertDomainIntoAllApplicableDomains( domain, discard_www = True ): # is an ip address or localhost, possibly with a port @@ -127,9 +184,6 @@ def ConvertPathTextToList( path: str ) -> typing.List[ str ]: def ConvertQueryDictToText( query_dict, single_value_parameters, param_order = None ): - # we now do everything with requests, which does all the unicode -> %20 business naturally, phew - # we still want to call str explicitly to coerce integers and so on that'll slip in here and there - if param_order is None: param_order = sorted( query_dict.keys() ) @@ -227,32 +281,6 @@ def ConvertQueryTextToDict( query_text ): return ( query_dict, single_value_parameters, param_order ) -def EnsureURLInfoIsEncoded( path_components: typing.List[ str ], query_dict: typing.Dict[ str, str ], single_value_parameters: typing.List[ str ] ): - - # ok so the user just posted a URL at us, and this query dict could either be from a real url, like "tags=skirt%20blonde_hair", or it could be a pretty URL they typed or whatever, "tags=skirt blonde_hair" - # so, let's do our best to figure out if the thing was pre-encoded or not, and wash it through a safe encoding process so it is encoded when we give it back - # what's the potential problem? '+' is a special character that may or may not be encoded, e.g. "tags=6%2Bgirls+skirt" WEW - - percent_encoding_re = re.compile( r'%[0-9A-Fa-f]{2}' ) - - all_gubbins = set( path_components ) - all_gubbins.update( query_dict.keys() ) - all_gubbins.update( query_dict.values() ) - all_gubbins.update( single_value_parameters ) - - there_are_percent_encoding_chars = True in ( percent_encoding_re.search( text ) is not None for text in all_gubbins ) - - # if there are percent-encoded characters anywhere, we have to assume the whole URL is already encoded correctly! - if not there_are_percent_encoding_chars: - - path_components = [ urllib.parse.quote( value, safe = '+' ) for value in path_components ] - query_dict = { urllib.parse.quote( key, safe = '+' ) : urllib.parse.quote( value, safe = '+' ) for ( key, value ) in query_dict.items() } - single_value_parameters = [ urllib.parse.quote( value, safe = '+' ) for value in single_value_parameters ] - - - return ( path_components, query_dict, single_value_parameters ) - - def ConvertURLIntoDomain( url ): parser_result = ParseURL( url ) @@ -323,6 +351,7 @@ def GetCookie( cookies, search_domain, cookie_name_string_match ): raise HydrusExceptions.DataMissing( 'Cookie "' + cookie_name_string_match.ToString() + '" not found for domain ' + search_domain + '!' ) + def GetSearchURLs( url ): search_urls = set() @@ -383,9 +412,9 @@ def GetSearchURLs( url ): netloc = 'www.' + netloc - r = urllib.parse.ParseResult( scheme, netloc, path, params, query, fragment ) + adjusted_url = urllib.parse.urlunparse( ( scheme, netloc, path, params, query, fragment ) ) - search_urls.add( r.geturl() ) + search_urls.add( adjusted_url ) for url in list( search_urls ): @@ -466,7 +495,7 @@ def ParseURL( url: str ) -> urllib.parse.ParseResult: -def WashURL( url: str, keep_fragment = True ) -> str: +def EnsureURLIsEncoded( url: str, keep_fragment = True ) -> str: if not LooksLikeAFullURL( url ): @@ -485,7 +514,9 @@ def WashURL( url: str, keep_fragment = True ) -> str: path_components = ConvertPathTextToList( p.path ) ( query_dict, single_value_parameters, param_order ) = ConvertQueryTextToDict( p.query ) - ( path_components, query_dict, single_value_parameters ) = EnsureURLInfoIsEncoded( path_components, query_dict, single_value_parameters ) + path_components = [ ensure_path_component_is_encoded( path_component ) for path_component in path_components ] + query_dict = { ensure_param_component_is_encoded( name ) : ensure_param_component_is_encoded( value ) for ( name, value ) in query_dict.items() } + single_value_parameters = [ ensure_param_component_is_encoded( single_value_parameter ) for single_value_parameter in single_value_parameters ] path = '/' + '/'.join( path_components ) query = ConvertQueryDictToText( query_dict, single_value_parameters ) @@ -495,9 +526,7 @@ def WashURL( url: str, keep_fragment = True ) -> str: fragment = '' - r = urllib.parse.ParseResult( scheme, netloc, path, params, query, fragment ) - - clean_url = r.geturl() + clean_url = urllib.parse.urlunparse( ( scheme, netloc, path, params, query, fragment ) ) return clean_url diff --git a/hydrus/client/networking/ClientNetworkingGUG.py b/hydrus/client/networking/ClientNetworkingGUG.py index ebee1ebc0..904572e91 100644 --- a/hydrus/client/networking/ClientNetworkingGUG.py +++ b/hydrus/client/networking/ClientNetworkingGUG.py @@ -109,59 +109,39 @@ def GenerateGalleryURL( self, query_text ): raise HydrusExceptions.GUGException( 'Replacement phrase not in URL template!' ) - if re.search( r'%[0-9A-Fa-f]{2}', query_text ) is not None: + query_text = query_text.replace( '%20', ' ' ) + + search_terms = query_text.split( ' ' ) + + looks_like_tags_are_going_into_params = False + + if '?' in self._url_template: - if ' ' in query_text: - - # against probability, there is probably a legit % character here that should be encoded - - search_terms = query_text.split( ' ' ) - - we_think_query_text_is_pre_encoded = False - - elif '%20' in query_text: - - # we are generally confident the user pasted a multi-tag query they copied from a notepad or something - - search_terms = query_text.split( '%20' ) - - # any % character entered here should be encoded as '%25' - we_think_query_text_is_pre_encoded = True - - else: - - # we simply do not know in this case. this is a single tag with a % not at the end, but it could be male%2Ffemale or it could be "120%120%hello", the hit new anime series - # assuming it is the former more often than the latter, we will not intrude on what the user sent here and cross our fingers - - search_terms = [ query_text ] + ( first_half, second_half ) = self._url_template.split( '?', 1 ) + + if self._replacement_phrase in second_half: - we_think_query_text_is_pre_encoded = True + looks_like_tags_are_going_into_params = True - else: - - search_terms = query_text.split( ' ' ) - - # normal, not pre-encoded text - we_think_query_text_is_pre_encoded = False - - if not we_think_query_text_is_pre_encoded: + if looks_like_tags_are_going_into_params: - encoded_search_terms = [ urllib.parse.quote( search_term, safe = '' ) for search_term in search_terms ] + # we pre-encode each term here because we are not talking about URL with RFC 3986 rules, but the taglist, which is 'encode each tag to %-encoding thanks' + # we need to handle the '6+girls+skirt' = '6%2Bgirls+skirt' scenario + + encoded_search_terms = [ ClientNetworkingFunctions.ensure_component_is_encoded( search_term, '' ) for search_term in search_terms ] else: - encoded_search_terms = search_terms + # in this case, we are just plonking an artist name in a path component, so we want normal path encoding rules, not 'encode everything mate' + encoded_search_terms = [ ClientNetworkingFunctions.ensure_path_component_is_encoded( search_term ) for search_term in search_terms ] try: search_phrase = self._search_terms_separator.join( encoded_search_terms ) - # we do not encode the whole thing here since we may want to keep tag-connector-+ for the '6+girls+skirt' = '6%2Bgirls+skirt' scenario - # some characters are optional or something when it comes to encoding. '+' is one of these - gallery_url = self._url_template.replace( self._replacement_phrase, search_phrase ) except Exception as e: @@ -169,7 +149,7 @@ def GenerateGalleryURL( self, query_text ): raise HydrusExceptions.GUGException( str( e ) ) - return gallery_url + return ClientNetworkingFunctions.EnsureURLIsEncoded( gallery_url ) def GenerateGalleryURLs( self, query_text ): diff --git a/hydrus/client/networking/ClientNetworkingJobs.py b/hydrus/client/networking/ClientNetworkingJobs.py index 565de2093..ef58663c5 100644 --- a/hydrus/client/networking/ClientNetworkingJobs.py +++ b/hydrus/client/networking/ClientNetworkingJobs.py @@ -139,7 +139,7 @@ def ConvertStatusCodeAndDataIntoExceptionInfo( status_code, data, is_hydrus_serv HydrusData.DebugPrint( large_chunk ) error_text = 'The server\'s error text was too long to display. The first part follows, while a larger chunk has been written to the log.' - error_text += os.linesep + error_text += '\n' error_text += smaller_chunk @@ -749,7 +749,7 @@ def _SendRequestAndGetResponse( self ) -> requests.Response: if HG.network_report_mode: - HydrusData.ShowText( 'Network Jobs Referral URLs for {}:{}Given: {}{}Used: {}'.format( url, os.linesep, self._referral_url, os.linesep, referral_url ) ) + HydrusData.ShowText( 'Network Jobs Referral URLs for {}:{}Given: {}{}Used: {}'.format( url, '\n', self._referral_url, '\n', referral_url ) ) if referral_url is not None: @@ -760,12 +760,21 @@ def _SendRequestAndGetResponse( self ) -> requests.Response: except UnicodeEncodeError: - # quick and dirty way to quote this url when it comes here with full unicode chars. not perfect, but does the job - referral_url = urllib.parse.quote( referral_url, "!#$%&'()*+,/:;=?@[]~" ) + try: + + # it prob has some weird unicode characters in it, so let's encode + referral_url = ClientNetworkingFunctions.EnsureURLIsEncoded( referral_url ) + + except: + + # ok this situation is crazy, so let's fall back to what I read in StackExchange an eon ago + # quick and dirty way to quote this url when it comes here with full unicode chars. not perfect, but does the job + referral_url = urllib.parse.quote( referral_url, "!#$%&'()*+,/:;=?@[]~" ) + if HG.network_report_mode: - HydrusData.ShowText( 'Network Jobs Quoted Referral URL for {}:{}{}'.format( url, os.linesep, referral_url ) ) + HydrusData.ShowText( 'Network Jobs Quoted Referral URL for {}:{}{}'.format( url, '\n', referral_url ) ) diff --git a/hydrus/client/networking/ClientNetworkingLogin.py b/hydrus/client/networking/ClientNetworkingLogin.py index d965e0e4b..9239443d3 100644 --- a/hydrus/client/networking/ClientNetworkingLogin.py +++ b/hydrus/client/networking/ClientNetworkingLogin.py @@ -563,7 +563,7 @@ def InvalidateLoginScript( self, login_domain, login_script_key, reason ): self._domains_to_login_info[ login_domain ] = ( login_script_key_and_name, credentials, login_access_type, login_access_text, active, validity, validity_error_text, no_work_until, no_work_until_reason ) - HydrusData.ShowText( 'The login for "' + login_domain + '" failed! It will not be reattempted until the problem is fixed. The failure reason was:' + os.linesep * 2 + validity_error_text ) + HydrusData.ShowText( 'The login for "' + login_domain + '" failed! It will not be reattempted until the problem is fixed. The failure reason was:' + '\n' * 2 + validity_error_text ) self._SetDirty() @@ -1672,6 +1672,7 @@ def session_to_cookie_strings( sess ): netloc = domain_to_hit path = self._path params = '' + query = '' fragment = '' single_value_parameters = [] @@ -1689,9 +1690,7 @@ def session_to_cookie_strings( sess ): test_result_body = ClientNetworkingFunctions.ConvertQueryDictToText( query_dict, single_value_parameters ) - r = urllib.parse.ParseResult( scheme, netloc, path, params, query, fragment ) - - url = r.geturl() + url = urllib.parse.urlunparse( ( scheme, netloc, path, params, query, fragment ) ) network_job = ClientNetworkingJobs.NetworkJob( self._method, url, body = body, referral_url = referral_url ) @@ -1699,9 +1698,7 @@ def session_to_cookie_strings( sess ): p = ClientNetworkingFunctions.ParseURL( url ) - r = urllib.parse.ParseResult( p.scheme, p.netloc, '', '', '', '' ) - - origin = r.geturl() # https://accounts.pixiv.net + origin = urllib.parse.urlunparse( ( p.scheme, p.netloc, '', '', '', '' ) ) # https://accounts.pixiv.net network_job.AddAdditionalHeader( 'origin', origin ) # GET/POST forms are supposed to have this for CSRF. we'll try it just with POST for now diff --git a/hydrus/client/networking/ClientNetworkingURLClass.py b/hydrus/client/networking/ClientNetworkingURLClass.py index e26a467f4..c453f058a 100644 --- a/hydrus/client/networking/ClientNetworkingURLClass.py +++ b/hydrus/client/networking/ClientNetworkingURLClass.py @@ -842,14 +842,35 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ): example_url ) = old_serialisable_info - def encode_fixed_string_match( s_m: ClientStrings.StringMatch ) -> ClientStrings.StringMatch: + def encode_fixed_string_match_param( s_m: ClientStrings.StringMatch ) -> ClientStrings.StringMatch: ( match_type, match_value, min_chars, max_chars, example_string ) = s_m.ToTuple() if match_type == ClientStrings.STRING_MATCH_FIXED: - match_value = urllib.parse.quote( match_value ) - example_string = urllib.parse.quote( example_string ) + match_value = ClientNetworkingFunctions.ensure_param_component_is_encoded( match_value ) + example_string = ClientNetworkingFunctions.ensure_param_component_is_encoded( example_string ) + + s_m = ClientStrings.StringMatch( + match_type = match_type, + match_value = match_value, + min_chars = min_chars, + max_chars = max_chars, + example_string = example_string + ) + + + return s_m + + + def encode_fixed_string_match_path( s_m: ClientStrings.StringMatch ) -> ClientStrings.StringMatch: + + ( match_type, match_value, min_chars, max_chars, example_string ) = s_m.ToTuple() + + if match_type == ClientStrings.STRING_MATCH_FIXED: + + match_value = ClientNetworkingFunctions.ensure_path_component_is_encoded( match_value ) + example_string = ClientNetworkingFunctions.ensure_path_component_is_encoded( example_string ) s_m = ClientStrings.StringMatch( match_type = match_type, @@ -868,11 +889,11 @@ def encode_fixed_string_match( s_m: ClientStrings.StringMatch ) -> ClientStrings for ( name, ( serialisable_value_string_match, default_value ) ) in serialisable_parameters: # we are converting from post[id] to post%5Bid%5D - name = urllib.parse.quote( name ) + name = ClientNetworkingFunctions.ensure_param_component_is_encoded( name ) value_string_match = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_value_string_match ) - value_string_match = encode_fixed_string_match( value_string_match ) + value_string_match = encode_fixed_string_match_param( value_string_match ) parameter = URLClassParameterFixedName( name = name, @@ -881,7 +902,7 @@ def encode_fixed_string_match( s_m: ClientStrings.StringMatch ) -> ClientStrings if default_value is not None: - default_value = urllib.parse.quote( default_value ) + default_value = ClientNetworkingFunctions.ensure_param_component_is_encoded( default_value ) parameter.SetDefaultValue( default_value ) @@ -897,11 +918,11 @@ def encode_fixed_string_match( s_m: ClientStrings.StringMatch ) -> ClientStrings for ( string_match, default ) in path_components: - string_match = encode_fixed_string_match( string_match ) + string_match = encode_fixed_string_match_path( string_match ) if default is not None: - default = urllib.parse.quote( default ) + default = ClientNetworkingFunctions.ensure_path_component_is_encoded( default ) new_path_components.append( ( string_match, default ) ) @@ -1142,9 +1163,9 @@ def GetNextGalleryPage( self, url ): raise NotImplementedError( 'Did not understand the next gallery page rules!' ) - r = urllib.parse.ParseResult( scheme, netloc, path, params, query, fragment ) + next_gallery_url = urllib.parse.urlunparse( ( scheme, netloc, path, params, query, fragment ) ) - return r.geturl() + return next_gallery_url def GetParameters( self ) -> typing.List[ URLClassParameterFixedName ]: @@ -1319,9 +1340,9 @@ def Normalise( self, url, for_server = False ): path = self._ClipAndFleshOutPath( path_components, for_server ) query = self._ClipAndFleshOutQuery( query_dict, single_value_parameters, param_order, for_server ) - r = urllib.parse.ParseResult( scheme, netloc, path, params, query, fragment ) + normalised_url = urllib.parse.urlunparse( ( scheme, netloc, path, params, query, fragment ) ) - return r.geturl() + return normalised_url def NoMorePathComponentsThanThis( self ) -> bool: diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 081e92f21..14846bc91 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -105,7 +105,7 @@ # Misc NETWORK_VERSION = 20 -SOFTWARE_VERSION = 569 +SOFTWARE_VERSION = 570 CLIENT_API_VERSION = 63 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 ) diff --git a/hydrus/core/HydrusDB.py b/hydrus/core/HydrusDB.py index ea8771516..9d8cc805c 100644 --- a/hydrus/core/HydrusDB.py +++ b/hydrus/core/HydrusDB.py @@ -286,7 +286,7 @@ def __init__( self, controller: HydrusControllerInterface.HydrusControllerInterf except: - e = Exception( 'Updating the ' + self._db_name + ' db to version ' + str( version + 1 ) + ' caused this error:' + os.linesep + traceback.format_exc() ) + e = Exception( 'Updating the ' + self._db_name + ' db to version ' + str( version + 1 ) + ' caused this error:' + '\n' + traceback.format_exc() ) try: @@ -384,7 +384,7 @@ def _CreateDB( self ): def _DisplayCatastrophicError( self, text ): message = 'The db encountered a serious error! This is going to be written to the log as well, but here it is for a screenshot:' - message += os.linesep * 2 + message += '\n' * 2 message += text HydrusData.DebugPrint( message ) @@ -513,7 +513,7 @@ def _InitDBConnection( self ): except Exception as e: - raise HydrusExceptions.DBAccessException( 'Could not connect to database! If the answer is not obvious to you, please let hydrus dev know. Error follows:' + os.linesep * 2 + str( e ) ) + raise HydrusExceptions.DBAccessException( 'Could not connect to database! If the answer is not obvious to you, please let hydrus dev know. Error follows:' + '\n' * 2 + str( e ) ) HydrusDBBase.TemporaryIntegerTableNameCache.instance().Clear() @@ -550,7 +550,7 @@ def _InitDBConnection( self ): message = 'The database seemed valid, but hydrus failed to read basic data from it. You may need to run the program in a different journal mode using --db_journal_mode. Full error information:' - message += os.linesep * 2 + message += '\n' * 2 message += str( e ) HydrusData.DebugPrint( message ) diff --git a/hydrus/core/HydrusData.py b/hydrus/core/HydrusData.py index 2bf5bb6ff..c32a7e1e9 100644 --- a/hydrus/core/HydrusData.py +++ b/hydrus/core/HydrusData.py @@ -690,6 +690,7 @@ def GenerateHumanTextSortKey(): return split_alphanum + HumanTextSortKey = GenerateHumanTextSortKey() def HumanTextSort( texts ): @@ -906,8 +907,8 @@ def ParseHashesFromRawHexText( hash_type, hex_hashes_raw ): if len( bad_hex_hashes ): m = 'Sorry, {} hashes should have {} hex characters! These did not:'.format( hash_type, expected_hex_length ) - m += os.linesep * 2 - m += os.linesep.join( ( '{} ({} characters)'.format( bad_hex_hash, len( bad_hex_hash ) ) for bad_hex_hash in bad_hex_hashes ) ) + m += '\n' * 2 + m += '\n'.join( ( '{} ({} characters)'.format( bad_hex_hash, len( bad_hex_hash ) ) for bad_hex_hash in bad_hex_hashes ) ) raise Exception( m ) @@ -1015,7 +1016,7 @@ def RecordRunningStart( db_path, instance ): me = psutil.Process() record_string += str( me.pid ) - record_string += os.linesep + record_string += '\n' record_string += str( me.create_time() ) except psutil.Error: diff --git a/hydrus/core/HydrusExceptions.py b/hydrus/core/HydrusExceptions.py index 74c3d5e31..07c19ac43 100644 --- a/hydrus/core/HydrusExceptions.py +++ b/hydrus/core/HydrusExceptions.py @@ -26,7 +26,7 @@ def __str__( self ): s = [ repr( self.args ) ] - return os.linesep.join( s ) + return '\n'.join( s ) diff --git a/hydrus/core/HydrusLogger.py b/hydrus/core/HydrusLogger.py index bfed4122a..68f9898c2 100644 --- a/hydrus/core/HydrusLogger.py +++ b/hydrus/core/HydrusLogger.py @@ -137,7 +137,7 @@ def write( self, value ) -> None: with self._lock: - if value in ( os.linesep, '\n' ): + if value in ( '\n', '\n' ): prefix = '' diff --git a/hydrus/core/HydrusPaths.py b/hydrus/core/HydrusPaths.py index 624302a39..6e06ff2e6 100644 --- a/hydrus/core/HydrusPaths.py +++ b/hydrus/core/HydrusPaths.py @@ -566,7 +566,7 @@ def do_it( launch_path ): except Exception as e: - HydrusData.ShowText( 'Could not launch a file! Command used was:' + os.linesep + str( cmd ) ) + HydrusData.ShowText( 'Could not launch a file! Command used was:' + '\n' + str( cmd ) ) HydrusData.ShowException( e ) diff --git a/hydrus/core/HydrusProfiling.py b/hydrus/core/HydrusProfiling.py index fd356173c..e29d0f145 100644 --- a/hydrus/core/HydrusProfiling.py +++ b/hydrus/core/HydrusProfiling.py @@ -31,12 +31,12 @@ def Profile( summary, code, global_vars, local_vars, min_duration_ms = 20, show_ stats.sort_stats( 'tottime' ) output.write( 'Stats' ) - output.write( os.linesep * 2 ) + output.write( '\n' * 2 ) stats.print_stats() output.write( 'Callers' ) - output.write( os.linesep * 2 ) + output.write( '\n' * 2 ) stats.print_callers() diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py index b581862bd..76f7b3e43 100644 --- a/hydrus/core/HydrusSerialisable.py +++ b/hydrus/core/HydrusSerialisable.py @@ -301,7 +301,7 @@ def InitialiseFromSerialisableInfo( self, original_version, serialisable_info, r if raise_error_on_future_version: message = 'Unfortunately, an object of type {} could not be loaded because it was created in a client/server that uses an updated version of that object! We support up to version {}, but the object was version {}.'.format( self.SERIALISABLE_NAME, self.SERIALISABLE_VERSION, original_version ) - message += os.linesep * 2 + message += '\n' * 2 message += 'Please update your client/server to import this object.' raise HydrusExceptions.SerialisationException( message ) diff --git a/hydrus/core/HydrusTags.py b/hydrus/core/HydrusTags.py index 260b056a6..973cd4abe 100644 --- a/hydrus/core/HydrusTags.py +++ b/hydrus/core/HydrusTags.py @@ -226,7 +226,7 @@ def CleanTag( tag ): except Exception as e: text = 'Was unable to parse the tag: ' + str( tag ) - text += os.linesep * 2 + text += '\n' * 2 text += str( e ) raise Exception( text ) @@ -686,7 +686,7 @@ def GetChangesSummaryText( self, old_tag_filter: "TagFilter" ): rows = [ 'Added rule: {} - {}'.format( HC.filter_black_white_str_lookup[ rule ], ConvertTagSliceToPrettyString( slice ) ) for ( slice, rule ) in new_rules ] - summary_components.append( os.linesep.join( rows ) ) + summary_components.append( '\n'.join( rows ) ) @@ -700,7 +700,7 @@ def GetChangesSummaryText( self, old_tag_filter: "TagFilter" ): rows = [ 'Flipped rule: to {} - {}'.format( HC.filter_black_white_str_lookup[ rule ], ConvertTagSliceToPrettyString( slice ) ) for ( slice, rule ) in changed_rules ] - summary_components.append( os.linesep.join( rows ) ) + summary_components.append( '\n'.join( rows ) ) @@ -714,11 +714,11 @@ def GetChangesSummaryText( self, old_tag_filter: "TagFilter" ): rows = [ 'Deleted rule: {} - {}'.format( HC.filter_black_white_str_lookup[ rule ], ConvertTagSliceToPrettyString( slice ) ) for ( slice, rule ) in deleted_rules ] - summary_components.append( os.linesep.join( rows ) ) + summary_components.append( '\n'.join( rows ) ) - return os.linesep.join( summary_components ) + return '\n'.join( summary_components ) def GetTagSlicesToRules( self ): diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py index 7e2a76ab0..915e736a2 100644 --- a/hydrus/core/HydrusText.py +++ b/hydrus/core/HydrusText.py @@ -35,7 +35,7 @@ def CleanNoteText( t: str ): t = t.strip() - # wash all newlines to be os.linesep + # wash all newlines lines = t.splitlines() @@ -94,6 +94,19 @@ def ElideText( text, max_length, elide_center = False ): return text + +def GetFirstLine( text: str ) -> str: + + if len( text ) > 0: + + return text.splitlines()[0] + + else: + + return '' + + + def LooksLikeHTML( file_data: typing.Union[ str, bytes ] ): # this will false-positive if it is json that contains html, ha ha diff --git a/hydrus/core/HydrusThreading.py b/hydrus/core/HydrusThreading.py index 08e80c716..e43419bb1 100644 --- a/hydrus/core/HydrusThreading.py +++ b/hydrus/core/HydrusThreading.py @@ -660,7 +660,7 @@ def GetPrettyJobSummary( self ) -> str: lines = [ HydrusData.ToHumanInt( num_jobs ) + ' jobs:' ] + job_lines - text = os.linesep.join( lines ) + text = '\n'.join( lines ) return text diff --git a/hydrus/core/files/HydrusVideoHandling.py b/hydrus/core/files/HydrusVideoHandling.py index 7f45db2b9..e09bf17ae 100644 --- a/hydrus/core/files/HydrusVideoHandling.py +++ b/hydrus/core/files/HydrusVideoHandling.py @@ -96,13 +96,13 @@ def GetFFMPEGVersion(): HydrusData.ShowText( message ) - message += os.linesep * 2 + message += '\n' * 2 message += str( sbp_kwargs ) - message += os.linesep * 2 + message += '\n' * 2 message += str( os.environ ) - message += os.linesep * 2 + message += '\n' * 2 message += 'STDOUT Response: {}'.format( stdout ) - message += os.linesep * 2 + message += '\n' * 2 message += 'STDERR Response: {}'.format( stderr ) HydrusData.Print( message ) @@ -155,7 +155,7 @@ def GetFFMPEGInfoLines( path, count_frames_manually = False, only_first_second = if not FFMPEG_MISSING_ERROR_PUBBED: message = 'FFMPEG, which hydrus uses to parse and render video, was not found! This may be due to it not being available on your system, or hydrus being unable to find it.' - message += os.linesep * 2 + message += '\n' * 2 if HC.PLATFORM_WINDOWS: @@ -166,7 +166,7 @@ def GetFFMPEGInfoLines( path, count_frames_manually = False, only_first_second = message += 'If you are certain that FFMPEG is installed on your OS and accessible in your PATH, please let hydrus_dev know, as this problem is likely due to an environment problem. You may be able to solve this problem immediately by putting a static build of the ffmpeg executable in your install_dir/bin folder.' - message += os.linesep * 2 + message += '\n' * 2 message += 'You can check your current FFMPEG status through help->about.' HydrusData.ShowText( message ) @@ -188,18 +188,18 @@ def GetFFMPEGInfoLines( path, count_frames_manually = False, only_first_second = if not FFMPEG_NO_CONTENT_ERROR_PUBBED: message = 'FFMPEG, which hydrus uses to parse and render video, did not return any data on a recent file metadata check! More debug info has been written to the log.' - message += os.linesep * 2 + message += '\n' * 2 message += 'You can check this info again through help->about.' HydrusData.ShowText( message ) - message += os.linesep * 2 + message += '\n' * 2 message += str( sbp_kwargs ) - message += os.linesep * 2 + message += '\n' * 2 message += str( os.environ ) - message += os.linesep * 2 + message += '\n' * 2 message += 'STDOUT Response: {}'.format( stdout ) - message += os.linesep * 2 + message += '\n' * 2 message += 'STDERR Response: {}'.format( stderr ) HydrusData.DebugPrint( message ) @@ -494,7 +494,7 @@ def RenderImageToImagePath( path, temp_image_path ): if not FFMPEG_MISSING_ERROR_PUBBED: message = 'FFMPEG, which hydrus uses to parse and render video, was not found! This may be due to it not being available on your system, or hydrus being unable to find it.' - message += os.linesep * 2 + message += '\n' * 2 if HC.PLATFORM_WINDOWS: @@ -505,7 +505,7 @@ def RenderImageToImagePath( path, temp_image_path ): message += 'If you are certain that FFMPEG is installed on your OS and accessible in your PATH, please let hydrus_dev know, as this problem is likely due to an environment problem. You may be able to solve this problem immediately by putting a static build of the ffmpeg executable in your install_dir/bin folder.' - message += os.linesep * 2 + message += '\n' * 2 message += 'You can check your current FFMPEG status through help->about.' HydrusData.ShowText( message ) diff --git a/hydrus/core/files/images/HydrusImageMetadata.py b/hydrus/core/files/images/HydrusImageMetadata.py index 5f67b3ec1..070ece5bb 100644 --- a/hydrus/core/files/images/HydrusImageMetadata.py +++ b/hydrus/core/files/images/HydrusImageMetadata.py @@ -42,7 +42,7 @@ def render_dict( d, prefix ): row_text = '{}{}:'.format( prefix, key ) - row_text += os.linesep + row_text += '\n' row_text += value_string texts.append( row_text ) @@ -50,7 +50,7 @@ def render_dict( d, prefix ): if len( texts ) > 0: - return os.linesep.join( texts ) + return '\n'.join( texts ) else: diff --git a/hydrus/core/networking/HydrusNATPunch.py b/hydrus/core/networking/HydrusNATPunch.py index 0c5550f05..75ce31cdc 100644 --- a/hydrus/core/networking/HydrusNATPunch.py +++ b/hydrus/core/networking/HydrusNATPunch.py @@ -107,7 +107,7 @@ def GetExternalIP(): if stderr is not None and len( stderr ) > 0: - raise Exception( 'Problem while trying to fetch External IP (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + os.linesep * 2 + str( stderr ) ) + raise Exception( 'Problem while trying to fetch External IP (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + '\n' * 2 + str( stderr ) ) else: @@ -192,17 +192,17 @@ def AddUPnPMappingCheckResponse( internal_client, internal_port, external_port, if 'UnknownError' in stdout: - raise HydrusExceptions.RouterException( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stdout ) + raise HydrusExceptions.RouterException( 'Problem while trying to add UPnP mapping:' + '\n' * 2 + stdout ) else: - raise Exception( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stdout ) + raise Exception( 'Problem while trying to add UPnP mapping:' + '\n' * 2 + stdout ) if stderr is not None and len( stderr ) > 0: - raise Exception( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stderr ) + raise Exception( 'Problem while trying to add UPnP mapping:' + '\n' * 2 + stderr ) def GetUPnPMappings(): @@ -233,7 +233,7 @@ def GetUPnPMappings(): if stderr is not None and len( stderr ) > 0: - raise Exception( 'Problem while trying to fetch UPnP mappings (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + os.linesep * 2 + stderr ) + raise Exception( 'Problem while trying to fetch UPnP mappings (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + '\n' * 2 + stderr ) else: @@ -316,7 +316,7 @@ def GetUPnPMappingsParseResponse( stdout ): HydrusData.Print( 'Full response follows:' ) HydrusData.Print( stdout ) - raise Exception( 'Problem while trying to parse UPnP mappings:' + os.linesep * 2 + str( e ) ) + raise Exception( 'Problem while trying to parse UPnP mappings:' + '\n' * 2 + str( e ) ) def RemoveUPnPMapping( external_port, protocol ): @@ -347,7 +347,7 @@ def RemoveUPnPMapping( external_port, protocol ): if stderr is not None and len( stderr ) > 0: - raise Exception( 'Problem while trying to remove UPnP mapping:' + os.linesep * 2 + stderr ) + raise Exception( 'Problem while trying to remove UPnP mapping:' + '\n' * 2 + stderr ) class ServicesUPnPManager( object ): diff --git a/hydrus/core/networking/HydrusNetworkVariableHandling.py b/hydrus/core/networking/HydrusNetworkVariableHandling.py index 723acdde8..1e87ba3d8 100644 --- a/hydrus/core/networking/HydrusNetworkVariableHandling.py +++ b/hydrus/core/networking/HydrusNetworkVariableHandling.py @@ -139,7 +139,9 @@ def DumpToGETQuery( args ): if name in args: - args[ name ] = urllib.parse.quote( args[ name ] ) + PARAM_EXCEPTION_CHARS = "!$&'()*+,;=@:/?" + + args[ name ] = urllib.parse.quote( args[ name ], safe = PARAM_EXCEPTION_CHARS ) @@ -198,7 +200,7 @@ def ParseFileArguments( path, decompression_bombs_ok = False ): tb = traceback.format_exc() - raise HydrusExceptions.BadRequestException( 'Could not generate thumbnail from that file:' + os.linesep + tb ) + raise HydrusExceptions.BadRequestException( 'Could not generate thumbnail from that file:' + '\n' + tb ) args[ 'thumbnail' ] = thumbnail_bytes diff --git a/hydrus/server/ServerController.py b/hydrus/server/ServerController.py index ff35323ac..2c3f5f4d9 100644 --- a/hydrus/server/ServerController.py +++ b/hydrus/server/ServerController.py @@ -106,7 +106,7 @@ def ShutdownSiblingInstance( db_dir ): except: text = 'Could not contact existing server\'s port ' + str( port ) + '!' - text += os.linesep + text += '\n' text += traceback.format_exc() raise HydrusExceptions.ShutdownException( text ) @@ -123,7 +123,7 @@ def ShutdownSiblingInstance( db_dir ): if not r.ok: text = 'When told to shut down, the existing server gave an error!' - text += os.linesep + text += '\n' text += r.text raise HydrusExceptions.ShutdownException( text ) diff --git a/hydrus/server/ServerDB.py b/hydrus/server/ServerDB.py index 8b3c1baf8..b14b715be 100644 --- a/hydrus/server/ServerDB.py +++ b/hydrus/server/ServerDB.py @@ -1358,7 +1358,7 @@ def _ManageDBError( self, job, e ): ( exception_type, value, tb ) = sys.exc_info() - new_e = type( e )( os.linesep.join( traceback.format_exception( exception_type, value, tb ) ) ) + new_e = type( e )( '\n'.join( traceback.format_exception( exception_type, value, tb ) ) ) job.PutResult( new_e ) diff --git a/hydrus/server/networking/ServerServerResources.py b/hydrus/server/networking/ServerServerResources.py index 03c7dba0a..0b8ecdfe2 100644 --- a/hydrus/server/networking/ServerServerResources.py +++ b/hydrus/server/networking/ServerServerResources.py @@ -1248,7 +1248,7 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): HydrusData.Print( 'Account {} changed the tag filter. Rule changes are:{}{}.'.format( request.hydrus_account.GetAccountKey().hex(), - os.linesep, + '\n', summary_text ) ) diff --git a/hydrus/test/TestClientMetadataMigration.py b/hydrus/test/TestClientMetadataMigration.py index 991932e2b..01fb5f590 100644 --- a/hydrus/test/TestClientMetadataMigration.py +++ b/hydrus/test/TestClientMetadataMigration.py @@ -118,7 +118,7 @@ def test_router( self ): with open( expected_input_path_1, 'w', encoding = 'utf-8' ) as f: - f.write( os.linesep.join( rows_1 ) ) + f.write( '\n'.join( rows_1 ) ) importer_1 = ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT( suffix = '1' ) @@ -127,7 +127,7 @@ def test_router( self ): with open( expected_input_path_2, 'w', encoding = 'utf-8' ) as f: - f.write( os.linesep.join( rows_2 ) ) + f.write( '\n'.join( rows_2 ) ) importer_2 = ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT( suffix = '2' ) @@ -472,7 +472,7 @@ def test_media_txt( self ): with open( expected_input_path, 'w', encoding = 'utf-8' ) as f: - f.write( os.linesep.join( rows ) ) + f.write( '\n'.join( rows ) ) importer = ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT() @@ -514,7 +514,7 @@ def test_media_txt( self ): with open( expected_input_path, 'w', encoding = 'utf-8' ) as f: - f.write( os.linesep.join( rows ) ) + f.write( '\n'.join( rows ) ) importer = ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT( string_processor = string_processor, suffix = 'tags' ) @@ -533,7 +533,7 @@ def test_media_txt( self ): with open( expected_input_path, 'w', encoding = 'utf-8' ) as f: - f.write( os.linesep.join( rows ) ) + f.write( '\n'.join( rows ) ) importer = ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT( remove_actual_filename_ext = True, filename_string_converter = ClientStrings.StringConverter( conversions = [ ( ClientStrings.STRING_CONVERSION_REMOVE_TEXT_FROM_BEGINNING, 1 ) ] ) ) diff --git a/hydrus/test/TestClientNetworking.py b/hydrus/test/TestClientNetworking.py index afcc687fd..d2774d960 100644 --- a/hydrus/test/TestClientNetworking.py +++ b/hydrus/test/TestClientNetworking.py @@ -19,6 +19,7 @@ from hydrus.client.networking import ClientNetworkingContexts from hydrus.client.networking import ClientNetworkingDomain from hydrus.client.networking import ClientNetworkingFunctions +from hydrus.client.networking import ClientNetworkingGUG from hydrus.client.networking import ClientNetworkingJobs from hydrus.client.networking import ClientNetworkingLogin from hydrus.client.networking import ClientNetworkingSessions @@ -225,6 +226,59 @@ def test_can_continue( self ): pass + +class TestGUGs( unittest.TestCase ): + + def test_some_basics( self ): + + gug = ClientNetworkingGUG.GalleryURLGenerator( + 'test', + url_template = 'https://blahbooru.com/post/search?tags=%tags%', + replacement_phrase = '%tags%', + search_terms_separator = '+', + initial_search_text = 'enter tags', + example_search_text = 'blonde_hair blue_eyes' + ) + + self.assertEqual( gug.GetExampleURL(), 'https://blahbooru.com/post/search?tags=blonde_hair+blue_eyes' ) + self.assertEqual( gug.GenerateGalleryURL( 'blonde_hair%20blue_eyes' ), 'https://blahbooru.com/post/search?tags=blonde_hair+blue_eyes' ) + self.assertEqual( gug.GenerateGalleryURL( '100% nice' ), 'https://blahbooru.com/post/search?tags=100%25+nice' ) + self.assertEqual( gug.GenerateGalleryURL( '6+girls blonde_hair' ), 'https://blahbooru.com/post/search?tags=6%2Bgirls+blonde_hair' ) + self.assertEqual( gug.GenerateGalleryURL( '@artistname' ), 'https://blahbooru.com/post/search?tags=%40artistname' ) + self.assertEqual( gug.GenerateGalleryURL( '日本 語版' ), 'https://blahbooru.com/post/search?tags=%E6%97%A5%E6%9C%AC+%E8%AA%9E%E7%89%88' ) + + gug = ClientNetworkingGUG.GalleryURLGenerator( + 'test', + url_template = 'https://blahsite.net/post/%username%', + replacement_phrase = '%username%', + search_terms_separator = '_', + initial_search_text = 'enter username', + example_search_text = 'someguy' + ) + + self.assertEqual( gug.GetExampleURL(), 'https://blahsite.net/post/someguy' ) + self.assertEqual( gug.GenerateGalleryURL( 'someguy' ), 'https://blahsite.net/post/someguy' ) + self.assertEqual( gug.GenerateGalleryURL( 'some guy' ), 'https://blahsite.net/post/some_guy' ) + self.assertEqual( gug.GenerateGalleryURL( '@someguy' ), 'https://blahsite.net/post/@someguy' ) # note this does not encode since this is a path component + self.assertEqual( gug.GenerateGalleryURL( '日本 語版' ), 'https://blahsite.net/post/%E6%97%A5%E6%9C%AC_%E8%AA%9E%E7%89%88' ) + + gug = ClientNetworkingGUG.GalleryURLGenerator( + 'test', + url_template = 'https://blahsite.net/post/%username%?page=1', + replacement_phrase = '%username%', + search_terms_separator = '_', + initial_search_text = 'enter username', + example_search_text = 'someguy' + ) + + self.assertEqual( gug.GetExampleURL(), 'https://blahsite.net/post/someguy?page=1' ) + self.assertEqual( gug.GenerateGalleryURL( 'someguy' ), 'https://blahsite.net/post/someguy?page=1' ) + self.assertEqual( gug.GenerateGalleryURL( 'some guy' ), 'https://blahsite.net/post/some_guy?page=1' ) + self.assertEqual( gug.GenerateGalleryURL( '@someguy' ), 'https://blahsite.net/post/@someguy?page=1' ) # note this does not encode since this is a path component + self.assertEqual( gug.GenerateGalleryURL( '日本 語版' ), 'https://blahsite.net/post/%E6%97%A5%E6%9C%AC_%E8%AA%9E%E7%89%88?page=1' ) + + + class TestURLClasses( unittest.TestCase ): def test_url_class_basics( self ): @@ -285,17 +339,23 @@ def test_encoding( self ): human_url = 'https://testbooru.cx/post/page.php?id=1234 56&s=view' encoded_url = 'https://testbooru.cx/post/page.php?id=1234%2056&s=view' - self.assertEqual( ClientNetworkingFunctions.WashURL( human_url ), encoded_url ) - self.assertEqual( ClientNetworkingFunctions.WashURL( encoded_url ), encoded_url ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url ), encoded_url ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url ), encoded_url ) human_url_with_fragment = 'https://testbooru.cx/post/page.php?id=1234 56&s=view#hello' encoded_url_with_fragment = 'https://testbooru.cx/post/page.php?id=1234%2056&s=view#hello' - self.assertEqual( ClientNetworkingFunctions.WashURL( human_url_with_fragment ), encoded_url_with_fragment ) - self.assertEqual( ClientNetworkingFunctions.WashURL( encoded_url_with_fragment ), encoded_url_with_fragment ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url_with_fragment ), encoded_url_with_fragment ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url_with_fragment ), encoded_url_with_fragment ) + + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url_with_fragment, keep_fragment = False ), encoded_url ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url_with_fragment, keep_fragment = False ), encoded_url ) + + human_url_with_mix = 'https://testbooru.cx/po@s%20t/page.php?id=1234 56&s=view%%25' + encoded_url_with_mix = 'https://testbooru.cx/po@s%20t/page.php?id=1234%2056&s=view%25%25' - self.assertEqual( ClientNetworkingFunctions.WashURL( human_url_with_fragment, keep_fragment = False ), encoded_url ) - self.assertEqual( ClientNetworkingFunctions.WashURL( encoded_url_with_fragment, keep_fragment = False ), encoded_url ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url_with_mix ), encoded_url_with_mix ) + self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url_with_mix ), encoded_url_with_mix ) def test_defaults( self ): diff --git a/hydrus/test/TestClientParsing.py b/hydrus/test/TestClientParsing.py index ef0754031..7a52ddf23 100644 --- a/hydrus/test/TestClientParsing.py +++ b/hydrus/test/TestClientParsing.py @@ -40,7 +40,7 @@ def ToPrettyString( self ): def ToPrettyMultilineString( self ): - return 'test dummy formula' + os.linesep + 'returns what you give it' + return 'test dummy formula' + '\n' + 'returns what you give it' diff --git a/static/default/gugs/nitter (.eu mirror) media lookup.png b/static/default/gugs/nitter (.eu mirror) media lookup.png deleted file mode 100644 index 50d10f352..000000000 Binary files a/static/default/gugs/nitter (.eu mirror) media lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter (.eu mirror) retweets lookup.png b/static/default/gugs/nitter (.eu mirror) retweets lookup.png deleted file mode 100644 index 7e6c09522..000000000 Binary files a/static/default/gugs/nitter (.eu mirror) retweets lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter (nixnet mirror) media lookup.png b/static/default/gugs/nitter (nixnet mirror) media lookup.png deleted file mode 100644 index d24e82f3e..000000000 Binary files a/static/default/gugs/nitter (nixnet mirror) media lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter (nixnet mirror) retweets lookup.png b/static/default/gugs/nitter (nixnet mirror) retweets lookup.png deleted file mode 100644 index febea3b60..000000000 Binary files a/static/default/gugs/nitter (nixnet mirror) retweets lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter media and retweets lookup.png b/static/default/gugs/nitter media and retweets lookup.png deleted file mode 100644 index c6ab7b9db..000000000 Binary files a/static/default/gugs/nitter media and retweets lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter media lookup.png b/static/default/gugs/nitter media lookup.png deleted file mode 100644 index 9bc55b3c6..000000000 Binary files a/static/default/gugs/nitter media lookup.png and /dev/null differ diff --git a/static/default/gugs/nitter retweets lookup.png b/static/default/gugs/nitter retweets lookup.png deleted file mode 100644 index 2f116778d..000000000 Binary files a/static/default/gugs/nitter retweets lookup.png and /dev/null differ diff --git a/static/default/parsers/nitter media parser.png b/static/default/parsers/nitter media parser.png deleted file mode 100644 index b7bda50f4..000000000 Binary files a/static/default/parsers/nitter media parser.png and /dev/null differ diff --git a/static/default/parsers/nitter retweet parser.png b/static/default/parsers/nitter retweet parser.png deleted file mode 100644 index 5be289c11..000000000 Binary files a/static/default/parsers/nitter retweet parser.png and /dev/null differ diff --git a/static/default/parsers/nitter tweet parser (video from koto.reisen).png b/static/default/parsers/nitter tweet parser (video from koto.reisen).png deleted file mode 100644 index 8f7eb9a89..000000000 Binary files a/static/default/parsers/nitter tweet parser (video from koto.reisen).png and /dev/null differ diff --git a/static/default/parsers/nitter tweet parser.png b/static/default/parsers/nitter tweet parser.png deleted file mode 100644 index 36319f6b2..000000000 Binary files a/static/default/parsers/nitter tweet parser.png and /dev/null differ