From 9b31343f6be0709e258c30c4a824741e2aa43bff Mon Sep 17 00:00:00 2001 From: Hydrus Network Developer Date: Wed, 13 May 2020 14:03:16 -0500 Subject: [PATCH] Version 397 --- README.md | 2 +- help/access_keys.html | 2 +- help/changelog.html | 45 + help/contact.html | 2 +- help/faq.html | 11 +- hydrus/client/ClientAPI.py | 4 +- hydrus/client/ClientController.py | 6 +- hydrus/client/ClientDB.py | 95 +- hydrus/client/ClientExporting.py | 8 +- hydrus/client/ClientFiles.py | 12 +- hydrus/client/ClientLocalServerResources.py | 11 +- hydrus/client/ClientManagers.py | 4 +- hydrus/client/ClientMedia.py | 106 +- hydrus/client/ClientMediaManagers.py | 8 +- hydrus/client/ClientOptions.py | 2 + hydrus/client/ClientParsing.py | 271 ++- hydrus/client/ClientSearch.py | 114 +- hydrus/client/ClientSerialisable.py | 4 +- hydrus/client/ClientServices.py | 88 +- hydrus/client/gui/ClientGUI.py | 72 +- hydrus/client/gui/ClientGUIACDropdown.py | 19 +- hydrus/client/gui/ClientGUICommon.py | 21 +- hydrus/client/gui/ClientGUIControls.py | 1524 +---------------- hydrus/client/gui/ClientGUIDialogs.py | 11 +- hydrus/client/gui/ClientGUIExport.py | 4 +- hydrus/client/gui/ClientGUIImport.py | 4 +- hydrus/client/gui/ClientGUIListBoxes.py | 17 +- hydrus/client/gui/ClientGUIListCtrl.py | 8 +- hydrus/client/gui/ClientGUILogin.py | 24 +- hydrus/client/gui/ClientGUIMedia.py | 12 +- hydrus/client/gui/ClientGUIPages.py | 90 +- hydrus/client/gui/ClientGUIParsing.py | 51 +- hydrus/client/gui/ClientGUIResults.py | 414 ++--- .../client/gui/ClientGUIScrolledPanelsEdit.py | 40 +- .../gui/ClientGUIScrolledPanelsManagement.py | 82 +- .../gui/ClientGUIScrolledPanelsReview.py | 4 +- hydrus/client/gui/ClientGUISearch.py | 68 +- .../client/gui/ClientGUIShortcutControls.py | 4 +- hydrus/client/gui/ClientGUIShortcuts.py | 6 +- hydrus/client/gui/ClientGUIStringControls.py | 606 +++++++ hydrus/client/gui/ClientGUIStringPanels.py | 1283 ++++++++++++++ hydrus/client/gui/ClientGUITagSuggestions.py | 4 +- hydrus/client/gui/QtPorting.py | 16 +- .../client/importing/ClientImportOptions.py | 4 +- .../importing/ClientImportSubscriptions.py | 26 +- .../networking/ClientNetworkingDomain.py | 81 +- .../client/networking/ClientNetworkingJobs.py | 28 +- .../networking/ClientNetworkingLogin.py | 11 +- hydrus/core/HydrusConstants.py | 2 +- hydrus/core/HydrusData.py | 4 +- hydrus/core/HydrusExceptions.py | 22 +- hydrus/core/HydrusImageHandling.py | 8 +- hydrus/core/HydrusNetwork.py | 4 +- hydrus/core/HydrusPaths.py | 25 +- hydrus/core/HydrusSerialisable.py | 2 + hydrus/test/TestClientAPI.py | 31 +- hydrus/test/TestClientDB.py | 16 +- hydrus/test/TestClientDBDuplicates.py | 23 - hydrus/test/TestClientImageHandling.py | 1 - hydrus/test/TestClientImportOptions.py | 1 - hydrus/test/TestClientImportSubscriptions.py | 15 +- hydrus/test/TestClientListBoxes.py | 6 - hydrus/test/TestClientMigration.py | 3 - hydrus/test/TestClientNetworking.py | 3 - hydrus/test/TestClientParsing.py | 233 +++ hydrus/test/TestClientTags.py | 4 - hydrus/test/TestClientThreading.py | 5 - hydrus/test/TestController.py | 10 +- hydrus/test/TestDialogs.py | 7 - hydrus/test/TestFunctions.py | 2 - hydrus/test/TestHydrusNATPunch.py | 4 - hydrus/test/TestHydrusNetworking.py | 4 - hydrus/test/TestHydrusSerialisable.py | 8 - hydrus/test/TestHydrusServer.py | 10 - hydrus/test/TestHydrusSessions.py | 3 - hydrus/test/TestServerDB.py | 27 - 76 files changed, 3396 insertions(+), 2416 deletions(-) create mode 100644 hydrus/client/gui/ClientGUIStringControls.py create mode 100644 hydrus/client/gui/ClientGUIStringPanels.py create mode 100644 hydrus/test/TestClientParsing.py diff --git a/README.md b/README.md index 82344a05..58effedc 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The client can do quite a lot! Please check out the help inside the release or [ * [endchan bunker](https://endchan.net/hydrus/) * [twitter](https://twitter.com/hydrusnetwork) * [tumblr](http://hydrus.tumblr.com/) -* [discord](https://discord.gg/3H8UTpb) +* [discord](https://discord.gg/wPHPCUZ) * [patreon](https://www.patreon.com/hydrus_dev) ## Attribution diff --git a/help/access_keys.html b/help/access_keys.html index 3bf7bc07..089f0399 100755 --- a/help/access_keys.html +++ b/help/access_keys.html @@ -6,7 +6,7 @@
-

The PTR is now run by users with more bandwidth than I had to give, so the bandwidth limits are gone! If you would like to talk with the new management, please check the discord.

+

The PTR is now run by users with more bandwidth than I had to give, so the bandwidth limits are gone! If you would like to talk with the new management, please check the discord.

A guide and schema for the new PTR is here.

first off

I have purposely not pre-baked any default repositories into the client. You have to choose to connect yourself. The client will never connect anywhere until you tell it to.

diff --git a/help/changelog.html b/help/changelog.html index a97a06ff..1c40e121 100755 --- a/help/changelog.html +++ b/help/changelog.html @@ -8,6 +8,51 @@

changelog

    +
  • version 397

  • +
      +
    • regular changelog:
    • +
    • added 'system:has/has no note with name xxx' to search for specific note names
    • +
    • in the normal system predicate list, the notes pred is now the generic 'system:notes' to launch a combined dialog for both num notes and named notes
    • +
    • favourite tag suggestions are now sorted in manage tags dialog according to the default tag sort
    • +
    • page names will now middle...elide when there are too many to fit into a row (and normally left/right buttons would be added). if the elided tabs still do not fit, the buttons will pop up as before. added a checkbox to options->gui pages to turn this text eliding off
    • +
    • pulled the 'page name' options on that panel into their own box and added some text regarding the 'my big row of import page tabs keeps scrolling weird' issue
    • +
    • when files are pixel duplicates, the filesize and age comparison statements will now have 0 score and thus be coloured neutral blue
    • +
    • the standard text entry dialog now always selects any default text it starts with, so you can now type to immediately overwrite. see how you like it and if there are some places where you think an exception should be made
    • +
    • updated the IPFS interface to work with the new IPFS 5.0. all api requests are now POST so it doesn't 405, and the User-Agent is overridden to one that IPFS will not 403 at, and I fixed a typo the new api is more strict about
    • +
    • a hack to get page splitters to lay out correctly on session load is rewritten from a hammer to a scalpel. pages now set their splitter positions on their first individual visible selection. this both reduces some minor ui lag on session/page load and improves splitter positions for clients that open minimised to the system tray
    • +
    • a long-time odd issue where loaded sessions would initially select the top-left-most non-page of pages is fixed. now the bottom-left-most page of any kind is selected
    • +
    • fixed tag autocomplete selecting the bottom-most pre-loading result. it now correctly selects at the top
    • +
    • fixed an issue setting certain values (typically loading a default) to a tag import options panel
    • +
    • the client is now more aggressive about clearing subscriptions from memory when they are finished running
    • +
    • in windows, the main method that copies files now checks for modified time of the source file. if it is before 1980-01-01 UTC, it does not copy the file metadata, as some Windows has trouble with this lmaoooo
    • +
    • cleaned up how some thumbnail 'current focus' media determination code works. should have fixed some weird errors when hitting certain shortcuts on collections
    • +
    • cleaned up basic list/sort code across the program
    • +
    • the 'queue' and add/edit/delete listboxes now emit change signals when new items are added or imported
    • +
    • pyparsing, a helper for cloudscraper, is now correctly bundled in the built releases. a new line in help->about displays this
    • +
    • help->about now lists cloudscraper version
    • +
    • updated the discord link to the new https://discord.gg/wPHPCUZ
    • +
    • .
    • +
    • upcoming string processing changes for advanced users:
    • +
    • I extended string parsing code this week, but I am not yet ready to turn it on. when it does come on, it will change all formulae from the fixed string match/converter pair a combined general string processing 'script' of n steps
    • +
    • wrote a new 'string splitter' object that takes one strings and splits it into up to n strings based on a separator phrase (such as ' ,')
    • +
    • wrote an edit panel for string splitters
    • +
    • wrote a new 'string processor' object that holds n ordered string match/converter/splitter objects and filters/converts/splits x strings into y strings based on those steps
    • +
    • wrote an edit panel for string processors. it has a notebook that live updates with test results for each step on every update
    • +
    • wrote unit tests for string match
    • +
    • wrote unit tests for string converter
    • +
    • wrote unit tests for string splitter
    • +
    • wrote unit tests for string processor
    • +
    • refactored string conversion edit panels to their own file
    • +
    • refactored string conversion controls to their own file
    • +
    • misc string processing cleanup and labelling improvements
    • +
    • .
    • +
    • technical url parsing stuff:
    • +
    • urls are now stripped of leading and trailing whitespace during normalisation, just in case a paste contains some extra whitespace. previously, it would sometimes throw a 'doesn't start with http' error
    • +
    • the hydrus url normalisation process now normalises the hostname according to the NKFC unicode format, meaning unusual characters like ?and e◌́ are now replaced with their normalised visual equivalent ? and é, and hence these urls will no longer throw errors when they are added
    • +
    • if '?' or '#' end up in a hostname (which are invalid characters), it is now converted to _, just to stop complete parse mangling when weird urls are submitted. this character replacement may become more sophisticated in future
    • +
    • the hydrus downloader should now support search terms that include '#'
    • +
    • download query parameters that contain '%23' ('#', encoded) are now not unquoted in url normalisation
    • +
  • version 396

  • diff --git a/help/faq.html b/help/faq.html index 654de2ad..854a2dc2 100755 --- a/help/faq.html +++ b/help/faq.html @@ -12,12 +12,13 @@

    wiki

    A tag is a small bit of text describing a single property of something. They make searching easy. Good examples are "flower" or "nicolas cage" or "the sopranos" or "2003". By combining several tags together ( e.g. [ 'tiger woods', 'sports illustrated', '2008' ] or [ 'cosplay', 'the legend of zelda' ] ), a huge image collection is reduced to a tiny and easy-to-digest sample.

    A good word for the connection of a particular tag to a particular file is mapping.

    -

    In the hydrus network, all tags are automatically converted to lower case. 'Sunset Drive' becomes 'sunset drive'. Why?

    +

    Hydrus is designed with the intention that tags are for searching, not describing. Workflows and UI are tuned for finding files and other similar files (e.g. by the same artist), and while it is possible to have nice metadata overlays around files, this is not considered their chief purpose. Trying to have 'perfect' descriptions for files is often a rabbit-hole that can consume hours of work with relatively little demonstrable benefit.

    +

    All tags are automatically converted to lower case. 'Sunset Drive' becomes 'sunset drive'. Why?

      -
    1. Although it may seem preferable to have 'The Lord of the Rings' rather than 'the lord of the rings', there are many, many special cases where style guides differ on which words to capitalise.
    2. -
    3. Searches become far easier when case is not matched. And When case does not matter, what point is there in recording it?
    4. +
    5. Although it is more beautiful to have 'The Lord of the Rings' rather than 'the lord of the rings', there are many, many special cases where style guides differ on which words to capitalise.
    6. +
    7. As 'The Lord of the Rings' and 'the lord of the rings' are semantically identical, it is natural to search in a case insensitive way. When case does not matter, what point is there in recording it?
    -

    Secondly, leading and trailing whitespace is removed, and multiple whitespace is collapsed to a single character.

    '  yellow   dress '
    becomes
    'yellow dress'

    +

    Furthermore, leading and trailing whitespace is removed, and multiple whitespace is collapsed to a single character.

    '  yellow   dress '
    becomes
    'yellow dress'

    what is a namespace?

    A namespace is a category that in hydrus prefixes a tag. An example is 'person' in the tag 'person:ron paul'--it lets people and software know that 'ron paul' is a name. You can create any namespace you like; just type one or more words and then a colon, and then the next string of text will have that namespace.

    The hydrus client gives namespaces different colours so you can pick out important tags more easily in a large list, and you can also search by a particular namespace, even creating complicated predicates like 'give all files that do not have any character tags', for instance.

    @@ -30,7 +31,7 @@
  • A filename is often--for ridiculous reasons--limited to a certain prohibitive character set. Even when utf-8 is supported, some arbitrary ascii characters are usually not, and different localisations, operating systems and formatting conventions only make it worse.

  • Folders can offer context, but they are clunky and time-consuming to change. If you put each chapter of a comic in a different folder, for instance, reading several volumes in one sitting can be a pain. Nesting many folders adds navigation-latency and tends to induce less informative "04.jpg"-type filenames.
-

So, the client tracks files by their hash.

+

So, the client tracks files by their hash. This technical identifier easily eliminates duplicates and permits the database to robustly attach other metadata like tags and ratings and known urls and notes and everything else, even across multiple clients and even if a file is deleted and later imported.

As a general rule, I suggest you not set up hydrus to parse and display all your imported files' filenames as tags. 'image.jpg' is useless as a tag. Shed the concept of filenames as you would chains.

can the client manage files from their original locations?

When the client imports a file, it makes a quickly accessible but human-ugly copy in its internal database, by default under install_dir/db/client_files. When it needs to access that file again, it always knows where it is, and it can be confident it is what it expects it to be. It never accesses the original again.

diff --git a/hydrus/client/ClientAPI.py b/hydrus/client/ClientAPI.py index c258c7e0..53f314cb 100644 --- a/hydrus/client/ClientAPI.py +++ b/hydrus/client/ClientAPI.py @@ -372,9 +372,7 @@ class APIPermissions( HydrusSerialisable.SerialisableBaseNamed ): with self._lock: - l = [ basic_permission_to_str_lookup[ p ] for p in self._basic_permissions ] - - l.sort() + l = sorted( ( basic_permission_to_str_lookup[ p ] for p in self._basic_permissions ) ) return ', '.join( l ) diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py index 16db93b9..68541ae6 100644 --- a/hydrus/client/ClientController.py +++ b/hydrus/client/ClientController.py @@ -228,11 +228,7 @@ class Controller( HydrusController.HydrusController ): def _ReportShutdownDaemonsStatus( self ): - names = { daemon.name for daemon in self._daemons if daemon.is_alive() } - - names = list( names ) - - names.sort() + names = sorted( { daemon.name for daemon in self._daemons if daemon.is_alive() } ) self.pub( 'splash_set_status_subtext', ', '.join( names ) ) diff --git a/hydrus/client/ClientDB.py b/hydrus/client/ClientDB.py index 69ed6493..a873c1c7 100644 --- a/hydrus/client/ClientDB.py +++ b/hydrus/client/ClientDB.py @@ -1622,11 +1622,7 @@ class DB( HydrusDB.HydrusDB ): existing_table_names = { name for name in existing_table_names if True in ( name.startswith( table_prefix ) for table_prefix in table_prefixes ) } - surplus_table_names = existing_table_names.difference( good_table_names ) - - surplus_table_names = list( surplus_table_names ) - - surplus_table_names.sort() + surplus_table_names = sorted( existing_table_names.difference( good_table_names ) ) for table_name in surplus_table_names: @@ -3369,9 +3365,7 @@ class DB( HydrusDB.HydrusDB ): distances_to_pairs = HydrusData.BuildKeyToListDict( ( ( distance, ( smaller_media_id, larger_media_id ) ) for ( smaller_media_id, larger_media_id, distance ) in result ) ) - distances = list( distances_to_pairs.keys() ) - - distances.sort() + distances = sorted( distances_to_pairs.keys() ) # we want to preference pairs that have the smallest distance between them. deciding on more similar files first helps merge dupes before dealing with alts so reduces potentials more quickly for distance in distances: @@ -4891,7 +4885,7 @@ class DB( HydrusDB.HydrusDB ): predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NOT_LOCAL, min_current_count = num_not_local ) ) - predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME ] ] ) + predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME ] ] ) if have_ratings: @@ -5256,6 +5250,13 @@ class DB( HydrusDB.HydrusDB ): return hash_ids + def _GetHashIdsFromNoteName( self, name: str, hash_ids_table_name: str ): + + label_id = self._GetLabelId( name ) + + return self._STS( self._c.execute( 'SELECT hash_id FROM file_notes NATURAL JOIN {} WHERE name_id = ?;'.format( hash_ids_table_name ), ( label_id, ) ) ) + + def _GetHashIdsFromNumNotes( self, min_num_notes: typing.Optional[ int ], max_num_notes: typing.Optional[ int ], hash_ids_table_name: str ): has_notes = max_num_notes is None and min_num_notes == 1 @@ -6178,6 +6179,40 @@ class DB( HydrusDB.HydrusDB ): + if 'has_note_names' in simple_preds: + + inclusive_note_names = simple_preds[ 'has_note_names' ] + + for note_name in inclusive_note_names: + + with HydrusDB.TemporaryIntegerTable( self._c, query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self._GetHashIdsFromNoteName( note_name, temp_table_name ) + + query_hash_ids = intersection_update_qhi( query_hash_ids, notes_hash_ids ) + + + + + if 'not_has_note_names' in simple_preds: + + exclusive_note_names = simple_preds[ 'not_has_note_names' ] + + for note_name in exclusive_note_names: + + with HydrusDB.TemporaryIntegerTable( self._c, query_hash_ids, 'hash_id' ) as temp_table_name: + + self._AnalyzeTempTable( temp_table_name ) + + notes_hash_ids = self._GetHashIdsFromNoteName( note_name, temp_table_name ) + + query_hash_ids.difference_update( notes_hash_ids ) + + + + for ( view_type, viewing_locations, operator, viewing_value ) in system_predicates.GetFileViewingStatsPredicates(): only_do_zero = ( operator in ( '=', '\u2248' ) and viewing_value == 0 ) or ( operator == '<' and viewing_value == 1 ) @@ -7000,9 +7035,7 @@ class DB( HydrusDB.HydrusDB ): possible_due_names.add( name ) - possible_due_names = list( possible_due_names ) - - possible_due_names.sort() + possible_due_names = sorted( possible_due_names ) if len( possible_due_names ) > 0: @@ -7934,9 +7967,7 @@ class DB( HydrusDB.HydrusDB ): hash_ids_i_can_process = set() - update_indices = list( update_indices_to_unprocessed_hash_ids.keys() ) - - update_indices.sort() + update_indices = sorted( update_indices_to_unprocessed_hash_ids.keys() ) for update_index in update_indices: @@ -8076,9 +8107,7 @@ class DB( HydrusDB.HydrusDB ): service_id = self._GetServiceId( service_key ) hash_ids = self._GetHashIds( hashes ) - result = [ filename for ( filename, ) in self._c.execute( 'SELECT filename FROM service_filenames WHERE service_id = ? AND hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';', ( service_id, ) ) ] - - result.sort() + result = sorted( ( filename for ( filename, ) in self._c.execute( 'SELECT filename FROM service_filenames WHERE service_id = ? AND hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';', ( service_id, ) ) ) ) return result @@ -9672,9 +9701,7 @@ class DB( HydrusDB.HydrusDB ): else: - children = [ ( HydrusData.Get64BitHammingDistance( phash, child_phash ), child_id, child_phash ) for ( child_id, child_phash ) in children ] - - children.sort() + children = sorted( ( ( HydrusData.Get64BitHammingDistance( phash, child_phash ), child_id, child_phash ) for ( child_id, child_phash ) in children ) ) median_index = len( children ) // 2 @@ -9885,9 +9912,7 @@ class DB( HydrusDB.HydrusDB ): for ( v_id, v_phash ) in viewpoints: - views = [ HydrusData.Get64BitHammingDistance( v_phash, s_phash ) for ( s_id, s_phash ) in sample if v_id != s_id ] - - views.sort() + views = sorted( ( HydrusData.Get64BitHammingDistance( v_phash, s_phash ) for ( s_id, s_phash ) in sample if v_id != s_id ) ) # let's figure out the ratio of left_children to right_children, preferring 1:1, and convert it to a discrete integer score @@ -11768,14 +11793,10 @@ class DB( HydrusDB.HydrusDB ): main_mappings_tables.update( ( name.split( '.' )[1] for name in GenerateMappingsTableNames( service_id ) ) ) - missing_main_tables = main_mappings_tables.difference( existing_mapping_tables ) + missing_main_tables = sorted( main_mappings_tables.difference( existing_mapping_tables ) ) if len( missing_main_tables ) > 0: - missing_main_tables = list( missing_main_tables ) - - missing_main_tables.sort() - message = 'On boot, some important mappings tables were missing! This could be due to the entire \'mappings\' database file being missing or some other problem. The tags in these tables are lost. The exact missing tables were:' message += os.linesep * 2 message += os.linesep.join( missing_main_tables ) @@ -11812,14 +11833,10 @@ class DB( HydrusDB.HydrusDB ): main_cache_tables.add( 'integer_subtags' ) - missing_main_tables = main_cache_tables.difference( existing_cache_tables ) + missing_main_tables = sorted( main_cache_tables.difference( existing_cache_tables ) ) if len( missing_main_tables ) > 0: - missing_main_tables = list( missing_main_tables ) - - missing_main_tables.sort() - message = 'On boot, some important caches tables were missing! This could be due to the entire \'caches\' database file being missing or some other problem. Data related to duplicate file search may have been lost. The exact missing tables were:' message += os.linesep * 2 message += os.linesep.join( missing_main_tables ) @@ -11847,14 +11864,10 @@ class DB( HydrusDB.HydrusDB ): mappings_cache_tables.add( 'local_tags_cache' ) - missing_main_tables = mappings_cache_tables.difference( existing_cache_tables ) + missing_main_tables = sorted( mappings_cache_tables.difference( existing_cache_tables ) ) if len( missing_main_tables ) > 0: - missing_main_tables = list( missing_main_tables ) - - missing_main_tables.sort() - message = 'On boot, some mapping caches tables were missing! This could be due to the entire \'caches\' database file being missing or due to some other problem. All of this data can be regenerated. The exact missing tables were:' message += os.linesep * 2 message += os.linesep.join( missing_main_tables ) @@ -12164,9 +12177,7 @@ class DB( HydrusDB.HydrusDB ): if store_backups: - existing_timestamps = self._STL( self._c.execute( 'SELECT timestamp FROM json_dumps_named WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) ) ) - - existing_timestamps.sort() + existing_timestamps = sorted( self._STI( self._c.execute( 'SELECT timestamp FROM json_dumps_named WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) ) ) ) if len( existing_timestamps ) > 0: diff --git a/hydrus/client/ClientExporting.py b/hydrus/client/ClientExporting.py index 451ef107..3b593f60 100644 --- a/hydrus/client/ClientExporting.py +++ b/hydrus/client/ClientExporting.py @@ -49,9 +49,7 @@ def GenerateExportFilename( destination_directory, media, terms, append_number = tags = tags_manager.GetNamespaceSlice( ( term, ), ClientTags.TAG_DISPLAY_SIBLINGS_AND_PARENTS ) - subtags = [ HydrusTags.SplitTag( tag )[1] for tag in tags ] - - subtags.sort() + subtags = sorted( ( HydrusTags.SplitTag( tag )[1] for tag in tags ) ) filename += clean_tag_text( ', '.join( subtags ) ) @@ -62,7 +60,7 @@ def GenerateExportFilename( destination_directory, media, terms, append_number = current = tags_manager.GetCurrent( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SIBLINGS_AND_PARENTS ) pending = tags_manager.GetPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SIBLINGS_AND_PARENTS ) - tags = list( current.union( pending ) ) + tags = sorted( current.union( pending ) ) if term == 'nn tags': @@ -73,8 +71,6 @@ def GenerateExportFilename( destination_directory, media, terms, append_number = tags = [ HydrusTags.SplitTag( tag )[1] for tag in tags ] - tags.sort() - filename += clean_tag_text( ', '.join( tags ) ) elif term == 'hash': diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py index 1371f78a..9080d606 100644 --- a/hydrus/client/ClientFiles.py +++ b/hydrus/client/ClientFiles.py @@ -286,9 +286,7 @@ class ClientFilesManager( object ): if len( correct_rows ) > 0: - summaries = [ '{} moved from {} to {}'.format( HydrusData.ToHumanInt( count ), missing_location, correct_location ) for ( ( missing_location, correct_location ), count ) in fixes_counter.items() ] - - summaries.sort() + summaries = sorted( ( '{} moved from {} to {}'.format( HydrusData.ToHumanInt( count ), missing_location, correct_location ) for ( ( missing_location, correct_location ), count ) in fixes_counter.items() ) ) summary_message = 'Some client file folders were missing, but they seem to be in other known locations! The folders are:' summary_message += os.linesep * 2 @@ -638,17 +636,13 @@ class ClientFilesManager( object ): missing_dict = HydrusData.BuildKeyToListDict( self._missing_locations ) - missing_locations = list( missing_dict.keys() ) - - missing_locations.sort() + missing_locations = sorted( missing_dict.keys() ) missing_string = '' for missing_location in missing_locations: - missing_prefixes = list( missing_dict[ missing_location ] ) - - missing_prefixes.sort() + missing_prefixes = sorted( missing_dict[ missing_location ] ) missing_prefixes_string = ' ' + os.linesep.join( ( ', '.join( block ) for block in HydrusData.SplitListIntoChunks( missing_prefixes, 32 ) ) ) diff --git a/hydrus/client/ClientLocalServerResources.py b/hydrus/client/ClientLocalServerResources.py index a3d4ce32..06754594 100644 --- a/hydrus/client/ClientLocalServerResources.py +++ b/hydrus/client/ClientLocalServerResources.py @@ -1477,9 +1477,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien metadata_row[ 'num_words' ] = file_info_manager.num_words metadata_row[ 'has_audio' ] = file_info_manager.has_audio - known_urls = list( media_result.GetLocationsManager().GetURLs() ) - - known_urls.sort() + known_urls = sorted( media_result.GetLocationsManager().GetURLs() ) metadata_row[ 'known_urls' ] = known_urls @@ -1656,11 +1654,8 @@ class HydrusResourceClientAPIRestrictedManageCookiesSetCookies( HydrusResourceCl if HG.client_controller.new_options.GetBoolean( 'notify_client_api_cookies' ) and len( domains_cleared ) + len( domains_set ) > 0: - domains_cleared = list( domains_cleared ) - domains_set = list( domains_set ) - - domains_cleared.sort() - domains_set.sort() + domains_cleared = sorted( domains_cleared ) + domains_set = sorted( domains_set ) message = 'Cookies sent from API:' diff --git a/hydrus/client/ClientManagers.py b/hydrus/client/ClientManagers.py index 20e1f8f5..a9b4ca0c 100644 --- a/hydrus/client/ClientManagers.py +++ b/hydrus/client/ClientManagers.py @@ -101,9 +101,7 @@ def CollapseTagSiblingPairs( groups_of_pairs ): for pairs in groups_of_pairs: - pairs = list( pairs ) - - pairs.sort() + pairs = sorted( pairs ) for ( bad, good ) in pairs: diff --git a/hydrus/client/ClientMedia.py b/hydrus/client/ClientMedia.py index 16c880e4..bc96d75d 100644 --- a/hydrus/client/ClientMedia.py +++ b/hydrus/client/ClientMedia.py @@ -89,6 +89,56 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ): s_size = shown_media.GetSize() c_size = comparison_media.GetSize() + is_a_pixel_dupe = False + + if shown_media.IsStaticImage() and comparison_media.IsStaticImage() and shown_media.GetResolution() == comparison_media.GetResolution(): + + global hashes_to_pixel_hashes + + if s_hash not in hashes_to_pixel_hashes: + + path = HG.client_controller.client_files_manager.GetFilePath( s_hash, s_mime ) + + hashes_to_pixel_hashes[ s_hash ] = HydrusImageHandling.GetImagePixelHash( path, s_mime ) + + + if c_hash not in hashes_to_pixel_hashes: + + path = HG.client_controller.client_files_manager.GetFilePath( c_hash, c_mime ) + + hashes_to_pixel_hashes[ c_hash ] = HydrusImageHandling.GetImagePixelHash( path, c_mime ) + + + s_pixel_hash = hashes_to_pixel_hashes[ s_hash ] + c_pixel_hash = hashes_to_pixel_hashes[ c_hash ] + + if s_pixel_hash == c_pixel_hash: + + is_a_pixel_dupe = True + + if s_mime == HC.IMAGE_PNG and c_mime != HC.IMAGE_PNG: + + statement = 'this is a pixel-for-pixel duplicate png!' + + score = -100 + + elif s_mime != HC.IMAGE_PNG and c_mime == HC.IMAGE_PNG: + + statement = 'other file is a pixel-for-pixel duplicate png!' + + score = 100 + + else: + + statement = 'images are pixel-for-pixel duplicates!' + + score = 0 + + + statements_and_scores[ 'pixel_duplicates' ] = ( statement, score ) + + + if s_size != c_size: size_ratio = s_size / c_size @@ -119,6 +169,11 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ): score = 0 + if is_a_pixel_dupe: + + score = 0 + + statement = '{} {} {}'.format( HydrusData.ToHumanBytes( s_size ), operator, HydrusData.ToHumanBytes( c_size ) ) statements_and_scores[ 'filesize' ] = ( statement, score ) @@ -321,6 +376,11 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ): score = -duplicate_comparison_score_older + if is_a_pixel_dupe: + + score = 0 + + statement = '{} {} {}'.format( HydrusData.TimestampToPrettyTimeDelta( s_ts ), operator, HydrusData.TimestampToPrettyTimeDelta( c_ts ) ) statements_and_scores[ 'time_imported' ] = ( statement, score ) @@ -384,52 +444,6 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ): - if shown_media.IsStaticImage() and comparison_media.IsStaticImage() and shown_media.GetResolution() == comparison_media.GetResolution(): - - global hashes_to_pixel_hashes - - if s_hash not in hashes_to_pixel_hashes: - - path = HG.client_controller.client_files_manager.GetFilePath( s_hash, s_mime ) - - hashes_to_pixel_hashes[ s_hash ] = HydrusImageHandling.GetImagePixelHash( path, s_mime ) - - - if c_hash not in hashes_to_pixel_hashes: - - path = HG.client_controller.client_files_manager.GetFilePath( c_hash, c_mime ) - - hashes_to_pixel_hashes[ c_hash ] = HydrusImageHandling.GetImagePixelHash( path, c_mime ) - - - s_pixel_hash = hashes_to_pixel_hashes[ s_hash ] - c_pixel_hash = hashes_to_pixel_hashes[ c_hash ] - - if s_pixel_hash == c_pixel_hash: - - if s_mime == HC.IMAGE_PNG and c_mime != HC.IMAGE_PNG: - - statement = 'this is a pixel-for-pixel duplicate png!' - - score = -100 - - elif s_mime != HC.IMAGE_PNG and c_mime == HC.IMAGE_PNG: - - statement = 'other file is a pixel-for-pixel duplicate png!' - - score = 100 - - else: - - statement = 'images are pixel-for-pixel duplicates!' - - score = 0 - - - statements_and_scores[ 'pixel_duplicates' ] = ( statement, score ) - - - return statements_and_scores def GetMediasTags( pool, tag_service_key, tag_display_type, content_statuses ): diff --git a/hydrus/client/ClientMediaManagers.py b/hydrus/client/ClientMediaManagers.py index fa437a60..30be0c79 100644 --- a/hydrus/client/ClientMediaManagers.py +++ b/hydrus/client/ClientMediaManagers.py @@ -442,9 +442,7 @@ class NotesManager( object ): def GetNames( self ): - names = list( self._names_to_notes.keys() ) - - names.sort() + names = sorted( self._names_to_notes.keys() ) return names @@ -792,9 +790,7 @@ class TagsManager( object ): for desired_namespace in namespaces: - subtags = [ HydrusTags.ConvertTagToSortable( subtag ) for ( namespace, subtag ) in pairs if namespace == desired_namespace ] - - subtags.sort() + subtags = sorted( ( HydrusTags.ConvertTagToSortable( subtag ) for ( namespace, subtag ) in pairs if namespace == desired_namespace ) ) slice.append( tuple( subtags ) ) diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py index 8b683488..03bad14f 100644 --- a/hydrus/client/ClientOptions.py +++ b/hydrus/client/ClientOptions.py @@ -144,6 +144,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): self._dictionary[ 'booleans' ][ 'use_system_ffmpeg' ] = False + self._dictionary[ 'booleans' ][ 'elide_page_tab_names' ] = True + self._dictionary[ 'booleans' ][ 'maintain_similar_files_duplicate_pairs_during_idle' ] = False self._dictionary[ 'booleans' ][ 'show_namespaces' ] = True diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py index 902bc2d1..e3af5e8a 100644 --- a/hydrus/client/ClientParsing.py +++ b/hydrus/client/ClientParsing.py @@ -2,6 +2,7 @@ import base64 import bs4 import calendar import codecs +import typing from hydrus.client.networking import ClientNetworkingDomain from hydrus.client.networking import ClientNetworkingJobs import collections @@ -173,9 +174,7 @@ def ConvertParsableContentToPrettyString( parsable_content, include_veto = False else: - hash_types = list( additional_infos ) - - hash_types.sort() + hash_types = sorted( additional_infos ) pretty_strings.append( 'hashes: ' + ', '.join( hash_types ) ) @@ -1404,9 +1403,7 @@ class ParseFormulaJSON( ParseFormula ): elif isinstance( root, dict ): - pairs = list( root.items() ) - - pairs.sort() + pairs = sorted( root.items() ) for ( key, value ) in pairs: @@ -1443,9 +1440,7 @@ class ParseFormulaJSON( ParseFormula ): string_match = parse_rule - pairs = list( root.items() ) - - pairs.sort() + pairs = sorted( root.items() ) for ( key, value ) in pairs: @@ -1485,9 +1480,7 @@ class ParseFormulaJSON( ParseFormula ): if isinstance( root, dict ): - pairs = list( root.items() ) - - pairs.sort() + pairs = sorted( root.items() ) for ( key, value ) in pairs: @@ -2131,9 +2124,7 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ): def GetSafeSummary( self ): - domains = list( { ClientNetworkingDomain.ConvertURLIntoDomain( url ) for url in self._example_urls } ) - - domains.sort() + domains = sorted( { ClientNetworkingDomain.ConvertURLIntoDomain( url ) for url in self._example_urls } ) return 'Parser "' + self._name + '" - ' + ', '.join( domains ) @@ -2821,7 +2812,14 @@ transformation_type_str_lookup[ STRING_TRANSFORMATION_DATE_DECODE ] = 'datestrin transformation_type_str_lookup[ STRING_TRANSFORMATION_INTEGER_ADDITION ] = 'integer addition' transformation_type_str_lookup[ STRING_TRANSFORMATION_DATE_ENCODE ] = 'timestamp to datestring' -class StringConverter( HydrusSerialisable.SerialisableBase ): +class StringProcessingStep( HydrusSerialisable.SerialisableBase ): + + def ToString( self, simple = False ) -> str: + + raise NotImplementedError() + + +class StringConverter( StringProcessingStep ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_CONVERTER SERIALISABLE_NAME = 'String Converter' @@ -2839,7 +2837,7 @@ class StringConverter( HydrusSerialisable.SerialisableBase ): example_string = 'example string' - HydrusSerialisable.SerialisableBase.__init__( self ) + StringProcessingStep.__init__( self ) self.transformations = transformations @@ -3068,6 +3066,36 @@ class StringConverter( HydrusSerialisable.SerialisableBase ): return len( self.transformations ) > 0 + def ToString( self, simple = False ) -> str: + + num_rules = len( self.transformations ) + + if num_rules == 0: + + if simple: + + label = 'no changes' + + else: + + label = 'no string transformations' + + + else: + + if simple: + + label = '{} changes'.format( HydrusData.ToHumanInt( num_rules ) ) + + else: + + label = '{} string transformations'.format( HydrusData.ToHumanInt( num_rules ) ) + + + + return label + + @staticmethod def TransformationToString( transformation ): @@ -3142,7 +3170,7 @@ ALPHA = 0 ALPHANUMERIC = 1 NUMERIC = 2 -class StringMatch( HydrusSerialisable.SerialisableBase ): +class StringMatch( StringProcessingStep ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH SERIALISABLE_NAME = 'String Match' @@ -3150,9 +3178,7 @@ class StringMatch( HydrusSerialisable.SerialisableBase ): def __init__( self, match_type = STRING_MATCH_ANY, match_value = '', min_chars = None, max_chars = None, example_string = 'example string' ): - HydrusSerialisable.SerialisableBase.__init__( self ) - # make a gui control that accepts one of these. displays expected input on the right and colours red/green (and does isvalid) based on current input - # think about replacing the veto stuff above with this. + StringProcessingStep.__init__( self ) self._match_type = match_type self._match_value = match_value @@ -3272,7 +3298,12 @@ class StringMatch( HydrusSerialisable.SerialisableBase ): return ( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string ) - def ToString( self ): + def ToString( self, simple = False ): + + if simple: + + return 'filter' + result = '' @@ -3342,3 +3373,199 @@ class StringMatch( HydrusSerialisable.SerialisableBase ): HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH ] = StringMatch + +class StringSplitter( StringProcessingStep ): + + SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_SPLITTER + SERIALISABLE_NAME = 'String Splitter' + SERIALISABLE_VERSION = 1 + + def __init__( self, separator: str = ',', max_splits: typing.Optional[ int ] = None ): + + StringProcessingStep.__init__( self ) + + self._separator = separator + self._max_splits = max_splits + + + def _GetSerialisableInfo( self ): + + return ( self._separator, self._max_splits ) + + + def _InitialiseFromSerialisableInfo( self, serialisable_info ): + + ( self._separator, self._max_splits ) = serialisable_info + + + def GetMaxSplits( self ): + + return self._max_splits + + + def GetSeparator( self ): + + return self._separator + + + def Split( self, text: str ) -> typing.List[ str ]: + + if self._max_splits is None: + + results = text.split( self._separator ) + + else: + + results = text.split( self._separator, self._max_splits ) + + + return [ result for result in results if result != '' ] + + + def ToString( self, simple = False ): + + if simple: + + return 'splitter' + + + result = 'splitting by "{}"'.format( self._separator ) + + if self._max_splits is not None: + + result = '{}, at most {} times'.format( result, HydrusData.ToHumanInt( self._max_splits ) ) + + + return result + + +HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_SPLITTER ] = StringSplitter + +class StringProcessor( HydrusSerialisable.SerialisableBase ): + + SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_PROCESSOR + SERIALISABLE_NAME = 'String Processor' + SERIALISABLE_VERSION = 1 + + def __init__( self ): + + StringProcessingStep.__init__( self ) + + self._processing_steps: typing.List[ StringProcessingStep ] = [] + + + def _GetSerialisableInfo( self ): + + return HydrusSerialisable.SerialisableList( self._processing_steps ).GetSerialisableTuple() + + + def _InitialiseFromSerialisableInfo( self, serialisable_info ): + + serialisable_processing_steps = serialisable_info + + self._processing_steps = list( HydrusSerialisable.CreateFromSerialisableTuple( serialisable_processing_steps ) ) + + + def GetProcessingSteps( self ): + + return list( self._processing_steps ) + + + def ProcessStrings( self, starting_strings: typing.Iterable[ str ], max_steps_allowed = None ) -> typing.List[ str ]: + + final_strings = [] + + for starting_string in starting_strings: + + current_strings = [ starting_string ] + + for ( i, processing_step ) in enumerate( self._processing_steps ): + + if max_steps_allowed is not None and i >= max_steps_allowed: + + break + + + next_strings = [] + + for current_string in current_strings: + + if isinstance( processing_step, StringConverter ): + + try: + + next_string = processing_step.Convert( current_string ) + + next_strings.append( next_string ) + + except HydrusExceptions.StringConvertException: + + continue + + + elif isinstance( processing_step, StringMatch ): + + try: + + if processing_step.Matches( current_string ): + + next_strings.append( current_string ) + + + except HydrusExceptions.StringMatchException: + + continue + + + elif isinstance( processing_step, StringSplitter ): + + split_strings = processing_step.Split( current_string ) + + next_strings.extend( split_strings ) + + + + current_strings = next_strings + + + final_strings.extend( current_strings ) + + + return final_strings + + + def SetProcessingSteps( self, processing_steps: typing.List[ StringProcessingStep ] ): + + self._processing_steps = list( processing_steps ) + + + def ToString( self ) -> str: + + if len( self._processing_steps ) == 0: + + return 'no string processing' + + else: + + components = [] + + if True in ( isinstance( ps, StringConverter ) for ps in self._processing_steps ): + + components = 'conversion' + + + if True in ( isinstance( ps, StringMatch ) for ps in self._processing_steps ): + + components = 'filtering' + + + if True in ( isinstance( ps, StringSplitter ) for ps in self._processing_steps ): + + components = 'splitting' + + + return 'some {}'.format( ', '.join( components ) ) + + + +HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_SPLITTER ] = StringSplitter diff --git a/hydrus/client/ClientSearch.py b/hydrus/client/ClientSearch.py index 7c31ac54..232643f3 100644 --- a/hydrus/client/ClientSearch.py +++ b/hydrus/client/ClientSearch.py @@ -54,42 +54,46 @@ PREDICATE_TYPE_SYSTEM_MODIFIED_TIME = 35 PREDICATE_TYPE_SYSTEM_FRAMERATE = 36 PREDICATE_TYPE_SYSTEM_NUM_FRAMES = 37 PREDICATE_TYPE_SYSTEM_NUM_NOTES = 38 +PREDICATE_TYPE_SYSTEM_NOTES = 39 +PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME = 40 -SYSTEM_PREDICATE_TYPES = set() - -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_EVERYTHING ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_INBOX ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_ARCHIVE ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_UNTAGGED ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NUM_TAGS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_LIMIT ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_SIZE ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_AGE ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_MODIFIED_TIME ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_HASH ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_WIDTH ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_HEIGHT ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_RATIO ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_DURATION ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FRAMERATE ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NUM_FRAMES ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_HAS_AUDIO ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_MIME ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_RATING ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_SIMILAR_TO ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_LOCAL ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NOT_LOCAL ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NUM_WORDS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NUM_NOTES ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FILE_SERVICE ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_NUM_PIXELS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_DIMENSIONS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_KNOWN_URLS ) -SYSTEM_PREDICATE_TYPES.add( PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ) +SYSTEM_PREDICATE_TYPES = { + PREDICATE_TYPE_SYSTEM_EVERYTHING, + PREDICATE_TYPE_SYSTEM_INBOX, + PREDICATE_TYPE_SYSTEM_ARCHIVE, + PREDICATE_TYPE_SYSTEM_UNTAGGED, + PREDICATE_TYPE_SYSTEM_NUM_TAGS, + PREDICATE_TYPE_SYSTEM_LIMIT, + PREDICATE_TYPE_SYSTEM_SIZE, + PREDICATE_TYPE_SYSTEM_AGE, + PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, + PREDICATE_TYPE_SYSTEM_HASH, + PREDICATE_TYPE_SYSTEM_WIDTH, + PREDICATE_TYPE_SYSTEM_HEIGHT, + PREDICATE_TYPE_SYSTEM_RATIO, + PREDICATE_TYPE_SYSTEM_DURATION, + PREDICATE_TYPE_SYSTEM_FRAMERATE, + PREDICATE_TYPE_SYSTEM_NUM_FRAMES, + PREDICATE_TYPE_SYSTEM_HAS_AUDIO, + PREDICATE_TYPE_SYSTEM_MIME, + PREDICATE_TYPE_SYSTEM_RATING, + PREDICATE_TYPE_SYSTEM_SIMILAR_TO, + PREDICATE_TYPE_SYSTEM_LOCAL, + PREDICATE_TYPE_SYSTEM_NOT_LOCAL, + PREDICATE_TYPE_SYSTEM_NUM_WORDS, + PREDICATE_TYPE_SYSTEM_NUM_NOTES, + PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, + PREDICATE_TYPE_SYSTEM_FILE_SERVICE, + PREDICATE_TYPE_SYSTEM_NUM_PIXELS, + PREDICATE_TYPE_SYSTEM_DIMENSIONS, + PREDICATE_TYPE_SYSTEM_NOTES, + PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, + PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, + PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT, + PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING, + PREDICATE_TYPE_SYSTEM_KNOWN_URLS, + PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS +} IGNORED_TAG_SEARCH_CHARACTERS = '[](){}/\\"\'-_' IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : ' ' for char in IGNORED_TAG_SEARCH_CHARACTERS } @@ -974,6 +978,27 @@ class FileSystemPredicates( object ): elif operator == '=': self._common_info[ 'num_notes' ] = num_notes + if predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME: + + ( operator, name ) = value + + if operator: + + label = 'has_note_names' + + else: + + label = 'not_has_note_names' + + + if label not in self._common_info: + + self._common_info[ label ] = set() + + + self._common_info[ label ].add( name ) + + if predicate_type == PREDICATE_TYPE_SYSTEM_NUM_WORDS: ( operator, num_words ) = value @@ -1545,6 +1570,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ): elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LOCAL: base = 'local' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOT_LOCAL: base = 'not local' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_DIMENSIONS: base = 'dimensions' + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NOTES: base = 'notes' elif self._predicate_type == PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS: base = 'file relationships' elif self._predicate_type in ( PREDICATE_TYPE_SYSTEM_WIDTH, PREDICATE_TYPE_SYSTEM_HEIGHT, PREDICATE_TYPE_SYSTEM_NUM_NOTES, PREDICATE_TYPE_SYSTEM_NUM_WORDS, PREDICATE_TYPE_SYSTEM_NUM_FRAMES ): @@ -1629,6 +1655,24 @@ class Predicate( HydrusSerialisable.SerialisableBase ): base += ' {} {}fps'.format( operator, HydrusData.ToHumanInt( value ) ) + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME: + + base = 'has note' + + if self._value is not None: + + ( operator, name ) = self._value + + if operator: + + base = 'has note with name "{}"'.format( name ) + + else: + + base = 'does not have note with name "{}"'.format( name ) + + + elif self._predicate_type == PREDICATE_TYPE_SYSTEM_NUM_TAGS: base = 'number of tags' diff --git a/hydrus/client/ClientSerialisable.py b/hydrus/client/ClientSerialisable.py index 2a862799..15a0f32f 100644 --- a/hydrus/client/ClientSerialisable.py +++ b/hydrus/client/ClientSerialisable.py @@ -168,7 +168,7 @@ def DumpToPng( width, payload_bytes, title, payload_description, text, path ): cv2.imwrite( temp_path, finished_image, [ cv2.IMWRITE_PNG_COMPRESSION, 9 ] ) - shutil.copy2( temp_path, path ) + HydrusPaths.MirrorFile( temp_path, path ) except Exception as e: @@ -243,7 +243,7 @@ def LoadFromPng( path ): try: - shutil.copy2( path, temp_path ) + HydrusPaths.MirrorFile( path, temp_path ) numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED ) diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py index ecc811ff..987d249a 100644 --- a/hydrus/client/ClientServices.py +++ b/hydrus/client/ClientServices.py @@ -2150,7 +2150,7 @@ class ServiceIPFS( ServiceRemote ): links_url = api_base_url + 'object/links/' + multihash - network_job = ClientNetworkingJobs.NetworkJob( 'GET', links_url ) + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', links_url ) if job_key is not None: @@ -2159,8 +2159,6 @@ class ServiceIPFS( ServiceRemote ): try: - network_job.OverrideBandwidth() - HG.client_controller.network_engine.AddJob( network_job ) network_job.WaitUntilDone() @@ -2234,10 +2232,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'config?arg=Experimental.FilestoreEnabled&arg={}&bool=true'.format( arg_value ) - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OnlyTryConnectionOnce() - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2259,10 +2254,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'version' - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OnlyTryConnectionOnce() - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2292,10 +2284,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'config?arg=Experimental.FilestoreEnabled' - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OnlyTryConnectionOnce() - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2423,21 +2412,40 @@ class ServiceIPFS( ServiceRemote ): # check if it is pinned. if we try to unpin something not pinned, the daemon 500s - url = api_base_url + '/pin/ls?arg={}'.format( multihash ) + url = api_base_url + 'pin/ls?arg={}'.format( multihash ) - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) - network_job.WaitUntilDone() - - parsing_text = network_job.GetContentText() - - j = json.loads( parsing_text ) - - file_is_pinned = 'Keys' in j and multihash in j['Keys'] + try: + + network_job.WaitUntilDone() + + parsing_text = network_job.GetContentText() + + j = json.loads( parsing_text ) + + file_is_pinned = False + + if 'PinLsList' in j: + + file_is_pinned = 'Keys' in j[ 'PinLsList' ] and multihash in j[ 'PinLsList' ]['Keys'] + + else: + + file_is_pinned = 'Keys' in j and multihash in j['Keys'] + + + except HydrusExceptions.ServerException: + + if 'not pinned' in network_job.GetContentText(): + + return False + + + raise + return file_is_pinned @@ -2454,9 +2462,7 @@ class ServiceIPFS( ServiceRemote ): file_info = [] - hashes = list( hashes ) - - hashes.sort() + hashes = sorted( hashes ) for ( i, hash ) in enumerate( hashes ): @@ -2506,9 +2512,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'object/new?arg=unixfs-dir' - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2538,9 +2542,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'object/patch/add-link?arg=' + object_multihash + '&arg=' + filename + '&arg=' + multihash - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2555,9 +2557,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'pin/add?arg=' + directory_multihash - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2650,12 +2650,10 @@ class ServiceIPFS( ServiceRemote ): files = { 'path' : ( hash.hex(), f, mime_string ) } - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) network_job.SetFiles( files ) - network_job.OverrideBandwidth() - HG.client_controller.network_engine.AddJob( network_job ) network_job.WaitUntilDone() @@ -2713,9 +2711,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'pin/rm/' + multihash - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) @@ -2738,9 +2734,7 @@ class ServiceIPFS( ServiceRemote ): url = api_base_url + 'pin/rm/' + multihash - network_job = ClientNetworkingJobs.NetworkJob( 'GET', url ) - - network_job.OverrideBandwidth() + network_job = ClientNetworkingJobs.NetworkJobIPFS( 'POST', url ) HG.client_controller.network_engine.AddJob( network_job ) diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py index de925efd..c51cfe63 100644 --- a/hydrus/client/gui/ClientGUI.py +++ b/hydrus/client/gui/ClientGUI.py @@ -518,9 +518,18 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ): library_versions = [] - library_versions.append( ( 'FFMPEG', HydrusVideoHandling.GetFFMPEGVersion() ) ) - library_versions.append( ( 'OpenCV', cv2.__version__ ) ) + # 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] + v = sys.version + + if ' ' in v: + + v = v.split( ' ' )[0] + + + library_versions.append( ( 'python', v ) ) library_versions.append( ( 'openssl', ssl.OPENSSL_VERSION ) ) + + library_versions.append( ( 'OpenCV', cv2.__version__ ) ) library_versions.append( ( 'Pillow', PIL.__version__ ) ) if ClientGUIMPV.MPV_IS_AVAILABLE: @@ -535,18 +544,12 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ): library_versions.append( ( 'mpv', 'not available' ) ) - # 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] - v = sys.version - - if ' ' in v: - - v = v.split( ' ' )[0] - - - library_versions.append( ( 'python', v ) ) + library_versions.append( ( 'FFMPEG', HydrusVideoHandling.GetFFMPEGVersion() ) ) library_versions.append( ( 'sqlite', sqlite3.sqlite_version ) ) + library_versions.append( ( 'Qt', QC.__version__ ) ) + if qtpy.PYSIDE2: import PySide2 @@ -562,15 +565,22 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ): library_versions.append( ( 'PyQt5', PYQT_VERSION_STR ) ) library_versions.append( ( 'sip', SIP_VERSION_STR ) ) - - library_versions.append( ( 'Qt', QC.__version__ ) ) - - library_versions.append( ( 'html5lib present: ', str( ClientParsing.HTML5LIB_IS_OK ) ) ) - library_versions.append( ( 'lxml present: ', str( ClientParsing.LXML_IS_OK ) ) ) + from hydrus.client.networking import ClientNetworkingJobs - library_versions.append( ( 'cloudscraper present: ', str( ClientNetworkingJobs.CLOUDSCRAPER_OK ) ) ) + if ClientNetworkingJobs.CLOUDSCRAPER_OK: + + library_versions.append( ( 'cloudscraper', ClientNetworkingJobs.cloudscraper.__version__ ) ) + + else: + + library_versions.append( ( 'cloudscraper present: ', 'False' ) ) + + + library_versions.append( ( 'pyparsing present: ', str( ClientNetworkingJobs.PYPARSING_OK ) ) ) + library_versions.append( ( 'html5lib present: ', str( ClientParsing.HTML5LIB_IS_OK ) ) ) + library_versions.append( ( 'lxml present: ', str( ClientParsing.LXML_IS_OK ) ) ) library_versions.append( ( 'lz4 present: ', str( ClientRendering.LZ4_OK ) ) ) library_versions.append( ( 'install dir', HC.BASE_DIR ) ) library_versions.append( ( 'db dir', HG.client_controller.db_dir ) ) @@ -1712,23 +1722,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ): - if not self._have_shown_once: - - self._have_shown_once = True - - for page in self._notebook.GetPages(): - - if isinstance( page, ClientGUIPages.PagesNotebook ): - - page.LayoutPages() - - - - for page in self._notebook.GetMediaPages(): - - page.SetupSplits() - - + self._have_shown_once = True page = self.GetCurrentPage() @@ -4590,7 +4584,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p site = ClientGUIMenus.AppendMenuItem( links, 'Endchan board bunker', 'Open hydrus dev\'s Endchan board, the bunker for when 8kun is unavailable. Try .org if .net is unavailable.', ClientPaths.LaunchURLInWebBrowser, 'https://endchan.net/hydrus/index.html' ) site = ClientGUIMenus.AppendMenuBitmapItem( links, 'twitter', 'Open hydrus dev\'s twitter, where he makes general progress updates and emergency notifications.', CC.global_pixmaps().twitter, ClientPaths.LaunchURLInWebBrowser, 'https://twitter.com/hydrusnetwork' ) site = ClientGUIMenus.AppendMenuBitmapItem( links, 'tumblr', 'Open hydrus dev\'s tumblr, where he makes release posts and other status updates.', CC.global_pixmaps().tumblr, ClientPaths.LaunchURLInWebBrowser, 'http://hydrus.tumblr.com/' ) - site = ClientGUIMenus.AppendMenuBitmapItem( links, 'discord', 'Open a discord channel where many hydrus users congregate. Hydrus dev visits regularly.', CC.global_pixmaps().discord, ClientPaths.LaunchURLInWebBrowser, 'https://discord.gg/vy8CUB4' ) + site = ClientGUIMenus.AppendMenuBitmapItem( links, 'discord', 'Open a discord channel where many hydrus users congregate. Hydrus dev visits regularly.', CC.global_pixmaps().discord, ClientPaths.LaunchURLInWebBrowser, 'https://discord.gg/wPHPCUZ' ) site = ClientGUIMenus.AppendMenuBitmapItem( links, 'patreon', 'Open hydrus dev\'s patreon, which lets you support development.', CC.global_pixmaps().patreon, ClientPaths.LaunchURLInWebBrowser, 'https://www.patreon.com/hydrus_dev' ) ClientGUIMenus.AppendMenu( menu, links, 'links' ) @@ -4880,9 +4874,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p sessions = QW.QMenu( menu ) - gui_session_names = list( gui_session_names ) - - gui_session_names.sort() + gui_session_names = sorted( gui_session_names ) if len( gui_session_names ) > 0: @@ -4908,9 +4900,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p append_backup = QW.QMenu( sessions ) - rows = list( gui_session_names_to_backup_timestamps.items() ) - - rows.sort() + rows = sorted( gui_session_names_to_backup_timestamps.items() ) for ( name, timestamps ) in rows: diff --git a/hydrus/client/gui/ClientGUIACDropdown.py b/hydrus/client/gui/ClientGUIACDropdown.py index 468543dc..21a76f74 100644 --- a/hydrus/client/gui/ClientGUIACDropdown.py +++ b/hydrus/client/gui/ClientGUIACDropdown.py @@ -519,7 +519,8 @@ class ListBoxTagsAC( ClientGUIListBoxes.ListBoxTagsPredicates ): skip_ors = True - skip_countless = HG.client_controller.new_options.GetBoolean( 'ac_select_first_with_count' ) + some_preds_have_count = True in ( predicate.GetCount() > 0 for predicate in predicates ) + skip_countless = HG.client_controller.new_options.GetBoolean( 'ac_select_first_with_count' ) and some_preds_have_count for ( index, predicate ) in enumerate( predicates ): @@ -1378,9 +1379,7 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ): def RefreshFavouriteTags( self ): - favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) - - favourite_tags.sort() + favourite_tags = sorted( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ] @@ -1667,6 +1666,10 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): folder_names.insert( 0, None ) + else: + + folder_names.sort() + for folder_name in folder_names: @@ -1681,9 +1684,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): ClientGUIMenus.AppendMenu( menu, menu_to_use, folder_name ) - names = list( folders_to_names[ folder_name ] ) - - names.sort() + names = sorted( folders_to_names[ folder_name ] ) for name in names: @@ -2384,9 +2385,7 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ): def RefreshFavouriteTags( self ): - favourite_tags = list( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) - - favourite_tags.sort() + favourite_tags = sorted( HG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, tag ) for tag in favourite_tags ] diff --git a/hydrus/client/gui/ClientGUICommon.py b/hydrus/client/gui/ClientGUICommon.py index 80679212..aa524cae 100644 --- a/hydrus/client/gui/ClientGUICommon.py +++ b/hydrus/client/gui/ClientGUICommon.py @@ -1,5 +1,6 @@ import os import re +import typing from qtpy import QtCore as QC from qtpy import QtWidgets as QW @@ -128,17 +129,13 @@ class ShortcutAwareToolTipMixin( object ): if len( names_to_shortcuts ) > 0: - names = list( names_to_shortcuts.keys() ) - - names.sort() + names = sorted( names_to_shortcuts.keys() ) for name in names: shortcuts = names_to_shortcuts[ name ] - shortcut_strings = [ shortcut.ToString() for shortcut in shortcuts ] - - shortcut_strings.sort() + shortcut_strings = sorted( ( shortcut.ToString() for shortcut in shortcuts ) ) tt += os.linesep * 2 @@ -381,6 +378,18 @@ class BetterNotebook( QW.QTabWidget ): + def DeleteAllPages( self ): + + while self.count() > 0: + + page = self.widget( 0 ) + + self.removeTab( 0 ) + + page.deleteLater() + + + def GetPages( self ): return [ self.widget( i ) for i in range( self.count() ) ] diff --git a/hydrus/client/gui/ClientGUIControls.py b/hydrus/client/gui/ClientGUIControls.py index baaa6d1b..47437ce5 100644 --- a/hydrus/client/gui/ClientGUIControls.py +++ b/hydrus/client/gui/ClientGUIControls.py @@ -1,6 +1,7 @@ +import typing + from qtpy import QtCore as QC from qtpy import QtWidgets as QW -from qtpy import QtGui as QG from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData @@ -9,10 +10,8 @@ from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusNetworking from hydrus.core import HydrusText from hydrus.client import ClientConstants as CC -from hydrus.client import ClientParsing from hydrus.client.gui import ClientGUICommon from hydrus.client.gui import ClientGUICore as CGC -from hydrus.client.gui import ClientGUIDialogs from hydrus.client.gui import ClientGUIFunctions from hydrus.client.gui import ClientGUIListCtrl from hydrus.client.gui import ClientGUIMenus @@ -275,7 +274,7 @@ class BytesControl( QW.QWidget ): return (self._spin.setValue( value ), self._unit.SetValue( unit )) - def SetValue( self, value ): + def SetValue( self, value: int ): max_unit = 1024 * 1024 * 1024 @@ -292,925 +291,6 @@ class BytesControl( QW.QWidget ): self._unit.SetValue( unit ) -class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ): - - def __init__( self, parent, string_converter, example_string_override = None ): - - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - - transformations_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - - columns = [ ( '#', 3 ), ( 'transformation', 30 ), ( 'result', -1 ) ] - - self._transformations = ClientGUIListCtrl.BetterListCtrl( transformations_panel, 'string_converter_transformations', 7, 35, columns, self._ConvertTransformationToListCtrlTuples, delete_key_callback = self._DeleteTransformation, activation_callback = self._EditTransformation ) - - transformations_panel.SetListCtrl( self._transformations ) - - transformations_panel.AddButton( 'add', self._AddTransformation ) - transformations_panel.AddButton( 'edit', self._EditTransformation, enabled_only_on_selection = True ) - transformations_panel.AddDeleteButton() - - transformations_panel.AddSeparator() - - transformations_panel.AddButton( 'move up', self._MoveUp, enabled_check_func = self._CanMoveUp ) - transformations_panel.AddButton( 'move down', self._MoveDown, enabled_check_func = self._CanMoveDown ) - - self._example_string = QW.QLineEdit( self ) - - # - - self._transformations.AddDatas( [ ( i + 1, transformation_type, data ) for ( i, ( transformation_type, data ) ) in enumerate( string_converter.transformations ) ] ) - - if example_string_override is None: - - self._example_string.setText( string_converter.example_string ) - - else: - - self._example_string.setText( example_string_override ) - - - self._transformations.UpdateDatas() # to refresh, now they are all in the list - - self._transformations.Sort( 0 ) - - # - - rows = [] - - rows.append( ( 'example string: ', self._example_string ) ) - - gridbox = ClientGUICommon.WrapInGrid( self, rows ) - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, transformations_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) - - self.widget().setLayout( vbox ) - - # - - self._example_string.textChanged.connect( self.EventUpdate ) - - - def _AddTransformation( self ): - - transformation_type = ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT - data = 'extra text' - - try: - - string_converter = self._GetValue() - - example_string_at_this_point = string_converter.Convert( self._example_string.text() ) - - except: - - example_string_at_this_point = self._example_string.text() - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg: - - panel = self._TransformationPanel( dlg, transformation_type, data, example_string_at_this_point ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - number = self._transformations.topLevelItemCount() + 1 - - ( transformation_type, data ) = panel.GetValue() - - enumerated_transformation = ( number, transformation_type, data ) - - self._transformations.AddDatas( ( enumerated_transformation, ) ) - - - - self._transformations.UpdateDatas() # need to refresh string after the insertion, so the new row can be included in the parsing calcs - - self._transformations.Sort() - - - def _CanMoveDown( self ): - - selected_data = self._transformations.GetData( only_selected = True ) - - if len( selected_data ) == 1: - - ( number, transformation_type, data ) = selected_data[0] - - if number < self._transformations.topLevelItemCount(): - - return True - - - - return False - - - def _CanMoveUp( self ): - - selected_data = self._transformations.GetData( only_selected = True ) - - if len( selected_data ) == 1: - - ( number, transformation_type, data ) = selected_data[0] - - if number > 1: - - return True - - - - return False - - - def _ConvertTransformationToListCtrlTuples( self, transformation ): - - ( number, transformation_type, data ) = transformation - - pretty_number = HydrusData.ToHumanInt( number ) - pretty_transformation = ClientParsing.StringConverter.TransformationToString( ( transformation_type, data ) ) - - string_converter = self._GetValue() - - try: - - pretty_result = ClientParsing.MakeParsedTextPretty( string_converter.Convert( self._example_string.text(), number ) ) - - except HydrusExceptions.StringConvertException as e: - - pretty_result = str( e ) - - - display_tuple = ( pretty_number, pretty_transformation, pretty_result ) - sort_tuple = ( number, number, number ) - - return ( display_tuple, sort_tuple ) - - - def _DeleteTransformation( self ): - - if len( self._transformations.GetData( only_selected = True ) ) > 0: - - text = 'Delete all selected?' - - from hydrus.client.gui import ClientGUIDialogsQuick - - result = ClientGUIDialogsQuick.GetYesNo( self, text ) - - if result == QW.QDialog.Accepted: - - self._transformations.DeleteSelected() - - - - # now we need to shuffle up any missing numbers - - num_rows = self._transformations.topLevelItemCount() - - i = 1 - search_i = i - - while i <= num_rows: - - try: - - transformation = self._GetTransformation( search_i ) - - if search_i != i: - - self._transformations.DeleteDatas( ( transformation, ) ) - - ( search_i, transformation_type, data ) = transformation - - transformation = ( i, transformation_type, data ) - - self._transformations.AddDatas( ( transformation, ) ) - - - i += 1 - search_i = i - - except HydrusExceptions.DataMissing: - - search_i += 1 - - - - self._transformations.UpdateDatas() - - self._transformations.Sort() - - - def _EditTransformation( self ): - - selected_data = self._transformations.GetData( only_selected = True ) - - for enumerated_transformation in selected_data: - - ( number, transformation_type, data ) = enumerated_transformation - - try: - - string_converter = self._GetValue() - - example_string_at_this_point = string_converter.Convert( self._example_string.text(), number - 1 ) - - except: - - example_string_at_this_point = self._example_string.text() - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg: - - panel = self._TransformationPanel( dlg, transformation_type, data, example_string_at_this_point ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - self._transformations.DeleteDatas( ( enumerated_transformation, ) ) - - ( transformation_type, data ) = panel.GetValue() - - enumerated_transformation = ( number, transformation_type, data ) - - self._transformations.AddDatas( ( enumerated_transformation, ) ) - - else: - - break - - - - - self._transformations.UpdateDatas() - - self._transformations.Sort() - - - def _GetTransformation( self, desired_number ): - - for transformation in self._transformations.GetData(): - - ( number, transformation_type, data ) = transformation - - if number == desired_number: - - return transformation - - - - raise HydrusExceptions.DataMissing() - - - def _GetValue( self ): - - enumerated_transformations = list( self._transformations.GetData() ) - - enumerated_transformations.sort() - - transformations = [ ( transformation_type, data ) for ( number, transformation_type, data ) in enumerated_transformations ] - - example_string = self._example_string.text() - - string_converter = ClientParsing.StringConverter( transformations, example_string ) - - return string_converter - - - def _MoveDown( self ): - - selected_transformation = self._transformations.GetData( only_selected = True )[0] - - ( number, transformation_type, data ) = selected_transformation - - swap_transformation = self._GetTransformation( number + 1 ) - - self._SwapTransformations( selected_transformation, swap_transformation ) - - self._transformations.UpdateDatas() - - self._transformations.Sort() - - - def _MoveUp( self ): - - selected_transformation = self._transformations.GetData( only_selected = True )[0] - - ( number, transformation_type, data ) = selected_transformation - - swap_transformation = self._GetTransformation( number - 1 ) - - self._SwapTransformations( selected_transformation, swap_transformation ) - - self._transformations.UpdateDatas() - - self._transformations.Sort() - - - def _SwapTransformations( self, one, two ): - - selected_data = self._transformations.GetData( only_selected = True ) - - one_selected = one in selected_data - two_selected = two in selected_data - - self._transformations.DeleteDatas( ( one, two ) ) - - ( number_1, transformation_type_1, data_1 ) = one - ( number_2, transformation_type_2, data_2 ) = two - - one = ( number_2, transformation_type_1, data_1 ) - two = ( number_1, transformation_type_2, data_2 ) - - self._transformations.AddDatas( ( one, two ) ) - - if one_selected: - - self._transformations.SelectDatas( ( one, ) ) - - - if two_selected: - - self._transformations.SelectDatas( ( two, ) ) - - - - def EventUpdate( self, text ): - - self._transformations.UpdateDatas() - - - def GetValue( self ): - - string_converter = self._GetValue() - - try: - - string_converter.Convert( self._example_string.text() ) - - except HydrusExceptions.StringConvertException: - - raise HydrusExceptions.VetoException( 'Please enter an example text that can be converted!' ) - - - return string_converter - - - class _TransformationPanel( ClientGUIScrolledPanels.EditPanel ): - - def __init__( self, parent, transformation_type, data, example_text ): - - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - - self._transformation_type = ClientGUICommon.BetterChoice( self ) - - for t_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_REVERSE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): - - self._transformation_type.addItem( ClientParsing.transformation_type_str_lookup[ t_type ], t_type ) - - - self._example_string = QW.QLineEdit( self ) - - min_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._example_string, 96 ) - - self._example_string.setMinimumWidth( min_width ) - - self._example_text = example_text - - if isinstance( self._example_text, bytes ): - - self._example_string.setText( repr( self._example_text ) ) - - else: - - self._example_string.setText( self._example_text ) - - - - - self._example_transformation = QW.QLineEdit( self ) - - # - - self._example_string.setReadOnly( True ) - self._example_transformation.setReadOnly( True ) - - self._data_text = QW.QLineEdit( self ) - self._data_number = QP.MakeQSpinBox( self, min=0, max=65535 ) - self._data_encoding = ClientGUICommon.BetterChoice( self ) - self._data_regex_repl = QW.QLineEdit( self ) - self._data_date_link = ClientGUICommon.BetterHyperLink( self, 'link to date info', 'https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior' ) - self._data_timezone_decode = ClientGUICommon.BetterChoice( self ) - self._data_timezone_encode = ClientGUICommon.BetterChoice( self ) - self._data_timezone_offset = QP.MakeQSpinBox( self, min=-86400, max=86400 ) - - for e in ( 'hex', 'base64', 'url percent encoding' ): - - self._data_encoding.addItem( e, e ) - - - self._data_timezone_decode.addItem( 'UTC', HC.TIMEZONE_GMT ) - self._data_timezone_decode.addItem( 'Local', HC.TIMEZONE_LOCAL ) - self._data_timezone_decode.addItem( 'Offset', HC.TIMEZONE_OFFSET ) - - self._data_timezone_encode.addItem( 'UTC', HC.TIMEZONE_GMT ) - self._data_timezone_encode.addItem( 'Local', HC.TIMEZONE_LOCAL ) - - # - - self._transformation_type.SetValue( transformation_type ) - - self._data_number.setValue( 1 ) - - # - - if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_ENCODE ): - - self._data_encoding.SetValue( data ) - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: - - ( pattern, repl ) = data - - self._data_text.setText( pattern ) - self._data_regex_repl.setText( repl ) - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: - - ( phrase, timezone_type, timezone_offset ) = data - - self._data_text.setText( phrase ) - self._data_timezone_decode.SetValue( timezone_type ) - self._data_timezone_offset.setValue( timezone_offset ) - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: - - ( phrase, timezone_type ) = data - - self._data_text.setText( phrase ) - self._data_timezone_encode.SetValue( timezone_type ) - - elif data is not None: - - if isinstance( data, int ): - - self._data_number.setValue( data ) - - else: - - self._data_text.setText( data ) - - - - # - - rows = [] - - # This mess needs to be all replaced with a nice QFormLayout subclass that can do row hide/show - # or just a whole separate panel for each transformation type, but w/e - - self._data_text_label = ClientGUICommon.BetterStaticText( self, 'string data: ' ) - self._data_number_label = ClientGUICommon.BetterStaticText( self, 'number data: ' ) - self._data_encoding_label = ClientGUICommon.BetterStaticText( self, 'encoding data: ' ) - self._data_regex_repl_label = ClientGUICommon.BetterStaticText( self, 'regex replacement: ' ) - self._data_date_link_label = ClientGUICommon.BetterStaticText( self, 'date info: ' ) - self._data_timezone_decode_label = ClientGUICommon.BetterStaticText( self, 'date decode timezone: ' ) - self._data_timezone_offset_label = ClientGUICommon.BetterStaticText( self, 'timezone offset: ' ) - self._data_timezone_encode_label = ClientGUICommon.BetterStaticText( self, 'date encode timezone: ' ) - - rows.append( ( 'example string: ', self._example_string ) ) - rows.append( ( 'transformed string: ', self._example_transformation ) ) - rows.append( ( 'transformation type: ', self._transformation_type ) ) - rows.append( ( self._data_text_label, self._data_text ) ) - rows.append( ( self._data_number_label, self._data_number ) ) - rows.append( ( self._data_encoding_label, self._data_encoding ) ) - rows.append( ( self._data_regex_repl_label, self._data_regex_repl ) ) - rows.append( ( self._data_date_link_label, self._data_date_link ) ) - rows.append( ( self._data_timezone_decode_label, self._data_timezone_decode ) ) - rows.append( ( self._data_timezone_offset_label, self._data_timezone_offset ) ) - rows.append( ( self._data_timezone_encode_label, self._data_timezone_encode ) ) - - self._control_gridbox = ClientGUICommon.WrapInGrid( self, rows ) - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, self._control_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) - QP.AddToLayout( vbox, QW.QWidget( self ), CC.FLAGS_EXPAND_BOTH_WAYS ) - - self.widget().setLayout( vbox ) - - self._UpdateDataControls() - - # - - self._transformation_type.currentIndexChanged.connect( self._UpdateDataControls ) - self._transformation_type.currentIndexChanged.connect( self._UpdateExampleText ) - - self._data_text.textEdited.connect( self._UpdateExampleText ) - self._data_number.valueChanged.connect( self._UpdateExampleText ) - self._data_encoding.currentIndexChanged.connect( self._UpdateExampleText ) - self._data_regex_repl.textEdited.connect( self._UpdateExampleText ) - self._data_timezone_decode.currentIndexChanged.connect( self._UpdateExampleText ) - self._data_timezone_offset.valueChanged.connect( self._UpdateExampleText ) - self._data_timezone_encode.currentIndexChanged.connect( self._UpdateExampleText ) - - self._data_timezone_decode.currentIndexChanged.connect( self._UpdateDataControls ) - self._data_timezone_encode.currentIndexChanged.connect( self._UpdateDataControls ) - - self._UpdateExampleText() - - - def _UpdateDataControls( self ): - - self._data_text_label.setVisible( False ) - self._data_number_label.setVisible( False ) - self._data_encoding_label.setVisible( False ) - self._data_regex_repl_label.setVisible( False ) - self._data_date_link_label.setVisible( False ) - self._data_timezone_decode_label.setVisible( False ) - self._data_timezone_offset_label.setVisible( False ) - self._data_timezone_encode_label.setVisible( False ) - - self._data_text.setVisible( False ) - self._data_number.setVisible( False ) - self._data_encoding.setVisible( False ) - self._data_regex_repl.setVisible( False ) - self._data_date_link.setVisible( False ) - self._data_timezone_decode.setVisible( False ) - self._data_timezone_offset.setVisible( False ) - self._data_timezone_encode.setVisible( False ) - - transformation_type = self._transformation_type.GetValue() - - if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE ): - - self._data_encoding_label.setVisible( True ) - self._data_encoding.setVisible( True ) - - elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB ): - - self._data_text_label.setVisible( True ) - self._data_text.setVisible( True ) - - data_text_label = 'string data: ' - - if transformation_type == ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT: - - data_text_label = 'text to prepend: ' - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT: - - data_text_label = 'text to append: ' - - elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE ): - - self._data_date_link_label.setVisible( True ) - self._data_date_link.setVisible( True ) - - if transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: - - data_text_label = 'date decode phrase: ' - - self._data_timezone_decode_label.setVisible( True ) - self._data_timezone_decode.setVisible( True ) - - if self._data_timezone_decode.GetValue() == HC.TIMEZONE_OFFSET: - - self._data_timezone_offset_label.setVisible( True ) - self._data_timezone_offset.setVisible( True ) - - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: - - data_text_label = 'date encode phrase: ' - - self._data_timezone_encode_label.setVisible( True ) - self._data_timezone_encode.setVisible( True ) - - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: - - data_text_label = 'regex pattern: ' - - self._data_regex_repl_label.setVisible( True ) - self._data_regex_repl.setVisible( True ) - - - self._data_text_label.setText( data_text_label ) - - elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): - - self._data_number_label.setVisible( True ) - self._data_number.setVisible( True ) - - if transformation_type == ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION: - - self._data_number.setMinimum( -65535 ) - - else: - - self._data_number.setMinimum( 0 ) - - - data_number_label = 'number data: ' - - if transformation_type == ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING: - - data_number_label = 'characters to remove: ' - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END: - - data_number_label = 'characters to remove: ' - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING: - - data_number_label = 'characters to take: ' - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END: - - data_number_label = 'characters to take: ' - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION: - - data_number_label = 'number to add: ' - - - self._data_number_label.setText( data_number_label ) - - - - def _UpdateExampleText( self ): - - try: - - transformations = [ self.GetValue() ] - - string_converter = ClientParsing.StringConverter( transformations, self._example_text ) - - example_transformation = string_converter.Convert( self._example_text ) - - try: - - self._example_transformation.setText( str( example_transformation ) ) - - except: - - self._example_transformation.setText( repr( example_transformation ) ) - - - except Exception as e: - - self._example_transformation.setText( str( e ) ) - - - - def GetValue( self ): - - transformation_type = self._transformation_type.GetValue() - - if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE ): - - data = self._data_encoding.GetValue() - - elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT ): - - data = self._data_text.text() - - elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): - - data = self._data_number.value() - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: - - pattern = self._data_text.text() - repl = self._data_regex_repl.text() - - data = ( pattern, repl ) - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: - - phrase = self._data_text.text() - timezone_time = self._data_timezone_decode.GetValue() - timezone_offset = self._data_timezone_offset.value() - - data = ( phrase, timezone_time, timezone_offset ) - - elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: - - phrase = self._data_text.text() - timezone_time = self._data_timezone_encode.GetValue() - - data = ( phrase, timezone_time ) - - else: - - data = None - - - return ( transformation_type, data ) - - - -class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ): - - def __init__( self, parent, string_match = None ): - - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - - if string_match is None: - - string_match = ClientParsing.StringMatch() - - - self._match_type = ClientGUICommon.BetterChoice( self ) - - self._match_type.addItem( 'any characters', ClientParsing.STRING_MATCH_ANY ) - self._match_type.addItem( 'fixed characters', ClientParsing.STRING_MATCH_FIXED ) - self._match_type.addItem( 'character set', ClientParsing.STRING_MATCH_FLEXIBLE ) - self._match_type.addItem( 'regex', ClientParsing.STRING_MATCH_REGEX ) - - self._match_value_text_input = QW.QLineEdit( self ) - - self._match_value_flexible_input = ClientGUICommon.BetterChoice( self ) - - self._match_value_flexible_input.addItem( 'alphabetic characters (a-zA-Z)', ClientParsing.ALPHA ) - self._match_value_flexible_input.addItem( 'alphanumeric characters (a-zA-Z0-9)', ClientParsing.ALPHANUMERIC ) - self._match_value_flexible_input.addItem( 'numeric characters (0-9)', ClientParsing.NUMERIC ) - - self._min_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' ) - self._max_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' ) - - self._example_string = QW.QLineEdit( self ) - - self._example_string_matches = ClientGUICommon.BetterStaticText( self ) - - # - - self.SetValue( string_match ) - - # - - rows = [] - - rows.append( ( 'match type: ', self._match_type ) ) - rows.append( ( 'match text: ', self._match_value_text_input ) ) - rows.append( ( 'match value (character set): ', self._match_value_flexible_input ) ) - rows.append( ( 'minimum allowed number of characters: ', self._min_chars ) ) - rows.append( ( 'maximum allowed number of characters: ', self._max_chars ) ) - rows.append( ( 'example string: ', self._example_string ) ) - - gridbox = ClientGUICommon.WrapInGrid( self, rows ) - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) - QP.AddToLayout( vbox, self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR ) - - self.widget().setLayout( vbox ) - - # - - self._match_type.currentIndexChanged.connect( self._UpdateControls ) - self._match_value_text_input.textChanged.connect( self._UpdateControls ) - self._match_value_flexible_input.currentIndexChanged.connect( self._UpdateControls ) - self._min_chars.valueChanged.connect( self._UpdateControls ) - self._max_chars.valueChanged.connect( self._UpdateControls ) - self._example_string.textChanged.connect( self._UpdateControls ) - - - def _GetValue( self ): - - match_type = self._match_type.GetValue() - - if match_type == ClientParsing.STRING_MATCH_ANY: - - match_value = '' - - elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE: - - match_value = self._match_value_flexible_input.GetValue() - - else: - - match_value = self._match_value_text_input.text() - - - min_chars = self._min_chars.GetValue() - max_chars = self._max_chars.GetValue() - - example_string = self._example_string.text() - - string_match = ClientParsing.StringMatch( match_type = match_type, match_value = match_value, min_chars = min_chars, max_chars = max_chars, example_string = example_string ) - - return string_match - - - def _UpdateControls( self ): - - match_type = self._match_type.GetValue() - - if match_type == ClientParsing.STRING_MATCH_ANY: - - self._match_value_text_input.setEnabled( False ) - self._match_value_flexible_input.setEnabled( False ) - - elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE: - - self._match_value_text_input.setEnabled( False ) - self._match_value_flexible_input.setEnabled( True ) - - else: - - self._match_value_text_input.setEnabled( True ) - self._match_value_flexible_input.setEnabled( False ) - - - if match_type == ClientParsing.STRING_MATCH_FIXED: - - self._min_chars.SetValue( None ) - self._max_chars.SetValue( None ) - - self._min_chars.setEnabled( False ) - self._max_chars.setEnabled( False ) - - self._example_string.blockSignals( True ) # Temporarily block the text changed signal here so we won't end up in infinite recursion - self._example_string.setText( self._match_value_text_input.text() ) - self._example_string.blockSignals( False ) - - self._example_string_matches.setText( '' ) - - else: - - self._min_chars.setEnabled( True ) - self._max_chars.setEnabled( True ) - - string_match = self._GetValue() - - try: - - string_match.Test( self._example_string.text() ) - - self._example_string_matches.setText( 'Example matches ok!' ) - self._example_string_matches.setObjectName( 'HydrusValid' ) - self._example_string_matches.style().polish( self._example_string_matches ) - - except HydrusExceptions.StringMatchException as e: - - reason = str( e ) - - self._example_string_matches.setText( 'Example does not match - '+reason ) - self._example_string_matches.setObjectName( 'HydrusInvalid' ) - self._example_string_matches.style().polish( self._example_string_matches ) - - - - - def GetValue( self ): - - string_match = self._GetValue() - - try: - - string_match.Test( self._example_string.text() ) - - except HydrusExceptions.StringMatchException: - - raise HydrusExceptions.VetoException( 'Please enter an example text that matches the given rules!' ) - - - return string_match - - - def SetValue( self, string_match ): - - ( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple() - - self._match_type.SetValue( match_type ) - - if match_type == ClientParsing.STRING_MATCH_FLEXIBLE: - - self._match_value_flexible_input.SetValue( match_value ) - - else: - - self._match_value_flexible_input.SetValue( ClientParsing.ALPHA ) - - self._match_value_text_input.setText( match_value ) - - - self._min_chars.SetValue( min_chars ) - self._max_chars.SetValue( max_chars ) - - self._example_string.setText( example_string ) - - self._UpdateControls() - - class NoneableBytesControl( QW.QWidget ): valueChanged = QC.Signal() @@ -1538,604 +618,6 @@ class NetworkJobControl( QW.QFrame ): -class StringConverterButton( ClientGUICommon.BetterButton ): - - stringConverterUpdate = QC.Signal() - - def __init__( self, parent, string_converter ): - - ClientGUICommon.BetterButton.__init__( self, parent, 'edit string converter', self._Edit ) - - self._string_converter = string_converter - - self._example_string_override = None - - self._UpdateLabel() - - - def _Edit( self ): - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string converter', frame_key = 'deeply_nested_dialog' ) as dlg: - - panel = EditStringConverterPanel( dlg, self._string_converter, example_string_override = self._example_string_override ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - self._string_converter = panel.GetValue() - - self._UpdateLabel() - - - self.stringConverterUpdate.emit() - - - def _UpdateLabel( self ): - - num_rules = len( self._string_converter.transformations ) - - if num_rules == 0: - - label = 'no string transformations' - - else: - - label = HydrusData.ToHumanInt( num_rules ) + ' string transformations' - - - self.setText( label ) - - - def GetValue( self ): - - return self._string_converter - - - def SetExampleString( self, example_string ): - - self._example_string_override = example_string - - - def SetValue( self, string_converter ): - - self._string_converter = string_converter - - self._UpdateLabel() - - -class StringMatchButton( ClientGUICommon.BetterButton ): - - def __init__( self, parent, string_match ): - - ClientGUICommon.BetterButton.__init__( self, parent, 'edit string match', self._Edit ) - - self._string_match = string_match - - self._UpdateLabel() - - - def _Edit( self ): - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string match', frame_key = 'deeply_nested_dialog' ) as dlg: - - panel = EditStringMatchPanel( dlg, self._string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - self._string_match = panel.GetValue() - - self._UpdateLabel() - - - - - def _UpdateLabel( self ): - - label = self._string_match.ToString() - - self.setText( label ) - - - def GetValue( self ): - - return self._string_match - - - def SetValue( self, string_match ): - - self._string_match = string_match - - self._UpdateLabel() - - -class StringMatchToStringMatchDictControl( QW.QWidget ): - - def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key' ): - - QW.QWidget.__init__( self, parent ) - - self._key_name = key_name - - listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - - columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ] - - self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, 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( list(initial_dict.items()) ) - - self._listctrl.Sort( 0 ) - - # - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) - - self.setLayout( vbox ) - - - def _ConvertDataToListCtrlTuples( self, data ): - - ( key_string_match, value_string_match ) = data - - pretty_key = key_string_match.ToString() - pretty_value = value_string_match.ToString() - - display_tuple = ( pretty_key, pretty_value ) - sort_tuple = ( pretty_key, pretty_value ) - - return ( display_tuple, sort_tuple ) - - - def _Add( self ): - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit ' + self._key_name ) as dlg: - - string_match = ClientParsing.StringMatch() - - panel = EditStringMatchPanel( dlg, string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - key_string_match = panel.GetValue() - - else: - - return - - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: - - string_match = ClientParsing.StringMatch() - - panel = EditStringMatchPanel( dlg, string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - value_string_match = panel.GetValue() - - data = ( key_string_match, value_string_match ) - - self._listctrl.AddDatas( ( data, ) ) - - - - - def _Edit( self ): - - for data in self._listctrl.GetData( only_selected = True ): - - ( key_string_match, value_string_match ) = data - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit ' + self._key_name ) as dlg: - - panel = EditStringMatchPanel( dlg, key_string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - key_string_match = panel.GetValue() - - else: - - break - - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: - - panel = EditStringMatchPanel( dlg, value_string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - value_string_match = panel.GetValue() - - else: - - break - - - - self._listctrl.DeleteDatas( ( data, ) ) - - edited_data = ( key_string_match, value_string_match ) - - self._listctrl.AddDatas( ( edited_data, ) ) - - - self._listctrl.Sort() - - - def GetValue( self ): - - value_dict = dict( self._listctrl.GetData() ) - - return value_dict - - -class StringToStringDictButton( ClientGUICommon.BetterButton ): - - def __init__( self, parent, label ): - - ClientGUICommon.BetterButton.__init__( self, parent, label, self._Edit ) - - self._value = {} - - - def _Edit( self ): - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string dictionary' ) as dlg: - - panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg ) - - control = StringToStringDictControl( panel, self._value ) - - panel.SetControl( control ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - self._value = control.GetValue() - - - - - def GetValue( self ): - - return self._value - - - def SetValue( self, value ): - - self._value = value - - -class StringToStringDictControl( QW.QWidget ): - - listCtrlChanged = QC.Signal() - - def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key', value_name = 'value', allow_add_delete = True, edit_keys = True ): - - QW.QWidget.__init__( self, parent ) - - self._key_name = key_name - self._value_name = value_name - - self._edit_keys = edit_keys - - listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - - columns = [ ( self._key_name, 20 ), ( self._value_name, -1 ) ] - - use_simple_delete = allow_add_delete - - self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_value', min_height, 36, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = use_simple_delete, activation_callback = self._Edit ) - self._listctrl.listCtrlChanged.connect( self.listCtrlChanged ) - - listctrl_panel.SetListCtrl( self._listctrl ) - - if allow_add_delete: - - listctrl_panel.AddButton( 'add', self._Add ) - - - listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True ) - - if allow_add_delete: - - listctrl_panel.AddDeleteButton() - - - # - - self._listctrl.AddDatas( list(initial_dict.items()) ) - - self._listctrl.Sort( 0 ) - - # - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) - - self.setLayout( vbox ) - - - def _ConvertDataToListCtrlTuples( self, data ): - - ( key, value ) = data - - display_tuple = ( key, value ) - sort_tuple = ( key, value ) - - return ( display_tuple, sort_tuple ) - - - def _Add( self ): - - with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - key = dlg.GetValue() - - if key in self._GetExistingKeys(): - - QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) - - return - - - with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._value_name, allow_blank = True ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - value = dlg.GetValue() - - data = ( key, value ) - - self._listctrl.AddDatas( ( data, ) ) - - - - - - - def _Edit( self ): - - for data in self._listctrl.GetData( only_selected = True ): - - ( key, value ) = data - - if self._edit_keys: - - with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - edited_key = dlg.GetValue() - - if edited_key != key and edited_key in self._GetExistingKeys(): - - QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) - - break - - - else: - - break - - - - else: - - edited_key = key - - - with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._value_name, default = value, allow_blank = True ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - edited_value = dlg.GetValue() - - else: - - break - - - - self._listctrl.DeleteDatas( ( data, ) ) - - edited_data = ( edited_key, edited_value ) - - self._listctrl.AddDatas( ( edited_data, ) ) - - - self._listctrl.Sort() - - - def _GetExistingKeys( self ): - - return { key for ( key, value ) in self._listctrl.GetData() } - - - def GetValue( self ): - - value_dict = dict( self._listctrl.GetData() ) - - return value_dict - - -class StringToStringMatchDictControl( QW.QWidget ): - - def __init__( self, parent, initial_dict, min_height = 10, key_name = 'key' ): - - QW.QWidget.__init__( self, parent ) - - self._key_name = key_name - - listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - - columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ] - - self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, 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( list(initial_dict.items()) ) - - self._listctrl.Sort( 0 ) - - # - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) - - self.setLayout( vbox ) - - - def _ConvertDataToListCtrlTuples( self, data ): - - ( key, string_match ) = data - - pretty_string_match = string_match.ToString() - - display_tuple = ( key, pretty_string_match ) - sort_tuple = ( key, pretty_string_match ) - - return ( display_tuple, sort_tuple ) - - - def _Add( self ): - - with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - key = dlg.GetValue() - - if key in self._GetExistingKeys(): - - QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) - - return - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: - - string_match = ClientParsing.StringMatch() - - panel = EditStringMatchPanel( dlg, string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - string_match = panel.GetValue() - - data = ( key, string_match ) - - self._listctrl.AddDatas( ( data, ) ) - - - - - - - def _Edit( self ): - - for data in self._listctrl.GetData( only_selected = True ): - - ( key, string_match ) = data - - with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - edited_key = dlg.GetValue() - - if edited_key != key and edited_key in self._GetExistingKeys(): - - QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) - - break - - - else: - - break - - - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: - - string_match = ClientParsing.StringMatch() - - panel = EditStringMatchPanel( dlg, string_match ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - edited_string_match = panel.GetValue() - - else: - - break - - - - self._listctrl.DeleteDatas( ( data, ) ) - - edited_data = ( edited_key, edited_string_match ) - - self._listctrl.AddDatas( ( edited_data, ) ) - - - self._listctrl.Sort() - - - def _GetExistingKeys( self ): - - return { key for ( key, value ) in self._listctrl.GetData() } - - - def GetValue( self ): - - value_dict = dict( self._listctrl.GetData() ) - - return value_dict - - class TextAndPasteCtrl( QW.QWidget ): def __init__( self, parent, add_callable, allow_empty_input = False ): diff --git a/hydrus/client/gui/ClientGUIDialogs.py b/hydrus/client/gui/ClientGUIDialogs.py index ea337270..b8051d2b 100644 --- a/hydrus/client/gui/ClientGUIDialogs.py +++ b/hydrus/client/gui/ClientGUIDialogs.py @@ -1121,7 +1121,16 @@ class DialogTextEntry( Dialog ): # self._text.setText( default ) - if placeholder is not None: self._text.setPlaceholderText( placeholder ) + + if placeholder is not None: + + self._text.setPlaceholderText( placeholder ) + + + if len( default ) > 0: + + self._text.setSelection( 0, len( default ) ) + self._CheckText() diff --git a/hydrus/client/gui/ClientGUIExport.py b/hydrus/client/gui/ClientGUIExport.py index 151276f9..0f32906b 100644 --- a/hydrus/client/gui/ClientGUIExport.py +++ b/hydrus/client/gui/ClientGUIExport.py @@ -732,9 +732,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ): tags.update( current_tags ) - tags = list( tags ) - - tags.sort() + tags = sorted( tags ) txt_path = path + '.txt' diff --git a/hydrus/client/gui/ClientGUIImport.py b/hydrus/client/gui/ClientGUIImport.py index 8454fee1..d50559d6 100644 --- a/hydrus/client/gui/ClientGUIImport.py +++ b/hydrus/client/gui/ClientGUIImport.py @@ -1578,9 +1578,7 @@ class EditLocalImportFilenameTaggingPanel( ClientGUIScrolledPanels.EditPanel ): tags.update( self._filename_tagging_panel.GetTags( index, path ) ) - tags = list( tags ) - - tags.sort() + tags = sorted( tags ) return tags diff --git a/hydrus/client/gui/ClientGUIListBoxes.py b/hydrus/client/gui/ClientGUIListBoxes.py index 72638e1d..1dd2fb31 100644 --- a/hydrus/client/gui/ClientGUIListBoxes.py +++ b/hydrus/client/gui/ClientGUIListBoxes.py @@ -93,6 +93,8 @@ class AddEditDeleteListBox( QW.QWidget ): self._AddData( data ) + self.listBoxChanged.emit() + def _AddAllDefaults( self, defaults_callable ): @@ -103,6 +105,8 @@ class AddEditDeleteListBox( QW.QWidget ): self._AddData( default ) + self.listBoxChanged.emit() + def _AddData( self, data ): @@ -144,6 +148,8 @@ class AddEditDeleteListBox( QW.QWidget ): + self.listBoxChanged.emit() + def _Delete( self ): @@ -401,6 +407,8 @@ class AddEditDeleteListBox( QW.QWidget ): QW.QMessageBox.critical( self, 'Error', message ) + self.listBoxChanged.emit() + def _SetNoneDupeName( self, obj ): @@ -619,6 +627,8 @@ class QueueListBox( QW.QWidget ): self._AddData( data ) + self.listBoxChanged.emit() + def _AddData( self, data ): @@ -653,6 +663,7 @@ class QueueListBox( QW.QWidget ): self.listBoxChanged.emit() + def _Down( self ): @@ -704,7 +715,7 @@ class QueueListBox( QW.QWidget ): new_item.setData( QC.Qt.UserRole, new_data ) self._listbox.insertItem( i, new_item ) - + self.listBoxChanged.emit() @@ -1814,9 +1825,7 @@ class ListBoxTags( ListBox ): if len( predicates ) > 0: - s = [ predicate.ToString() for predicate in predicates ] - - s.sort() + s = sorted( ( predicate.ToString() for predicate in predicates ) ) page_name = ', '.join( s ) diff --git a/hydrus/client/gui/ClientGUIListCtrl.py b/hydrus/client/gui/ClientGUIListCtrl.py index 786d3549..caf3f82d 100644 --- a/hydrus/client/gui/ClientGUIListCtrl.py +++ b/hydrus/client/gui/ClientGUIListCtrl.py @@ -166,9 +166,7 @@ class BetterListCtrl( QW.QTreeWidget ): def _RecalculateIndicesAfterDelete( self ): - indices_and_data_info = list( self._indices_to_data_info.items() ) - - indices_and_data_info.sort() + indices_and_data_info = sorted( self._indices_to_data_info.items() ) self._indices_to_data_info = {} self._data_to_indices = {} @@ -537,9 +535,7 @@ class BetterListCtrl( QW.QTreeWidget ): # keep it sorted here, which is sometimes useful - indices_and_datas = [ ( index, data ) for ( data, index ) in self._data_to_indices.items() ] - - indices_and_datas.sort() + indices_and_datas = sorted( ( ( index, data ) for ( data, index ) in self._data_to_indices.items() ) ) datas = [ data for ( index, data ) in indices_and_datas ] diff --git a/hydrus/client/gui/ClientGUILogin.py b/hydrus/client/gui/ClientGUILogin.py index 48afd22e..fed40b8e 100644 --- a/hydrus/client/gui/ClientGUILogin.py +++ b/hydrus/client/gui/ClientGUILogin.py @@ -22,6 +22,7 @@ from hydrus.client.gui import ClientGUIListBoxes from hydrus.client.gui import ClientGUIListCtrl from hydrus.client.gui import ClientGUIParsing from hydrus.client.gui import ClientGUIScrolledPanels +from hydrus.client.gui import ClientGUIStringControls from hydrus.client.gui import ClientGUITopLevelWindowsPanels from hydrus.client.gui import QtPorting as QP from hydrus.client.importing import ClientImporting @@ -211,7 +212,7 @@ class EditLoginCredentialDefinitionPanel( ClientGUIScrolledPanels.EditPanel ): string_match = credential_definition.GetStringMatch() - self._string_match = ClientGUIControls.StringMatchButton( self, string_match ) + self._string_match = ClientGUIStringControls.StringMatchButton( self, string_match ) # @@ -334,9 +335,7 @@ class EditLoginsPanel( ClientGUIScrolledPanels.EditPanel ): domains_in_use = { login_domain for ( login_domain, login_script_key_and_name, credentials_tuple, login_access_type, login_access_text, active, validity, validity_error_text, no_work_until, no_work_until_reason ) in self._domains_and_login_info.GetData() } - available_examples = list( example_domains.difference( domains_in_use ) ) - - available_examples.sort() + available_examples = sorted( example_domains.difference( domains_in_use ) ) if len( available_examples ) > 0: @@ -1307,7 +1306,7 @@ class EditLoginScriptPanel( ClientGUIScrolledPanels.EditPanel ): required_cookies_info_box_panel = ClientGUICommon.StaticBox( self, 'cookies required to consider session logged in' ) - self._required_cookies_info = ClientGUIControls.StringMatchToStringMatchDictControl( required_cookies_info_box_panel, login_script.GetRequiredCookiesInfo(), min_height = 4, key_name = 'cookie name' ) + self._required_cookies_info = ClientGUIStringControls.StringMatchToStringMatchDictControl( required_cookies_info_box_panel, login_script.GetRequiredCookiesInfo(), min_height = 4, key_name = 'cookie name' ) # @@ -1664,9 +1663,7 @@ class EditLoginScriptPanel( ClientGUIScrolledPanels.EditPanel ): if self._test_domain == '': - example_domains = list( login_script.GetExampleDomains() ) - - example_domains.sort() + example_domains = sorted( login_script.GetExampleDomains() ) if len( example_domains ) > 0: @@ -1952,8 +1949,7 @@ class EditLoginScriptsPanel( ClientGUIScrolledPanels.EditPanel ): name = login_script.GetName() - example_domains = list( login_script.GetExampleDomains() ) - example_domains.sort() + example_domains = sorted( login_script.GetExampleDomains() ) pretty_name = name pretty_example_domains = ', '.join( example_domains ) @@ -2040,25 +2036,25 @@ class EditLoginStepPanel( ClientGUIScrolledPanels.EditPanel ): required_credentials_panel = ClientGUICommon.StaticBox( self, 'credentials to send' ) - self._required_credentials = ClientGUIControls.StringToStringDictControl( required_credentials_panel, required_credentials, min_height = 4, key_name = 'credential name', value_name = 'parameter name' ) + self._required_credentials = ClientGUIStringControls.StringToStringDictControl( required_credentials_panel, required_credentials, min_height = 4, key_name = 'credential name', value_name = 'parameter name' ) # static_args_panel = ClientGUICommon.StaticBox( self, 'static variables to send' ) - self._static_args = ClientGUIControls.StringToStringDictControl( static_args_panel, static_args, min_height = 4, key_name = 'parameter name', value_name = 'value' ) + self._static_args = ClientGUIStringControls.StringToStringDictControl( static_args_panel, static_args, min_height = 4, key_name = 'parameter name', value_name = 'value' ) # temp_args_panel = ClientGUICommon.StaticBox( self, 'temporary variables to send' ) - self._temp_args = ClientGUIControls.StringToStringDictControl( temp_args_panel, temp_args, min_height = 4, key_name = 'temp variable name', value_name = 'parameter name' ) + self._temp_args = ClientGUIStringControls.StringToStringDictControl( temp_args_panel, temp_args, min_height = 4, key_name = 'temp variable name', value_name = 'parameter name' ) # required_cookies_info_box_panel = ClientGUICommon.StaticBox( self, 'cookies required to consider step successful' ) - self._required_cookies_info = ClientGUIControls.StringMatchToStringMatchDictControl( required_cookies_info_box_panel, required_cookies_info, min_height = 4, key_name = 'cookie name' ) + self._required_cookies_info = ClientGUIStringControls.StringMatchToStringMatchDictControl( required_cookies_info_box_panel, required_cookies_info, min_height = 4, key_name = 'cookie name' ) # diff --git a/hydrus/client/gui/ClientGUIMedia.py b/hydrus/client/gui/ClientGUIMedia.py index fc4b0286..012e6d1b 100644 --- a/hydrus/client/gui/ClientGUIMedia.py +++ b/hydrus/client/gui/ClientGUIMedia.py @@ -29,9 +29,7 @@ def CopyMediaURLs( medias ): urls.update( media_urls ) - urls = list( urls ) - - urls.sort() + urls = sorted( urls ) urls_string = os.linesep.join( urls ) @@ -54,9 +52,7 @@ def CopyMediaURLClassURLs( medias, url_class ): - urls = list( urls ) - - urls.sort() + urls = sorted( urls ) urls_string = os.linesep.join( urls ) @@ -139,9 +135,7 @@ def OpenExternally( media ): def OpenURLs( urls ): - urls = list( urls ) - - urls.sort() + urls = sorted( urls ) if len( urls ) > 1: diff --git a/hydrus/client/gui/ClientGUIPages.py b/hydrus/client/gui/ClientGUIPages.py index 376e30eb..106047a1 100644 --- a/hydrus/client/gui/ClientGUIPages.py +++ b/hydrus/client/gui/ClientGUIPages.py @@ -424,6 +424,8 @@ class Page( QW.QSplitter ): self._search_preview_split = QW.QSplitter( self ) + self._done_split_setups = False + self._management_panel = ClientGUIManagement.CreateManagementPanel( self._search_preview_split, self, self._controller, self._management_controller ) file_service_key = self._management_controller.GetKey( 'file_service' ) @@ -442,8 +444,6 @@ class Page( QW.QSplitter ): self._preview_panel.setLayout( vbox ) - self.SetupSplits() - self.widget( 0 ).setMinimumWidth( 120 ) self.widget( 1 ).setMinimumWidth( 120 ) self.setStretchFactor( 0, 0 ) @@ -740,6 +740,13 @@ class Page( QW.QSplitter ): def PageShown( self ): + if self.isVisible() and not self._done_split_setups: + + self.SetupSplits() + + self._done_split_setups = True + + self._management_panel.PageShown() self._media_panel.PageShown() self._preview_canvas.PageShown() @@ -931,6 +938,11 @@ class PagesNotebook( QP.TabWidgetWithDnD ): QP.TabWidgetWithDnD.__init__( self, parent ) + if HG.client_controller.new_options.GetBoolean( 'elide_page_tab_names' ): + + self.tabBar().setElideMode( QC.Qt.ElideMiddle ) + + self._parent_notebook = parent # this is disabled for now because it seems borked in Qt @@ -963,7 +975,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ): self._widget_event_filter.EVT_LEFT_DOWN( lambda ev: ev.accept() ) self._widget_event_filter.EVT_LEFT_DOWN( lambda ev: ev.accept() ) - self.currentChanged.connect( self.EventPageChanged ) + self.currentChanged.connect( self.pageJustChanged ) self.pageDragAndDropped.connect( self._RefreshPageNamesAfterDnD ) self._previous_page_index = -1 @@ -1373,11 +1385,13 @@ class PagesNotebook( QP.TabWidgetWithDnD ): safe_page_name = ClientGUIFunctions.EscapeMnemonics( page_name ) - existing_page_name = self.tabText( index ) + tab_bar = self.tabBar() + + existing_page_name = tab_bar.tabText( index ) if existing_page_name not in ( safe_page_name, page_name ): - self.setTabText( index, safe_page_name ) + tab_bar.setTabText( index, safe_page_name ) @@ -1890,28 +1904,6 @@ class PagesNotebook( QP.TabWidgetWithDnD ): notebook._ChooseNewPage() - def EventPageChanged( self, index ): - - old_selection = self._previous_page_index - selection = index - - if old_selection != -1 and old_selection < self.count(): - - self.widget( old_selection ).PageHidden() - - - if selection != -1: - - self.widget( selection ).PageShown() - - - self._controller.gui.RefreshStatusBar() - - self._previous_page_index = index - - self._controller.pub( 'notify_page_change' ) - - def GetAPIInfoDict( self, simple ): return {} @@ -2296,6 +2288,8 @@ class PagesNotebook( QP.TabWidgetWithDnD ): for page_tuple in page_tuples: + select_page = not done_first_page + ( page_type, page_data ) = page_tuple if page_type == 'pages': @@ -2304,7 +2298,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ): try: - page = self.NewPagesNotebook( name, forced_insertion_index = forced_insertion_index, give_it_a_blank_page = False, select_page = False ) + page = self.NewPagesNotebook( name, forced_insertion_index = forced_insertion_index, give_it_a_blank_page = False, select_page = select_page ) page.AppendSessionPageTuples( subpage_tuples ) @@ -2319,12 +2313,8 @@ class PagesNotebook( QP.TabWidgetWithDnD ): try: - select_page = not done_first_page - self.NewPage( management_controller, initial_hashes = initial_hashes, forced_insertion_index = forced_insertion_index, select_page = select_page ) - done_first_page = True - except Exception as e: HydrusData.ShowException( e ) @@ -2333,6 +2323,8 @@ class PagesNotebook( QP.TabWidgetWithDnD ): forced_insertion_index += 1 + done_first_page = True + def IsMultipleWatcherPage( self ): @@ -2552,10 +2544,6 @@ class PagesNotebook( QP.TabWidgetWithDnD ): self.setCurrentIndex( insertion_index ) - self.LayoutPages() - - page.SetupSplits() - self._controller.pub( 'refresh_page_name', page.GetPageKey() ) self._controller.pub( 'notify_new_pages' ) @@ -2699,9 +2687,11 @@ class PagesNotebook( QP.TabWidgetWithDnD ): page_name = page.GetName() self.insertTab( insertion_index, page, page_name ) - if select_page: self.setCurrentIndex( insertion_index ) - self.LayoutPages() + if select_page: + + self.setCurrentIndex( insertion_index ) + self._controller.pub( 'refresh_page_name', page.GetPageKey() ) @@ -2749,6 +2739,30 @@ class PagesNotebook( QP.TabWidgetWithDnD ): + def pageJustChanged( self, index ): + + old_selection = self._previous_page_index + selection = index + + if old_selection != -1 and old_selection < self.count(): + + self.widget( old_selection ).PageHidden() + + + if selection != -1: + + new_page = self.widget( selection ) + + new_page.PageShown() + + + self._controller.gui.RefreshStatusBar() + + self._previous_page_index = index + + self._controller.pub( 'notify_page_change' ) + + def PageShown( self ): result = self.currentWidget() diff --git a/hydrus/client/gui/ClientGUIParsing.py b/hydrus/client/gui/ClientGUIParsing.py index bec4c279..38a894d5 100644 --- a/hydrus/client/gui/ClientGUIParsing.py +++ b/hydrus/client/gui/ClientGUIParsing.py @@ -32,6 +32,8 @@ from hydrus.client.gui import ClientGUIListCtrl from hydrus.client.gui import ClientGUIScrolledPanels from hydrus.client.gui import ClientGUIScrolledPanelsEdit from hydrus.client.gui import ClientGUISerialisable +from hydrus.client.gui import ClientGUIStringControls +from hydrus.client.gui import ClientGUIStringPanels from hydrus.client.gui import ClientGUITopLevelWindowsPanels from hydrus.client.networking import ClientNetworkingContexts from hydrus.client.networking import ClientNetworkingDomain @@ -304,9 +306,7 @@ class DownloaderExportPanel( ClientGUIScrolledPanels.ReviewPanel ): - gug_names = list( gug_names ) - - gug_names.sort() + gug_names = sorted( gug_names ) num_gugs = len( gug_names ) @@ -392,9 +392,7 @@ class DownloaderExportPanel( ClientGUIScrolledPanels.ReviewPanel ): domains = domains.difference( existing_domains ) - domains = list( domains ) - - domains.sort() + domains = sorted( domains ) domain_metadatas = [] @@ -556,9 +554,9 @@ class EditCompoundFormulaPanel( ClientGUIScrolledPanels.EditPanel ): ( formulae, sub_phrase, string_match, string_converter ) = formula.ToTuple() - self._string_match_button = ClientGUIControls.StringMatchButton( edit_panel, string_match ) + self._string_match_button = ClientGUIStringControls.StringMatchButton( edit_panel, string_match ) - self._string_converter_button = ClientGUIControls.StringConverterButton( edit_panel, string_converter ) + self._string_converter_button = ClientGUIStringControls.StringConverterButton( edit_panel, string_converter ) # @@ -777,9 +775,9 @@ class EditContextVariableFormulaPanel( ClientGUIScrolledPanels.EditPanel ): ( variable_name, string_match, string_converter ) = formula.ToTuple() - self._string_match_button = ClientGUIControls.StringMatchButton( edit_panel, string_match ) + self._string_match_button = ClientGUIStringControls.StringMatchButton( edit_panel, string_match ) - self._string_converter_button = ClientGUIControls.StringConverterButton( edit_panel, string_converter ) + self._string_converter_button = ClientGUIStringControls.StringConverterButton( edit_panel, string_converter ) # @@ -1045,7 +1043,7 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ): self._tag_name = QW.QLineEdit( self ) - self._tag_attributes = ClientGUIControls.StringToStringDictControl( self, tag_attributes, min_height = 4 ) + 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 = 0, max = 255 ) @@ -1053,7 +1051,7 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ): self._should_test_tag_string = QW.QCheckBox( self ) - self._tag_string_string_match = ClientGUIControls.StringMatchButton( self, tag_string_string_match ) + self._tag_string_string_match = ClientGUIStringControls.StringMatchButton( self, tag_string_string_match ) # @@ -1250,9 +1248,9 @@ class EditHTMLFormulaPanel( ClientGUIScrolledPanels.EditPanel ): ( tag_rules, content_to_fetch, attribute_to_fetch, string_match, string_converter ) = formula.ToTuple() - self._string_match_button = ClientGUIControls.StringMatchButton( edit_panel, string_match ) + self._string_match_button = ClientGUIStringControls.StringMatchButton( edit_panel, string_match ) - self._string_converter_button = ClientGUIControls.StringConverterButton( edit_panel, string_converter ) + self._string_converter_button = ClientGUIStringControls.StringConverterButton( edit_panel, string_converter ) # @@ -1478,7 +1476,7 @@ class EditJSONParsingRulePanel( ClientGUIScrolledPanels.EditPanel ): self._parse_rule_type.addItem( 'all dictionary/list items', ClientParsing.JSON_PARSE_RULE_TYPE_ALL_ITEMS ) self._parse_rule_type.addItem( 'indexed list item', ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM ) - self._string_match = ClientGUIControls.EditStringMatchPanel( self, string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'posts', example_string = 'posts' ) ) + self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self, string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'posts', example_string = 'posts' ) ) self._index = QP.MakeQSpinBox( self, min=0, max=65535 ) @@ -1601,9 +1599,9 @@ class EditJSONFormulaPanel( ClientGUIScrolledPanels.EditPanel ): ( parse_rules, content_to_fetch, string_match, string_converter ) = formula.ToTuple() - self._string_match_button = ClientGUIControls.StringMatchButton( edit_panel, string_match ) + self._string_match_button = ClientGUIStringControls.StringMatchButton( edit_panel, string_match ) - self._string_converter_button = ClientGUIControls.StringConverterButton( edit_panel, string_converter ) + self._string_converter_button = ClientGUIStringControls.StringConverterButton( edit_panel, string_converter ) # @@ -1883,7 +1881,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ): self._veto_panel = QW.QWidget( self._content_panel ) self._veto_if_matches_found = QW.QCheckBox( self._veto_panel ) - self._string_match = ClientGUIControls.EditStringMatchPanel( self._veto_panel ) + self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self._veto_panel ) self._temp_variable_panel = QW.QWidget( self._content_panel ) @@ -2903,7 +2901,7 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ): string_converter = parser.GetStringConverter() - self._string_converter = ClientGUIControls.StringConverterButton( conversion_panel, string_converter ) + self._string_converter = ClientGUIStringControls.StringConverterButton( conversion_panel, string_converter ) # @@ -3135,9 +3133,7 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ): produces = page_parser.GetParsableContent() - produces = list( produces ) - - produces.sort() + produces = sorted( produces ) pretty_name = name pretty_formula = formula.ToPrettyString() @@ -3358,10 +3354,9 @@ class EditParsersPanel( ClientGUIScrolledPanels.EditPanel ): name = parser.GetName() - example_urls = list( parser.GetExampleURLs() ) - example_urls.sort() + example_urls = sorted( parser.GetExampleURLs() ) - produces = list( parser.GetParsableContent() ) + produces = sorted( parser.GetParsableContent() ) pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces ) @@ -3456,13 +3451,13 @@ class EditParsingScriptFileLookupPanel( ClientGUIScrolledPanels.EditPanel ): self._file_identifier_type.addItem( ClientParsing.file_identifier_string_lookup[ t], t ) - self._file_identifier_string_converter = ClientGUIControls.StringConverterButton( query_panel, file_identifier_string_converter ) + self._file_identifier_string_converter = ClientGUIStringControls.StringConverterButton( query_panel, file_identifier_string_converter ) self._file_identifier_arg_name = QW.QLineEdit( query_panel ) static_args_panel = ClientGUICommon.StaticBox( query_panel, 'static arguments' ) - self._static_args = ClientGUIControls.StringToStringDictControl( static_args_panel, static_args, min_height = 4 ) + self._static_args = ClientGUIStringControls.StringToStringDictControl( static_args_panel, static_args, min_height = 4 ) children_panel = ClientGUICommon.StaticBox( edit_panel, 'content parsing children' ) @@ -4250,7 +4245,7 @@ class TestPanel( QW.QWidget ): self._object_callable = object_callable - self._example_parsing_context = ClientGUIControls.StringToStringDictButton( self, 'edit example parsing context' ) + self._example_parsing_context = ClientGUIStringControls.StringToStringDictButton( self, 'edit example parsing context' ) self._data_preview_notebook = QW.QTabWidget( self ) diff --git a/hydrus/client/gui/ClientGUIResults.py b/hydrus/client/gui/ClientGUIResults.py index 8328613a..b8ab7826 100644 --- a/hydrus/client/gui/ClientGUIResults.py +++ b/hydrus/client/gui/ClientGUIResults.py @@ -148,20 +148,18 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): if self._focused_media is not None: - display_media = self._focused_media.GetDisplayMedia() - - if display_media is None: + if self._HasFocusSingleton(): - return + media = self._GetFocusSingleton() - - if display_media.GetMime() in HC.IMAGES: - - HG.client_controller.pub( 'clipboard', 'bmp', display_media ) - - else: - - QW.QMessageBox.critical( self, 'Error', 'Sorry, cannot take bmps of anything but static images right now!' ) + if media.GetMime() in HC.IMAGES: + + HG.client_controller.pub( 'clipboard', 'bmp', media ) + + else: + + QW.QMessageBox.critical( self, 'Error', 'Sorry, cannot take bmps of anything but static images right now!' ) + @@ -184,41 +182,42 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): paths.append( path ) - HG.client_controller.pub( 'clipboard', 'paths', paths ) + if len( paths ) > 0: + + HG.client_controller.pub( 'clipboard', 'paths', paths ) + def _CopyHashToClipboard( self, hash_type ): - display_media = self._focused_media.GetDisplayMedia() - - if display_media is None: + if self._HasFocusSingleton(): - return + media = self._GetFocusSingleton() - - sha256_hash = display_media.GetHash() - - if hash_type == 'sha256': + sha256_hash = media.GetHash() - hex_hash = sha256_hash.hex() - - else: - - if display_media.GetLocationsManager().IsLocal(): + if hash_type == 'sha256': - ( other_hash, ) = HG.client_controller.Read( 'file_hashes', ( sha256_hash, ), 'sha256', hash_type ) - - hex_hash = other_hash.hex() + hex_hash = sha256_hash.hex() else: - QW.QMessageBox.critical( self, 'Error', 'Unfortunately, you do not have that file in your database, so its non-sha256 hashes are unknown.' ) - - return + if media.GetLocationsManager().IsLocal(): + + ( other_hash, ) = HG.client_controller.Read( 'file_hashes', ( sha256_hash, ), 'sha256', hash_type ) + + hex_hash = other_hash.hex() + + else: + + QW.QMessageBox.critical( self, 'Error', 'Unfortunately, you do not have that file in your database, so its non-sha256 hashes are unknown.' ) + + return + - - HG.client_controller.pub( 'clipboard', 'text', hex_hash ) + HG.client_controller.pub( 'clipboard', 'text', hex_hash ) + def _CopyHashesToClipboard( self, hash_type ): @@ -250,18 +249,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _CopyPathToClipboard( self ): - display_media = self._focused_media.GetDisplayMedia() - - if display_media is None: + if self._HasFocusSingleton(): - return + media = self._GetFocusSingleton() + + client_files_manager = HG.client_controller.client_files_manager + + path = client_files_manager.GetFilePath( media.GetHash(), media.GetMime() ) + + HG.client_controller.pub( 'clipboard', 'text', path ) - - client_files_manager = HG.client_controller.client_files_manager - - path = client_files_manager.GetFilePath( display_media.GetHash(), display_media.GetMime() ) - - HG.client_controller.pub( 'clipboard', 'text', path ) def _CopyPathsToClipboard( self ): @@ -277,34 +274,35 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): paths.append( client_files_manager.GetFilePath( media_result.GetHash(), media_result.GetMime(), check_file_exists = False ) ) - text = os.linesep.join( paths ) - - HG.client_controller.pub( 'clipboard', 'text', text ) + if len( paths ) > 0: + + text = os.linesep.join( paths ) + + HG.client_controller.pub( 'clipboard', 'text', text ) + def _CopyServiceFilenameToClipboard( self, service_key ): - display_media = self._focused_media.GetDisplayMedia() - - if display_media is None: + if self._HasFocusSingleton(): - return + media = self._GetFocusSingleton() - - hash = display_media.GetHash() - - ( filename, ) = HG.client_controller.Read( 'service_filenames', service_key, { hash } ) - - service = HG.client_controller.services_manager.GetService( service_key ) - - if service.GetServiceType() == HC.IPFS: + hash = media.GetHash() - multihash_prefix = service.GetMultihashPrefix() + ( filename, ) = HG.client_controller.Read( 'service_filenames', service_key, { hash } ) - filename = multihash_prefix + filename + service = HG.client_controller.services_manager.GetService( service_key ) + + if service.GetServiceType() == HC.IPFS: + + multihash_prefix = service.GetMultihashPrefix() + + filename = multihash_prefix + filename + + + HG.client_controller.pub( 'clipboard', 'text', filename ) - - HG.client_controller.pub( 'clipboard', 'text', filename ) def _CopyServiceFilenamesToClipboard( self, service_key ): @@ -464,23 +462,18 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _LaunchMediaViewer( self, first_media = None ): - if self._focused_media is not None: + if self._HasFocusSingleton(): - display_media = self._focused_media.GetDisplayMedia() - - if display_media is None: - - return - + media = self._GetFocusSingleton() new_options = HG.client_controller.new_options - ( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( display_media.GetMime() ) + ( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( media.GetMime() ) if media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY: - hash = display_media.GetHash() - mime = display_media.GetMime() + hash = media.GetHash() + mime = media.GetMime() client_files_manager = HG.client_controller.client_files_manager @@ -535,6 +528,21 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): + def _GetFocusSingleton( self ) -> ClientMedia.MediaSingleton: + + if self._focused_media is not None: + + media_singleton = self._focused_media.GetDisplayMedia() + + if media_singleton is not None: + + return media_singleton + + + + raise HydrusExceptions.DataMissing( 'No media singleton!' ) + + def _GetNumSelected( self ): return sum( [ media.GetNumFiles() for media in self._selected_media ] ) @@ -784,6 +792,20 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): return ( sorted_mime_descriptor, selected_mime_descriptor ) + def _HasFocusSingleton( self ) -> bool: + + try: + + media = self._GetFocusSingleton() + + return True + + except HydrusExceptions.DataMissing: + + return False + + + def _HitMedia( self, media, ctrl, shift ): if media is None: @@ -878,22 +900,15 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _ManageNotes( self ): - if self._focused_media is None: + if self._HasFocusSingleton(): - return + media = self._GetFocusSingleton() - - media = self._focused_media.GetDisplayMedia() - - if media is None: + ClientGUIMediaActions.EditFileNotes( self, media ) - return + self.setFocus( QC.Qt.OtherFocusReason ) - ClientGUIMediaActions.EditFileNotes( self, media ) - - self.setFocus( QC.Qt.OtherFocusReason ) - def _ManageRatings( self ): @@ -983,21 +998,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _OpenExternally( self ): - if self._focused_media is not None: + if self._HasFocusSingleton(): - open_externally_media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if open_externally_media is None: - - return - - - if open_externally_media.GetLocationsManager().IsLocal(): + if media.GetLocationsManager().IsLocal(): self.SetFocusedMedia( None ) - hash = open_externally_media.GetHash() - mime = open_externally_media.GetMime() + hash = media.GetHash() + mime = media.GetMime() client_files_manager = HG.client_controller.client_files_manager @@ -1014,12 +1024,14 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _OpenFileInWebBrowser( self ): - if self._focused_media is not None: + if self._HasFocusSingleton(): - if self._focused_media.GetLocationsManager().IsLocal(): + focused_singleton = self._GetFocusSingleton() + + if focused_singleton.GetLocationsManager().IsLocal(): - hash = self._focused_media.GetHash() - mime = self._focused_media.GetMime() + hash = focused_singleton.GetHash() + mime = focused_singleton.GetMime() client_files_manager = HG.client_controller.client_files_manager @@ -1034,12 +1046,14 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _OpenFileLocation( self ): - if self._focused_media is not None: + if self._HasFocusSingleton(): - if self._focused_media.GetLocationsManager().IsLocal(): + focused_singleton = self._GetFocusSingleton() + + if focused_singleton.GetLocationsManager().IsLocal(): - hash = self._focused_media.GetHash() - mime = self._focused_media.GetMime() + hash = focused_singleton.GetHash() + mime = focused_singleton.GetMime() client_files_manager = HG.client_controller.client_files_manager @@ -1054,9 +1068,11 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _OpenKnownURL( self ): - if self._focused_media is not None: + if self._HasFocusSingleton(): - ClientGUIMedia.DoOpenKnownURLFromShortcut( self, self._focused_media ) + focused_singleton = self._GetFocusSingleton() + + ClientGUIMedia.DoOpenKnownURLFromShortcut( self, focused_singleton ) @@ -1533,39 +1549,47 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): def _SetDuplicatesFocusedBetter( self, duplicate_action_options = None ): - flat_media = self._GetSelectedFlatMedia() - - if self._focused_media is None or self._focused_media.GetDisplayMedia() is None: + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + focused_hash = focused_singleton.GetHash() + + flat_media = self._GetSelectedFlatMedia() + + ( better_media, ) = [ media for media in flat_media if media.GetHash() == focused_hash ] + + worse_flat_media = [ media for media in flat_media if media.GetHash() != focused_hash ] + + media_pairs = [ ( better_media, worse_media ) for worse_media in worse_flat_media ] + + self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs ) + + else: QW.QMessageBox.warning( self, 'Warning', 'No file is focused, so cannot set the focused file as better!' ) return - focused_hash = self._focused_media.GetDisplayMedia().GetHash() - - ( better_media, ) = [ media for media in flat_media if media.GetHash() == focused_hash ] - - worse_flat_media = [ media for media in flat_media if media.GetHash() != focused_hash ] - - media_pairs = [ ( better_media, worse_media ) for worse_media in worse_flat_media ] - - self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs ) - def _SetDuplicatesFocusedKing( self ): - if self._focused_media is None or self._focused_media.GetDisplayMedia() is None: + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + focused_hash = media.GetHash() + + HG.client_controller.WriteSynchronous( 'duplicate_set_king', focused_hash ) + + else: QW.QMessageBox.warning( self, 'Warning', 'No file is focused, so cannot set the focused file as king!' ) return - focused_hash = self._focused_media.GetDisplayMedia().GetHash() - - HG.client_controller.WriteSynchronous( 'duplicate_set_king', focused_hash ) - def _SetDuplicatesPotential( self ): @@ -1842,16 +1866,13 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): elif action == 'duplicate_media_clear_focused_false_positives': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.ClearFalsePositives( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.ClearFalsePositives( self, ( hash, ) ) elif action == 'duplicate_media_clear_false_positives': @@ -1865,16 +1886,13 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): elif action == 'duplicate_media_dissolve_focused_alternate_group': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.DissolveAlternateGroup( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.DissolveAlternateGroup( self, ( hash, ) ) elif action == 'duplicate_media_dissolve_alternate_group': @@ -1888,16 +1906,13 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): elif action == 'duplicate_media_dissolve_focused_duplicate_group': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.DissolveDuplicateGroup( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.DissolveDuplicateGroup( self, ( hash, ) ) elif action == 'duplicate_media_dissolve_duplicate_group': @@ -1911,44 +1926,35 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): elif action == 'duplicate_media_remove_focused_from_alternate_group': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.RemoveFromAlternateGroup( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.RemoveFromAlternateGroup( self, ( hash, ) ) elif action == 'duplicate_media_remove_focused_from_duplicate_group': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.RemoveFromDuplicateGroup( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.RemoveFromDuplicateGroup( self, ( hash, ) ) elif action == 'duplicate_media_reset_focused_potential_search': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.ResetPotentialSearch( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.ResetPotentialSearch( self, ( hash, ) ) elif action == 'duplicate_media_reset_potential_search': @@ -1962,16 +1968,13 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ): elif action == 'duplicate_media_remove_focused_potentials': - if self._focused_media is not None: + if self._HasFocusSingleton(): - media = self._focused_media.GetDisplayMedia() + media = self._GetFocusSingleton() - if media is not None: - - hash = media.GetHash() - - ClientGUIDuplicates.RemovePotentials( self, ( hash, ) ) - + hash = media.GetHash() + + ClientGUIDuplicates.RemovePotentials( self, ( hash, ) ) elif action == 'duplicate_media_remove_potentials': @@ -2392,6 +2395,11 @@ class MediaPanelThumbnails( MediaPanel ): display_media = thumbnail.GetDisplayMedia() + if display_media is None: + + continue + + hash = display_media.GetHash() if hash in self._hashes_faded and thumbnail_cache.HasThumbnailCached( thumbnail ): @@ -2438,6 +2446,13 @@ class MediaPanelThumbnails( MediaPanel ): for thumbnail in thumbnails: + display_media = thumbnail.GetDisplayMedia() + + if display_media is None: + + continue + + try: thumbnail_index = self._sorted_media.index( thumbnail ) @@ -2454,22 +2469,17 @@ class MediaPanelThumbnails( MediaPanel ): continue - display_media = thumbnail.GetDisplayMedia() + hash = display_media.GetHash() - if display_media is not None: - - hash = display_media.GetHash() - - self._hashes_faded.add( hash ) - - self._StopFading( hash ) - - bmp = thumbnail.GetQtImage() - - alpha_bmp = QP.AdjustOpacity( bmp, 0.20 ) - - self._thumbnails_being_faded_in[ hash ] = ( bmp, alpha_bmp, thumbnail_index, thumbnail, now_precise, 0 ) - + self._hashes_faded.add( hash ) + + self._StopFading( hash ) + + bmp = thumbnail.GetQtImage() + + alpha_bmp = QP.AdjustOpacity( bmp, 0.20 ) + + self._thumbnails_being_faded_in[ hash ] = ( bmp, alpha_bmp, thumbnail_index, thumbnail, now_precise, 0 ) HG.client_controller.gui.RegisterAnimationUpdateWindow( self ) @@ -3231,7 +3241,9 @@ class MediaPanelThumbnails( MediaPanel ): menu = QW.QMenu( self.window() ) - if self._focused_media is not None and self._focused_media.GetDisplayMedia() is not None: + if self._HasFocusSingleton(): + + focus_singleton = self._GetFocusSingleton() # variables @@ -3437,7 +3449,7 @@ class MediaPanelThumbnails( MediaPanel ): else: - pretty_info_lines = self._focused_media.GetPrettyInfoLines() + pretty_info_lines = focus_singleton.GetPrettyInfoLines() top_line = pretty_info_lines.pop( 0 ) @@ -3565,7 +3577,9 @@ class MediaPanelThumbnails( MediaPanel ): - if self._focused_media is not None and self._focused_media.GetDisplayMedia() is not None: + if self._HasFocusSingleton(): + + focus_singleton = self._GetFocusSingleton() if selection_has_inbox: @@ -3605,7 +3619,7 @@ class MediaPanelThumbnails( MediaPanel ): ClientGUIMenus.AppendMenuItem( manage_menu, 'urls', 'Manage urls for the selected files.', self._ManageURLs ) - num_notes = self._focused_media.GetDisplayMedia().GetNotesManager().GetNumNotes() + num_notes = focus_singleton.GetNotesManager().GetNumNotes() notes_str = 'notes' @@ -3707,7 +3721,7 @@ class MediaPanelThumbnails( MediaPanel ): duplicates_menu = QW.QMenu( manage_menu ) - focused_hash = self._focused_media.GetDisplayMedia().GetHash() + focused_hash = focus_singleton.GetHash() if HG.client_controller.DBCurrentlyDoingJob(): @@ -3722,7 +3736,7 @@ class MediaPanelThumbnails( MediaPanel ): focus_is_in_alternate_group = False focus_has_fps = False focus_has_potentials = False - focus_can_be_searched = self._focused_media.GetDisplayMedia().GetMime() in HC.MIMES_WE_CAN_PHASH + focus_can_be_searched = focus_singleton.GetMime() in HC.MIMES_WE_CAN_PHASH if file_duplicate_info is None: @@ -4004,7 +4018,7 @@ class MediaPanelThumbnails( MediaPanel ): if advanced_mode: - hash_id_str = str( self._focused_media.GetDisplayMedia().GetHashId() ) + hash_id_str = str( focus_singleton.GetHashId() ) ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', HG.client_controller.pub, 'clipboard', 'text', hash_id_str ) diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py index 80420dcb..bcc3ba1b 100644 --- a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py +++ b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py @@ -35,6 +35,7 @@ from hydrus.client.gui import ClientGUIShortcuts from hydrus.client.gui import ClientGUIFileSeedCache from hydrus.client.gui import ClientGUIGallerySeedLog from hydrus.client.gui import ClientGUIMPV +from hydrus.client.gui import ClientGUIStringControls from hydrus.client.gui import ClientGUITags from hydrus.client.gui import ClientGUITime from hydrus.client.gui import ClientGUITopLevelWindowsPanels @@ -1811,9 +1812,7 @@ class EditFileNotesPanel( ClientGUIScrolledPanels.EditPanel ): else: - names = list( names_to_notes.keys() ) - - names.sort() + names = sorted( names_to_notes.keys() ) for name in names: @@ -1979,9 +1978,7 @@ class EditFileNotesPanel( ClientGUIScrolledPanels.EditPanel ): ( names_to_notes, deletee_names ) = self.GetValue() - empty_note_names = [ name for ( name, note ) in names_to_notes.items() if note == '' ] - - empty_note_names.sort() + empty_note_names = sorted( ( name for ( name, note ) in names_to_notes.items() if note == '' ) ) if len( empty_note_names ) > 0: @@ -4346,11 +4343,8 @@ But if 2 is--and is also perhaps accompanied by many 'could not parse' errors--t current_query_texts = self._GetCurrentQueryTexts() - already_existing_query_texts = list( current_query_texts.intersection( query_texts ) ) - new_query_texts = list( set( query_texts ).difference( current_query_texts ) ) - - already_existing_query_texts.sort() - new_query_texts.sort() + already_existing_query_texts = sorted( current_query_texts.intersection( query_texts ) ) + new_query_texts = sorted( set( query_texts ).difference( current_query_texts ) ) if len( already_existing_query_texts ) > 0: @@ -5721,9 +5715,7 @@ class EditTagImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): url_class_keys_to_url_classes = { url_class.GetMatchKey() : url_class for url_class in url_classes } - url_class_names_and_default_tag_import_options = [ ( url_class_keys_to_url_classes[ url_class_key ].GetName(), url_class_keys_to_default_tag_import_options[ url_class_key ] ) for url_class_key in list( url_class_keys_to_default_tag_import_options.keys() ) if url_class_key in url_class_keys_to_url_classes ] - - url_class_names_and_default_tag_import_options.sort() + url_class_names_and_default_tag_import_options = sorted( ( ( url_class_keys_to_url_classes[ url_class_key ].GetName(), url_class_keys_to_default_tag_import_options[ url_class_key ] ) for url_class_key in list( url_class_keys_to_default_tag_import_options.keys() ) if url_class_key in url_class_keys_to_url_classes ) ) choice_tuples.extend( url_class_names_and_default_tag_import_options ) @@ -6148,9 +6140,9 @@ class EditServiceTagImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): return service_tag_import_options - def SetValue( self, service_tag_import_options ): + def SetValue( self, service_tag_import_options: ClientImportOptions.ServiceTagImportOptions ): - ( get_tags, get_tags_filter, self._additional_tags, self._to_new_files, self._to_already_in_inbox, self._to_already_in_archive, self._only_add_existing_tags, self._only_add_existing_tags_filter ) = service_tag_import_options.ToTuple() + ( get_tags, get_tags_filter, self._additional_tags, self._to_new_files, self._to_already_in_inbox, self._to_already_in_archive, self._only_add_existing_tags, self._only_add_existing_tags_filter, self._get_tags_overwrite_deleted, self._additional_tags_overwrite_deleted ) = service_tag_import_options.ToTuple() self._get_tags_checkbox.setChecked( get_tags ) @@ -6505,7 +6497,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): self._send_referral_url.setToolTip( tt ) - self._referral_url_converter = ClientGUIControls.StringConverterButton( self, referral_url_converter ) + self._referral_url_converter = ClientGUIStringControls.StringConverterButton( self, 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.' @@ -6514,7 +6506,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): self._referral_url = QW.QLineEdit() self._referral_url.setReadOnly( True ) - self._api_lookup_converter = ClientGUIControls.StringConverterButton( self, api_lookup_converter ) + self._api_lookup_converter = ClientGUIStringControls.StringConverterButton( self, api_lookup_converter ) 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).' @@ -6675,7 +6667,9 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit value' ) as dlg: - panel = ClientGUIControls.EditStringMatchPanel( dlg, string_match ) + from hydrus.client.gui import ClientGUIStringPanels + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) dlg.SetPanel( panel ) @@ -6805,7 +6799,9 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit value' ) as dlg: - panel = ClientGUIControls.EditStringMatchPanel( dlg, original_string_match ) + from hydrus.client.gui import ClientGUIStringPanels + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, original_string_match ) dlg.SetPanel( panel ) @@ -6865,7 +6861,9 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit path component' ) as dlg: - panel = ClientGUIControls.EditStringMatchPanel( dlg, string_match ) + from hydrus.client.gui import ClientGUIStringPanels + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) dlg.SetPanel( panel ) diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py index ac87055a..dd42c99a 100644 --- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py +++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py @@ -38,6 +38,7 @@ from hydrus.client.gui import ClientGUISearch from hydrus.client.gui import ClientGUIScrolledPanels from hydrus.client.gui import ClientGUIScrolledPanelsEdit from hydrus.client.gui import ClientGUIShortcuts +from hydrus.client.gui import ClientGUIStringControls from hydrus.client.gui import ClientGUIStyle from hydrus.client.gui import ClientGUITime from hydrus.client.gui import ClientGUITopLevelWindowsPanels @@ -539,9 +540,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ): hta = HydrusTagArchive.HydrusTagArchive( hta_path ) - archive_namespaces = list( hta.GetNamespaces() ) - - archive_namespaces.sort() + archive_namespaces = sorted( hta.GetNamespaces() ) choice_tuples = [ ( HydrusData.ConvertUglyNamespaceToPrettyString( namespace ), namespace, False ) for namespace in archive_namespaces ] @@ -587,9 +586,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ): hta = HydrusTagArchive.HydrusTagArchive( hta_path ) - archive_namespaces = list( hta.GetNamespaces() ) - - archive_namespaces.sort() + archive_namespaces = sorted( hta.GetNamespaces() ) choice_tuples = [ ( HydrusData.ConvertUglyNamespaceToPrettyString( namespace ), namespace, namespace in existing_namespaces ) for namespace in archive_namespaces ] @@ -1405,7 +1402,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ): help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this path remapping control -->', QG.QColor( 0, 0, 255 ) ) - self._nocopy_abs_path_translations = ClientGUIControls.StringToStringDictControl( self, abs_initial_dict, key_name = 'hydrus path', value_name = 'ipfs path', allow_add_delete = False, edit_keys = False ) + self._nocopy_abs_path_translations = ClientGUIStringControls.StringToStringDictControl( self, abs_initial_dict, key_name = 'hydrus path', value_name = 'ipfs path', allow_add_delete = False, edit_keys = False ) self._multihash_prefix = QW.QLineEdit( self ) @@ -2862,17 +2859,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._notebook_tabs_on_left = QW.QCheckBox( self ) - self._max_page_name_chars = QP.MakeQSpinBox( self, min=1, max=256 ) - - self._page_file_count_display = ClientGUICommon.BetterChoice( self ) - - for display_type in ( CC.PAGE_FILE_COUNT_DISPLAY_ALL, CC.PAGE_FILE_COUNT_DISPLAY_ONLY_IMPORTERS, CC.PAGE_FILE_COUNT_DISPLAY_NONE ): - - self._page_file_count_display.addItem( CC.page_file_count_display_string_lookup[ display_type], display_type ) - - - self._import_page_progress_display = QW.QCheckBox( self ) - self._total_pages_warning = QP.MakeQSpinBox( self, min=5, max=200 ) self._reverse_page_shift_drag_behaviour = QW.QCheckBox( self ) @@ -2882,6 +2868,22 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): # + self._page_names_panel = ClientGUICommon.StaticBox( self, 'page tab names' ) + + self._max_page_name_chars = QP.MakeQSpinBox( self._page_names_panel, min=1, max=256 ) + self._elide_page_tab_names = QW.QCheckBox( self._page_names_panel ) + + self._page_file_count_display = ClientGUICommon.BetterChoice( self._page_names_panel ) + + for display_type in ( CC.PAGE_FILE_COUNT_DISPLAY_ALL, CC.PAGE_FILE_COUNT_DISPLAY_ONLY_IMPORTERS, CC.PAGE_FILE_COUNT_DISPLAY_NONE ): + + self._page_file_count_display.addItem( CC.page_file_count_display_string_lookup[ display_type], display_type ) + + + self._import_page_progress_display = QW.QCheckBox( self._page_names_panel ) + + # + gui_session_names = HG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_GUI_SESSION ) if 'last session' not in gui_session_names: @@ -2919,6 +2921,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._max_page_name_chars.setValue( self._new_options.GetInteger( 'max_page_name_chars' ) ) + self._elide_page_tab_names.setChecked( self._new_options.GetBoolean( 'elide_page_tab_names' ) ) + self._page_file_count_display.SetValue( self._new_options.GetInteger( 'page_file_count_display' ) ) self._import_page_progress_display.setChecked( self._new_options.GetBoolean( 'import_page_progress_display' ) ) @@ -2941,17 +2945,41 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): rows.append( ( 'When switching to a page, focus its input field (if any): ', self._set_search_focus_on_page_change ) ) rows.append( ( 'Switch to main window when opening tag search page from media viewer: ', self._activate_window_on_tag_search_page_activation ) ) rows.append( ( 'Line notebook tabs down the left: ', self._notebook_tabs_on_left ) ) - rows.append( ( 'Max characters to display in a page name: ', self._max_page_name_chars ) ) - rows.append( ( 'Show page file count after its name: ', self._page_file_count_display ) ) - rows.append( ( 'Show import page x/y progress after its name: ', self._import_page_progress_display ) ) rows.append( ( 'Warn at this many total pages: ', self._total_pages_warning ) ) rows.append( ( 'Reverse page tab shift-drag behaviour: ', self._reverse_page_shift_drag_behaviour ) ) gridbox = ClientGUICommon.WrapInGrid( self, rows ) + rows = [] + + rows.append( ( 'Max characters to display in a page name: ', self._max_page_name_chars ) ) + rows.append( ( 'When there are too many tabs to fit, \'...\' elide their names so they fit: ', self._elide_page_tab_names ) ) + rows.append( ( 'Show page file count after its name: ', self._page_file_count_display ) ) + rows.append( ( 'Show import page x/y progress after its name: ', self._import_page_progress_display ) ) + + page_names_gridbox = ClientGUICommon.WrapInGrid( self, 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 += 'Due to an unfortunate Qt issue, the tab bar will scroll so the current tab is right-most visible whenever a page is renamed.' + label += os.linesep + label += 'Therefore, if you set pages to have current file count or import progress in their name (which will update from time to time), do not put import pages in a long row of tabs, as it will reset scroll position on every progress update.' + label += os.linesep + 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.setWordWrap( True ) + + self._page_names_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR ) + + self._page_names_panel.Add( page_names_gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + vbox = QP.VBoxLayout() QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + QP.AddToLayout( vbox, self._page_names_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) + QP.AddToLayout( vbox, QW.QWidget( self ), CC.FLAGS_EXPAND_BOTH_WAYS ) self.setLayout( vbox ) @@ -2974,6 +3002,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._new_options.SetInteger( 'max_page_name_chars', self._max_page_name_chars.value() ) + self._new_options.SetBoolean( 'elide_page_tab_names', self._elide_page_tab_names.isChecked() ) + self._new_options.SetInteger( 'page_file_count_display', self._page_file_count_display.GetValue() ) self._new_options.SetBoolean( 'import_page_progress_display', self._import_page_progress_display.isChecked() ) @@ -4661,9 +4691,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): self._favourite_file_lookup_script = ClientGUICommon.BetterChoice( suggested_tags_file_lookup_script_panel ) - script_names = list( HG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP ) ) - - script_names.sort() + script_names = sorted( HG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP ) ) for name in script_names: @@ -5265,9 +5293,7 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ): def _Copy( self ): - urls = list( self._current_urls_count.keys() ) - - urls.sort() + urls = sorted( self._current_urls_count.keys() ) text = os.linesep.join( urls ) @@ -5294,7 +5320,7 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ): addee_hashes = { m.GetHash() for m in addee_media } - content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( ( url, ), addee_hashes ) ) + content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( ( normalised_url, ), addee_hashes ) ) for m in addee_media: diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py index e4ad77cc..e9c2e50f 100644 --- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py +++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py @@ -3213,9 +3213,7 @@ class ReviewLocalFileImports( ClientGUIScrolledPanels.ReviewPanel ): del self._current_path_data[ path ] - flat_path_data = [ ( index, path, mime, size ) for ( path, ( index, mime, size ) ) in self._current_path_data.items() ] - - flat_path_data.sort() + flat_path_data = sorted( ( ( index, path, mime, size ) for ( path, ( index, mime, size ) ) in self._current_path_data.items() ) ) new_index = 1 diff --git a/hydrus/client/gui/ClientGUISearch.py b/hydrus/client/gui/ClientGUISearch.py index a1f77060..93547a4f 100644 --- a/hydrus/client/gui/ClientGUISearch.py +++ b/hydrus/client/gui/ClientGUISearch.py @@ -24,6 +24,28 @@ from hydrus.client.gui import ClientGUIShortcuts from hydrus.client.gui import ClientGUITime from hydrus.client.gui import QtPorting as QP +FLESH_OUT_SYSTEM_PRED_TYPES = { + ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, + ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, + ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, + ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, + ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, + ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, + ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, + ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, + ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, + ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, + ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, + ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, + ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, + ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, + ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, + ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, + ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, + ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES, + ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS +} + def FleshOutPredicates( widget: QW.QWidget, predicates: typing.Iterable[ ClientSearch.Predicate ] ) -> typing.List[ ClientSearch.Predicate ]: window = widget.window() @@ -36,7 +58,7 @@ def FleshOutPredicates( widget: QW.QWidget, predicates: typing.Iterable[ ClientS ( predicate_type, value, inclusive ) = predicate.GetInfo() - if value is None and predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ]: + if value is None and predicate_type in FLESH_OUT_SYSTEM_PRED_TYPES: from hydrus.client.gui import ClientGUITopLevelWindowsPanels @@ -625,12 +647,13 @@ class InputFileSystemPredicate( ClientGUIScrolledPanels.EditPanel ): editable_pred_panel_classes.append( PanelPredicateSystemNumTags ) - elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES: + elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES: static_pred_buttons.append( StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '>', 0 ) ), ) ) ) static_pred_buttons.append( StaticSystemPredicateButton( self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '=', 0 ) ), ) ) ) editable_pred_panel_classes.append( PanelPredicateSystemNumNotes ) + editable_pred_panel_classes.append( PanelPredicateSystemHasNoteName ) elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS: @@ -1394,6 +1417,47 @@ class PanelPredicateSystemHash( PanelPredicateSystem ): return ( hashes, hash_type ) +class PanelPredicateSystemHasNoteName( PanelPredicateSystem ): + + PREDICATE_TYPE = ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME + + def __init__( self, parent ): + + PanelPredicateSystem.__init__( self, parent ) + + self._operator = ClientGUICommon.BetterChoice( self ) + + self._operator.addItem( 'has note with name ', True ) + self._operator.addItem( 'does not have note with name', False ) + + self._name = QW.QLineEdit( self ) + self._name.setFixedWidth( 250 ) + + hbox = QP.HBoxLayout() + + QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:note name'), CC.FLAGS_VCENTER ) + QP.AddToLayout( hbox, self._operator, CC.FLAGS_VCENTER ) + QP.AddToLayout( hbox, self._name, CC.FLAGS_VCENTER ) + + hbox.addStretch( 1 ) + + self.setLayout( hbox ) + + + def GetInfo( self ): + + name = self._name.text() + + if name == '': + + name = 'notes' + + + info = ( self._operator.GetValue(), name ) + + return info + + class PanelPredicateSystemHeight( PanelPredicateSystem ): PREDICATE_TYPE = ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT diff --git a/hydrus/client/gui/ClientGUIShortcutControls.py b/hydrus/client/gui/ClientGUIShortcutControls.py index 5a65a67f..23a185e6 100644 --- a/hydrus/client/gui/ClientGUIShortcutControls.py +++ b/hydrus/client/gui/ClientGUIShortcutControls.py @@ -88,9 +88,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ): choices = ClientGUIShortcuts.simple_shortcut_name_to_action_lookup[ 'custom' ] - choices = list( choices ) - - choices.sort() + choices = sorted( choices ) self._simple_actions = QW.QComboBox( self ) self._simple_actions.addItems( choices ) diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py index a7d0715d..046e8fbc 100644 --- a/hydrus/client/gui/ClientGUIShortcuts.py +++ b/hydrus/client/gui/ClientGUIShortcuts.py @@ -534,7 +534,7 @@ class Shortcut( HydrusSerialisable.SerialisableBase ): shortcut_key += 32 # convert A to a - modifiers.sort() + modifiers = sorted( modifiers ) HydrusSerialisable.SerialisableBase.__init__( self ) @@ -1173,9 +1173,7 @@ class ShortcutsHandler( QC.QObject ): def GetCustomShortcutNames( self ): - custom_names = [ name for name in self._shortcuts_names if name not in SHORTCUTS_RESERVED_NAMES ] - - custom_names.sort() + custom_names = sorted( ( name for name in self._shortcuts_names if name not in SHORTCUTS_RESERVED_NAMES ) ) return custom_names diff --git a/hydrus/client/gui/ClientGUIStringControls.py b/hydrus/client/gui/ClientGUIStringControls.py new file mode 100644 index 00000000..f5b42032 --- /dev/null +++ b/hydrus/client/gui/ClientGUIStringControls.py @@ -0,0 +1,606 @@ +import typing + +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW + +from hydrus.core import HydrusData +from hydrus.client import ClientConstants as CC +from hydrus.client import ClientParsing +from hydrus.client.gui import ClientGUICommon +from hydrus.client.gui import ClientGUIDialogs +from hydrus.client.gui import ClientGUIListCtrl +from hydrus.client.gui import ClientGUIScrolledPanels +from hydrus.client.gui import ClientGUIStringPanels +from hydrus.client.gui import ClientGUITopLevelWindowsPanels +from hydrus.client.gui import QtPorting as QP + +class StringConverterButton( ClientGUICommon.BetterButton ): + + stringConverterUpdate = QC.Signal() + + def __init__( self, parent, string_converter: ClientParsing.StringConverter ): + + ClientGUICommon.BetterButton.__init__( self, parent, 'edit string converter', self._Edit ) + + self._string_converter = string_converter + + self._example_string_override = None + + self._UpdateLabel() + + + def _Edit( self ): + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string converter', frame_key = 'deeply_nested_dialog' ) as dlg: + + panel = ClientGUIStringPanels.EditStringConverterPanel( dlg, self._string_converter, example_string_override = self._example_string_override ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + self._string_converter = panel.GetValue() + + self._UpdateLabel() + + + self.stringConverterUpdate.emit() + + + def _UpdateLabel( self ): + + label = self._string_converter.ToString() + + self.setText( label ) + + + def GetValue( self ): + + return self._string_converter + + + def SetExampleString( self, example_string ): + + self._example_string_override = example_string + + + def SetValue( self, string_converter ): + + self._string_converter = string_converter + + self._UpdateLabel() + + +class StringMatchButton( ClientGUICommon.BetterButton ): + + def __init__( self, parent, string_match: ClientParsing.StringMatch ): + + ClientGUICommon.BetterButton.__init__( self, parent, 'edit string match', self._Edit ) + + self._string_match = string_match + + self._UpdateLabel() + + + def _Edit( self ): + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string match', frame_key = 'deeply_nested_dialog' ) as dlg: + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, self._string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + self._string_match = panel.GetValue() + + self._UpdateLabel() + + + + + def _UpdateLabel( self ): + + label = self._string_match.ToString() + + self.setText( label ) + + + def GetValue( self ): + + return self._string_match + + + def SetValue( self, string_match ): + + self._string_match = string_match + + self._UpdateLabel() + + +class StringMatchToStringMatchDictControl( QW.QWidget ): + + def __init__( self, parent, initial_dict: typing.Dict[ ClientParsing.StringMatch, ClientParsing.StringMatch ], min_height = 10, key_name = 'key' ): + + QW.QWidget.__init__( self, parent ) + + self._key_name = key_name + + listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) + + columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ] + + self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, 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( list(initial_dict.items()) ) + + self._listctrl.Sort( 0 ) + + # + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.setLayout( vbox ) + + + def _ConvertDataToListCtrlTuples( self, data ): + + ( key_string_match, value_string_match ) = data + + pretty_key = key_string_match.ToString() + pretty_value = value_string_match.ToString() + + display_tuple = ( pretty_key, pretty_value ) + sort_tuple = ( pretty_key, pretty_value ) + + return ( display_tuple, sort_tuple ) + + + def _Add( self ): + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit ' + self._key_name ) as dlg: + + string_match = ClientParsing.StringMatch() + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + key_string_match = panel.GetValue() + + else: + + return + + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: + + string_match = ClientParsing.StringMatch() + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + value_string_match = panel.GetValue() + + data = ( key_string_match, value_string_match ) + + self._listctrl.AddDatas( ( data, ) ) + + + + + def _Edit( self ): + + for data in self._listctrl.GetData( only_selected = True ): + + ( key_string_match, value_string_match ) = data + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit ' + self._key_name ) as dlg: + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, key_string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + key_string_match = panel.GetValue() + + else: + + break + + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, value_string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + value_string_match = panel.GetValue() + + else: + + break + + + + self._listctrl.DeleteDatas( ( data, ) ) + + edited_data = ( key_string_match, value_string_match ) + + self._listctrl.AddDatas( ( edited_data, ) ) + + + self._listctrl.Sort() + + + def GetValue( self ) -> typing.Dict[ str, ClientParsing.StringMatch ]: + + value_dict = dict( self._listctrl.GetData() ) + + return value_dict + + +class StringToStringDictButton( ClientGUICommon.BetterButton ): + + def __init__( self, parent, label ): + + ClientGUICommon.BetterButton.__init__( self, parent, label, self._Edit ) + + self._value: typing.Dict[ str, str ] = {} + + + def _Edit( self ): + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit string dictionary' ) as dlg: + + panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg ) + + control = StringToStringDictControl( panel, self._value ) + + panel.SetControl( control ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + self._value = control.GetValue() + + + + + def GetValue( self ) -> typing.Dict[ str, str ]: + + return self._value + + + def SetValue( self, value ): + + self._value = value + + +class StringToStringDictControl( QW.QWidget ): + + listCtrlChanged = QC.Signal() + + def __init__( self, parent, initial_dict: typing.Dict[ str, str ], min_height = 10, key_name = 'key', value_name = 'value', allow_add_delete = True, edit_keys = True ): + + QW.QWidget.__init__( self, parent ) + + self._key_name = key_name + self._value_name = value_name + + self._edit_keys = edit_keys + + listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) + + columns = [ ( self._key_name, 20 ), ( self._value_name, -1 ) ] + + use_simple_delete = allow_add_delete + + self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_value', min_height, 36, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = use_simple_delete, activation_callback = self._Edit ) + self._listctrl.listCtrlChanged.connect( self.listCtrlChanged ) + + listctrl_panel.SetListCtrl( self._listctrl ) + + if allow_add_delete: + + listctrl_panel.AddButton( 'add', self._Add ) + + + listctrl_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True ) + + if allow_add_delete: + + listctrl_panel.AddDeleteButton() + + + # + + self._listctrl.AddDatas( list(initial_dict.items()) ) + + self._listctrl.Sort( 0 ) + + # + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.setLayout( vbox ) + + + def _ConvertDataToListCtrlTuples( self, data ): + + ( key, value ) = data + + display_tuple = ( key, value ) + sort_tuple = ( key, value ) + + return ( display_tuple, sort_tuple ) + + + def _Add( self ): + + with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + key = dlg.GetValue() + + if key in self._GetExistingKeys(): + + QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) + + return + + + with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._value_name, allow_blank = True ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + value = dlg.GetValue() + + data = ( key, value ) + + self._listctrl.AddDatas( ( data, ) ) + + + + + + + def _Edit( self ): + + for data in self._listctrl.GetData( only_selected = True ): + + ( key, value ) = data + + if self._edit_keys: + + with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + edited_key = dlg.GetValue() + + if edited_key != key and edited_key in self._GetExistingKeys(): + + QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) + + break + + + else: + + break + + + + else: + + edited_key = key + + + with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._value_name, default = value, allow_blank = True ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + edited_value = dlg.GetValue() + + else: + + break + + + + self._listctrl.DeleteDatas( ( data, ) ) + + edited_data = ( edited_key, edited_value ) + + self._listctrl.AddDatas( ( edited_data, ) ) + + + self._listctrl.Sort() + + + def _GetExistingKeys( self ): + + return { key for ( key, value ) in self._listctrl.GetData() } + + + def GetValue( self ) -> typing.Dict[ str, str ]: + + value_dict = dict( self._listctrl.GetData() ) + + return value_dict + + +class StringToStringMatchDictControl( QW.QWidget ): + + def __init__( self, parent, initial_dict: typing.Dict[ str, ClientParsing.StringMatch ], min_height = 10, key_name = 'key' ): + + QW.QWidget.__init__( self, parent ) + + self._key_name = key_name + + listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) + + columns = [ ( self._key_name, 20 ), ( 'matching', -1 ) ] + + self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, 'key_to_string_match', min_height, 36, columns, self._ConvertDataToListCtrlTuples, 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( initial_dict.items() ) + + self._listctrl.Sort( 0 ) + + # + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.setLayout( vbox ) + + + def _ConvertDataToListCtrlTuples( self, data ): + + ( key, string_match ) = data + + pretty_string_match = string_match.ToString() + + display_tuple = ( key, pretty_string_match ) + sort_tuple = ( key, pretty_string_match ) + + return ( display_tuple, sort_tuple ) + + + def _Add( self ): + + with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + key = dlg.GetValue() + + if key in self._GetExistingKeys(): + + QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) + + return + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: + + string_match = ClientParsing.StringMatch() + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + string_match = panel.GetValue() + + data = ( key, string_match ) + + self._listctrl.AddDatas( ( data, ) ) + + + + + + + def _Edit( self ): + + for data in self._listctrl.GetData( only_selected = True ): + + ( key, string_match ) = data + + with ClientGUIDialogs.DialogTextEntry( self, 'edit the ' + self._key_name, default = key, allow_blank = False ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + edited_key = dlg.GetValue() + + if edited_key != key and edited_key in self._GetExistingKeys(): + + QW.QMessageBox.warning( self, 'Warning', 'That {} already exists!'.format( self._key_name ) ) + + break + + + else: + + break + + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit match' ) as dlg: + + string_match = ClientParsing.StringMatch() + + panel = ClientGUIStringPanels.EditStringMatchPanel( dlg, string_match ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + edited_string_match = panel.GetValue() + + else: + + break + + + + self._listctrl.DeleteDatas( ( data, ) ) + + edited_data = ( edited_key, edited_string_match ) + + self._listctrl.AddDatas( ( edited_data, ) ) + + + self._listctrl.Sort() + + + def _GetExistingKeys( self ): + + return { key for ( key, value ) in self._listctrl.GetData() } + + + def GetValue( self ) -> typing.Dict[ str, ClientParsing.StringMatch ]: + + value_dict = dict( self._listctrl.GetData() ) + + return value_dict + + + diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py new file mode 100644 index 00000000..88056b6e --- /dev/null +++ b/hydrus/client/gui/ClientGUIStringPanels.py @@ -0,0 +1,1283 @@ +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.client import ClientConstants as CC +from hydrus.client import ClientParsing +from hydrus.client.gui import ClientGUICommon +from hydrus.client.gui import ClientGUIDialogsQuick +from hydrus.client.gui import ClientGUIFunctions +from hydrus.client.gui import ClientGUIListBoxes +from hydrus.client.gui import ClientGUIListCtrl +from hydrus.client.gui import ClientGUIScrolledPanels +from hydrus.client.gui import ClientGUITopLevelWindowsPanels +from hydrus.client.gui import QtPorting as QP + +class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ): + + def __init__( self, parent, string_converter, example_string_override = None ): + + ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + + transformations_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) + + columns = [ ( '#', 3 ), ( 'transformation', 30 ), ( 'result', -1 ) ] + + self._transformations = ClientGUIListCtrl.BetterListCtrl( transformations_panel, 'string_converter_transformations', 7, 35, columns, self._ConvertTransformationToListCtrlTuples, delete_key_callback = self._DeleteTransformation, activation_callback = self._EditTransformation ) + + transformations_panel.SetListCtrl( self._transformations ) + + transformations_panel.AddButton( 'add', self._AddTransformation ) + transformations_panel.AddButton( 'edit', self._EditTransformation, enabled_only_on_selection = True ) + transformations_panel.AddDeleteButton() + + transformations_panel.AddSeparator() + + transformations_panel.AddButton( 'move up', self._MoveUp, enabled_check_func = self._CanMoveUp ) + transformations_panel.AddButton( 'move down', self._MoveDown, enabled_check_func = self._CanMoveDown ) + + self._example_string = QW.QLineEdit( self ) + + # + + self._transformations.AddDatas( [ ( i + 1, transformation_type, data ) for ( i, ( transformation_type, data ) ) in enumerate( string_converter.transformations ) ] ) + + if example_string_override is None: + + self._example_string.setText( string_converter.example_string ) + + else: + + self._example_string.setText( example_string_override ) + + + self._transformations.UpdateDatas() # to refresh, now they are all in the list + + self._transformations.Sort( 0 ) + + # + + rows = [] + + rows.append( ( 'example string: ', self._example_string ) ) + + gridbox = ClientGUICommon.WrapInGrid( self, rows ) + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, transformations_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + + self.widget().setLayout( vbox ) + + # + + self._example_string.textChanged.connect( self.EventUpdate ) + + + def _AddTransformation( self ): + + transformation_type = ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT + data = 'extra text' + + try: + + string_converter = self._GetValue() + + example_string_at_this_point = string_converter.Convert( self._example_string.text() ) + + except: + + example_string_at_this_point = self._example_string.text() + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg: + + panel = self._TransformationPanel( dlg, transformation_type, data, example_string_at_this_point ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + number = self._transformations.topLevelItemCount() + 1 + + ( transformation_type, data ) = panel.GetValue() + + enumerated_transformation = ( number, transformation_type, data ) + + self._transformations.AddDatas( ( enumerated_transformation, ) ) + + + + self._transformations.UpdateDatas() # need to refresh string after the insertion, so the new row can be included in the parsing calcs + + self._transformations.Sort() + + + def _CanMoveDown( self ): + + selected_data = self._transformations.GetData( only_selected = True ) + + if len( selected_data ) == 1: + + ( number, transformation_type, data ) = selected_data[0] + + if number < self._transformations.topLevelItemCount(): + + return True + + + + return False + + + def _CanMoveUp( self ): + + selected_data = self._transformations.GetData( only_selected = True ) + + if len( selected_data ) == 1: + + ( number, transformation_type, data ) = selected_data[0] + + if number > 1: + + return True + + + + return False + + + def _ConvertTransformationToListCtrlTuples( self, transformation ): + + ( number, transformation_type, data ) = transformation + + pretty_number = HydrusData.ToHumanInt( number ) + pretty_transformation = ClientParsing.StringConverter.TransformationToString( ( transformation_type, data ) ) + + string_converter = self._GetValue() + + try: + + pretty_result = ClientParsing.MakeParsedTextPretty( string_converter.Convert( self._example_string.text(), number ) ) + + except HydrusExceptions.StringConvertException as e: + + pretty_result = str( e ) + + + display_tuple = ( pretty_number, pretty_transformation, pretty_result ) + sort_tuple = ( number, number, number ) + + return ( display_tuple, sort_tuple ) + + + def _DeleteTransformation( self ): + + if len( self._transformations.GetData( only_selected = True ) ) > 0: + + text = 'Delete all selected?' + + from hydrus.client.gui import ClientGUIDialogsQuick + + result = ClientGUIDialogsQuick.GetYesNo( self, text ) + + if result == QW.QDialog.Accepted: + + self._transformations.DeleteSelected() + + + + # now we need to shuffle up any missing numbers + + num_rows = self._transformations.topLevelItemCount() + + i = 1 + search_i = i + + while i <= num_rows: + + try: + + transformation = self._GetTransformation( search_i ) + + if search_i != i: + + self._transformations.DeleteDatas( ( transformation, ) ) + + ( search_i, transformation_type, data ) = transformation + + transformation = ( i, transformation_type, data ) + + self._transformations.AddDatas( ( transformation, ) ) + + + i += 1 + search_i = i + + except HydrusExceptions.DataMissing: + + search_i += 1 + + + + self._transformations.UpdateDatas() + + self._transformations.Sort() + + + def _EditTransformation( self ): + + selected_data = self._transformations.GetData( only_selected = True ) + + for enumerated_transformation in selected_data: + + ( number, transformation_type, data ) = enumerated_transformation + + try: + + string_converter = self._GetValue() + + example_string_at_this_point = string_converter.Convert( self._example_string.text(), number - 1 ) + + except: + + example_string_at_this_point = self._example_string.text() + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit transformation', frame_key = 'deeply_nested_dialog' ) as dlg: + + panel = self._TransformationPanel( dlg, transformation_type, data, example_string_at_this_point ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + self._transformations.DeleteDatas( ( enumerated_transformation, ) ) + + ( transformation_type, data ) = panel.GetValue() + + enumerated_transformation = ( number, transformation_type, data ) + + self._transformations.AddDatas( ( enumerated_transformation, ) ) + + else: + + break + + + + + self._transformations.UpdateDatas() + + self._transformations.Sort() + + + def _GetTransformation( self, desired_number ): + + for transformation in self._transformations.GetData(): + + ( number, transformation_type, data ) = transformation + + if number == desired_number: + + return transformation + + + + raise HydrusExceptions.DataMissing() + + + def _GetValue( self ): + + enumerated_transformations = sorted( self._transformations.GetData() ) + + transformations = [ ( transformation_type, data ) for ( number, transformation_type, data ) in enumerated_transformations ] + + example_string = self._example_string.text() + + string_converter = ClientParsing.StringConverter( transformations, example_string ) + + return string_converter + + + def _MoveDown( self ): + + selected_transformation = self._transformations.GetData( only_selected = True )[0] + + ( number, transformation_type, data ) = selected_transformation + + swap_transformation = self._GetTransformation( number + 1 ) + + self._SwapTransformations( selected_transformation, swap_transformation ) + + self._transformations.UpdateDatas() + + self._transformations.Sort() + + + def _MoveUp( self ): + + selected_transformation = self._transformations.GetData( only_selected = True )[0] + + ( number, transformation_type, data ) = selected_transformation + + swap_transformation = self._GetTransformation( number - 1 ) + + self._SwapTransformations( selected_transformation, swap_transformation ) + + self._transformations.UpdateDatas() + + self._transformations.Sort() + + + def _SwapTransformations( self, one, two ): + + selected_data = self._transformations.GetData( only_selected = True ) + + one_selected = one in selected_data + two_selected = two in selected_data + + self._transformations.DeleteDatas( ( one, two ) ) + + ( number_1, transformation_type_1, data_1 ) = one + ( number_2, transformation_type_2, data_2 ) = two + + one = ( number_2, transformation_type_1, data_1 ) + two = ( number_1, transformation_type_2, data_2 ) + + self._transformations.AddDatas( ( one, two ) ) + + if one_selected: + + self._transformations.SelectDatas( ( one, ) ) + + + if two_selected: + + self._transformations.SelectDatas( ( two, ) ) + + + + def EventUpdate( self, text ): + + self._transformations.UpdateDatas() + + + def GetValue( self ): + + string_converter = self._GetValue() + + try: + + string_converter.Convert( self._example_string.text() ) + + except HydrusExceptions.StringConvertException: + + raise HydrusExceptions.VetoException( 'Please enter an example text that can be converted!' ) + + + return string_converter + + + class _TransformationPanel( ClientGUIScrolledPanels.EditPanel ): + + def __init__( self, parent, transformation_type, data, example_text ): + + ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + + self._transformation_type = ClientGUICommon.BetterChoice( self ) + + for t_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_REVERSE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): + + self._transformation_type.addItem( ClientParsing.transformation_type_str_lookup[ t_type ], t_type ) + + + self._example_string = QW.QLineEdit( self ) + + min_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._example_string, 96 ) + + self._example_string.setMinimumWidth( min_width ) + + self._example_text = example_text + + if isinstance( self._example_text, bytes ): + + self._example_string.setText( repr( self._example_text ) ) + + else: + + self._example_string.setText( self._example_text ) + + + self._example_transformation = QW.QLineEdit( self ) + + # + + self._example_string.setReadOnly( True ) + self._example_transformation.setReadOnly( True ) + + self._data_text = QW.QLineEdit( self ) + self._data_number = QP.MakeQSpinBox( self, min=0, max=65535 ) + self._data_encoding = ClientGUICommon.BetterChoice( self ) + self._data_regex_repl = QW.QLineEdit( self ) + self._data_date_link = ClientGUICommon.BetterHyperLink( self, 'link to date info', 'https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior' ) + self._data_timezone_decode = ClientGUICommon.BetterChoice( self ) + self._data_timezone_encode = ClientGUICommon.BetterChoice( self ) + self._data_timezone_offset = QP.MakeQSpinBox( self, min=-86400, max=86400 ) + + for e in ( 'hex', 'base64', 'url percent encoding' ): + + self._data_encoding.addItem( e, e ) + + + self._data_timezone_decode.addItem( 'UTC', HC.TIMEZONE_GMT ) + self._data_timezone_decode.addItem( 'Local', HC.TIMEZONE_LOCAL ) + self._data_timezone_decode.addItem( 'Offset', HC.TIMEZONE_OFFSET ) + + self._data_timezone_encode.addItem( 'UTC', HC.TIMEZONE_GMT ) + self._data_timezone_encode.addItem( 'Local', HC.TIMEZONE_LOCAL ) + + # + + self._transformation_type.SetValue( transformation_type ) + + self._data_number.setValue( 1 ) + + # + + if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_ENCODE ): + + self._data_encoding.SetValue( data ) + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: + + ( pattern, repl ) = data + + self._data_text.setText( pattern ) + self._data_regex_repl.setText( repl ) + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: + + ( phrase, timezone_type, timezone_offset ) = data + + self._data_text.setText( phrase ) + self._data_timezone_decode.SetValue( timezone_type ) + self._data_timezone_offset.setValue( timezone_offset ) + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: + + ( phrase, timezone_type ) = data + + self._data_text.setText( phrase ) + self._data_timezone_encode.SetValue( timezone_type ) + + elif data is not None: + + if isinstance( data, int ): + + self._data_number.setValue( data ) + + else: + + self._data_text.setText( data ) + + + + # + + rows = [] + + # This mess needs to be all replaced with a nice QFormLayout subclass that can do row hide/show + # or just a whole separate panel for each transformation type, but w/e + + self._data_text_label = ClientGUICommon.BetterStaticText( self, 'string data: ' ) + self._data_number_label = ClientGUICommon.BetterStaticText( self, 'number data: ' ) + self._data_encoding_label = ClientGUICommon.BetterStaticText( self, 'encoding data: ' ) + self._data_regex_repl_label = ClientGUICommon.BetterStaticText( self, 'regex replacement: ' ) + self._data_date_link_label = ClientGUICommon.BetterStaticText( self, 'date info: ' ) + self._data_timezone_decode_label = ClientGUICommon.BetterStaticText( self, 'date decode timezone: ' ) + self._data_timezone_offset_label = ClientGUICommon.BetterStaticText( self, 'timezone offset: ' ) + self._data_timezone_encode_label = ClientGUICommon.BetterStaticText( self, 'date encode timezone: ' ) + + rows.append( ( 'example string: ', self._example_string ) ) + rows.append( ( 'transformed string: ', self._example_transformation ) ) + rows.append( ( 'transformation type: ', self._transformation_type ) ) + rows.append( ( self._data_text_label, self._data_text ) ) + rows.append( ( self._data_number_label, self._data_number ) ) + rows.append( ( self._data_encoding_label, self._data_encoding ) ) + rows.append( ( self._data_regex_repl_label, self._data_regex_repl ) ) + rows.append( ( self._data_date_link_label, self._data_date_link ) ) + rows.append( ( self._data_timezone_decode_label, self._data_timezone_decode ) ) + rows.append( ( self._data_timezone_offset_label, self._data_timezone_offset ) ) + rows.append( ( self._data_timezone_encode_label, self._data_timezone_encode ) ) + + self._control_gridbox = ClientGUICommon.WrapInGrid( self, rows ) + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, self._control_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + QP.AddToLayout( vbox, QW.QWidget( self ), CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.widget().setLayout( vbox ) + + self._UpdateDataControls() + + # + + self._transformation_type.currentIndexChanged.connect( self._UpdateDataControls ) + self._transformation_type.currentIndexChanged.connect( self._UpdateExampleText ) + + self._data_text.textEdited.connect( self._UpdateExampleText ) + self._data_number.valueChanged.connect( self._UpdateExampleText ) + self._data_encoding.currentIndexChanged.connect( self._UpdateExampleText ) + self._data_regex_repl.textEdited.connect( self._UpdateExampleText ) + self._data_timezone_decode.currentIndexChanged.connect( self._UpdateExampleText ) + self._data_timezone_offset.valueChanged.connect( self._UpdateExampleText ) + self._data_timezone_encode.currentIndexChanged.connect( self._UpdateExampleText ) + + self._data_timezone_decode.currentIndexChanged.connect( self._UpdateDataControls ) + self._data_timezone_encode.currentIndexChanged.connect( self._UpdateDataControls ) + + self._UpdateExampleText() + + + def _UpdateDataControls( self ): + + self._data_text_label.setVisible( False ) + self._data_number_label.setVisible( False ) + self._data_encoding_label.setVisible( False ) + self._data_regex_repl_label.setVisible( False ) + self._data_date_link_label.setVisible( False ) + self._data_timezone_decode_label.setVisible( False ) + self._data_timezone_offset_label.setVisible( False ) + self._data_timezone_encode_label.setVisible( False ) + + self._data_text.setVisible( False ) + self._data_number.setVisible( False ) + self._data_encoding.setVisible( False ) + self._data_regex_repl.setVisible( False ) + self._data_date_link.setVisible( False ) + self._data_timezone_decode.setVisible( False ) + self._data_timezone_offset.setVisible( False ) + self._data_timezone_encode.setVisible( False ) + + transformation_type = self._transformation_type.GetValue() + + if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE ): + + self._data_encoding_label.setVisible( True ) + self._data_encoding.setVisible( True ) + + elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB ): + + self._data_text_label.setVisible( True ) + self._data_text.setVisible( True ) + + data_text_label = 'string data: ' + + if transformation_type == ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT: + + data_text_label = 'text to prepend: ' + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT: + + data_text_label = 'text to append: ' + + elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE ): + + self._data_date_link_label.setVisible( True ) + self._data_date_link.setVisible( True ) + + if transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: + + data_text_label = 'date decode phrase: ' + + self._data_timezone_decode_label.setVisible( True ) + self._data_timezone_decode.setVisible( True ) + + if self._data_timezone_decode.GetValue() == HC.TIMEZONE_OFFSET: + + self._data_timezone_offset_label.setVisible( True ) + self._data_timezone_offset.setVisible( True ) + + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: + + data_text_label = 'date encode phrase: ' + + self._data_timezone_encode_label.setVisible( True ) + self._data_timezone_encode.setVisible( True ) + + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: + + data_text_label = 'regex pattern: ' + + self._data_regex_repl_label.setVisible( True ) + self._data_regex_repl.setVisible( True ) + + + self._data_text_label.setText( data_text_label ) + + elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): + + self._data_number_label.setVisible( True ) + self._data_number.setVisible( True ) + + if transformation_type == ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION: + + self._data_number.setMinimum( -65535 ) + + else: + + self._data_number.setMinimum( 0 ) + + + data_number_label = 'number data: ' + + if transformation_type == ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING: + + data_number_label = 'characters to remove: ' + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END: + + data_number_label = 'characters to remove: ' + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING: + + data_number_label = 'characters to take: ' + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END: + + data_number_label = 'characters to take: ' + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION: + + data_number_label = 'number to add: ' + + + self._data_number_label.setText( data_number_label ) + + + + def _UpdateExampleText( self ): + + try: + + transformations = [ self.GetValue() ] + + string_converter = ClientParsing.StringConverter( transformations, self._example_text ) + + example_transformation = string_converter.Convert( self._example_text ) + + try: + + self._example_transformation.setText( str( example_transformation ) ) + + except: + + self._example_transformation.setText( repr( example_transformation ) ) + + + except Exception as e: + + self._example_transformation.setText( str( e ) ) + + + + def GetValue( self ): + + transformation_type = self._transformation_type.GetValue() + + if transformation_type in ( ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE ): + + data = self._data_encoding.GetValue() + + elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT ): + + data = self._data_text.text() + + elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ): + + data = self._data_number.value() + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_REGEX_SUB: + + pattern = self._data_text.text() + repl = self._data_regex_repl.text() + + data = ( pattern, repl ) + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE: + + phrase = self._data_text.text() + timezone_time = self._data_timezone_decode.GetValue() + timezone_offset = self._data_timezone_offset.value() + + data = ( phrase, timezone_time, timezone_offset ) + + elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE: + + phrase = self._data_text.text() + timezone_time = self._data_timezone_encode.GetValue() + + data = ( phrase, timezone_time ) + + else: + + data = None + + + return ( transformation_type, data ) + + + +class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ): + + def __init__( self, parent, string_match = None ): + + ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + + if string_match is None: + + string_match = ClientParsing.StringMatch() + + + self._match_type = ClientGUICommon.BetterChoice( self ) + + self._match_type.addItem( 'any characters', ClientParsing.STRING_MATCH_ANY ) + self._match_type.addItem( 'fixed characters', ClientParsing.STRING_MATCH_FIXED ) + self._match_type.addItem( 'character set', ClientParsing.STRING_MATCH_FLEXIBLE ) + self._match_type.addItem( 'regex', ClientParsing.STRING_MATCH_REGEX ) + + self._match_value_text_input = QW.QLineEdit( self ) + + self._match_value_flexible_input = ClientGUICommon.BetterChoice( self ) + + self._match_value_flexible_input.addItem( 'alphabetic characters (a-zA-Z)', ClientParsing.ALPHA ) + self._match_value_flexible_input.addItem( 'alphanumeric characters (a-zA-Z0-9)', ClientParsing.ALPHANUMERIC ) + self._match_value_flexible_input.addItem( 'numeric characters (0-9)', ClientParsing.NUMERIC ) + + self._min_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' ) + self._max_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' ) + + self._example_string = QW.QLineEdit( self ) + + self._example_string_matches = ClientGUICommon.BetterStaticText( self ) + + # + + self.SetValue( string_match ) + + # + + rows = [] + + rows.append( ( 'match type: ', self._match_type ) ) + rows.append( ( 'match text: ', self._match_value_text_input ) ) + rows.append( ( 'match value (character set): ', self._match_value_flexible_input ) ) + rows.append( ( 'minimum allowed number of characters: ', self._min_chars ) ) + rows.append( ( 'maximum allowed number of characters: ', self._max_chars ) ) + rows.append( ( 'example string: ', self._example_string ) ) + + gridbox = ClientGUICommon.WrapInGrid( self, rows ) + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + QP.AddToLayout( vbox, self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR ) + + self.widget().setLayout( vbox ) + + # + + self._match_type.currentIndexChanged.connect( self._UpdateControls ) + self._match_value_text_input.textChanged.connect( self._UpdateControls ) + self._match_value_flexible_input.currentIndexChanged.connect( self._UpdateControls ) + self._min_chars.valueChanged.connect( self._UpdateControls ) + self._max_chars.valueChanged.connect( self._UpdateControls ) + self._example_string.textChanged.connect( self._UpdateControls ) + + + def _GetValue( self ): + + match_type = self._match_type.GetValue() + + if match_type == ClientParsing.STRING_MATCH_ANY: + + match_value = '' + + elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE: + + match_value = self._match_value_flexible_input.GetValue() + + else: + + match_value = self._match_value_text_input.text() + + + min_chars = self._min_chars.GetValue() + max_chars = self._max_chars.GetValue() + + example_string = self._example_string.text() + + string_match = ClientParsing.StringMatch( match_type = match_type, match_value = match_value, min_chars = min_chars, max_chars = max_chars, example_string = example_string ) + + return string_match + + + def _UpdateControls( self ): + + match_type = self._match_type.GetValue() + + if match_type == ClientParsing.STRING_MATCH_ANY: + + self._match_value_text_input.setEnabled( False ) + self._match_value_flexible_input.setEnabled( False ) + + elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE: + + self._match_value_text_input.setEnabled( False ) + self._match_value_flexible_input.setEnabled( True ) + + else: + + self._match_value_text_input.setEnabled( True ) + self._match_value_flexible_input.setEnabled( False ) + + + if match_type == ClientParsing.STRING_MATCH_FIXED: + + self._min_chars.SetValue( None ) + self._max_chars.SetValue( None ) + + self._min_chars.setEnabled( False ) + self._max_chars.setEnabled( False ) + + self._example_string.blockSignals( True ) # Temporarily block the text changed signal here so we won't end up in infinite recursion + self._example_string.setText( self._match_value_text_input.text() ) + self._example_string.blockSignals( False ) + + self._example_string_matches.setText( '' ) + + else: + + self._min_chars.setEnabled( True ) + self._max_chars.setEnabled( True ) + + string_match = self._GetValue() + + try: + + string_match.Test( self._example_string.text() ) + + self._example_string_matches.setText( 'Example matches ok!' ) + self._example_string_matches.setObjectName( 'HydrusValid' ) + self._example_string_matches.style().polish( self._example_string_matches ) + + except HydrusExceptions.StringMatchException as e: + + reason = str( e ) + + self._example_string_matches.setText( 'Example does not match - '+reason ) + self._example_string_matches.setObjectName( 'HydrusInvalid' ) + self._example_string_matches.style().polish( self._example_string_matches ) + + + + + def GetValue( self ): + + string_match = self._GetValue() + + try: + + string_match.Test( self._example_string.text() ) + + except HydrusExceptions.StringMatchException: + + raise HydrusExceptions.VetoException( 'Please enter an example text that matches the given rules!' ) + + + return string_match + + + def SetValue( self, string_match: ClientParsing.StringMatch ): + + ( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple() + + self._match_type.SetValue( match_type ) + + if match_type == ClientParsing.STRING_MATCH_FLEXIBLE: + + self._match_value_flexible_input.SetValue( match_value ) + + else: + + self._match_value_flexible_input.SetValue( ClientParsing.ALPHA ) + + self._match_value_text_input.setText( match_value ) + + + self._min_chars.SetValue( min_chars ) + self._max_chars.SetValue( max_chars ) + + self._example_string.setText( example_string ) + + self._UpdateControls() + + +class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ): + + def __init__( self, parent, string_splitter: ClientParsing.StringSplitter, example_string: str = '' ): + + ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + + # + + self._controls_panel = ClientGUICommon.StaticBox( self, 'splitter values' ) + + self._separator = QW.QLineEdit( self._controls_panel ) + self._max_splits = ClientGUICommon.NoneableSpinCtrl( self._controls_panel, min = 1, max = 65535, unit = 'splits', none_phrase = 'no limit' ) + + # + + self._example_panel = ClientGUICommon.StaticBox( self, 'test results' ) + + self._example_string = QW.QLineEdit( self._example_panel ) + + self._example_string_splits = QW.QListWidget( self._example_panel ) + self._example_string_splits.setSelectionMode( QW.QListWidget.NoSelection ) + + # + + self._example_string.setText( example_string ) + + self.SetValue( string_splitter ) + + # + + rows = [] + + rows.append( ( 'separator: ', self._separator ) ) + rows.append( ( 'max splits: ', self._max_splits ) ) + + gridbox = ClientGUICommon.WrapInGrid( self._controls_panel, rows ) + + self._controls_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + + rows = [] + + rows.append( ( 'example string: ', self._example_string ) ) + + gridbox = ClientGUICommon.WrapInGrid( self._example_panel, rows ) + + self._example_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + self._example_panel.Add( ClientGUICommon.BetterStaticText( self, label = 'result:' ), CC.FLAGS_EXPAND_PERPENDICULAR ) + self._example_panel.Add( self._example_string_splits, CC.FLAGS_EXPAND_BOTH_WAYS ) + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, self._controls_panel, CC.FLAGS_EXPAND_PERPENDICULAR ) + QP.AddToLayout( vbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.widget().setLayout( vbox ) + + # + + self._separator.textChanged.connect( self._UpdateControls ) + self._max_splits.valueChanged.connect( self._UpdateControls ) + self._example_string.textChanged.connect( self._UpdateControls ) + + + def _GetValue( self ): + + separator = self._separator.text() + max_splits = self._max_splits.GetValue() + + string_splitter = ClientParsing.StringSplitter( separator = separator, max_splits = max_splits ) + + return string_splitter + + + def _UpdateControls( self ): + + string_splitter = self._GetValue() + + results = string_splitter.Split( self._example_string.text() ) + + self._example_string_splits.clear() + + for result in results: + + self._example_string_splits.addItem( result ) + + + + def GetValue( self ): + + string_match = self._GetValue() + + return string_match + + + def SetValue( self, string_splitter: ClientParsing.StringSplitter ): + + separator = string_splitter.GetSeparator() + max_splits = string_splitter.GetMaxSplits() + + self._separator.setText( separator ) + self._max_splits.SetValue( max_splits ) + + self._UpdateControls() + + +class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ): + + NO_RESULTS_TEXT = 'no results' + + def __init__( self, parent, string_processor: ClientParsing.StringProcessor, example_string: str = '' ): + + ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + + # + + self._controls_panel = ClientGUICommon.StaticBox( self, 'processing steps' ) + + self._processing_steps = ClientGUIListBoxes.QueueListBox( self, 8, self._ConvertDataToListBoxString, add_callable = self._Add, edit_callable = self._Edit ) + + # + + self._example_panel = ClientGUICommon.StaticBox( self, 'test results' ) + + self._example_string = QW.QLineEdit( self._example_panel ) + + self._example_results = ClientGUICommon.BetterNotebook( self._example_panel ) + + ( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._example_panel, ( 64, 24 ) ) + + self._example_panel.setMinimumSize( w, h ) + + # + + self._example_string.setText( example_string ) + + # + + self._controls_panel.Add( self._processing_steps, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + + rows = [] + + rows.append( ( 'example string: ', self._example_string ) ) + + gridbox = ClientGUICommon.WrapInGrid( self._example_panel, rows ) + + self._example_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ) + self._example_panel.Add( ClientGUICommon.BetterStaticText( self, label = 'result:' ), CC.FLAGS_EXPAND_PERPENDICULAR ) + self._example_panel.Add( self._example_results, CC.FLAGS_EXPAND_BOTH_WAYS ) + + hbox = QP.VBoxLayout() + + QP.AddToLayout( hbox, self._controls_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( hbox, self._example_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + self.widget().setLayout( hbox ) + + # + + self._processing_steps.listBoxChanged.connect( self._UpdateControls ) + self._example_string.textChanged.connect( self._UpdateControls ) + + self.SetValue( string_processor ) + + + def _Add( self ): + + choice_tuples = [ + ( 'String Match', ClientParsing.StringMatch, 'An object that filters strings.' ), + ( 'String Transformer', ClientParsing.StringConverter, 'An object that converts strings from one thing to another.' ), + ( 'String Splitter', ClientParsing.StringSplitter, 'An object that breaks strings into smaller strings.' ) + ] + + try: + + string_processing_step_type = ClientGUIDialogsQuick.SelectFromListButtons( self, 'Which type of processing step?', choice_tuples ) + + except HydrusExceptions.CancelledException: + + raise HydrusExceptions.VetoException() + + + if string_processing_step_type == ClientParsing.StringMatch: + + string_processing_step = ClientParsing.StringMatch( example_string = self._example_string.text() ) + + else: + + string_processing_step = string_processing_step_type() + + + return self._Edit( string_processing_step ) + + + def _Edit( self, string_processing_step: ClientParsing.StringProcessingStep ): + + current_string_processor = self._GetValue() + + current_string_processing_steps = current_string_processor.GetProcessingSteps() + + if string_processing_step in current_string_processing_steps: + + example_text_index = current_string_processing_steps.index( string_processing_step ) + + else: + + example_text_index = len( current_string_processing_steps ) + + + example_text = self._example_string.text() + + if 0 < example_text_index < self._example_results.count() + 1: + + try: + + t = self._example_results.widget( example_text_index - 1 ).item( 0 ).text() + + if t != self.NO_RESULTS_TEXT: + + example_text = t + + + except: + + pass + + + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit processing step' ) as dlg: + + if isinstance( string_processing_step, ClientParsing.StringMatch ): + + panel = EditStringMatchPanel( dlg, string_processing_step ) + + elif isinstance( string_processing_step, ClientParsing.StringConverter ): + + panel = EditStringConverterPanel( dlg, string_processing_step, example_string_override = example_text ) + + elif isinstance( string_processing_step, ClientParsing.StringSplitter ): + + panel = EditStringSplitterPanel( dlg, string_processing_step, example_string = example_text ) + + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + string_processing_step = panel.GetValue() + + return string_processing_step + + else: + + raise HydrusExceptions.VetoException() + + + + + def _ConvertDataToListBoxString( self, string_processing_step: ClientParsing.StringProcessingStep ): + + return string_processing_step.ToString() + + + def _GetValue( self ): + + processing_steps = self._processing_steps.GetData() + + string_processor = ClientParsing.StringProcessor() + + string_processor.SetProcessingSteps( processing_steps ) + + return string_processor + + + def _UpdateControls( self ): + + string_processor = self._GetValue() + + processing_steps = string_processor.GetProcessingSteps() + + current_selected_index = self._example_results.currentIndex() + + self._example_results.DeleteAllPages() + + example_string = self._example_string.text() + + stop_now = False + + for i in range( len( processing_steps ) ): + + try: + + results = string_processor.ProcessStrings( [ example_string ], max_steps_allowed = i + 1 ) + + except Exception as e: + + results = [ 'error: {}'.format( str( e ) ) ] + + stop_now = True + + + results_list = QW.QListWidget( self._example_panel ) + results_list.setSelectionMode( QW.QListWidget.NoSelection ) + + if len( results ) == 0: + + results_list.addItem( self.NO_RESULTS_TEXT ) + + stop_now = True + + else: + + for result in results: + + results_list.addItem( result ) + + + + tab_label = '{} ({})'.format( processing_steps[i].ToString( simple = True ), HydrusData.ToHumanInt( len( results ) ) ) + + self._example_results.addTab( results_list, tab_label ) + + if stop_now: + + break + + + + if self._example_results.count() > current_selected_index: + + self._example_results.setCurrentIndex( current_selected_index ) + + + + def GetValue( self ): + + string_processor = self._GetValue() + + return string_processor + + + def SetValue( self, string_processor: ClientParsing.StringProcessor ): + + processing_steps = string_processor.GetProcessingSteps() + + self._processing_steps.AddDatas( processing_steps ) + + self._UpdateControls() + + diff --git a/hydrus/client/gui/ClientGUITagSuggestions.py b/hydrus/client/gui/ClientGUITagSuggestions.py index f26290f5..fcedbb37 100644 --- a/hydrus/client/gui/ClientGUITagSuggestions.py +++ b/hydrus/client/gui/ClientGUITagSuggestions.py @@ -162,7 +162,9 @@ class FavouritesTagsPanel( QW.QWidget ): def _UpdateTagDisplay( self ): - favourites = HG.client_controller.new_options.GetSuggestedTagsFavourites( self._service_key ) + favourites = list( HG.client_controller.new_options.GetSuggestedTagsFavourites( self._service_key ) ) + + ClientTags.SortTags( HC.options[ 'default_tag_sort' ], favourites ) tags = FilterSuggestedTagsForMedia( favourites, self._media, self._service_key ) diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py index f9cf362a..e32ca5c6 100644 --- a/hydrus/client/gui/QtPorting.py +++ b/hydrus/client/gui/QtPorting.py @@ -383,6 +383,11 @@ class TabBar( QW.QTabBar ): self._last_clicked_global_pos = None + def event( self, event ): + + return QW.QTabBar.event( self, event ) + + def mouseMoveEvent( self, e ): e.ignore() @@ -465,7 +470,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): def _LayoutPagesHelper( self ): - + current_index = self.currentIndex() for i in range( self.count() ): @@ -482,7 +487,10 @@ class TabWidgetWithDnD( QW.QTabWidget ): def LayoutPages( self ): - + + # hydev adds: I no longer call this, as I moved splitter setting to a thing called per page when page is first visibly shown + # leaving it here for now in case I need it again + # Momentarily switch to each page, then back, forcing a layout update. # If this is not done, the splitters on the hidden pages won't resize their widgets properly when we restore # splitter sizes after this, since they would never became visible. @@ -2550,10 +2558,8 @@ class CollectComboCtrl( QW.QComboBox ): text_and_data_tuples.update( namespaces ) - text_and_data_tuples = list( [ ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ] ) + text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) ) - text_and_data_tuples.sort() - ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) ) for ratings_service in ratings_services: diff --git a/hydrus/client/importing/ClientImportOptions.py b/hydrus/client/importing/ClientImportOptions.py index eb5ea758..874f5e0d 100644 --- a/hydrus/client/importing/ClientImportOptions.py +++ b/hydrus/client/importing/ClientImportOptions.py @@ -1622,9 +1622,7 @@ class ServiceTagImportOptions( HydrusSerialisable.SerialisableBase ): if len( self._additional_tags ) > 0: - pretty_additional_tags = list( self._additional_tags ) - - pretty_additional_tags.sort() + pretty_additional_tags = sorted( self._additional_tags ) statements.append( 'additional tags: ' + ', '.join( pretty_additional_tags ) ) diff --git a/hydrus/client/importing/ClientImportSubscriptions.py b/hydrus/client/importing/ClientImportSubscriptions.py index d4b0d83c..ec880eb8 100644 --- a/hydrus/client/importing/ClientImportSubscriptions.py +++ b/hydrus/client/importing/ClientImportSubscriptions.py @@ -1,3 +1,4 @@ +import gc import os import random import threading @@ -1504,6 +1505,8 @@ class SubscriptionsManager( object ): def _ClearFinishedSubscriptions( self ): + done_some = False + for ( name, ( thread, job, subscription ) ) in list( self._running_subscriptions.items() ): if job.IsDone(): @@ -1512,8 +1515,12 @@ class SubscriptionsManager( object ): del self._running_subscriptions[ name ] + done_some = True + + return done_some + def _GetNameReadyToGo( self ): @@ -1725,7 +1732,12 @@ class SubscriptionsManager( object ): with self._lock: - self._ClearFinishedSubscriptions() + some_cleared = self._ClearFinishedSubscriptions() + + if some_cleared: + + gc.collect() + wait_time = self._GetMainLoopWaitTime() @@ -1782,17 +1794,13 @@ class SubscriptionsManager( object ): with self._lock: - subs = list( self._current_subscription_names ) - subs.sort() + subs = sorted( self._current_subscription_names ) - running = list( self._running_subscriptions.keys() ) - running.sort() + running = sorted( self._running_subscriptions.keys() ) - cannot_run = list( self._names_that_cannot_run ) - cannot_run.sort() + cannot_run = sorted( self._names_that_cannot_run ) - next_times = list( self._names_to_next_work_time.items() ) - next_times.sort( key = lambda n, nwt: nwt ) + next_times = sorted( self._names_to_next_work_time.items(), key = lambda n, nwt: nwt ) message = '{} subs: {}'.format( HydrusData.ToHumanInt( len( self._current_subscription_names ) ), ', '.join( subs ) ) message += os.linesep * 2 diff --git a/hydrus/client/networking/ClientNetworkingDomain.py b/hydrus/client/networking/ClientNetworkingDomain.py index 0ed14252..fc09295e 100644 --- a/hydrus/client/networking/ClientNetworkingDomain.py +++ b/hydrus/client/networking/ClientNetworkingDomain.py @@ -14,6 +14,7 @@ import os import re import threading import time +import unicodedata import urllib.parse def AddCookieToSession( session, name, value, domain, path, expires, secure = False, rest = None ): @@ -123,9 +124,7 @@ def ConvertQueryDictToText( query_dict, param_order = None ): if param_order is None: - param_pairs = list( query_dict.items() ) - - param_pairs.sort() + param_pairs = sorted( query_dict.items() ) else: @@ -147,7 +146,7 @@ def ConvertQueryTextToDict( query_text ): # so if there are a mix of encoded and non-encoded, we won't touch it here m8 # except these chars, which screw with GET arg syntax when unquoted - bad_chars = [ '&', '=', '/', '?' ] + bad_chars = [ '&', '=', '/', '?', '#' ] param_order = [] @@ -248,7 +247,7 @@ def ConvertURLClassesIntoAPIPairs( url_classes ): def ConvertURLIntoDomain( url ): - parser_result = urllib.parse.urlparse( url ) + parser_result = ParseURL( url ) if parser_result.scheme == '': @@ -335,7 +334,7 @@ def GetSearchURLs( url ): for url in list( search_urls ): - p = urllib.parse.urlparse( url ) + p = ParseURL( url ) scheme = p.scheme netloc = p.netloc @@ -379,6 +378,62 @@ def GetSearchURLs( url ): return search_urls +def ParseURL( url: str ) -> urllib.parse.ParseResult: + + url = url.strip() + + url = UnicodeNormaliseURL( url ) + + return urllib.parse.urlparse( url ) + +OH_NO_NO_NETLOC_CHARACTERS = '?#' +OH_NO_NO_NETLOC_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : '_' for char in OH_NO_NO_NETLOC_CHARACTERS } + +def UnicodeNormaliseURL( url: str ): + + if url.startswith( 'file:' ): + + return url + + + # the issue is netloc, blah.com, cannot have certain unicode characters that look like others, or double ( e + accent ) characters that can be one accented-e, so we normalise + # urllib.urlparse throws a valueerror if these are in, so let's switch out + + scheme_splitter = '://' + netloc_splitter = '/' + + if scheme_splitter in url: + + ( scheme, netloc_and_path_and_rest ) = url.split( scheme_splitter, 1 ) + + if netloc_splitter in netloc_and_path_and_rest: + + ( netloc, path_and_rest ) = netloc_and_path_and_rest.split( netloc_splitter, 1 ) + + else: + + netloc = netloc_and_path_and_rest + path_and_rest = None + + + netloc = unicodedata.normalize( 'NFKC', netloc ) + + netloc = netloc.translate( OH_NO_NO_NETLOC_CHARACTERS_UNICODE_TRANSLATE ) + + scheme_and_netlock = scheme_splitter.join( ( scheme, netloc ) ) + + if path_and_rest is None: + + url = scheme_and_netlock + + else: + + url = netloc_splitter.join( ( scheme_and_netlock, path_and_rest ) ) + + + + return url + VALID_DENIED = 0 VALID_APPROVED = 1 VALID_UNKNOWN = 2 @@ -692,9 +747,7 @@ class NetworkDomainManager( HydrusSerialisable.SerialisableBase ): namespaces.update( parser.GetNamespaces() ) - self._parser_namespaces = list( namespaces ) - - self._parser_namespaces.sort() + self._parser_namespaces = sorted( namespaces ) def _SetDirty( self ): @@ -1602,7 +1655,7 @@ class NetworkDomainManager( HydrusSerialisable.SerialisableBase ): if url_class is None: - p = urllib.parse.urlparse( url ) + p = ParseURL( url ) scheme = p.scheme netloc = p.netloc @@ -2410,7 +2463,7 @@ class GalleryURLGenerator( HydrusSerialisable.SerialisableBaseNamed ): # when the tags separator is '+' but the tags include '6+girls', we run into fun internet land - bad_chars = [ self._search_terms_separator, '&', '=', '/', '?' ] + bad_chars = [ self._search_terms_separator, '&', '=', '/', '?', '#' ] if True in ( bad_char in search_term for bad_char in bad_chars ): @@ -3078,7 +3131,7 @@ class URLClass( HydrusSerialisable.SerialisableBaseNamed ): url = self.Normalise( url ) - p = urllib.parse.urlparse( url ) + p = ParseURL( url ) scheme = p.scheme netloc = p.netloc @@ -3254,7 +3307,7 @@ class URLClass( HydrusSerialisable.SerialisableBaseNamed ): def Normalise( self, url ): - p = urllib.parse.urlparse( url ) + p = ParseURL( url ) scheme = self._preferred_scheme params = '' @@ -3318,7 +3371,7 @@ class URLClass( HydrusSerialisable.SerialisableBaseNamed ): def Test( self, url ): - p = urllib.parse.urlparse( url ) + p = ParseURL( url ) if self._match_subdomains: diff --git a/hydrus/client/networking/ClientNetworkingJobs.py b/hydrus/client/networking/ClientNetworkingJobs.py index 8da9a0ea..199777ce 100644 --- a/hydrus/client/networking/ClientNetworkingJobs.py +++ b/hydrus/client/networking/ClientNetworkingJobs.py @@ -22,9 +22,22 @@ try: CLOUDSCRAPER_OK = True + try: + + # help pyinstaller + import pyparsing + + PYPARSING_OK = True + + except: + + PYPARSING_OK = False + + except: CLOUDSCRAPER_OK = False + PYPARSING_OK = False def ConvertStatusCodeAndDataIntoExceptionInfo( status_code, data, is_hydrus_service = False ): @@ -121,6 +134,7 @@ class NetworkJob( object ): WILLING_TO_WAIT_ON_INVALID_LOGIN = True IS_HYDRUS_SERVICE = False + IS_IPFS_SERVICE = False def __init__( self, method, url, body = None, referral_url = None, temp_path = None ): @@ -256,7 +270,7 @@ class NetworkJob( object ): data = self._body files = self._files - if self.IS_HYDRUS_SERVICE: + if self.IS_HYDRUS_SERVICE or self.IS_IPFS_SERVICE: headers[ 'User-Agent' ] = 'hydrus client/' + str( HC.NETWORK_VERSION ) @@ -1588,6 +1602,18 @@ class NetworkJobHydrus( NetworkJob ): +class NetworkJobIPFS( NetworkJob ): + + def __init__( self, method, url, body = None, referral_url = None, temp_path = None ): + + NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path ) + + self.OnlyTryConnectionOnce() + self.OverrideBandwidth() + + + IS_IPFS_SERVICE = True + class NetworkJobWatcherPage( NetworkJob ): def __init__( self, watcher_key, method, url, body = None, referral_url = None, temp_path = None ): diff --git a/hydrus/client/networking/ClientNetworkingLogin.py b/hydrus/client/networking/ClientNetworkingLogin.py index 3502e4ec..ddc323a7 100644 --- a/hydrus/client/networking/ClientNetworkingLogin.py +++ b/hydrus/client/networking/ClientNetworkingLogin.py @@ -1170,8 +1170,7 @@ class LoginScriptDomain( HydrusSerialisable.SerialisableBaseNamed ): if len( missing_givens ) > 0: - missing_givens = list( missing_givens ) - missing_givens.sort() + missing_givens = sorted( missing_givens ) raise HydrusExceptions.ValidationException( 'Missing required credentials: ' + ', '.join( missing_givens ) ) @@ -1202,8 +1201,7 @@ class LoginScriptDomain( HydrusSerialisable.SerialisableBaseNamed ): if len( missing_definitions ) > 0: - missing_definitions = list( missing_definitions ) - missing_definitions.sort() + missing_definitions = sorted( missing_definitions ) raise HydrusExceptions.ValidationException( 'Missing required credential definitions: ' + ', '.join( missing_definitions ) ) @@ -1220,8 +1218,7 @@ class LoginScriptDomain( HydrusSerialisable.SerialisableBaseNamed ): if len( missing_vars ) > 0: - missing_vars = list( missing_vars ) - missing_vars.sort() + missing_vars = sorted( missing_vars ) raise HydrusExceptions.ValidationException( 'Missing temp variables for login step "' + login_step.GetName() + '": ' + ', '.join( missing_vars ) ) @@ -1673,7 +1670,7 @@ class LoginStep( HydrusSerialisable.SerialisableBaseNamed ): if self._method == 'POST' and referral_url is not None: - p = urllib.parse.urlparse( referral_url ) + p = ClientNetworkingDomain.ParseURL( url ) r = urllib.parse.ParseResult( p.scheme, p.netloc, '', '', '', '' ) diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 978bcc0f..11fc2b00 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -73,7 +73,7 @@ options = {} # Misc NETWORK_VERSION = 18 -SOFTWARE_VERSION = 396 +SOFTWARE_VERSION = 397 CLIENT_API_VERSION = 11 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 ) diff --git a/hydrus/core/HydrusData.py b/hydrus/core/HydrusData.py index b4d23c92..f7343176 100644 --- a/hydrus/core/HydrusData.py +++ b/hydrus/core/HydrusData.py @@ -432,9 +432,7 @@ def ConvertUglyNamespaceToPrettyString( namespace ): def ConvertUglyNamespacesToPrettyStrings( namespaces ): - namespaces = list( namespaces ) - - namespaces.sort() + namespaces = sorted( namespaces ) result = [ ConvertUglyNamespaceToPrettyString( namespace ) for namespace in namespaces ] diff --git a/hydrus/core/HydrusExceptions.py b/hydrus/core/HydrusExceptions.py index 1eedb001..4fa7b699 100644 --- a/hydrus/core/HydrusExceptions.py +++ b/hydrus/core/HydrusExceptions.py @@ -1,5 +1,6 @@ import os import traceback +import collections.abc class HydrusException( Exception ): @@ -7,17 +8,24 @@ class HydrusException( Exception ): s = [] - for arg in self.args: + if isinstance( self.args, collections.abc.Iterable ): - try: + for arg in self.args: - s.append( str( arg ) ) - - except: - - s.append( repr( arg ) ) + try: + + s.append( str( arg ) ) + + except: + + s.append( repr( arg ) ) + + else: + + s = [ repr( self.args ) ] + return os.linesep.join( s ) diff --git a/hydrus/core/HydrusImageHandling.py b/hydrus/core/HydrusImageHandling.py index bd80f8d9..48b310a9 100644 --- a/hydrus/core/HydrusImageHandling.py +++ b/hydrus/core/HydrusImageHandling.py @@ -8,9 +8,11 @@ import numpy.core.multiarray # important this comes before cv! try: - import numpy.random.common # more hidden imports for pyinstaller - import numpy.random.bounded_integers # more hidden imports for pyinstaller - import numpy.random.entropy # more hidden imports for pyinstaller + # more hidden imports for pyinstaller + + import numpy.random.common # pylint: disable=E0401 + import numpy.random.bounded_integers # pylint: disable=E0401 + import numpy.random.entropy # pylint: disable=E0401 except: diff --git a/hydrus/core/HydrusNetwork.py b/hydrus/core/HydrusNetwork.py index 363d0eb8..a8fb38e5 100644 --- a/hydrus/core/HydrusNetwork.py +++ b/hydrus/core/HydrusNetwork.py @@ -1808,9 +1808,7 @@ class Metadata( HydrusSerialisable.SerialisableBase ): with self._lock: - data = list( self._metadata.items() ) - - data.sort() + data = sorted( self._metadata.items() ) for ( update_index, ( update_hashes, begin, end ) ) in data: diff --git a/hydrus/core/HydrusPaths.py b/hydrus/core/HydrusPaths.py index 77105909..0d0cec61 100644 --- a/hydrus/core/HydrusPaths.py +++ b/hydrus/core/HydrusPaths.py @@ -12,7 +12,6 @@ import shlex import shutil import stat import subprocess -import sys import tempfile import threading import traceback @@ -727,8 +726,28 @@ def MirrorFile( source, dest ): MakeFileWritable( dest ) - # this overwrites on conflict without hassle - shutil.copy2( source, dest ) + copy_metadata = True + + if HC.PLATFORM_WINDOWS: + + mtime = os.path.getmtime( source ) + + # this is 1980-01-01 UTC, before which Windows can have trouble copying lmaoooooo + if mtime < 315532800: + + copy_metadata = False + + + + if copy_metadata: + + # this overwrites on conflict without hassle + shutil.copy2( source, dest ) + + else: + + shutil.copy( source, dest ) + except Exception as e: diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py index bde96095..ac2ec591 100644 --- a/hydrus/core/HydrusSerialisable.py +++ b/hydrus/core/HydrusSerialisable.py @@ -98,6 +98,8 @@ SERIALISABLE_TYPE_TAG_DISPLAY_MANAGER = 79 SERIALISABLE_TYPE_TAG_SEARCH_CONTEXT = 80 SERIALISABLE_TYPE_FAVOURITE_SEARCH_MANAGER = 81 SERIALISABLE_TYPE_NOTE_IMPORT_OPTIONS = 82 +SERIALISABLE_TYPE_STRING_SPLITTER = 83 +SERIALISABLE_TYPE_STRING_PROCESSOR = 84 SERIALISABLE_TYPES_TO_OBJECT_TYPES = {} diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py index d5200ee5..630f0fda 100644 --- a/hydrus/test/TestClientAPI.py +++ b/hydrus/test/TestClientAPI.py @@ -673,11 +673,8 @@ class TestClientAPI( unittest.TestCase ): expected_content_updates = expected_service_keys_to_content_updates[ service_key ] - c_u_tuples = [ c_u.ToTuple() for c_u in content_updates ] - e_c_u_tuples = [ e_c_u.ToTuple() for e_c_u in expected_content_updates ] - - c_u_tuples.sort() - e_c_u_tuples.sort() + c_u_tuples = sorted( ( c_u.ToTuple() for c_u in content_updates ) ) + e_c_u_tuples = sorted( ( e_c_u.ToTuple() for e_c_u in expected_content_updates ) ) self.assertEqual( c_u_tuples, e_c_u_tuples ) @@ -712,11 +709,8 @@ class TestClientAPI( unittest.TestCase ): expected_content_updates = expected_service_keys_to_content_updates[ service_key ] - c_u_tuples = [ c_u.ToTuple() for c_u in content_updates ] - e_c_u_tuples = [ e_c_u.ToTuple() for e_c_u in expected_content_updates ] - - c_u_tuples.sort() - e_c_u_tuples.sort() + c_u_tuples = sorted( ( c_u.ToTuple() for c_u in content_updates ) ) + e_c_u_tuples = sorted( ( e_c_u.ToTuple() for e_c_u in expected_content_updates ) ) self.assertEqual( c_u_tuples, e_c_u_tuples ) @@ -781,11 +775,8 @@ class TestClientAPI( unittest.TestCase ): expected_content_updates = expected_service_keys_to_content_updates[ service_key ] - c_u_tuples = [ c_u.ToTuple() for c_u in content_updates ] - e_c_u_tuples = [ e_c_u.ToTuple() for e_c_u in expected_content_updates ] - - c_u_tuples.sort() - e_c_u_tuples.sort() + c_u_tuples = sorted( ( c_u.ToTuple() for c_u in content_updates ) ) + e_c_u_tuples = sorted( ( e_c_u.ToTuple() for e_c_u in expected_content_updates ) ) self.assertEqual( c_u_tuples, e_c_u_tuples ) @@ -820,11 +811,8 @@ class TestClientAPI( unittest.TestCase ): expected_content_updates = expected_service_keys_to_content_updates[ service_key ] - c_u_tuples = [ c_u.ToTuple() for c_u in content_updates ] - e_c_u_tuples = [ e_c_u.ToTuple() for e_c_u in expected_content_updates ] - - c_u_tuples.sort() - e_c_u_tuples.sort() + c_u_tuples = sorted( ( c_u.ToTuple() for c_u in content_updates ) ) + e_c_u_tuples = sorted( ( e_c_u.ToTuple() for e_c_u in expected_content_updates ) ) self.assertEqual( c_u_tuples, e_c_u_tuples ) @@ -1584,8 +1572,7 @@ class TestClientAPI( unittest.TestCase ): urls = { "https://gelbooru.com/index.php?page=post&s=view&id=4841557", "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg" } - sorted_urls = list( urls ) - sorted_urls.sort() + sorted_urls = sorted( urls ) for ( file_id, hash ) in file_ids_to_hashes.items(): diff --git a/hydrus/test/TestClientDB.py b/hydrus/test/TestClientDB.py index 893dfbba..47f86a80 100644 --- a/hydrus/test/TestClientDB.py +++ b/hydrus/test/TestClientDB.py @@ -1,37 +1,23 @@ from hydrus.client import ClientConstants as CC -from hydrus.client import ClientData from hydrus.client import ClientDB from hydrus.client import ClientDefaults -from hydrus.client import ClientDownloading from hydrus.client import ClientExporting -from hydrus.client import ClientFiles from hydrus.client.gui import ClientGUIManagement from hydrus.client.gui import ClientGUIPages -from hydrus.client.importing import ClientImporting from hydrus.client.importing import ClientImportLocal from hydrus.client.importing import ClientImportOptions from hydrus.client.importing import ClientImportFileSeeds -from hydrus.client import ClientRatings from hydrus.client import ClientSearch from hydrus.client import ClientServices from hydrus.client import ClientTags -import collections from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData -from hydrus.core import HydrusExceptions -from hydrus.core import HydrusVideoHandling from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusNetwork from hydrus.core import HydrusSerialisable -import itertools import os -from hydrus.server import ServerDB -import shutil -import sqlite3 -import stat from hydrus.test import TestController import time -import threading import unittest class TestClientDB( unittest.TestCase ): @@ -646,7 +632,7 @@ class TestClientDB( unittest.TestCase ): predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING, min_current_count = 1 ) ) predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_INBOX, min_current_count = 1 ) ) predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_ARCHIVE, min_current_count = 0 ) ) - predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ] ] ) + predicates.extend( [ ClientSearch.Predicate( predicate_type ) for predicate_type in [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE, ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS, ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS, ClientSearch.PREDICATE_TYPE_SYSTEM_MIME, ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE, ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS, ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS ] ] ) self.assertEqual( set( result ), set( predicates ) ) diff --git a/hydrus/test/TestClientDBDuplicates.py b/hydrus/test/TestClientDBDuplicates.py index e3944596..dc873957 100644 --- a/hydrus/test/TestClientDBDuplicates.py +++ b/hydrus/test/TestClientDBDuplicates.py @@ -1,37 +1,14 @@ from hydrus.client import ClientConstants as CC -from hydrus.client import ClientData from hydrus.client import ClientDB -from hydrus.client import ClientDefaults -from hydrus.client import ClientDownloading -from hydrus.client import ClientExporting -from hydrus.client import ClientFiles -from hydrus.client.gui import ClientGUIManagement -from hydrus.client.gui import ClientGUIPages -from hydrus.client.importing import ClientImporting -from hydrus.client.importing import ClientImportLocal from hydrus.client.importing import ClientImportOptions from hydrus.client.importing import ClientImportFileSeeds -from hydrus.client import ClientRatings from hydrus.client import ClientSearch -from hydrus.client import ClientServices -from hydrus.client import ClientTags -import collections from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData -from hydrus.core import HydrusExceptions -from hydrus.core import HydrusVideoHandling from hydrus.core import HydrusGlobals as HG -from hydrus.core import HydrusNetwork -from hydrus.core import HydrusSerialisable -import itertools import os -from hydrus.server import ServerDB -import shutil -import sqlite3 -import stat from hydrus.test import TestController import time -import threading import unittest class TestClientDBDuplicates( unittest.TestCase ): diff --git a/hydrus/test/TestClientImageHandling.py b/hydrus/test/TestClientImageHandling.py index a8c732db..c82aea05 100644 --- a/hydrus/test/TestClientImageHandling.py +++ b/hydrus/test/TestClientImageHandling.py @@ -1,6 +1,5 @@ from hydrus.client import ClientConstants as CC from hydrus.client import ClientImageHandling -import collections from hydrus.core import HydrusConstants as HC import os import unittest diff --git a/hydrus/test/TestClientImportOptions.py b/hydrus/test/TestClientImportOptions.py index 0b90ce03..e944cd57 100644 --- a/hydrus/test/TestClientImportOptions.py +++ b/hydrus/test/TestClientImportOptions.py @@ -8,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.client import ClientRatings import collections import os import random diff --git a/hydrus/test/TestClientImportSubscriptions.py b/hydrus/test/TestClientImportSubscriptions.py index b1d1ddd1..dc7d9a52 100644 --- a/hydrus/test/TestClientImportSubscriptions.py +++ b/hydrus/test/TestClientImportSubscriptions.py @@ -1,24 +1,13 @@ -from hydrus.client import ClientConstants as CC + from hydrus.client import ClientDefaults -from hydrus.client.importing import ClientImportSubscriptions from hydrus.client.networking import ClientNetworking from hydrus.client.networking import ClientNetworkingBandwidth from hydrus.client.networking import ClientNetworkingDomain from hydrus.client.networking import ClientNetworkingLogin from hydrus.client.networking import ClientNetworkingSessions -import collections -from hydrus.core import HydrusConstants as HC -from hydrus.core import HydrusData -from hydrus.core import HydrusExceptions -from hydrus.core import HydrusNetworking -import os from hydrus.test import TestController -import threading -import time import unittest -from hydrus.core import HydrusGlobals as HG -from httmock import all_requests, urlmatch, HTTMock, response -from mock import patch +from httmock import all_requests MISSING_RESPONSE = '404, bad result' ERROR_RESPONSE = '500, it done broke' diff --git a/hydrus/test/TestClientListBoxes.py b/hydrus/test/TestClientListBoxes.py index 7f190a04..ee4f5442 100644 --- a/hydrus/test/TestClientListBoxes.py +++ b/hydrus/test/TestClientListBoxes.py @@ -1,12 +1,6 @@ -from hydrus.client import ClientConstants as CC -from hydrus.client import ClientDefaults from hydrus.client.gui import ClientGUIListBoxes -import collections -from hydrus.core import HydrusConstants as HC -import os import random from hydrus.test import TestController -import time import unittest from hydrus.core import HydrusGlobals as HG from qtpy import QtCore as QC diff --git a/hydrus/test/TestClientMigration.py b/hydrus/test/TestClientMigration.py index 791070f8..893a7c42 100644 --- a/hydrus/test/TestClientMigration.py +++ b/hydrus/test/TestClientMigration.py @@ -8,13 +8,10 @@ from hydrus.client import ClientMigration from hydrus.client import ClientServices from hydrus.client import ClientTags import collections -import hashlib from hydrus.core import HydrusConstants as HC -from hydrus.core import HydrusExceptions from hydrus.core import HydrusTagArchive import os import random -import shutil import time import unittest from hydrus.core import HydrusData diff --git a/hydrus/test/TestClientNetworking.py b/hydrus/test/TestClientNetworking.py index e1c5f69f..025d334c 100644 --- a/hydrus/test/TestClientNetworking.py +++ b/hydrus/test/TestClientNetworking.py @@ -8,14 +8,11 @@ from hydrus.client.networking import ClientNetworkingLogin from hydrus.client.networking import ClientNetworkingSessions from hydrus.client import ClientParsing from hydrus.client import ClientServices -import collections from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusNetworking -import os from hydrus.test import TestController -import threading import time import unittest from hydrus.core import HydrusGlobals as HG diff --git a/hydrus/test/TestClientParsing.py b/hydrus/test/TestClientParsing.py new file mode 100644 index 00000000..64cac93c --- /dev/null +++ b/hydrus/test/TestClientParsing.py @@ -0,0 +1,233 @@ +from hydrus.core import HydrusConstants as HC +from hydrus.client import ClientParsing +import unittest + +class TestStringConverter( unittest.TestCase ): + + def test_basics( self ): + + transformations = [] + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, 1 ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), '123456789' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, 1 ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), '12345678' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, 7 ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), '1234567' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, 6 ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), '234567' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, 'abc' ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'abc234567' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, 'x z' ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'abc234567x z' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_ENCODE, 'url percent encoding' ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'abc234567x%20z' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_DECODE, 'url percent encoding' ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'abc234567x z' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_REVERSE, None ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'z x765432cba' ) + + # + + transformations.append( ( ClientParsing.STRING_TRANSFORMATION_REGEX_SUB, ( '\\d', 'd' ) ) ) + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '0123456789' ), 'z xddddddcba' ) + + # + + transformations = [ ( ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ( '%Y-%m-%d %H:%M:%S', HC.TIMEZONE_GMT, 0 ) ) ] + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '1970-01-02 00:00:00' ), '86400' ) + + # + + transformations = [ ( ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ( '%Y-%m-%d %H:%M:%S', 0 ) ) ] + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '86400' ), '1970-01-02 00:00:00' ) + + # + + transformations = [ ( ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION, 5 ) ] + + string_converter = ClientParsing.StringConverter( transformations = transformations ) + + self.assertEqual( string_converter.Convert( '4' ), '9' ) + + +class TestStringMatch( unittest.TestCase ): + + def test_basics( self ): + + all_string_match = ClientParsing.StringMatch() + + self.assertTrue( all_string_match.Matches( '123' ) ) + self.assertTrue( all_string_match.Matches( 'abc' ) ) + self.assertTrue( all_string_match.Matches( 'abc123' ) ) + + # + + min_string_match = ClientParsing.StringMatch( min_chars = 4 ) + + self.assertFalse( min_string_match.Matches( '123' ) ) + self.assertFalse( min_string_match.Matches( 'abc' ) ) + self.assertTrue( min_string_match.Matches( 'abc123' ) ) + + # + + max_string_match = ClientParsing.StringMatch( max_chars = 4 ) + + self.assertTrue( max_string_match.Matches( '123' ) ) + self.assertTrue( max_string_match.Matches( 'abc' ) ) + self.assertFalse( max_string_match.Matches( 'abc123' ) ) + + # + + min_max_string_match = ClientParsing.StringMatch( min_chars = 4, max_chars = 10 ) + + self.assertFalse( min_max_string_match.Matches( '123' ) ) + self.assertFalse( min_max_string_match.Matches( 'abc' ) ) + self.assertTrue( min_max_string_match.Matches( 'abc123' ) ) + + # + + alpha_string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.ALPHA ) + + self.assertFalse( alpha_string_match.Matches( '123' ) ) + self.assertTrue( alpha_string_match.Matches( 'abc' ) ) + self.assertFalse( alpha_string_match.Matches( 'abc123' ) ) + + # + + alphanum_string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.ALPHANUMERIC ) + + self.assertTrue( alphanum_string_match.Matches( '123' ) ) + self.assertTrue( alphanum_string_match.Matches( 'abc' ) ) + self.assertTrue( alphanum_string_match.Matches( 'abc123' ) ) + + # + + num_string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.NUMERIC ) + + self.assertTrue( num_string_match.Matches( '123' ) ) + self.assertFalse( num_string_match.Matches( 'abc' ) ) + self.assertFalse( num_string_match.Matches( 'abc123' ) ) + + # + + fixed_string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = '123' ) + + self.assertTrue( fixed_string_match.Matches( '123' ) ) + self.assertFalse( fixed_string_match.Matches( 'abc' ) ) + self.assertFalse( fixed_string_match.Matches( 'abc123' ) ) + + # + + re_string_match = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_REGEX, match_value = '\\d' ) + + self.assertTrue( re_string_match.Matches( '123' ) ) + self.assertFalse( re_string_match.Matches( 'abc' ) ) + self.assertTrue( re_string_match.Matches( 'abc123' ) ) + + +class TestStringSplitter( unittest.TestCase ): + + def test_basics( self ): + + splitter = ClientParsing.StringSplitter( separator = ', ' ) + + self.assertTrue( splitter.Split( '123' ), [ '123' ] ) + self.assertTrue( splitter.Split( '1,2,3' ), [ '1,2,3' ] ) + self.assertTrue( splitter.Split( '1, 2, 3' ), [ '1', '2', '3' ] ) + + splitter = ClientParsing.StringSplitter( separator = ', ', max_splits = 2 ) + + self.assertTrue( splitter.Split( '123' ), [ '123' ] ) + self.assertTrue( splitter.Split( '1,2,3' ), [ '1,2,3' ] ) + self.assertTrue( splitter.Split( '1, 2, 3, 4' ), [ '1', '2', '3,4' ] ) + + + +class TestStringProcessor( unittest.TestCase ): + + def test_basics( self ): + + processor = ClientParsing.StringProcessor() + + self.assertEqual( processor.ProcessStrings( [] ), [] ) + self.assertEqual( processor.ProcessStrings( [ 'test' ] ), [ 'test' ] ) + self.assertEqual( processor.ProcessStrings( [ 'test', 'test', '', 'test2' ] ), [ 'test', 'test', '', 'test2' ] ) + + processing_steps = [] + + processing_steps.append( ClientParsing.StringSplitter( separator = ',', max_splits = 2 ) ) + + processing_steps.append( ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.NUMERIC ) ) + + transformations = [ ( ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, 'abc' ) ] + + processing_steps.append( ClientParsing.StringConverter( transformations = transformations ) ) + + processor.SetProcessingSteps( processing_steps ) + + expected_result = [ '1abc', '123abc' ] + + self.assertEqual( processor.ProcessStrings( [ '1,a,2,3', 'test', '123' ] ), expected_result ) + + diff --git a/hydrus/test/TestClientTags.py b/hydrus/test/TestClientTags.py index 307ac35c..706af39a 100644 --- a/hydrus/test/TestClientTags.py +++ b/hydrus/test/TestClientTags.py @@ -1,16 +1,12 @@ import collections -from hydrus.client import ClientCaches from hydrus.client import ClientConstants as CC from hydrus.client import ClientManagers -from hydrus.client import ClientMedia from hydrus.client import ClientMediaManagers from hydrus.client import ClientSearch from hydrus.client import ClientTags from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData -from hydrus.core import HydrusExceptions from hydrus.core import HydrusGlobals as HG -import os import unittest class TestMergeTagsManagers( unittest.TestCase ): diff --git a/hydrus/test/TestClientThreading.py b/hydrus/test/TestClientThreading.py index 00db80b6..05149420 100644 --- a/hydrus/test/TestClientThreading.py +++ b/hydrus/test/TestClientThreading.py @@ -1,10 +1,5 @@ -from hydrus.client import ClientConstants as CC from hydrus.client import ClientThreading -from hydrus.core import HydrusConstants as HC -from hydrus.core import HydrusExceptions from hydrus.core import HydrusGlobals as HG -import os -import threading import time import unittest diff --git a/hydrus/test/TestController.py b/hydrus/test/TestController.py index 0caea199..fe60c3e2 100644 --- a/hydrus/test/TestController.py +++ b/hydrus/test/TestController.py @@ -1,10 +1,7 @@ import collections import os -import random import threading import collections -import shutil -import sys import tempfile import time import traceback @@ -24,11 +21,9 @@ from hydrus.client.networking import ClientNetworkingSessions from hydrus.client import ClientServices from hydrus.client import ClientTags from hydrus.client import ClientThreading -from hydrus.core import HydrusDB from hydrus.core import HydrusExceptions from hydrus.core import HydrusPubSub from hydrus.core import HydrusSessions -from hydrus.core import HydrusTags from hydrus.core import HydrusThreading from hydrus.test import TestClientAPI from hydrus.test import TestClientConstants @@ -42,19 +37,17 @@ from hydrus.test import TestClientImportSubscriptions from hydrus.test import TestClientListBoxes from hydrus.test import TestClientMigration from hydrus.test import TestClientNetworking +from hydrus.test import TestClientParsing from hydrus.test import TestClientTags from hydrus.test import TestClientThreading from hydrus.test import TestDialogs from hydrus.test import TestFunctions -from hydrus.test import TestHydrusNATPunch from hydrus.test import TestHydrusNetworking from hydrus.test import TestHydrusSerialisable from hydrus.test import TestHydrusServer from hydrus.test import TestHydrusSessions from hydrus.test import TestServerDB -from twisted.internet import reactor from hydrus.client import ClientCaches -from hydrus.client import ClientData from hydrus.client import ClientOptions from hydrus.core import HydrusData from hydrus.core import HydrusPaths @@ -671,6 +664,7 @@ class Controller( object ): suites.append( unittest.TestLoader().loadTestsFromModule( TestClientConstants ) ) suites.append( unittest.TestLoader().loadTestsFromModule( TestClientData ) ) suites.append( unittest.TestLoader().loadTestsFromModule( TestClientImportOptions ) ) + suites.append( unittest.TestLoader().loadTestsFromModule( TestClientParsing ) ) suites.append( unittest.TestLoader().loadTestsFromModule( TestClientTags ) ) suites.append( unittest.TestLoader().loadTestsFromModule( TestClientThreading ) ) suites.append( unittest.TestLoader().loadTestsFromModule( TestFunctions ) ) diff --git a/hydrus/test/TestDialogs.py b/hydrus/test/TestDialogs.py index c6c872e7..3e9a6d35 100644 --- a/hydrus/test/TestDialogs.py +++ b/hydrus/test/TestDialogs.py @@ -1,13 +1,6 @@ -from hydrus.client import ClientConstants as CC -from hydrus.client import ClientDefaults from hydrus.client.gui import ClientGUIDialogs from hydrus.client.gui import ClientGUIScrolledPanelsEdit -from hydrus.client.gui import ClientGUIScrolledPanelsManagement from hydrus.client.gui import ClientGUITopLevelWindowsPanels -from hydrus.client import ClientThreading -import collections -from hydrus.core import HydrusConstants as HC -import os import unittest from hydrus.core import HydrusGlobals as HG from qtpy import QtCore as QC diff --git a/hydrus/test/TestFunctions.py b/hydrus/test/TestFunctions.py index 78dc4c75..67729a31 100644 --- a/hydrus/test/TestFunctions.py +++ b/hydrus/test/TestFunctions.py @@ -1,9 +1,7 @@ -import collections from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusGlobals as HG from hydrus.client import ClientData from hydrus.client import ClientTags -import os import unittest from hydrus.core import HydrusData from hydrus.client import ClientConstants as CC diff --git a/hydrus/test/TestHydrusNATPunch.py b/hydrus/test/TestHydrusNATPunch.py index 06d2236d..c95d549e 100644 --- a/hydrus/test/TestHydrusNATPunch.py +++ b/hydrus/test/TestHydrusNATPunch.py @@ -1,9 +1,5 @@ -from hydrus.client import ClientConstants as CC -from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusNATPunch -import os import random -import time import unittest class TestNATPunch( unittest.TestCase ): diff --git a/hydrus/test/TestHydrusNetworking.py b/hydrus/test/TestHydrusNetworking.py index 44167dd4..d3d79645 100644 --- a/hydrus/test/TestHydrusNetworking.py +++ b/hydrus/test/TestHydrusNetworking.py @@ -1,11 +1,7 @@ -import collections from hydrus.core import HydrusConstants as HC -import os import random -import time import unittest from hydrus.core import HydrusData -from hydrus.core import HydrusGlobals as HG from hydrus.core import HydrusNetworking from mock import patch diff --git a/hydrus/test/TestHydrusSerialisable.py b/hydrus/test/TestHydrusSerialisable.py index e4a607b7..4a4bd88a 100644 --- a/hydrus/test/TestHydrusSerialisable.py +++ b/hydrus/test/TestHydrusSerialisable.py @@ -1,28 +1,20 @@ -from hydrus.client import ClientCaches from hydrus.client import ClientConstants as CC from hydrus.client import ClientData from hydrus.client import ClientDefaults -from hydrus.client import ClientDownloading from hydrus.client import ClientDuplicates from hydrus.client.gui import ClientGUIShortcuts -from hydrus.client.importing import ClientImporting from hydrus.client.importing import ClientImportOptions from hydrus.client.importing import ClientImportSubscriptions from hydrus.client.importing import ClientImportSubscriptionQuery from hydrus.client import ClientMedia from hydrus.client import ClientMediaManagers -from hydrus.client.networking import ClientNetworkingDomain -from hydrus.client import ClientRatings from hydrus.client import ClientSearch from hydrus.client import ClientTags from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData -from hydrus.core import HydrusNetwork from hydrus.core import HydrusSerialisable from hydrus.test import TestController as TC -import os import unittest -from qtpy import QtCore as QC class TestSerialisables( unittest.TestCase ): diff --git a/hydrus/test/TestHydrusServer.py b/hydrus/test/TestHydrusServer.py index c10a7ab3..f2f9d6ed 100644 --- a/hydrus/test/TestHydrusServer.py +++ b/hydrus/test/TestHydrusServer.py @@ -1,21 +1,14 @@ from hydrus.client import ClientConstants as CC -from hydrus.client import ClientAPI from hydrus.client import ClientLocalServer from hydrus.client import ClientMedia from hydrus.client import ClientMediaManagers -from hydrus.client import ClientRatings from hydrus.client import ClientServices -from hydrus.client import ClientTags import hashlib import http.client from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusEncryption from hydrus.core import HydrusNetwork from hydrus.core import HydrusPaths -from hydrus.core import HydrusServer -from hydrus.core import HydrusServerResources -from hydrus.core import HydrusText -import json import os import random from hydrus.server import ServerFiles @@ -24,10 +17,7 @@ import ssl from hydrus.test import TestController import time import unittest -import urllib from twisted.internet import reactor -#from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol -from twisted.internet.defer import deferredGenerator, waitForDeferred import twisted.internet.ssl from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG diff --git a/hydrus/test/TestHydrusSessions.py b/hydrus/test/TestHydrusSessions.py index 5653daaa..26badd75 100644 --- a/hydrus/test/TestHydrusSessions.py +++ b/hydrus/test/TestHydrusSessions.py @@ -1,11 +1,8 @@ -from hydrus.client import ClientConstants as CC -import collections import hashlib from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusExceptions from hydrus.core import HydrusNetwork from hydrus.core import HydrusSessions -import os import unittest from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG diff --git a/hydrus/test/TestServerDB.py b/hydrus/test/TestServerDB.py index 17d54e4b..69348de9 100644 --- a/hydrus/test/TestServerDB.py +++ b/hydrus/test/TestServerDB.py @@ -1,37 +1,10 @@ -from hydrus.client import ClientConstants as CC -from hydrus.client import ClientData -from hydrus.client import ClientDB -from hydrus.client import ClientDefaults -from hydrus.client import ClientDownloading -from hydrus.client import ClientExporting -from hydrus.client import ClientFiles -from hydrus.client.gui import ClientGUIManagement -from hydrus.client.gui import ClientGUIPages -from hydrus.client.importing import ClientImporting -from hydrus.client.importing import ClientImportLocal -from hydrus.client.importing import ClientImportOptions -from hydrus.client.importing import ClientImportFileSeeds -from hydrus.client import ClientRatings -from hydrus.client import ClientSearch -from hydrus.client import ClientServices -from hydrus.client import ClientTags -import collections from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusExceptions -from hydrus.core import HydrusVideoHandling from hydrus.core import HydrusGlobals as HG -from hydrus.core import HydrusNetwork -from hydrus.core import HydrusSerialisable -import itertools -import os from hydrus.server import ServerDB -import shutil -import sqlite3 -import stat from hydrus.test import TestController import time -import threading import unittest class TestServerDB( unittest.TestCase ):