Version 419

This commit is contained in:
Hydrus Network Developer 2020-11-25 16:24:15 -06:00 committed by GitHub
commit 193cfce4ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 4223 additions and 3361 deletions

View File

@ -8,6 +8,49 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 419</h3></li>
<ul>
<li>tag lists and editing predicates:</li>
<li>you can now set the default value for any editable system predicate. a star button beside each panel lets you set or clear the custom default</li>
<li>all editable system predicate panels now put 'recent' predicate buttons up top, for the five most entered predicates of the respective types. this is a little jank and grows pretty tall with multi-pred-type panels, but let me know what you think</li>
<li>all tag lists now support drag-selection!</li>
<li>taglists now have 'open a new OR page' menu entry when more than one tag is selected</li>
<li>when taglists can change the current search, they now have an 'add an OR to current search' menu entry when more than one tag is selected</li>
<li>OR Predicates are now editable! they launch their own little autocomplete input that is a little jank because you can technically make nested ORs, but it works!</li>
<li>system:rating is now editable! it launches the whole stack every time. the stack alignment is messed up though :/</li>
<li>invertible predicates (inbox/archive, tag/-tag, etc...) now flip on double-click only if you have one selected. if you have more than one selected, they appear as invertible buttons along with the rest of the edit UI</li>
<li>the active search predicates taglist now has an 'edit search terms' menu entry, if you find shift+double-click a pain</li>
<li>when you shift+double-click on more than one tag to add them to the current search, this is now added as an OR</li>
<li>similarly if you shift+middle-click on more than one tag, the new page is now an OR</li>
<li>when editing predicates, edited predicates now stay selected</li>
<li>shift+clicking on an already selected tag no longer adds any new selections (i.e. shift+click filling-in). this should make it nicer to do shift+double-click on selections. furthermore, the 'last clicked' focus ghost (from which a shift+click selection cascade starts) on tag lists is now cleared on edits or removes, which should reduce some other crazy/annoying select behaviour here</li>
<li>the list of active search predicates now correctly initialises sorted</li>
<li>entering hex hashes into system:hash or :similar_to now has unified hash parsing, auto-removes 'md5:'-style prefixes, and presents detailed error information when a hash is too long or short</li>
<li>.</li>
<li>faster and snappier file and tag searching:</li>
<li>searching for files by complicated wildcard (i.e. a search phrase that includes an asterisk in a non-rightmost character position) is now greatly optimised when the tag does not start with an asterisk (e.g. 'sm*l' is now much faster, '*all' is still hellmode), and now cancels (due to hitting the stop button or changing the query before results come in) much faster thanks to a new unified results fetching and cancel-checking routine</li>
<li>rewrote my autocomplete tag search to use the new namespace and subtag lookup code from the virtual siblings and parents system, unifying lookup logic and benefitting from the same new complicated wildcard optimisation and fast-cancel tech</li>
<li>autocomplete tag count aggregation (a later step, after the initial lookup) benefits from a little faster cancel tech</li>
<li>all file queries based on tag, wildcard, namespace, tag count, and tag existence now use the new fast-cancel tech. if you put in a 'has >4 tags' query and it is taking ages, changing the query or just hitting the 'stop' button should now free up the db pretty fast</li>
<li>related tags suggestions also gets the cancel tech and is now more timing precise for tags with either huge or tiny count</li>
<li>.</li>
<li>client api:</li>
<li>the /get_files/file_metadata call now returns a service_names_to_statuses_to_displayed_tags structure, which reflects the sibling-collapsed and parent-added tags, as displayed to the user in UI. the help is updated to reflect this</li>
<li>the client api version is now 15</li>
<li>.</li>
<li>the rest:</li>
<li>fixed an issue where regenerating the tag definition search cache would not tidy up the 'I am busy' modal dialog once it was done, resulting in a soft lock</li>
<li>fixed another upnp error handling bug, this time in the upnp daemon</li>
<li>updated Qt to 5.15.2 on Windows and Linux builds. this should fix the unusual button clicking area problem for some custom styles</li>
<li>.</li>
<li>boring specific code changes:</li>
<li>wrote widgets to edit invertible preds and OR preds</li>
<li>pulled the messy rating code out of the rating system predicate ui code to their own widgets</li>
<li>wrote some special predicate ui definitions and initialisation handling for OR preds and grouped 'multiple' preds (for ratings)</li>
<li>refactored search and predicate ui code to a new 'search' module</li>
<li>refactored collect and sort widgets away from search code</li>
<li>misc layout improvements for system pred edit ui</li>
</ul>
<li><h3>version 418</h3></li>
<ul>
<li>almost all system predicates are now editable if you shift+double-click them! you can also edit several at once in the same dialog</li>

View File

@ -1058,6 +1058,7 @@
"is_trashed" : false,
"known_urls" : [],
"service_names_to_statuses_to_tags" : {}
"service_names_to_statuses_to_display_tags" : {}
},
{
"file_id" : 4567,
@ -1085,9 +1086,19 @@
"2" : [ "process this later" ]
},
"my tag repository" : {
"0" : [ "blonde hair", "blue eyes", "looking at viewer" ]
"0" : [ "blonde_hair", "blue_eyes", "looking_at_viewer" ]
"1" : [ "bodysuit" ]
}
},
"service_names_to_statuses_to_display_tags" : {
"my tags" : {
"0" : [ "favourites" ]
"2" : [ "process this later", "processing" ]
},
"my tag repository" : {
"0" : [ "blonde hair", "blue eyes", "looking at viewer" ]
"1" : [ "bodysuit", "clothing" ]
}
}
}
]
@ -1113,7 +1124,7 @@
</ul>
</li>
<p>Size is in bytes. Duration is in milliseconds, and may be an int or a float.</p>
<p>The service_names_to_statuses_to_tags structure is similar to the /add_tags/add_tags scheme, excepting that the status numbers are:</p>
<p>The service_names_to_statuses_to_tags structures are similar to the /add_tags/add_tags scheme, excepting that the status numbers are:</p>
<ul>
<li>0 - current</li>
<li>1 - pending</li>
@ -1121,6 +1132,7 @@
<li>3 - petitioned</li>
</ul>
<p>Note that since JSON Object keys must be strings, these status numbers are strings, not ints.</p>
<p>While service_names_to_statuses_to_tags represents the actual tags stored on the database for a file, the service_names_to_statuses_to_display_tags structure reflects how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, use service_names_to_statuses_to_tags. If you want to render to the user, use service_names_to_statuses_to_displayed_tags.</p>
<p>If you add detailed_url_information=true, a new entry, 'detailed_known_urls', will be added for each file, with a list of the same structure as /add_urls/get_url_info. This may be an expensive request if you are querying thousands of files at once.</p>
<p>For example:</p>
<ul>

View File

@ -8,7 +8,6 @@ import psutil
import random
import re
import sqlite3
import statistics
import time
import traceback
import typing
@ -7004,7 +7003,7 @@ class DB( HydrusDB.HydrusDB ):
return ( current_count, pending_count )
def _GetAutocompleteCounts( self, tag_display_type, tag_service_id, file_service_id, tag_ids, include_current, include_pending ):
def _GetAutocompleteCounts( self, tag_display_type, tag_service_id, file_service_id, tag_ids, include_current, include_pending, job_key = None ):
if tag_service_id == self._combined_tag_service_id and file_service_id == self._combined_file_service_id:
@ -7032,6 +7031,11 @@ class DB( HydrusDB.HydrusDB ):
for search_tag_service_id in search_tag_service_ids:
if job_key is not None and job_key.IsCancelled():
return {}
cache_results.extend( self._CacheSpecificMappingsGetAutocompleteCounts( tag_display_type, file_service_id, search_tag_service_id, tag_ids ) )
@ -7164,166 +7168,64 @@ class DB( HydrusDB.HydrusDB ):
return set()
table_join = 'tags'
predicates = []
parameters = []
if exact_match:
table_join = 'subtags_searchable_map NATURAL JOIN tags'
searchable_subtag = half_complete_searchable_subtag
if self._SubtagExists( searchable_subtag ):
searchable_subtag_id = self._GetSubtagId( searchable_subtag )
predicates.append( 'searchable_subtag_id = ?' )
parameters.append( searchable_subtag_id )
else:
if not self._SubtagExists( searchable_subtag ):
return set()
if namespace != '':
searchable_subtag_id = self._GetSubtagId( searchable_subtag )
if namespace == '':
if self._NamespaceExists( namespace ):
namespace_id = self._GetNamespaceId( namespace )
predicates.append( 'namespace_id = ?' )
parameters.append( namespace_id )
else:
tag_ids = self._STS( self._c.execute( 'SELECT tag_id FROM subtags_searchable_map NATURAL JOIN tags WHERE searchable_subtag_id = ?;', ( searchable_subtag_id, ) ) )
else:
if not self._NamespaceExists( namespace ):
return set()
namespace_id = self._GetNamespaceId( namespace )
# force the smaller of the two to go first
tag_ids = self._STS( self._c.execute( 'SELECT tag_id FROM subtags_searchable_map CROSS JOIN tags USING ( subtag_id ) WHERE searchable_subtag_id = ? AND namespace_id = ?;', ( searchable_subtag_id, namespace_id ) ) )
else:
# Note: you'll have trouble doing MATCH with other OR predicates. namespace LIKE "blah" OR subtag MATCH "blah" is trouble
# AND is OK
subtag_ids = self._GetSubtagIdsFromWildcard( half_complete_searchable_subtag, job_key = job_key )
def GetSubTagSearchInfo( half_complete_searchable_subtag ):
if namespace in ( '', '*' ):
# complicated queries are passed to LIKE, because MATCH only supports appended wildcards 'gun*', and not complex stuff like '*gun*'
if ClientSearch.IsComplexWildcard( half_complete_searchable_subtag ):
# we search the 'searchable subtags', then link the various mappings back to real tags
like_param = ConvertWildcardToSQLiteLikeParameter( half_complete_searchable_subtag )
t_j = ', subtags_searchable_map, subtags ON ( tags.subtag_id = subtags_searchable_map.subtag_id AND subtags_searchable_map.searchable_subtag_id = subtags.subtag_id )'
pred = 'subtag LIKE ?'
param = like_param
else:
# simple 'sam*' style subtag, so we can search fts4 no prob
subtags_fts4_param = '"{}"'.format( half_complete_searchable_subtag )
t_j = ', subtags_fts4 ON ( subtag_id = docid )'
pred = 'subtag MATCH ?'
param = subtags_fts4_param
return ( t_j, pred, param )
if namespace != '':
if namespace == '*':
if tag_service_id != self._combined_tag_service_id:
combined_ac_cache_table_name = GenerateCombinedFilesMappingsACCacheTableName( tag_display_type, tag_service_id )
table_join = '{} NATURAL JOIN tags'.format( combined_ac_cache_table_name )
predicates.append( '1=1' )
elif '*' in namespace:
table_join = 'tags NATURAL JOIN namespaces'
like_param = ConvertWildcardToSQLiteLikeParameter( namespace )
predicates.append( 'namespace LIKE ?' )
parameters.append( like_param )
else:
result = self._c.execute( 'SELECT namespace_id FROM namespaces WHERE namespace = ?;', ( namespace, ) ).fetchone()
if result is None:
return set()
else:
( namespace_id, ) = result
predicates.append( 'namespace_id = ' + str( namespace_id ) )
if half_complete_searchable_subtag == '*':
if tag_service_id != self._combined_tag_service_id:
combined_ac_cache_table_name = GenerateCombinedFilesMappingsACCacheTableName( tag_display_type, tag_service_id )
table_join = '{} NATURAL JOIN {}'.format( table_join, combined_ac_cache_table_name )
predicates.append( '1=1' )
tag_ids = self._GetTagIdsFromSubtagIds( subtag_ids, job_key = job_key )
else:
( t_j, pred, param ) = GetSubTagSearchInfo( half_complete_searchable_subtag )
if '*' in namespace:
namespace_ids = self._GetNamespaceIdsFromWildcard( namespace )
else:
if not self._NamespaceExists( namespace ):
return set()
namespace_ids = ( self._GetNamespaceId( namespace ), )
table_join += t_j
predicates.append( pred )
parameters.append( param )
tag_ids = self._GetTagIdsFromNamespaceIdsSubtagIds( namespace_ids, subtag_ids, job_key = job_key )
predicates_phrase = ' AND '.join( predicates )
tag_ids = set()
query = 'SELECT DISTINCT tag_id FROM {} WHERE {};'.format( table_join, predicates_phrase )
if len( parameters ) > 0:
cursor = self._c.execute( query, parameters )
else:
cursor = self._c.execute( query )
group_of_tag_id_tuples = cursor.fetchmany( 1000 )
while len( group_of_tag_id_tuples ) > 0:
if job_key is not None and job_key.IsCancelled():
return set()
tag_ids.update( ( tag_id for ( tag_id, ) in group_of_tag_id_tuples ) )
group_of_tag_id_tuples = cursor.fetchmany( 1000 )
# now fetch siblings, add to set
final_tag_ids = set( tag_ids )
@ -7395,7 +7297,7 @@ class DB( HydrusDB.HydrusDB ):
return []
ids_to_count = self._GetAutocompleteCounts( tag_display_type, tag_service_id, file_service_id, group_of_tag_ids, include_current, include_pending )
ids_to_count = self._GetAutocompleteCounts( tag_display_type, tag_service_id, file_service_id, group_of_tag_ids, include_current, include_pending, job_key = job_key )
if len( ids_to_count ) == 0:
@ -8012,11 +7914,11 @@ class DB( HydrusDB.HydrusDB ):
return hash_ids
def _GetHashIdsFromNamespaceIdsSubtagIds( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, namespace_ids, subtag_ids, hash_ids = None, hash_ids_table_name = None ):
def _GetHashIdsFromNamespaceIdsSubtagIds( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, namespace_ids, subtag_ids, hash_ids = None, hash_ids_table_name = None, job_key = None ):
tag_ids = self._GetTagIdsFromNamespaceIdsSubtagIds( namespace_ids, subtag_ids )
tag_ids = self._GetTagIdsFromNamespaceIdsSubtagIds( namespace_ids, subtag_ids, job_key = job_key )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
def _GetHashIdsFromNoteName( self, name: str, hash_ids_table_name: str ):
@ -8570,11 +8472,11 @@ class DB( HydrusDB.HydrusDB ):
if query_hash_ids is None:
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag )
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, job_key = job_key )
elif is_inbox and len( query_hash_ids ) == len( self._inbox_hash_ids ):
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = self._inbox_hash_ids, hash_ids_table_name = 'file_inbox' )
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = self._inbox_hash_ids, hash_ids_table_name = 'file_inbox', job_key = job_key )
else:
@ -8582,7 +8484,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
tag_query_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
@ -8626,7 +8528,7 @@ class DB( HydrusDB.HydrusDB ):
if query_hash_ids is None:
wildcard_query_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard )
wildcard_query_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard, job_key = job_key )
else:
@ -8634,7 +8536,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
wildcard_query_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
wildcard_query_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
@ -8771,7 +8673,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
unwanted_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
unwanted_hash_ids = self._GetHashIdsFromTag( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, tag, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
query_hash_ids.difference_update( unwanted_hash_ids )
@ -8805,7 +8707,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
unwanted_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
unwanted_hash_ids = self._GetHashIdsFromWildcard( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, wildcard, hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
query_hash_ids.difference_update( unwanted_hash_ids )
@ -9126,7 +9028,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
good_hash_ids = self._GetHashIdsThatHaveTagAsNum( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, namespace, num, '>', hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
good_hash_ids = self._GetHashIdsThatHaveTagAsNum( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, namespace, num, '>', hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
query_hash_ids = intersection_update_qhi( query_hash_ids, good_hash_ids )
@ -9140,7 +9042,7 @@ class DB( HydrusDB.HydrusDB ):
self._AnalyzeTempTable( temp_table_name )
good_hash_ids = self._GetHashIdsThatHaveTagAsNum( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, namespace, num, '<', hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name )
good_hash_ids = self._GetHashIdsThatHaveTagAsNum( ClientTags.TAG_DISPLAY_ACTUAL, file_service_key, tag_search_context, namespace, num, '<', hash_ids = query_hash_ids, hash_ids_table_name = temp_table_name, job_key = job_key )
query_hash_ids = intersection_update_qhi( query_hash_ids, good_hash_ids )
@ -9190,14 +9092,14 @@ class DB( HydrusDB.HydrusDB ):
return query_hash_ids
def _GetHashIdsFromSubtagIds( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, subtag_ids, hash_ids = None, hash_ids_table_name = None ):
def _GetHashIdsFromSubtagIds( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, subtag_ids, hash_ids = None, hash_ids_table_name = None, job_key = None ):
tag_ids = self._GetTagIdsFromSubtagIds( subtag_ids )
tag_ids = self._GetTagIdsFromSubtagIds( subtag_ids, job_key = job_key )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
def _GetHashIdsFromTag( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, tag, hash_ids = None, hash_ids_table_name = None, allow_unnamespaced_to_fetch_namespaced = True ):
def _GetHashIdsFromTag( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, tag, hash_ids = None, hash_ids_table_name = None, allow_unnamespaced_to_fetch_namespaced = True, job_key = None ):
( namespace, subtag ) = HydrusTags.SplitTag( tag )
@ -9224,10 +9126,10 @@ class DB( HydrusDB.HydrusDB ):
tag_ids = ( tag_id, )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromTagIds( tag_display_type, file_service_key, tag_search_context, tag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
def _GetHashIdsFromTagIds( self, tag_display_type: int, file_service_key: bytes, tag_search_context: ClientSearch.TagSearchContext, tag_ids: typing.Collection[ int ], hash_ids = None, hash_ids_table_name = None ):
def _GetHashIdsFromTagIds( self, tag_display_type: int, file_service_key: bytes, tag_search_context: ClientSearch.TagSearchContext, tag_ids: typing.Collection[ int ], hash_ids = None, hash_ids_table_name = None, job_key = None ):
do_hash_table_join = False
@ -9250,6 +9152,13 @@ class DB( HydrusDB.HydrusDB ):
table_names = self._GetMappingTables( tag_display_type, file_service_key, tag_search_context )
cancelled_hook = None
if job_key is not None:
cancelled_hook = job_key.IsCancelled
if len( tag_ids ) == 1:
( tag_id, ) = tag_ids
@ -9266,7 +9175,9 @@ class DB( HydrusDB.HydrusDB ):
for query in queries:
result_hash_ids.update( self._STI( self._c.execute( query, ( tag_id, ) ) ) )
cursor = self._c.execute( query, ( tag_id, ) )
result_hash_ids.update( self._STI( HydrusDB.ReadFromCancellableCursor( cursor, 1024, cancelled_hook ) ) )
else:
@ -9286,7 +9197,9 @@ class DB( HydrusDB.HydrusDB ):
for query in queries:
result_hash_ids.update( self._STI( self._c.execute( query ) ) )
cursor = self._c.execute( query )
result_hash_ids.update( self._STI( HydrusDB.ReadFromCancellableCursor( cursor, 1024, cancelled_hook ) ) )
@ -9412,21 +9325,21 @@ class DB( HydrusDB.HydrusDB ):
def _GetHashIdsFromWildcard( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, wildcard, hash_ids = None, hash_ids_table_name = None ):
def _GetHashIdsFromWildcard( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, wildcard, hash_ids = None, hash_ids_table_name = None, job_key = None ):
( namespace_wildcard, subtag_wildcard ) = HydrusTags.SplitTag( wildcard )
possible_subtag_ids = self._GetSubtagIdsFromWildcard( subtag_wildcard )
possible_subtag_ids = self._GetSubtagIdsFromWildcard( subtag_wildcard, job_key = job_key )
if namespace_wildcard != '':
possible_namespace_ids = self._GetNamespaceIdsFromWildcard( namespace_wildcard )
return self._GetHashIdsFromNamespaceIdsSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_namespace_ids, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromNamespaceIdsSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_namespace_ids, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
else:
return self._GetHashIdsFromSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
@ -9466,20 +9379,15 @@ class DB( HydrusDB.HydrusDB ):
results = []
cursor = self._c.execute( query )
group_of_results = cursor.fetchmany( 1000 )
cancelled_hook = None
while len( group_of_results ) > 0:
if job_key is not None:
if job_key is not None and job_key.IsCancelled():
return []
results.extend( group_of_results )
group_of_results = cursor.fetchmany( 1000 )
cancelled_hook = job_key.IsCancelled
results = HydrusDB.ReadFromCancellableCursor( cursor, 256, cancelled_hook = cancelled_hook )
return results
@ -9557,13 +9465,13 @@ class DB( HydrusDB.HydrusDB ):
return nonzero_tag_hash_ids
def _GetHashIdsThatHaveTagAsNum( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, namespace, num, operator, hash_ids = None, hash_ids_table_name = None ):
def _GetHashIdsThatHaveTagAsNum( self, tag_display_type: int, file_service_key, tag_search_context: ClientSearch.TagSearchContext, namespace, num, operator, hash_ids = None, hash_ids_table_name = None, job_key = None ):
possible_subtag_ids = self._STS( self._c.execute( 'SELECT subtag_id FROM integer_subtags WHERE integer_subtag ' + operator + ' ' + str( num ) + ';' ) )
if namespace == '':
return self._GetHashIdsFromSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
else:
@ -9571,7 +9479,7 @@ class DB( HydrusDB.HydrusDB ):
possible_namespace_ids = { namespace_id }
return self._GetHashIdsFromNamespaceIdsSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_namespace_ids, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name )
return self._GetHashIdsFromNamespaceIdsSubtagIds( tag_display_type, file_service_key, tag_search_context, possible_namespace_ids, possible_subtag_ids, hash_ids = hash_ids, hash_ids_table_name = hash_ids_table_name, job_key = job_key )
@ -10647,23 +10555,13 @@ class DB( HydrusDB.HydrusDB ):
with HydrusDB.TemporaryIntegerTable( self._c, tag_ids, 'tag_id' ) as temp_table_name:
# temp tags to mappings
query = self._c.execute( 'SELECT hash_id FROM {} CROSS JOIN {} USING ( tag_id );'.format( temp_table_name, current_mappings_table_name ) )
cursor = self._c.execute( 'SELECT hash_id FROM {} CROSS JOIN {} USING ( tag_id );'.format( temp_table_name, current_mappings_table_name ) )
results = self._STL( query.fetchmany( 100 ) )
cancelled_hook = lambda: HydrusData.TimeHasPassedPrecise( stop_time_for_finding_files )
while len( results ) > 0:
for ( hash_id, ) in HydrusDB.ReadFromCancellableCursor( cursor, 128, cancelled_hook = cancelled_hook ):
for hash_id in results:
hash_ids_counter[ hash_id ] += 1
if HydrusData.TimeHasPassedPrecise( stop_time_for_finding_files ):
break
results = self._STL( query.fetchmany( 100 ) )
hash_ids_counter[ hash_id ] += 1
@ -11220,7 +11118,7 @@ class DB( HydrusDB.HydrusDB ):
return subtag_id
def _GetSubtagIdsFromWildcard( self, subtag_wildcard ):
def _GetSubtagIdsFromWildcard( self, subtag_wildcard, job_key = None ):
if '*' in subtag_wildcard:
@ -11230,7 +11128,20 @@ class DB( HydrusDB.HydrusDB ):
like_param = ConvertWildcardToSQLiteLikeParameter( subtag_wildcard )
return self._STL( self._c.execute( 'SELECT subtags_searchable_map.subtag_id FROM subtags_searchable_map, subtags ON ( subtags_searchable_map.searchable_subtag_id = subtags.subtag_id ) WHERE subtag LIKE ?;', ( like_param, ) ) )
if subtag_wildcard.startswith( '*' ):
cursor = self._c.execute( 'SELECT subtags_searchable_map.subtag_id FROM subtags_searchable_map, subtags ON ( subtags_searchable_map.searchable_subtag_id = subtags.subtag_id ) WHERE subtag LIKE ?;', ( like_param, ) )
else:
# we have an optimisation here--rather than searching all subtags for bl*ah, let's search all the bl* subtags for bl*ah!
prefix_fts4_wildcard = subtag_wildcard.split( '*' )[0]
prefix_fts4_wildcard_param = '"{}*"'.format( prefix_fts4_wildcard )
cursor = self._c.execute( 'SELECT docid FROM subtags_fts4 WHERE subtag MATCH ? AND EXISTS ( SELECT 1 FROM subtags_searchable_map, subtags ON ( subtags_searchable_map.searchable_subtag_id = subtags.subtag_id ) WHERE subtags_searchable_map.subtag_id = docid AND subtag LIKE ? );', ( prefix_fts4_wildcard_param, like_param ) )
else:
@ -11238,14 +11149,14 @@ class DB( HydrusDB.HydrusDB ):
subtags_fts4_param = '"{}"'.format( subtag_wildcard )
return self._STL( self._c.execute( 'SELECT docid FROM subtags_fts4 WHERE subtag MATCH ?;', ( subtags_fts4_param, ) ) )
cursor = self._c.execute( 'SELECT docid FROM subtags_fts4 WHERE subtag MATCH ?;', ( subtags_fts4_param, ) )
else:
if self._SubtagExists( subtag_wildcard ):
return self._STL( self._c.execute( 'SELECT subtags_searchable_map.subtag_id FROM subtags_searchable_map, subtags ON ( subtags_searchable_map.searchable_subtag_id = subtags.subtag_id ) WHERE subtag = ?;', ( subtag_wildcard, ) ) )
cursor = self._c.execute( 'SELECT subtags_searchable_map.subtag_id FROM subtags_searchable_map, subtags ON ( subtags_searchable_map.searchable_subtag_id = subtags.subtag_id ) WHERE subtag = ?;', ( subtag_wildcard, ) )
else:
@ -11253,6 +11164,19 @@ class DB( HydrusDB.HydrusDB ):
results = []
cancelled_hook = None
if job_key is not None:
cancelled_hook = job_key.IsCancelled
results = self._STL( HydrusDB.ReadFromCancellableCursor( cursor, 1024, cancelled_hook = cancelled_hook ) )
return results
def _GetTag( self, tag_id ):
@ -11288,26 +11212,44 @@ class DB( HydrusDB.HydrusDB ):
return tag_id
def _GetTagIdsFromNamespaceIdsSubtagIds( self, namespace_ids: typing.Collection[ int ], subtag_ids: typing.Collection[ int ] ):
def _GetTagIdsFromNamespaceIdsSubtagIds( self, namespace_ids: typing.Collection[ int ], subtag_ids: typing.Collection[ int ], job_key = None ):
with HydrusDB.TemporaryIntegerTable( self._c, subtag_ids, 'subtag_id' ) as temp_subtag_ids_table_name:
with HydrusDB.TemporaryIntegerTable( self._c, namespace_ids, 'namespace_id' ) as temp_namespace_ids_table_name:
# temp subtags to tags to temp namespaces
tag_ids = self._STS( self._c.execute( 'SELECT tag_id FROM {} CROSS JOIN tags USING ( subtag_id ) CROSS JOIN {} USING ( namespace_id );'.format( temp_subtag_ids_table_name, temp_namespace_ids_table_name ) ) )
cursor = self._c.execute( 'SELECT tag_id FROM {} CROSS JOIN tags USING ( subtag_id ) CROSS JOIN {} USING ( namespace_id );'.format( temp_subtag_ids_table_name, temp_namespace_ids_table_name ) )
cancelled_hook = None
if job_key is not None:
cancelled_hook = job_key.IsCancelled
tag_ids = self._STS( HydrusDB.ReadFromCancellableCursor( cursor, 128, cancelled_hook ) )
return tag_ids
def _GetTagIdsFromSubtagIds( self, subtag_ids: typing.Collection[ int ] ):
def _GetTagIdsFromSubtagIds( self, subtag_ids: typing.Collection[ int ], job_key = None ):
with HydrusDB.TemporaryIntegerTable( self._c, subtag_ids, 'subtag_id' ) as temp_subtag_ids_table_name:
# temp subtags to tags
tag_ids = self._STS( self._c.execute( 'SELECT tag_id FROM {} CROSS JOIN tags USING ( subtag_id );'.format( temp_subtag_ids_table_name ) ) )
cursor = self._c.execute( 'SELECT tag_id FROM {} CROSS JOIN tags USING ( subtag_id );'.format( temp_subtag_ids_table_name ) )
cancelled_hook = None
if job_key is not None:
cancelled_hook = job_key.IsCancelled
tag_ids = self._STS( HydrusDB.ReadFromCancellableCursor( cursor, 128, cancelled_hook = cancelled_hook ) )
return tag_ids
@ -18109,8 +18051,6 @@ class DB( HydrusDB.HydrusDB ):
raise
if version == 397:

View File

@ -1678,6 +1678,28 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
metadata_row[ 'service_names_to_statuses_to_tags' ] = service_names_to_statuses_to_tags
#
service_names_to_statuses_to_tags = {}
service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL )
for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items():
if service_key not in service_keys_to_names:
service_keys_to_names[ service_key ] = services_manager.GetName( service_key )
service_name = service_keys_to_names[ service_key ]
service_names_to_statuses_to_tags[ service_name ] = { str( status ) : list( tags ) for ( status, tags ) in statuses_to_tags.items() }
metadata_row[ 'service_names_to_statuses_to_display_tags' ] = service_names_to_statuses_to_tags
#
metadata.append( metadata_row )

View File

@ -452,6 +452,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
#
self._dictionary[ 'custom_default_predicates' ] = HydrusSerialisable.SerialisableList()
self._dictionary[ 'predicate_types_to_recent_predicates' ] = HydrusSerialisable.SerialisableDictionary()
#
self._dictionary[ 'favourite_tag_filters' ] = HydrusSerialisable.SerialisableDictionary()
#
@ -775,6 +781,32 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def ClearCustomDefaultSystemPredicates( self, predicate_type = None, comparable_predicate = None ):
with self._lock:
custom_default_predicates = self._dictionary[ 'custom_default_predicates' ]
if predicate_type is not None:
new_custom_default_predicates = HydrusSerialisable.SerialisableList( [ pred for pred in custom_default_predicates if pred.GetType() != predicate_type ] )
self._dictionary[ 'custom_default_predicates' ] = new_custom_default_predicates
return
if comparable_predicate is not None:
new_custom_default_predicates = HydrusSerialisable.SerialisableList( [ pred for pred in custom_default_predicates if not pred.IsUIEditable( comparable_predicate ) ] )
self._dictionary[ 'custom_default_predicates' ] = new_custom_default_predicates
return
def FlipBoolean( self, name ):
with self._lock:
@ -814,6 +846,26 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetCustomDefaultSystemPredicates( self, predicate_type = None, comparable_predicate = None ):
with self._lock:
custom_default_predicates = self._dictionary[ 'custom_default_predicates' ]
if predicate_type is not None:
return [ pred for pred in custom_default_predicates if pred.GetType() == predicate_type ]
if comparable_predicate is not None:
return [ pred for pred in custom_default_predicates if pred.IsUIEditable( comparable_predicate ) ]
return []
def GetDefaultFileImportOptions( self, options_type ):
with self._lock:
@ -1026,6 +1078,26 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetRecentPredicates( self, predicate_types ):
with self._lock:
result = []
predicate_types_to_recent_predicates = self._dictionary[ 'predicate_types_to_recent_predicates' ]
for predicate_type in predicate_types:
if predicate_type in predicate_types_to_recent_predicates:
result.extend( predicate_types_to_recent_predicates[ predicate_type ] )
return result
def GetSimpleDownloaderFormulae( self ):
with self._lock:
@ -1085,6 +1157,38 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def PushRecentPredicates( self, predicates ):
with self._lock:
predicate_types_to_recent_predicates = self._dictionary[ 'predicate_types_to_recent_predicates' ]
for predicate in predicates:
predicate_type = predicate.GetType()
if predicate_type not in predicate_types_to_recent_predicates:
predicate_types_to_recent_predicates[ predicate_type ] = HydrusSerialisable.SerialisableList()
recent_predicates = predicate_types_to_recent_predicates[ predicate_type ]
if predicate in recent_predicates:
recent_predicates.remove( predicate )
recent_predicates.insert( 0, predicate )
while len( recent_predicates ) > 5:
recent_predicates.pop( 5 )
def SetBoolean( self, name, value ):
with self._lock:
@ -1120,6 +1224,44 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def SetCustomDefaultSystemPredicates( self, predicate_type = None, predicates = None, comparable_predicates = None ):
with self._lock:
custom_default_predicates = self._dictionary[ 'custom_default_predicates' ]
if predicate_type is not None and predicates is not None:
new_custom_default_predicates = HydrusSerialisable.SerialisableList( [ pred for pred in custom_default_predicates if pred.GetType() != predicate_type ] )
new_custom_default_predicates.extend( predicates )
self._dictionary[ 'custom_default_predicates' ] = new_custom_default_predicates
return
if comparable_predicates is not None:
new_custom_default_predicates = HydrusSerialisable.SerialisableList()
for pred in custom_default_predicates:
if True not in ( pred.IsUIEditable( comparable_predicate ) for comparable_predicate in comparable_predicates ):
new_custom_default_predicates.append( pred )
new_custom_default_predicates.extend( comparable_predicates )
self._dictionary[ 'custom_default_predicates' ] = new_custom_default_predicates
return
def SetDefaultFileImportOptions( self, options_type, file_import_options ):
with self._lock:

View File

@ -2038,6 +2038,11 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
u = re.sub( r'^.*\shttp', 'http', u )
while u.startswith( 'https://https://' ):
u = u[8:]
return u

View File

@ -1664,6 +1664,11 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
return self._inclusive
def IsInvertible( self ):
return self.GetInverseCopy() is not None
def IsMutuallyExclusive( self, predicate ):
if self._predicate_type == PREDICATE_TYPE_SYSTEM_EVERYTHING:
@ -1671,7 +1676,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
return True
if predicate == self.GetInverseCopy():
if self.IsInvertible() and predicate == self.GetInverseCopy():
return True

View File

@ -3101,7 +3101,14 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
status_hook = lambda s: job_key.SetVariable( 'popup_text_1', s )
self._controller.Write( 'repopulate_tag_search_cache', status_hook = status_hook )
def do_it():
self._controller.WriteSynchronous( 'repopulate_tag_search_cache', status_hook = status_hook )
job_key.Finish()
self._controller.CallToThreadLongRunning( do_it )

View File

@ -8,11 +8,11 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUIACDropdown
class RatingLikeSubPanel( QW.QWidget ):

View File

@ -10,13 +10,13 @@ from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUIFrames
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.search import ClientGUIACDropdown
class Dialog( QP.Dialog ):

View File

@ -14,7 +14,6 @@ from hydrus.core import HydrusPaths
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientExporting
from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIScrolledPanels
@ -25,6 +24,7 @@ from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.metadata import ClientTags
from hydrus.client.gui.search import ClientGUIACDropdown
class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ):

View File

@ -16,7 +16,6 @@ from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIControls
from hydrus.client.gui import ClientGUICore as CGC
@ -35,6 +34,7 @@ from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.importing import ClientImporting
from hydrus.client.importing import ClientImportLocal
from hydrus.client.importing import ClientImportOptions

View File

@ -21,7 +21,6 @@ from hydrus.client import ClientParsing
from hydrus.client import ClientPaths
from hydrus.client import ClientSearch
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICanvas
from hydrus.client.gui import ClientGUICanvasFrame
from hydrus.client.gui import ClientGUICommon
@ -34,16 +33,18 @@ from hydrus.client.gui import ClientGUIImport
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIParsing
from hydrus.client.gui import ClientGUIResults
from hydrus.client.gui import ClientGUIResultsSortCollect
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIFileSeedCache
from hydrus.client.gui import ClientGUIGallerySeedLog
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUISearch
from hydrus.client.importing import ClientImportGallery
from hydrus.client.importing import ClientImportLocal
from hydrus.client.importing import ClientImportOptions
@ -681,6 +682,11 @@ class ListBoxTagsMediaManagementPanel( ClientGUIListBoxes.ListBoxTagsMedia ):
if len( predicates ) > 0:
if shift_down and len( predicates ) > 1:
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, predicates ), )
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates )
return True
@ -713,23 +719,41 @@ class ListBoxTagsMediaManagementPanel( ClientGUIListBoxes.ListBoxTagsMedia ):
def _ProcessMenuPredicateEvent( self, command ):
( predicates, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
p = None
permit_remove = True
permit_add = True
if command == 'add_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates, permit_remove = False )
p = predicates
permit_remove = False
elif command == 'add_or_predicate':
p = ( or_predicate, )
permit_remove = False
elif command == 'remove_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates, permit_add = False )
p = predicates
permit_add = False
elif command == 'add_inverse_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, inverse_predicates, permit_remove = False )
p = inverse_predicates
permit_remove = False
elif command == 'remove_inverse_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, inverse_predicates, permit_add = False )
p = predicates
permit_add = False
if p is not None:
HG.client_controller.pub( 'enter_predicates', self._page_key, p, permit_remove = permit_remove, permit_add = permit_add )
@ -764,11 +788,11 @@ class ManagementPanel( QW.QScrollArea ):
self._current_selection_tags_list = None
self._media_sort = ClientGUISearch.MediaSortControl( self, management_controller = self._management_controller )
self._media_sort = ClientGUIResultsSortCollect.MediaSortControl( self, management_controller = self._management_controller )
silent_collect = not self.SHOW_COLLECT
self._media_collect = ClientGUISearch.MediaCollectControl( self, management_controller = self._management_controller, silent = silent_collect )
self._media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self, management_controller = self._management_controller, silent = silent_collect )
if not self.SHOW_COLLECT:

View File

@ -0,0 +1,483 @@
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 HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import QtPorting as QP
from hydrus.client.media import ClientMedia
class MediaCollectControl( QW.QWidget ):
def __init__( self, parent, management_controller = None, silent = False ):
QW.QWidget.__init__( self, parent )
# this is trash, rewrite it to deal with the media_collect object, not the management controller
self._management_controller = management_controller
if self._management_controller is not None and self._management_controller.HasVariable( 'media_collect' ):
self._media_collect = self._management_controller.GetVariable( 'media_collect' )
else:
self._media_collect = HG.client_controller.new_options.GetDefaultCollect()
self._silent = silent
self._collect_comboctrl = QP.CollectComboCtrl( self, self._media_collect )
self._collect_unmatched = ClientGUICommon.BetterChoice( self )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._collect_unmatched, 19 )
self._collect_unmatched.setMinimumWidth( width )
self._collect_unmatched.addItem( 'collect unmatched', True )
self._collect_unmatched.addItem( 'leave unmatched', False )
#
self._collect_unmatched.SetValue( self._media_collect.collect_unmatched )
#
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._collect_comboctrl, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._collect_unmatched, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
#
self._UpdateLabel()
self._collect_unmatched.currentIndexChanged.connect( self.CollectValuesChanged )
self._collect_comboctrl.itemChanged.connect( self.CollectValuesChanged )
HG.client_controller.sub( self, 'SetCollectFromPage', 'set_page_collect' )
def _BroadcastCollect( self ):
if not self._silent and self._management_controller is not None:
self._management_controller.SetVariable( 'media_collect', self._media_collect )
page_key = self._management_controller.GetKey( 'page' )
HG.client_controller.pub( 'collect_media', page_key, self._media_collect )
HG.client_controller.pub( 'a_collect_happened', page_key )
def _UpdateLabel( self ):
( namespaces, rating_service_keys, description ) = self._collect_comboctrl.GetValues()
self._collect_comboctrl.SetValue( description )
def GetValue( self ):
return self._media_collect
def CollectValuesChanged( self ):
( namespaces, rating_service_keys, description ) = self._collect_comboctrl.GetValues()
self._UpdateLabel()
collect_unmatched = self._collect_unmatched.GetValue()
self._media_collect = ClientMedia.MediaCollect( namespaces = namespaces, rating_service_keys = rating_service_keys, collect_unmatched = collect_unmatched )
self._BroadcastCollect()
def SetCollect( self, media_collect ):
self._media_collect = media_collect
self._collect_comboctrl.blockSignals( True )
self._collect_unmatched.blockSignals( True )
self._collect_comboctrl.SetCollectByValue( self._media_collect )
self._collect_unmatched.SetValue( self._media_collect.collect_unmatched )
self._UpdateLabel()
self._collect_comboctrl.blockSignals( False )
self._collect_unmatched.blockSignals( False )
self._BroadcastCollect()
def SetCollectFromPage( self, page_key, media_collect ):
if page_key == self._management_controller.GetKey( 'page' ):
self.SetCollect( media_collect )
self._BroadcastCollect()
class MediaSortControl( QW.QWidget ):
sortChanged = QC.Signal( ClientMedia.MediaSort )
def __init__( self, parent, management_controller = None ):
QW.QWidget.__init__( self, parent )
self._management_controller = management_controller
self._sort_type = ( 'system', CC.SORT_FILES_BY_FILESIZE )
self._sort_type_button = ClientGUICommon.BetterButton( self, 'sort', self._SortTypeButtonClick )
self._sort_order_choice = ClientGUICommon.BetterChoice( self )
type_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._sort_type_button, 14 )
self._sort_type_button.setMinimumWidth( type_width )
asc_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._sort_order_choice, 14 )
self._sort_order_choice.setMinimumWidth( asc_width )
self._sort_order_choice.addItem( '', CC.SORT_ASC )
self._UpdateSortTypeLabel()
self._UpdateAscLabels()
#
hbox = QP.HBoxLayout( margin = 0 )
QP.AddToLayout( hbox, self._sort_type_button, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._sort_order_choice, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
HG.client_controller.sub( self, 'ACollectHappened', 'a_collect_happened' )
HG.client_controller.sub( self, 'BroadcastSort', 'do_page_sort' )
if self._management_controller is not None and self._management_controller.HasVariable( 'media_sort' ):
media_sort = self._management_controller.GetVariable( 'media_sort' )
try:
self.SetSort( media_sort )
except:
default_sort = ClientMedia.MediaSort( ( 'system', CC.SORT_FILES_BY_FILESIZE ), CC.SORT_ASC )
self.SetSort( default_sort )
self._sort_order_choice.currentIndexChanged.connect( self.EventSortAscChoice )
def _BroadcastSort( self ):
media_sort = self._GetCurrentSort()
if self._management_controller is not None:
self._management_controller.SetVariable( 'media_sort', media_sort )
self.sortChanged.emit( media_sort )
def _GetCurrentSort( self ) -> ClientMedia.MediaSort:
sort_order = self._sort_order_choice.GetValue()
media_sort = ClientMedia.MediaSort( self._sort_type, sort_order )
return media_sort
def _PopulateSortMenuOrList( self, menu = None ):
sort_types = []
menu_items_and_sort_types = []
submetatypes_to_menus = {}
for system_sort_type in CC.SYSTEM_SORT_TYPES:
sort_type = ( 'system', system_sort_type )
sort_types.append( sort_type )
if menu is not None:
submetatype = CC.system_sort_type_submetatype_string_lookup[ system_sort_type ]
if submetatype is None:
menu_to_add_to = menu
else:
if submetatype not in submetatypes_to_menus:
submenu = QW.QMenu( menu )
submetatypes_to_menus[ submetatype ] = submenu
ClientGUIMenus.AppendMenu( menu, submenu, submetatype )
menu_to_add_to = submetatypes_to_menus[ submetatype ]
label = CC.sort_type_basic_string_lookup[ system_sort_type ]
menu_item = ClientGUIMenus.AppendMenuItem( menu_to_add_to, label, 'Select this sort type.', self._SetSortType, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
namespace_sort_types = HC.options[ 'sort_by' ]
if len( namespace_sort_types ) > 0:
if menu is not None:
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenu( menu, submenu, 'namespaces' )
for ( namespaces_text, namespaces_list ) in namespace_sort_types:
sort_type = ( namespaces_text, tuple( namespaces_list ) )
sort_types.append( sort_type )
if menu is not None:
example_sort = ClientMedia.MediaSort( sort_type, CC.SORT_ASC )
label = example_sort.GetSortTypeString()
menu_item = ClientGUIMenus.AppendMenuItem( submenu, label, 'Select this sort type.', self._SetSortType, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
rating_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
if len( rating_service_keys ) > 0:
if menu is not None:
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenu( menu, submenu, 'ratings' )
for service_key in rating_service_keys:
sort_type = ( 'rating', service_key )
sort_types.append( sort_type )
if menu is not None:
example_sort = ClientMedia.MediaSort( sort_type, CC.SORT_ASC )
label = example_sort.GetSortTypeString()
menu_item = ClientGUIMenus.AppendMenuItem( submenu, label, 'Select this sort type.', self._SetSortType, sort_type )
menu_items_and_sort_types.append( ( menu_item, sort_type ) )
if menu is not None:
for ( menu_item, sort_choice ) in menu_items_and_sort_types:
if sort_choice == self._sort_type:
menu_item.setCheckable( True )
menu_item.setChecked( True )
return sort_types
def _SortTypeButtonClick( self ):
menu = QW.QMenu()
self._PopulateSortMenuOrList( menu = menu )
CGC.core().PopupMenu( self, menu )
def _SetSortType( self, sort_type ):
self._sort_type = sort_type
self._UpdateSortTypeLabel()
self._UpdateAscLabels( set_default_asc = True )
self._UserChoseASort()
self._BroadcastSort()
def _UpdateAscLabels( self, set_default_asc = False ):
media_sort = self._GetCurrentSort()
self._sort_order_choice.clear()
if media_sort.CanAsc():
( asc_str, desc_str, default_sort_order ) = media_sort.GetSortOrderStrings()
self._sort_order_choice.addItem( asc_str, CC.SORT_ASC )
self._sort_order_choice.addItem( desc_str, CC.SORT_DESC )
if set_default_asc:
sort_order_to_set = default_sort_order
else:
sort_order_to_set = media_sort.sort_order
self._sort_order_choice.SetValue( sort_order_to_set )
self._sort_order_choice.setEnabled( True )
else:
self._sort_order_choice.addItem( '', CC.SORT_ASC )
self._sort_order_choice.addItem( '', CC.SORT_DESC )
self._sort_order_choice.SetValue( CC.SORT_ASC )
self._sort_order_choice.setEnabled( False )
def _UpdateSortTypeLabel( self ):
example_sort = ClientMedia.MediaSort( self._sort_type, CC.SORT_ASC )
self._sort_type_button.setText( example_sort.GetSortTypeString() )
def _UserChoseASort( self ):
if HG.client_controller.new_options.GetBoolean( 'save_page_sort_on_change' ):
media_sort = self._GetCurrentSort()
HG.client_controller.new_options.SetDefaultSort( media_sort )
def ACollectHappened( self, page_key ):
if self._management_controller is not None:
my_page_key = self._management_controller.GetKey( 'page' )
if page_key == my_page_key:
self._BroadcastSort()
def BroadcastSort( self, page_key = None ):
if page_key is not None and page_key != self._management_controller.GetKey( 'page' ):
return
self._BroadcastSort()
def EventSortAscChoice( self, index ):
self._UserChoseASort()
self._BroadcastSort()
def GetSort( self ) -> ClientMedia.MediaSort:
return self._GetCurrentSort()
def wheelEvent( self, event ):
if event.angleDelta().y() > 0:
index_delta = -1
else:
index_delta = 1
sort_types = self._PopulateSortMenuOrList()
if self._sort_type in sort_types:
index = sort_types.index( self._sort_type )
new_index = ( index + index_delta ) % len( sort_types )
new_sort_type = sort_types[ new_index ]
self._SetSortType( new_sort_type )
event.accept()
def SetSort( self, media_sort: ClientMedia.MediaSort ):
self._sort_type = media_sort.sort_type
self._sort_order_choice.SetValue( media_sort.sort_order )
self._UpdateSortTypeLabel()
self._UpdateAscLabels()

View File

@ -19,14 +19,13 @@ from hydrus.core import HydrusText
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIControls
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIImport
from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUIResultsSortCollect
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIShortcuts
@ -37,6 +36,8 @@ from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUISearch
from hydrus.client.media import ClientMedia
from hydrus.client.networking import ClientNetworkingSessions
@ -2334,13 +2335,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
QW.QWidget.__init__( self, parent )
self._default_media_sort = ClientGUISearch.MediaSortControl( self )
self._default_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self )
self._fallback_media_sort = ClientGUISearch.MediaSortControl( self )
self._fallback_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self )
self._save_page_sort_on_change = QW.QCheckBox( self )
self._default_media_collect = ClientGUISearch.MediaCollectControl( self, silent = True )
self._default_media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self, silent = True )
namespace_sorting_box = ClientGUICommon.StaticBox( self, 'namespace sorting' )

View File

@ -31,7 +31,6 @@ from hydrus.client import ClientSearch
from hydrus.client import ClientSerialisable
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIDragDrop
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIDialogs
@ -45,6 +44,7 @@ from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.metadata import ClientTags
from hydrus.client.networking import ClientNetworkingContexts
from hydrus.client.networking import ClientNetworkingDomain

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientManagers
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIControls
from hydrus.client.gui import ClientGUICore as CGC
@ -39,6 +38,7 @@ from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientTags
from hydrus.client.metadata import ClientTagsHandling

View File

@ -145,7 +145,7 @@ class DialogApplyCancel( DialogThatTakesScrollablePanel ):
if len( message ) > 0:
QW.QMessageBox.critical( self, 'Error', message )
QW.QMessageBox.warning( self, 'Cannot OK!', message )
return False

View File

@ -22,9 +22,9 @@ from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUISearch
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientTags
@ -953,6 +953,7 @@ class ListBox( QW.QScrollArea ):
#
self.setMouseTracking( True )
self.setFont( QW.QApplication.font() )
self._widget_event_filter = QP.WidgetEventFilter( self.widget() )
@ -1016,6 +1017,11 @@ class ListBox( QW.QScrollArea ):
return action_occurred
def _AddEditMenu( self, menu: QW.QMenu ):
pass
def _DeleteActivate( self ):
pass
@ -1126,11 +1132,11 @@ class ListBox( QW.QScrollArea ):
predicates.append( term )
possible_inverse = term.GetInverseCopy()
if possible_inverse is not None:
if term.IsInvertible():
inverse_predicates.append( possible_inverse )
inverse_predicate = term.GetInverseCopy()
inverse_predicates.append( inverse_predicate )
else:
@ -1142,7 +1148,16 @@ class ListBox( QW.QScrollArea ):
return ( predicates, inverse_predicates )
if len( predicates ) > 1 and ClientSearch.PREDICATE_TYPE_OR_CONTAINER not in ( p.GetType() for p in predicates ):
or_predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, value = list( predicates ) )
else:
or_predicate = None
return ( predicates, or_predicate, inverse_predicates )
def _GetSafeHitIndex( self, hit_index, direction = None ):
@ -1235,7 +1250,7 @@ class ListBox( QW.QScrollArea ):
self._Hit( shift, ctrl, hit_index )
def _Hit( self, shift, ctrl, hit_index ):
def _Hit( self, shift, ctrl, hit_index, only_add = False ):
hit_index = self._GetSafeHitIndex( hit_index )
@ -1244,16 +1259,24 @@ class ListBox( QW.QScrollArea ):
deselect_all = False
if shift:
if only_add:
if hit_index is not None:
to_select.add( hit_index )
elif shift:
# we are now saying if you shift-click on something already selected, we'll make no changes, but we'll move focus ghost
if hit_index is not None and not self._IsSelected( hit_index ):
if self._last_hit_index is not None:
lower = min( hit_index, self._last_hit_index )
upper = max( hit_index, self._last_hit_index )
to_select = list( range( lower, upper + 1) )
to_select = list( range( lower, upper + 1 ) )
else:
@ -1543,6 +1566,8 @@ class ListBox( QW.QScrollArea ):
self._selected_terms.discard( term )
self._last_hit_index = None
del self._terms_to_texts[ term ]
@ -1728,6 +1753,22 @@ class ListBox( QW.QScrollArea ):
def mouseMoveEvent( self, event ):
is_dragging = event.buttons() & QC.Qt.LeftButton
if is_dragging:
hit_index = self._GetIndexUnderMouse( event )
self._Hit( False, False, hit_index, only_add = True )
else:
event.ignore()
def EventMouseSelect( self, event ):
self._HandleClick( event )
@ -2058,61 +2099,28 @@ class ListBoxTags( ListBox ):
return False
def _NewSearchPage( self ):
predicates = []
def _NewSearchPages( self, pages_of_predicates ):
for term in self._selected_terms:
activate_window = HG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
for predicates in pages_of_predicates:
if isinstance( term, ClientSearch.Predicate ):
predicates.append( term )
else:
predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, term ) )
predicates = ClientGUISearch.FleshOutPredicates( self, predicates )
predicates = ClientGUISearch.FleshOutPredicates( self, predicates )
if len( predicates ) > 0:
if len( predicates ) == 0:
break
s = sorted( ( predicate.ToString() for predicate in predicates ) )
page_name = ', '.join( s )
activate_window = HG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
file_service_key = self._GetCurrentFileServiceKey()
HG.client_controller.pub( 'new_page_query', file_service_key, initial_predicates = predicates, page_name = page_name, activate_window = activate_window )
def _NewSearchPageForEach( self ):
predicates = []
for term in self._selected_terms:
if isinstance( term, ClientSearch.Predicate ):
predicates.append( term )
else:
predicates.append( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, term ) )
predicates = ClientGUISearch.FleshOutPredicates( self, predicates )
for predicate in predicates:
page_name = predicate.ToString()
HG.client_controller.pub( 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY, initial_predicates = ( predicate, ), page_name = page_name )
activate_window = False
@ -2243,7 +2251,19 @@ class ListBoxTags( ListBox ):
if self.can_spawn_new_windows:
self._NewSearchPage()
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
if len( predicates ) > 0:
shift_down = event.modifiers() & QC.Qt.ShiftModifier
if shift_down and or_predicate is not None:
predicates = ( or_predicate, )
self._NewSearchPages( [ predicates ] )
@ -2591,17 +2611,28 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendMenu( menu, select_menu, 'select' )
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
if self.can_spawn_new_windows:
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'open a new search page for ' + selection_string, 'Open a new search page starting with the selected predicates.', self._NewSearchPage )
ClientGUIMenus.AppendMenuItem( menu, 'open a new search page for ' + selection_string, 'Open a new search page starting with the selected predicates.', self._NewSearchPages, [ predicates ] )
if len( self._selected_terms ) > 1:
if or_predicate is not None:
ClientGUIMenus.AppendMenuItem( menu, 'open new search pages for each in selection', 'Open one new search page for each selected predicate.', self._NewSearchPageForEach )
ClientGUIMenus.AppendMenuItem( menu, 'open a new OR search page for ' + selection_string, 'Open a new search page starting with the selected merged as an OR search predicate.', self._NewSearchPages, [ ( or_predicate, ) ] )
if len( predicates ) > 1:
for_each_predicates = [ ( predicate, ) for predicate in predicates ]
ClientGUIMenus.AppendMenuItem( menu, 'open new search pages for each in selection', 'Open one new search page for each selected predicate.', self._NewSearchPages, for_each_predicates )
self._AddEditMenu( menu )
if self._CanProvideCurrentPagePredicates():
@ -2609,8 +2640,6 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendSeparator( menu )
( predicates, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
predicates = set( predicates )
inverse_predicates = set( inverse_predicates )
@ -2629,14 +2658,19 @@ class ListBoxTags( ListBox ):
if some_selected_in_current:
ClientGUIMenus.AppendMenuItem( menu, 'discard {} from current search'.format( predicates_selection_string ), 'Remove the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_predicates' )
ClientGUIMenus.AppendMenuItem( menu, 'remove {} from current search'.format( predicates_selection_string ), 'Remove the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_predicates' )
some_selected_not_in_current = len( predicates.intersection( current_predicates ) ) < len( predicates )
if some_selected_not_in_current:
ClientGUIMenus.AppendMenuItem( menu, 'require {} for current search'.format( predicates_selection_string ), 'Add the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'add_predicates' )
ClientGUIMenus.AppendMenuItem( menu, 'add {} to current search'.format( predicates_selection_string ), 'Add the selected predicates to the current search.', self._ProcessMenuPredicateEvent, 'add_predicates' )
if or_predicate is not None:
ClientGUIMenus.AppendMenuItem( menu, 'add an OR of {} to current search'.format( predicates_selection_string ), 'Add the selected predicates as an OR predicate to the current search.', self._ProcessMenuPredicateEvent, 'add_or_predicate' )
some_selected_are_excluded_explicitly = HydrusData.SetsIntersect( inverse_predicates, current_predicates )
@ -2868,13 +2902,6 @@ class ListBoxTagsPredicates( ListBoxTags ):
return predicate in self._terms
def _Hit( self, shift, ctrl, hit_index ):
hit_index = self._GetSafeHitIndex( hit_index )
ListBoxTags._Hit( self, shift, ctrl, hit_index )
def _Select( self, index ):
to_select = self._GetWithParentIndices( index )

View File

@ -21,12 +21,13 @@ from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIResultsSortCollect
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.search import ClientGUISearch
from hydrus.client.metadata import ClientTags
from hydrus.external import LogicExpressionQueryParser
@ -1494,7 +1495,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
searchChanged = QC.Signal( ClientSearch.FileSearchContext )
searchCancelled = QC.Signal()
def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearch.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUISearch.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUISearch.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, force_system_everything = False, hide_favourites_edit_actions = False ):
def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearch.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, force_system_everything = False, hide_favourites_edit_actions = False ):
self._page_key = page_key
@ -1834,7 +1835,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
def _ManageFavouriteSearches( self, favourite_search_row_to_save = None ):
from hydrus.client.gui import ClientGUISearchPanels
from hydrus.client.gui.search import ClientGUISearchPanels
favourite_searches_rows = HG.client_controller.favourite_search_manager.GetFavouriteSearchRows()
@ -2118,6 +2119,8 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate
self._AppendTerm( predicate )
self._SortByText()
self._DataHasChanged()
@ -2143,6 +2146,29 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate
return False
def _AddEditMenu( self, menu: QW.QMenu ):
( editable_predicates, non_editable_predicates ) = ClientGUISearch.GetEditablePredicates( self._selected_terms )
if len( editable_predicates ) > 0:
ClientGUIMenus.AppendSeparator( menu )
if len( editable_predicates ) == 1:
desc = list( editable_predicates )[0].ToString()
else:
desc = '{} search terms'.format( HydrusData.ToHumanInt( len( editable_predicates ) ) )
label = 'edit {}'.format( desc )
ClientGUIMenus.AppendMenuItem( menu, label, 'Edit these predicates and refresh the search. Not all predicates are editable.', self._EditPredicates, editable_predicates )
def _CanProvideCurrentPagePredicates( self ):
return True
@ -2188,6 +2214,8 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate
self._AppendTerm( predicate )
self._selected_terms.update( predicates_to_add )
self._SortByText()
self._DataHasChanged()
@ -2264,12 +2292,19 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate
def _ProcessMenuPredicateEvent( self, command ):
( predicates, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
( predicates, or_predicate, inverse_predicates ) = self._GetSelectedPredicatesAndInverseCopies()
if command == 'add_predicates':
self._EnterPredicates( predicates, permit_remove = False )
elif command == 'add_or_predicate':
if or_predicate is not None:
self._EnterPredicates( ( or_predicate, ), permit_remove = False )
elif command == 'remove_predicates':
self._EnterPredicates( predicates, permit_add = False )
@ -2301,6 +2336,8 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate
self._AppendTerm( predicate )
self._SortByText()
self._DataHasChanged()

View File

@ -0,0 +1,418 @@
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 HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIRatings
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUIPredicatesSingle
from hydrus.client.metadata import ClientRatings
class PredicateSystemRatingLikeControl( QW.QWidget ):
def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Optional[ ClientSearch.Predicate ] ):
QW.QWidget.__init__( self, parent )
self._service_key = service_key
service = HG.client_controller.services_manager.GetService( self._service_key )
name = service.GetName()
name_st = ClientGUICommon.BetterStaticText( self, name )
name_st.setAlignment( QC.Qt.AlignLeft | QC.Qt.AlignVCenter )
self._rated_checkbox = QW.QCheckBox( 'rated', self )
self._not_rated_checkbox = QW.QCheckBox( 'not rated', self )
self._rating_control = ClientGUIRatings.RatingLikeDialog( self, service_key )
#
if predicate is not None:
value = predicate.GetValue()
if value is not None:
( operator, rating, service_key ) = value
if rating == 'rated':
self._rated_checkbox.setChecked( True )
elif rating == 'not rated':
self._not_rated_checkbox.setChecked( True )
else:
if rating == 0:
self._rating_control.SetRatingState( ClientRatings.DISLIKE )
else:
self._rating_control.SetRatingState( ClientRatings.LIKE )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, name_st, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._rated_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._not_rated_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._rating_control, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
def GetPredicates( self ):
rating = None
if self._rated_checkbox.isChecked():
rating = 'rated'
elif self._not_rated_checkbox.isChecked():
rating = 'not rated'
else:
rating_state = self._rating_control.GetRatingState()
if rating_state == ClientRatings.LIKE:
rating = 1
elif rating_state == ClientRatings.DISLIKE:
rating = 0
if rating is None:
return []
else:
predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ( '=', rating, self._service_key ) )
return [ predicate ]
class PredicateSystemRatingNumericalControl( QW.QWidget ):
def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Optional[ ClientSearch.Predicate ] ):
QW.QWidget.__init__( self, parent )
self._service_key = service_key
service = HG.client_controller.services_manager.GetService( self._service_key )
name = service.GetName()
name_st = ClientGUICommon.BetterStaticText( self, name )
name_st.setAlignment( QC.Qt.AlignLeft | QC.Qt.AlignVCenter )
self._rated_checkbox = QW.QCheckBox( 'rated', self )
self._not_rated_checkbox = QW.QCheckBox( 'not rated', self )
self._operator = QP.RadioBox( self, choices = [ '>', '<', '=', '\u2248'] )
self._rating_control = ClientGUIRatings.RatingNumericalDialog( self, service_key )
self._operator.Select( 2 )
#
if predicate is not None:
value = predicate.GetValue()
if value is not None:
( operator, rating, service_key ) = value
if rating == 'rated':
self._rated_checkbox.setChecked( True )
elif rating == 'not rated':
self._not_rated_checkbox.setChecked( True )
else:
self._operator.SetStringSelection( operator )
self._rating_control.SetRating( rating )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, name_st, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._rated_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._not_rated_checkbox, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._operator, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._rating_control, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
def GetPredicates( self ):
rating = None
if self._rated_checkbox.isChecked():
operator = '='
rating = 'rated'
elif self._not_rated_checkbox.isChecked():
operator = '='
rating = 'not rated'
elif self._rating_control.GetRatingState() != ClientRatings.NULL:
operator = self._operator.GetStringSelection()
rating = self._rating_control.GetRating()
if rating is None:
return []
else:
predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, ( operator, rating, self._service_key ) )
return [ predicate ]
class PanelPredicateSystemMultiple( ClientGUIPredicatesSingle.PanelPredicateSystem ):
def _FilterWhatICanEdit( self, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.Collection[ ClientSearch.Predicate ]:
raise NotImplementedError()
def _GetPredicatesToInitialisePanelWith( self, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.Collection[ ClientSearch.Predicate ]:
raise NotImplementedError()
def ClearCustomDefault( self ):
raise NotImplementedError()
def GetDefaultPredicates( self ) -> typing.Collection[ ClientSearch.Predicate ]:
raise NotImplementedError()
def GetPredicates( self ):
raise NotImplementedError()
def SaveCustomDefault( self ):
raise NotImplementedError()
def SetPredicates( self, predicates: typing.Collection[ ClientSearch.Predicate ] ):
raise NotImplementedError()
def UsesCustomDefault( self ) -> bool:
raise NotImplementedError()
class PanelPredicateSystemRating( PanelPredicateSystemMultiple ):
def __init__( self, parent, predicates ):
PanelPredicateSystemMultiple.__init__( self, parent )
#
local_like_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, ) )
self._like_checkboxes_to_info = {}
self._like_rating_ctrls = []
gridbox = QP.GridLayout( cols = 5 )
gridbox.setColumnStretch( 0, 1 )
predicates = self._GetPredicatesToInitialisePanelWith( predicates )
service_keys_to_predicates = { predicate.GetValue()[2] : predicate for predicate in predicates }
self._rating_panels = []
for service_key in local_like_service_keys:
if service_key in service_keys_to_predicates:
predicate = service_keys_to_predicates[ service_key ]
else:
predicate = None
panel = PredicateSystemRatingLikeControl( self, service_key, predicate )
self._rating_panels.append( panel )
#
local_numerical_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_NUMERICAL, ) )
self._numerical_checkboxes_to_info = {}
self._numerical_rating_ctrls_to_info = {}
for service_key in local_numerical_service_keys:
if service_key in service_keys_to_predicates:
predicate = service_keys_to_predicates[ service_key ]
else:
predicate = None
panel = PredicateSystemRatingNumericalControl( self, service_key, predicate )
self._rating_panels.append( panel )
#
vbox = QP.VBoxLayout()
for panel in self._rating_panels:
QP.AddToLayout( vbox, panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
def _FilterWhatICanEdit( self, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.Collection[ ClientSearch.Predicate ]:
local_rating_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
good_predicates = []
for predicate in predicates:
value = predicate.GetValue()
if value is not None:
( operator, rating, service_key ) = value
if service_key in local_rating_service_keys:
good_predicates.append( predicate )
return good_predicates
def _GetPredicatesToInitialisePanelWith( self, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.Collection[ ClientSearch.Predicate ]:
predicates = self._FilterWhatICanEdit( predicates )
if len( predicates ) > 0:
return predicates
custom_default_predicates = HG.client_controller.new_options.GetCustomDefaultSystemPredicates( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_RATING )
custom_default_predicates = self._FilterWhatICanEdit( custom_default_predicates )
if len( custom_default_predicates ) > 0:
return custom_default_predicates
default_predicates = self.GetDefaultPredicates()
return default_predicates
def ClearCustomDefault( self ):
HG.client_controller.new_options.ClearCustomDefaultSystemPredicates( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_RATING )
def GetDefaultPredicates( self ):
return []
def GetPredicates( self ):
predicates = []
for panel in self._rating_panels:
predicates.extend( panel.GetPredicates() )
return predicates
def SaveCustomDefault( self ):
predicates = self.GetPredicates()
HG.client_controller.new_options.SetCustomDefaultSystemPredicates( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_RATING, predicates = predicates )
def UsesCustomDefault( self ) -> bool:
custom_default_predicates = HG.client_controller.new_options.GetCustomDefaultSystemPredicates( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_RATING )
custom_default_predicates = self._FilterWhatICanEdit( custom_default_predicates )
return len( custom_default_predicates ) > 0

View File

@ -0,0 +1,73 @@
import typing
from qtpy import QtWidgets as QW
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientSearch
from hydrus.client.gui import QtPorting as QP
# ultimately, rewrite acread to be two classes, acread and acreadthatsupportsor
# and then this guy only imports the base class, and only the supportsor will know about this
# otherwise we have jank imports, and also nested OR lmao
# also this should take file and tag domain
class ORPredicateControl( QW.QWidget ):
def __init__( self, parent: QW.QWidget, predicate: ClientSearch.Predicate ):
QW.QWidget.__init__( self, parent )
from hydrus.client.gui.search import ClientGUIACDropdown
if predicate.GetType() != ClientSearch.PREDICATE_TYPE_OR_CONTAINER:
raise Exception( 'Launched an ORPredicateControl without an OR Pred!' )
predicates = predicate.GetValue()
page_key = HydrusData.GenerateKey()
file_search_context = ClientSearch.FileSearchContext( file_service_key = CC.LOCAL_FILE_SERVICE_KEY, predicates = predicates )
self._search_control = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context, hide_favourites_edit_actions = True )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._search_control, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( vbox )
def CheckValid( self ):
try:
predicates = self.GetPredicates()
except Exception as e:
raise HydrusExceptions.VetoException( str( e ) )
def GetPredicates( self ):
or_sub_predicates = self._search_control.GetPredicates()
if len( or_sub_predicates ) == 0:
return []
elif len( or_sub_predicates ) == 1:
return or_sub_predicates
or_predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, or_sub_predicates )
return [ or_predicate ]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,699 @@
import os
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 HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUIPredicatesMultiple
from hydrus.client.gui.search import ClientGUIPredicatesSingle
from hydrus.client.gui.search import ClientGUIPredicatesOR
EDIT_PRED_TYPES = {
ClientSearch.PREDICATE_TYPE_SYSTEM_AGE,
ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT,
ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH,
ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO,
ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS,
ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION,
ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE,
ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES,
ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE,
ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS,
ClientSearch.PREDICATE_TYPE_SYSTEM_HASH,
ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT,
ClientSearch.PREDICATE_TYPE_SYSTEM_MIME,
ClientSearch.PREDICATE_TYPE_SYSTEM_RATING,
ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS,
ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES,
ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME,
ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS,
ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO,
ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE,
ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER,
ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT,
ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS,
ClientSearch.PREDICATE_TYPE_OR_CONTAINER
}
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 EditPredicates( widget: QW.QWidget, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.List[ ClientSearch.Predicate ]:
( editable_predicates, non_editable_predicates ) = GetEditablePredicates( predicates )
window = widget.window()
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
if len( editable_predicates ) == 1 and editable_predicates[0].IsInvertible():
result = list( non_editable_predicates )
result.append( editable_predicates[0].GetInverseCopy() )
return result
elif len( editable_predicates ) > 0:
with ClientGUITopLevelWindowsPanels.DialogEdit( window, 'edit predicates' ) as dlg:
panel = EditPredicatesPanel( dlg, predicates )
dlg.SetPanel( panel )
if dlg.exec() == QW.QDialog.Accepted:
edited_predicates = panel.GetValue()
HG.client_controller.new_options.PushRecentPredicates( edited_predicates )
result = list( non_editable_predicates )
result.extend( edited_predicates )
return result
raise HydrusExceptions.CancelledException()
def FilterAndConvertLabelPredicates( predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.List[ ClientSearch.Predicate ]:
good_predicates = []
for predicate in predicates:
predicate = predicate.GetCountlessCopy()
predicate_type = predicate.GetType()
if predicate_type in ( ClientSearch.PREDICATE_TYPE_LABEL, ClientSearch.PREDICATE_TYPE_PARENT ):
continue
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_UNTAGGED:
predicate = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( None, '=', 0 ) )
good_predicates.append( predicate )
return good_predicates
def FleshOutPredicates( widget: QW.QWidget, predicates: typing.Collection[ ClientSearch.Predicate ] ) -> typing.List[ ClientSearch.Predicate ]:
window = widget.window()
predicates = FilterAndConvertLabelPredicates( predicates )
good_predicates = []
for predicate in predicates:
value = predicate.GetValue()
predicate_type = predicate.GetType()
if value is None and predicate_type in FLESH_OUT_SYSTEM_PRED_TYPES:
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
with ClientGUITopLevelWindowsPanels.DialogEdit( window, 'input predicate', hide_buttons = True ) as dlg:
panel = FleshOutPredicatePanel( dlg, predicate )
dlg.SetPanel( panel )
if dlg.exec() == QW.QDialog.Accepted:
preds = panel.GetValue()
HG.client_controller.new_options.PushRecentPredicates( preds )
good_predicates.extend( preds )
else:
good_predicates.append( predicate )
return good_predicates
def GetEditablePredicates( predicates: typing.Collection[ ClientSearch.Predicate ] ):
editable_predicates = [ predicate for predicate in predicates if predicate.GetType() in EDIT_PRED_TYPES or predicate.IsInvertible() ]
non_editable_predicates = [ predicate for predicate in predicates if predicate not in editable_predicates ]
return ( editable_predicates, non_editable_predicates )
class EditPredicatesPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, predicates: typing.Collection[ ClientSearch.Predicate ] ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
predicates = list( predicates )
predicates.sort( key = lambda p: p.ToString( with_count = False ) )
self._uneditable_predicates = []
self._invertible_pred_buttons = []
self._editable_pred_panels = []
rating_preds = []
# I hate this pred comparison stuff, but let's hang in there until we split this stuff up by type mate
# then we can just have a dict type->panel_class lookup or whatever
# also it would be nice to have proper rating editing here, think about it
AGE_DELTA_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '>', 'delta', ( 2000, 1, 1, 1 ) ) )
MODIFIED_DELTA_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME, ( '>', 'delta', ( 2000, 1, 1, 1 ) ) )
KNOWN_URL_EXACT = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ( True, 'exact_match', '', '' ) )
KNOWN_URL_DOMAIN = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ( True, 'domain', '', '' ) )
KNOWN_URL_REGEX = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS, ( True, 'regex', '', '' ) )
FILE_VIEWS_PRED = ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS, ( 'views', ( 'media', ), '>', 0 ) )
for predicate in predicates:
predicate_type = predicate.GetType()
if predicate_type == ClientSearch.PREDICATE_TYPE_OR_CONTAINER:
self._editable_pred_panels.append( ClientGUIPredicatesOR.ORPredicateControl( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_AGE:
if predicate.IsUIEditable( AGE_DELTA_PRED ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemAgeDelta( self, predicate ) )
else:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemAgeDate( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:
if predicate.IsUIEditable( MODIFIED_DELTA_PRED ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDelta( self, predicate ) )
else:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDate( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemHeight( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemWidth( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemRatio( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumPixels( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemDuration( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemFramerate( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumFrames( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemFileService( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
if predicate.IsUIEditable( KNOWN_URL_EXACT ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsExactURL( self, predicate ) )
elif predicate.IsUIEditable( KNOWN_URL_DOMAIN ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsDomain( self, predicate ) )
elif predicate.IsUIEditable( KNOWN_URL_REGEX ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsRegex( self, predicate ) )
else:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsURLClass( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_HASH:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemHash( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemLimit( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_MIME:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemMime( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumTags( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumNotes( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemHasNoteName( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemNumWords( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemSimilarTo( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemSize( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemTagAsNumber( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_COUNT:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemDuplicateRelationships( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
if predicate.IsUIEditable( FILE_VIEWS_PRED ):
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemFileViewingStatsViews( self, predicate ) )
else:
self._editable_pred_panels.append( ClientGUIPredicatesSingle.PanelPredicateSystemFileViewingStatsViewtime( self, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_RATING:
rating_preds.append( predicate )
elif predicate.IsInvertible():
self._invertible_pred_buttons.append( ClientGUIPredicatesSingle.InvertiblePredicateButton( self, predicate ) )
else:
self._uneditable_predicates.append( predicate )
if len( rating_preds ) > 0:
self._editable_pred_panels.append( ClientGUIPredicatesMultiple.PanelPredicateSystemRating( self, rating_preds ) )
vbox = QP.VBoxLayout()
for button in self._invertible_pred_buttons:
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_PERPENDICULAR )
for panel in self._editable_pred_panels:
QP.AddToLayout( vbox, panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
def CheckValid( self ):
for panel in self._editable_pred_panels:
panel.CheckValid()
def GetValue( self ):
return_predicates = list( self._uneditable_predicates )
for button in self._invertible_pred_buttons:
return_predicates.append( button.GetPredicate() )
for panel in self._editable_pred_panels:
return_predicates.extend( panel.GetPredicates() )
return return_predicates
class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent, predicate: ClientSearch.Predicate ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
predicate_type = predicate.GetType()
self._predicates = []
label = None
editable_pred_panels = []
static_pred_buttons = []
recent_predicate_types = [ predicate_type ]
if predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_AGE:
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 0, 1, 0 ) ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 0, 7, 0 ) ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, ( '<', 'delta', ( 0, 1, 0, 0 ) ) ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemAgeDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemAgeDate, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDelta, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemModifiedDate, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_DIMENSIONS:
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_PIXELS ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 16, 9 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 9, 16 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 4, 3 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_RATIO, ( '=', 1, 1 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1920 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 1080 ) ) ), forced_label = '1080p' ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 1280 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 720 ) ) ), forced_label = '720p' ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_WIDTH, ( '=', 3840 ) ), ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HEIGHT, ( '=', 2160 ) ) ), forced_label = '4k' ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHeight, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemWidth, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemRatio, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumPixels, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION:
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_FRAMES ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '>', 0 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_DURATION, ( '=', 0 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( '=', 30 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FRAMERATE, ( '=', 60 ) ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemDuration, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFramerate, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumFrames, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_SERVICE:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFileService, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_KNOWN_URLS:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsExactURL, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsDomain, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsRegex, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemKnownURLsURLClass, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO:
recent_predicate_types = []
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, True ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_AUDIO, False ), ) ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_HASH:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHash, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT:
label = 'system:limit clips a large search result down to the given number of files. It is very useful for processing in smaller batches.'
label += os.linesep * 2
label += 'For all the simpler sorts (filesize, duration, etc...), it will select the n largest/smallest in the result set appropriate for that sort. For complicated sorts like tags, it will sample randomly.'
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 64 ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 256 ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_LIMIT, 1024 ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemLimit, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_MIME:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemMime, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS:
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( None, '>', 0 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_TAGS, ( None, '=', 0 ) ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumTags, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NOTES:
recent_predicate_types = [ ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME ]
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '>', 0 ) ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '=', 0 ) ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumNotes, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemHasNoteName, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_WORDS:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemNumWords, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_RATING:
services_manager = HG.client_controller.services_manager
ratings_services = services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
if len( ratings_services ) > 0:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesMultiple.PanelPredicateSystemRating, ( predicate, ) ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemSimilarTo, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_SIZE:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemSize, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemTagAsNumber, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS:
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING, False ), ) ) )
static_pred_buttons.append( ClientGUIPredicatesSingle.StaticSystemPredicateButton( self, self, ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_RELATIONSHIPS_KING, True ), ) ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemDuplicateRelationships, predicate ) )
elif predicate_type == ClientSearch.PREDICATE_TYPE_SYSTEM_FILE_VIEWING_STATS:
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFileViewingStatsViews, predicate ) )
editable_pred_panels.append( self._PredOKPanel( self, ClientGUIPredicatesSingle.PanelPredicateSystemFileViewingStatsViewtime, predicate ) )
vbox = QP.VBoxLayout()
if label is not None:
st = ClientGUICommon.BetterStaticText( self, label = label )
st.setWordWrap( True )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
recent_predicates = []
if len( recent_predicate_types ) > 0:
recent_predicates = HG.client_controller.new_options.GetRecentPredicates( recent_predicate_types )
if len( recent_predicates ) > 0:
recent_predicates_box = ClientGUICommon.StaticBox( self, 'recent' )
for recent_predicate in recent_predicates:
button = ClientGUIPredicatesSingle.StaticSystemPredicateButton( recent_predicates_box, self, ( recent_predicate, ) )
recent_predicates_box.Add( button, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, recent_predicates_box, CC.FLAGS_EXPAND_PERPENDICULAR )
for button in static_pred_buttons:
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_PERPENDICULAR )
for panel in editable_pred_panels:
QP.AddToLayout( vbox, panel, CC.FLAGS_EXPAND_PERPENDICULAR )
if len( static_pred_buttons ) > 0 and len( editable_pred_panels ) == 0:
HG.client_controller.CallAfterQtSafe( static_pred_buttons[0], static_pred_buttons[0].setFocus, QC.Qt.OtherFocusReason )
self.widget().setLayout( vbox )
def GetValue( self ):
return self._predicates
def SubPanelOK( self, predicates ):
self._predicates = predicates
self.parentWidget().DoOK()
class _PredOKPanel( QW.QWidget ):
def __init__( self, parent, predicate_panel_class, predicate ):
QW.QWidget.__init__( self, parent )
self._defaults_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().star, self._DefaultsMenu )
self._defaults_button.setToolTip( 'Set a new default.' )
self._predicate_panel = predicate_panel_class( self, predicate )
self._parent = parent
self._ok = QW.QPushButton( 'ok', self )
self._ok.clicked.connect( self._DoOK )
self._ok.setObjectName( 'HydrusAccept' )
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._defaults_button, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._predicate_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( hbox, self._ok, CC.FLAGS_CENTER_PERPENDICULAR )
self.setLayout( hbox )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason )
def _DefaultsMenu( self ):
menu = QW.QMenu()
ClientGUIMenus.AppendMenuItem( menu, 'set this as new default', 'Set the current value of this as the new default for this predicate.', self._predicate_panel.SaveCustomDefault )
if self._predicate_panel.UsesCustomDefault():
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'reset to original default', 'Clear the custom default value for this predicate.', self._predicate_panel.ClearCustomDefault )
CGC.core().PopupMenu( self, menu )
def _DoOK( self ):
try:
self._predicate_panel.CheckValid()
except Exception as e:
QW.QMessageBox.warning( self, 'Predicate not valid!', str( e ) )
return
predicates = self._predicate_panel.GetPredicates()
self._parent.SubPanelOK( predicates )
def keyPressEvent( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ):
self._DoOK()
else:
event.ignore()

View File

@ -12,8 +12,8 @@ from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIResultsSortCollect
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
@ -31,12 +31,12 @@ class EditFavouriteSearchPanel( ClientGUIScrolledPanels.EditPanel ):
self._foldername = QW.QLineEdit( self )
self._name = QW.QLineEdit( self )
self._media_sort = ClientGUISearch.MediaSortControl( self )
self._media_collect = ClientGUISearch.MediaCollectControl( self, silent = True )
self._media_sort = ClientGUIResultsSortCollect.MediaSortControl( self )
self._media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self, silent = True )
page_key = HydrusData.GenerateKey()
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUIACDropdown
self._tag_autocomplete = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context, media_sort_widget = self._media_sort, media_collect_widget = self._media_collect, synchronised = synchronised, hide_favourites_edit_actions = True )

View File

@ -0,0 +1 @@

View File

@ -70,8 +70,8 @@ options = {}
# Misc
NETWORK_VERSION = 19
SOFTWARE_VERSION = 418
CLIENT_API_VERSION = 14
SOFTWARE_VERSION = 419
CLIENT_API_VERSION = 15
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -46,6 +46,38 @@ def CheckCanVacuum( db_path, stop_time = None ):
HydrusPaths.CheckHasSpaceForDBTransaction( db_dir, vacuum_estimate )
def ReadFromCancellableCursor( cursor, largest_group_size, cancelled_hook = None ):
if cancelled_hook is None:
return cursor.fetchall()
NUM_TO_GET = 1
results = []
group_of_results = cursor.fetchmany( NUM_TO_GET )
while len( group_of_results ) > 0:
results.extend( group_of_results )
if cancelled_hook():
break
if NUM_TO_GET < 1024:
NUM_TO_GET *= 2
group_of_results = cursor.fetchmany( NUM_TO_GET )
return results
def ReadLargeIdQueryInSeparateChunks( cursor, select_statement, chunk_size ):
table_name = 'tempbigread' + os.urandom( 32 ).hex()

View File

@ -1038,6 +1038,43 @@ def PartitionIteratorIntoLists( pred: typing.Callable[ [ object ], bool ], strea
return ( list( a ), list( b ) )
def ParseHashesFromRawHexText( hash_type, hex_hashes_raw ):
hash_type_to_hex_length = {
'md5' : 32,
'sha1' : 40,
'sha256' : 64,
'sha512' : 128
}
hex_hashes = HydrusText.DeserialiseNewlinedTexts( hex_hashes_raw )
# convert md5:abcd to abcd
hex_hashes = [ hex_hash.split( ':' )[-1] for hex_hash in hex_hashes ]
hex_hashes = [ HydrusText.HexFilter( hex_hash ) for hex_hash in hex_hashes ]
expected_hex_length = hash_type_to_hex_length[ hash_type ]
bad_hex_hashes = [ hex_hash for hex_hash in hex_hashes if len( hex_hash ) != expected_hex_length ]
if len( bad_hex_hashes ):
m = 'Sorry, {} hashes should have {} hex characters! These did not:'.format( hash_type, expected_hex_length )
m += os.linesep * 2
m += os.linesep.join( ( '{} ({} characters)'.format( bad_hex_hash, len( bad_hex_hash ) ) for bad_hex_hash in bad_hex_hashes ) )
raise Exception( m )
hex_hashes = [ hex_hash for hex_hash in hex_hashes if len( hex_hash ) % 2 == 0 ]
hex_hashes = DedupeList( hex_hashes )
hashes = tuple( [ bytes.fromhex( hex_hash ) for hex_hash in hex_hashes ] )
return hashes
def Print( text ):
try:

View File

@ -329,7 +329,7 @@ class ServicesUPnPManager( object ):
except HydrusExceptions.RouterException:
HydrusData.Print( 'The UPnP Daemon tried to add ' + local_ip + ':' + internal_port + '->external:' + upnp_port + ' but it failed. Please try it manually to get a full log of what happened.' )
HydrusData.Print( 'The UPnP Daemon tried to add {}:{}->external:{} but it failed. Please try it manually to get a full log of what happened.'.format( local_ip, internal_port, upnp_port ) )
return

View File

@ -9,6 +9,7 @@ except:
CHARDET_OK = False
import json
import os
import re
re_newlines = re.compile( '[\r\n]+' )

View File

@ -1681,8 +1681,8 @@ class TestClientAPI( unittest.TestCase ):
file_info_manager = ClientMediaManagers.FileInfoManager( file_id, hash, size = size, mime = mime, width = width, height = height, duration = duration, has_audio = has_audio )
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue_eyes', 'blonde_hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit', 'clothing' ] } }
tags_manager = ClientMediaManagers.TagsManager( service_keys_to_statuses_to_tags, service_keys_to_statuses_to_display_tags )
@ -1747,6 +1747,24 @@ class TestClientAPI( unittest.TestCase ):
metadata_row[ 'service_names_to_statuses_to_tags' ] = service_names_to_statuses_to_tags
service_names_to_statuses_to_tags = {}
service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags( ClientTags.TAG_DISPLAY_ACTUAL )
for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items():
if service_key not in service_keys_to_names:
service_keys_to_names[ service_key ] = services_manager.GetName( service_key )
service_name = service_keys_to_names[ service_key ]
service_names_to_statuses_to_tags[ service_name ] = { str( status ) : list( tags ) for ( status, tags ) in statuses_to_tags.items() }
metadata_row[ 'service_names_to_statuses_to_display_tags' ] = service_names_to_statuses_to_tags
metadata.append( metadata_row )
detailed_known_urls_metadata_row = dict( metadata_row )
@ -1903,8 +1921,8 @@ class TestClientAPI( unittest.TestCase ):
file_info_manager = ClientMediaManagers.FileInfoManager( file_id, hash, size = size, mime = mime, width = width, height = height, duration = duration )
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue_eyes', 'blonde_hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit', 'clothing' ] } }
tags_manager = ClientMediaManagers.TagsManager( service_keys_to_statuses_to_tags, service_keys_to_statuses_to_display_tags )