Version 251

This commit is contained in:
Hydrus Network Developer 2017-04-12 16:46:46 -05:00
parent 1034350dd8
commit b4049ccb7f
25 changed files with 1588 additions and 521 deletions

View File

@ -8,8 +8,35 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 251</h3></li>
<ul>
<li>started shortcut overhaul by updating shortcut storage and underlying objects</li>
<li>shortcuts will now support multiple modifiers (ctrl, alt, shift) in some places</li>
<li>the regular shortcut entry control now supports mouse events through a radiobox (although only the duplicate filter will capture and deal with mouse events on the new system!)</li>
<li>on db creation or update, a new 'duplicate_filter' shortcut set will be generated that includes mouse shortcuts and new duplicate_filter commands</li>
<li>the duplicate filter now obeys this shortcut set</li>
<li>duplicate filter shortcut edit cog menu now works, and the active shortcut set will update if edited</li>
<li>wrote simple duplicate pair status update db code</li>
<li>added dupe filtering optimisation--given A > B, anything else better than A will be set to be better than B, and anything worse than B will be set to be worse than A</li>
<li>added dupe filtering optimisation--after any duplicate status change affecting A and B, any 'same file' siblings of A and B will receive the same relationship</li>
<li>the 'process now' button on review services is now gated by a yes/no dialog that better explains what's about to happen</li>
<li>the repository buttons on review services will disable when they can't do anything</li>
<li>repository update processing will now cancel mid-job much faster</li>
<li>reintroduced the 'service-wide update' button to tag services in review services</li>
<li>reintroduced the 'clear trash' button to the trash file service in review services</li>
<li>reintroduced num_files rating service reporting in review services</li>
<li>fixed mouse scroll wheel events from the ratings hover window not being correctly processed by the main media canvas</li>
<li>added some 'touch' event detection to try to better deal with media dragging through a touchscreen</li>
<li>import folders will now 'action' their import paths as they go, rather than only at the end of their import run</li>
<li>the new taglists are now better at remembering their selection through a content change</li>
<li>opening a file or path from a non-windows client should now create a non-child process for open/xdg-open calls that block (so closing the client should not then close the child process movie player application or whatever)</li>
<li>added unit tests for the new shortcut object</li>
<li>added unit tests for the new application command object</li>
<li>fixed unit tests for the updated shortcuts object</li>
<li>misc cleanup</li>
</ul>
<li><h3>version 250</h3></li>
<ul>
<li>improved 'file storage locations' help page descriptive text, including adding statements for actual current storage percentages</li>
<li>improved boot missing file storage error handling</li>
<li>wrote a 'repair file storage' dialog panel to manually fix missing folders during the boot phase</li>

View File

@ -75,7 +75,7 @@
<p>If you are sure you want to keep a file long-term, you should <b>archive</b> it, which will remove it from the inbox. You can archive from your selected thumbnails' right-click menu, or by pressing F7. If you make a mistake, you can spam Ctrl-Z for undo or hit Shift-F7 on any set of files to explicitly return them to the inbox.</p>
<p>Anything you do not want to keep should be deleted by selecting from the right-click menu or by hitting the delete key. Deleted files are sent to the trash. They will get a little trash icon:</p>
<p><img src="processed_imports.png" /></p>
<p>A trashed file will not appear in subsequent normal searches, although you can search the trash specifically by clicking the 'local files' button on the autocomplete dropdown. Undeleting a file (shift+delete) will return it to 'local files' as if nothing had happened. Files that remain in the trash will be permanently deleted, usually after a few days. You can change this behaviour in the client's options.</p>
<p>A trashed file will not appear in subsequent normal searches, although you can search the trash specifically by clicking the 'my files' button on the autocomplete dropdown and changing the file domain to 'trash'. Undeleting a file (shift+delete) will return it to 'my files' as if nothing had happened. Files that remain in the trash will be permanently deleted, usually after a few days. You can change the permanent deletion behaviour in the client's options.</p>
<p>A quick way of processing new files is—</p>
<h3>filtering</h3>
<p>Lets say you just downloaded a good thread, or perhaps you just imported an old folder of miscellany. You now have a whole bunch of files in your inbox—some good, some awful. You probably want to quickly go through them, saying <i>yes, yes, yes, no, yes, no, no, yes</i>, where <i>yes</i> means 'keep and archive' and <i>no</i> means 'delete this trash'. <b>Filtering</b> is the solution.</p>

View File

@ -11,6 +11,9 @@ ID_TIMER_UPDATES = wx.NewId()
#
APPLICATION_COMMAND_TYPE_SIMPLE = 0
APPLICATION_COMMAND_TYPE_CONTENT = 1
BLANK_PHASH = '\x80\x00\x00\x00\x00\x00\x00\x00' # first bit 1 but everything else 0 means only significant part of dct was [0,0], which represents flat colour
CAN_HIDE_MOUSE = True
@ -226,6 +229,40 @@ media_viewer_scale_string_lookup[ MEDIA_VIEWER_SCALE_100 ] = 'show at 100%'
media_viewer_scale_string_lookup[ MEDIA_VIEWER_SCALE_MAX_REGULAR ] = 'scale to the largest regular zoom that fits'
media_viewer_scale_string_lookup[ MEDIA_VIEWER_SCALE_TO_CANVAS ] = 'scale to the canvas size'
SHORTCUT_MODIFIER_CTRL = 0
SHORTCUT_MODIFIER_ALT = 1
SHORTCUT_MODIFIER_SHIFT = 2
shortcut_wx_to_hydrus_lookup = {}
shortcut_wx_to_hydrus_lookup[ wx.ACCEL_ALT ] = SHORTCUT_MODIFIER_ALT
shortcut_wx_to_hydrus_lookup[ wx.ACCEL_CTRL ] = SHORTCUT_MODIFIER_CTRL
shortcut_wx_to_hydrus_lookup[ wx.ACCEL_CMD ] = SHORTCUT_MODIFIER_CTRL
shortcut_wx_to_hydrus_lookup[ wx.ACCEL_SHIFT ] = SHORTCUT_MODIFIER_SHIFT
SHORTCUT_MOUSE_LEFT = 0
SHORTCUT_MOUSE_RIGHT = 1
SHORTCUT_MOUSE_MIDDLE = 2
SHORTCUT_MOUSE_SCROLL_UP = 3
SHORTCUT_MOUSE_SCROLL_DOWN = 4
shortcut_mouse_string_lookup = {}
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_LEFT ] = 'left-click'
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_RIGHT ] = 'right-click'
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_MIDDLE ] = 'middle-click'
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_SCROLL_UP ] = 'scroll up'
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_SCROLL_DOWN ] = 'scroll down'
SHORTCUT_TYPE_KEYBOARD = 0
SHORTCUT_TYPE_MOUSE = 1
SHORTCUTS_RESERVED_NAMES = [ 'duplicate_filter' ]
# shortcut commands
DUPLICATE_FILTER_ACTIONS = [ 'duplicate_filter_this_is_better', 'duplicate_filter_exactly_the_same', 'duplicate_filter_alternates', 'duplicate_filter_not_dupes', 'duplicate_filter_custom_action', 'duplicate_filter_skip' ]
SHUTDOWN_TIMESTAMP_VACUUM = 0
SHUTDOWN_TIMESTAMP_FATTEN_AC_CACHE = 1
SHUTDOWN_TIMESTAMP_DELETE_ORPHANS = 2

View File

@ -823,7 +823,6 @@ class DB( HydrusDB.HydrusDB ):
self._CacheSimilarFilesDisassociatePHashes( hash_id, phash_ids )
self._c.execute( 'DELETE FROM shape_search_cache WHERE hash_id = ?;', ( hash_id, ) )
self._c.execute( 'DELETE FROM duplicate_pairs WHERE smaller_hash_id = ? or larger_hash_id = ?;', ( hash_id, hash_id ) )
self._c.execute( 'DELETE FROM shape_maintenance_phash_regen WHERE hash_id = ?;', ( hash_id, ) )
@ -1001,6 +1000,8 @@ class DB( HydrusDB.HydrusDB ):
duplicate_types_to_count = collections.Counter( dict( self._c.execute( 'SELECT duplicate_type, COUNT( * ) FROM ' + table_join + ' WHERE ' + predicate_string + ' GROUP BY duplicate_type;' ) ) )
duplicate_types_to_count[ HC.DUPLICATE_BETTER ] = duplicate_types_to_count[ HC.DUPLICATE_SMALLER_BETTER ] + duplicate_types_to_count[ HC.DUPLICATE_LARGER_BETTER ]
return ( num_phashes_to_regen, num_branches_to_regen, searched_distances_to_count, duplicate_types_to_count )
@ -1637,6 +1638,182 @@ class DB( HydrusDB.HydrusDB ):
return similar_hash_ids
def _CacheSimilarFilesSetDuplicatePairStatus( self, duplicate_status, hash_a, hash_b, merge_options = None ):
if duplicate_status == HC.DUPLICATE_WORSE:
t = hash_a
hash_a = hash_b
hash_b = t
duplicate_status = HC.DUPLICATE_BETTER
hash_id_a = self._GetHashId( hash_a )
hash_id_b = self._GetHashId( hash_b )
self._CacheSimilarFilesSetDuplicatePairStatusSingleRow( duplicate_status, hash_id_a, hash_id_b, merge_options )
if duplicate_status == HC.DUPLICATE_BETTER:
# anything better than A is now better than B
# i.e. for all X for which X > A, set X > B
# anything worse than B is now worse than A
# i.e. for all X for which B > X, set A > X
better_than_a = set()
better_than_a.update( self._STI( self._c.execute( 'SELECT smaller_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND larger_hash_id = ?;', ( HC.DUPLICATE_SMALLER_BETTER, hash_id_a ) ) ) )
better_than_a.update( self._STI( self._c.execute( 'SELECT larger_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND smaller_hash_id = ?;', ( HC.DUPLICATE_LARGER_BETTER, hash_id_a ) ) ) )
for better_than_a_hash_id in better_than_a:
self._CacheSimilarFilesSetDuplicatePairStatusSingleRow( HC.DUPLICATE_BETTER, better_than_a_hash_id, hash_id_b, merge_options )
worse_than_b = set()
worse_than_b.update( self._STI( self._c.execute( 'SELECT smaller_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND larger_hash_id = ?;', ( HC.DUPLICATE_LARGER_BETTER, hash_id_b ) ) ) )
worse_than_b.update( self._STI( self._c.execute( 'SELECT larger_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND smaller_hash_id = ?;', ( HC.DUPLICATE_SMALLER_BETTER, hash_id_b ) ) ) )
for worse_than_b_hash_id in worse_than_b:
self._CacheSimilarFilesSetDuplicatePairStatusSingleRow( HC.DUPLICATE_BETTER, hash_id_a, worse_than_b_hash_id, merge_options )
# do a sync for better dupes that applies not_dupe and alternate relationships across better-than groups
self._CacheSimilarFilesSyncSameFileDuplicates( hash_id_a, merge_options )
self._CacheSimilarFilesSyncSameFileDuplicates( hash_id_b, merge_options )
def _CacheSimilarFilesSetDuplicatePairStatusSingleRow( self, duplicate_status, hash_id_a, hash_id_b, merge_options, only_update_given_previous_status = None ):
smaller_hash_id = min( hash_id_a, hash_id_b )
larger_hash_id = max( hash_id_a, hash_id_b )
if duplicate_status == HC.DUPLICATE_BETTER:
if smaller_hash_id == hash_id_a:
duplicate_status = HC.DUPLICATE_SMALLER_BETTER
else:
duplicate_status = HC.DUPLICATE_LARGER_BETTER
elif duplicate_status == HC.DUPLICATE_WORSE:
if smaller_hash_id == hash_id_a:
duplicate_status = HC.DUPLICATE_LARGER_BETTER
else:
duplicate_status = HC.DUPLICATE_SMALLER_BETTER
change_occured = False
if only_update_given_previous_status is None:
result = self._c.execute( 'SELECT 1 FROM duplicate_pairs WHERE smaller_hash_id = ? AND larger_hash_id = ? AND duplicate_type = ?;', ( smaller_hash_id, larger_hash_id, duplicate_status ) ).fetchone()
if result is None: # i.e. we are not needlessly overwriting
self._c.execute( 'REPLACE INTO duplicate_pairs ( smaller_hash_id, larger_hash_id, duplicate_type ) VALUES ( ?, ?, ? );', ( smaller_hash_id, larger_hash_id, duplicate_status ) )
change_occured = True
else:
self._c.execute( 'UPDATE duplicate_pairs SET duplicate_type = ? WHERE smaller_hash_id = ? AND larger_hash_id = ? AND duplicate_type = ?;', ( duplicate_status, smaller_hash_id, larger_hash_id, only_update_given_previous_status ) )
if self._GetRowCount() > 0:
change_occured = True
if change_occured and merge_options is not None:
# follow merge_options
# if better:
# do tags
# do ratings
# delete file
# if same:
# do tags
# do ratings
pass
def _CacheSimilarFilesSyncSameFileDuplicates( self, hash_id, merge_options ):
# for every known relationship our file has, that should be replicated to all of its 'same file' siblings
all_relationships = set()
for ( smaller_hash_id, duplicate_status ) in self._c.execute( 'SELECT smaller_hash_id, duplicate_type FROM duplicate_pairs WHERE duplicate_type != ? AND larger_hash_id = ?;', ( HC.DUPLICATE_UNKNOWN, hash_id ) ):
if duplicate_status == HC.DUPLICATE_SMALLER_BETTER:
duplicate_status = HC.DUPLICATE_WORSE
elif duplicate_status == HC.DUPLICATE_LARGER_BETTER:
duplicate_status = HC.DUPLICATE_BETTER
all_relationships.add( ( smaller_hash_id, duplicate_status ) )
for ( larger_hash_id, duplicate_status ) in self._c.execute( 'SELECT larger_hash_id, duplicate_type FROM duplicate_pairs WHERE duplicate_type != ? AND smaller_hash_id = ?;', ( HC.DUPLICATE_UNKNOWN, hash_id ) ):
if duplicate_status == HC.DUPLICATE_SMALLER_BETTER:
duplicate_status = HC.DUPLICATE_BETTER
elif duplicate_status == HC.DUPLICATE_LARGER_BETTER:
duplicate_status = HC.DUPLICATE_WORSE
all_relationships.add( ( larger_hash_id, duplicate_status ) )
all_siblings = set()
all_siblings.update( self._STI( self._c.execute( 'SELECT smaller_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND larger_hash_id = ?;', ( HC.DUPLICATE_SAME_FILE, hash_id ) ) ) )
all_siblings.update( self._STI( self._c.execute( 'SELECT larger_hash_id FROM duplicate_pairs WHERE duplicate_type = ? AND smaller_hash_id = ?;', ( HC.DUPLICATE_SAME_FILE, hash_id ) ) ) )
for sibling_hash_id in all_siblings:
for ( other_hash_id, duplicate_status ) in all_relationships:
if other_hash_id == sibling_hash_id:
continue
self._CacheSimilarFilesSetDuplicatePairStatusSingleRow( duplicate_status, sibling_hash_id, other_hash_id, merge_options )
def _CacheSpecificMappingsAddFiles( self, file_service_id, tag_service_id, hash_ids ):
( cache_files_table_name, cache_current_mappings_table_name, cache_pending_mappings_table_name, ac_cache_table_name ) = GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id )
@ -2252,6 +2429,13 @@ class DB( HydrusDB.HydrusDB ):
self._SetJSONDump( new_options )
list_of_shortcuts = ClientDefaults.GetDefaultShortcuts()
for shortcuts in list_of_shortcuts:
self._SetJSONDump( shortcuts )
self._c.execute( 'INSERT INTO namespaces ( namespace_id, namespace ) VALUES ( ?, ? );', ( 1, '' ) )
self._c.execute( 'INSERT INTO version ( version ) VALUES ( ? );', ( HC.SOFTWARE_VERSION, ) )
@ -5894,6 +6078,13 @@ class DB( HydrusDB.HydrusDB ):
for chunk in HydrusData.SplitListIntoChunks( content_update.GetNewFiles(), FILES_CHUNK_SIZE ):
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
files_info_rows = []
@ -5927,6 +6118,13 @@ class DB( HydrusDB.HydrusDB ):
for chunk in HydrusData.SplitListIntoChunks( content_update.GetDeletedFiles(), FILES_CHUNK_SIZE ):
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
service_hash_ids = chunk
@ -5947,6 +6145,13 @@ class DB( HydrusDB.HydrusDB ):
for chunk in HydrusData.SplitMappingListIntoChunks( content_update.GetNewMappings(), MAPPINGS_CHUNK_SIZE ):
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
mappings_ids = []
@ -5975,6 +6180,13 @@ class DB( HydrusDB.HydrusDB ):
for chunk in HydrusData.SplitMappingListIntoChunks( content_update.GetDeletedMappings(), MAPPINGS_CHUNK_SIZE ):
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
deleted_mappings_ids = []
@ -6003,6 +6215,13 @@ class DB( HydrusDB.HydrusDB ):
for chunk in HydrusData.SplitListIntoChunks( content_update.GetNewTagParents(), NEW_TAG_PARENTS_CHUNK_SIZE ):
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
parent_ids = []
@ -6031,6 +6250,13 @@ class DB( HydrusDB.HydrusDB ):
if len( deleted_parents ) > 0:
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
parent_ids = []
@ -6059,6 +6285,13 @@ class DB( HydrusDB.HydrusDB ):
if len( new_siblings ) > 0:
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
sibling_ids = []
@ -6087,6 +6320,13 @@ class DB( HydrusDB.HydrusDB ):
if len( deleted_siblings ) > 0:
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
return False
precise_timestamp = HydrusData.GetNowPrecise()
sibling_ids = []
@ -6109,6 +6349,8 @@ class DB( HydrusDB.HydrusDB ):
job_key.SetVariable( 'popup_gauge_2', ( rows_processed, total_rows ) )
return True
def _ProcessRepositoryDefinitionUpdate( self, service_id, definition_update ):
@ -6131,115 +6373,6 @@ class DB( HydrusDB.HydrusDB ):
def _ProcessContentUpdatePackage( self, service_key, content_update_package, job_key ):
( previous_journal_mode, ) = self._c.execute( 'PRAGMA journal_mode;' ).fetchone()
if previous_journal_mode == 'wal' and not self._fast_big_transaction_wal:
self._Commit()
self._c.execute( 'PRAGMA journal_mode = TRUNCATE;' )
self._BeginImmediate()
c_u_p_num_rows = content_update_package.GetNumRows()
c_u_p_total_weight_processed = 0
update_speed_string = u'writing\u2026'
content_update_index_string = 'content row ' + HydrusData.ConvertValueRangeToPrettyString( c_u_p_total_weight_processed, c_u_p_num_rows ) + ': '
quit_early = False
package_precise_timestamp = HydrusData.GetNowPrecise()
for ( content_updates, weight ) in content_update_package.IterateContentUpdateChunks():
options = self._controller.GetOptions()
if options[ 'pause_repo_sync' ]:
quit_early = True
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit:
quit_early = True
if quit_early:
package_took = HydrusData.GetNowPrecise() - package_precise_timestamp
rows_s = c_u_p_total_weight_processed / package_took
committing_string = 'wrote ' + HydrusData.ConvertIntToPrettyString( c_u_p_num_rows ) + ' rows at ' + HydrusData.ConvertIntToPrettyString( rows_s ) + ' rows/s - now committing to disk'
job_key.SetVariable( 'popup_text_2', committing_string )
HydrusData.Print( job_key.ToString() )
if previous_journal_mode == 'wal' and not self._fast_big_transaction_wal:
self._Commit()
self._c.execute( 'PRAGMA journal_mode = WAL;' )
self._BeginImmediate()
return ( False, c_u_p_total_weight_processed )
content_update_index_string = 'content row ' + HydrusData.ConvertValueRangeToPrettyString( c_u_p_total_weight_processed, c_u_p_num_rows ) + ': '
self._controller.pub( 'splash_set_status_text', content_update_index_string + update_speed_string, print_to_log = False )
job_key.SetVariable( 'popup_text_2', content_update_index_string + update_speed_string )
job_key.SetVariable( 'popup_gauge_2', ( c_u_p_total_weight_processed, c_u_p_num_rows ) )
chunk_precise_timestamp = HydrusData.GetNowPrecise()
self._ProcessContentUpdates( { service_key : content_updates }, do_pubsubs = False )
chunk_took = HydrusData.GetNowPrecise() - chunk_precise_timestamp
rows_s = weight / chunk_took
update_speed_string = 'writing at ' + HydrusData.ConvertIntToPrettyString( rows_s ) + ' rows/s'
c_u_p_total_weight_processed += weight
package_took = HydrusData.GetNowPrecise() - package_precise_timestamp
rows_s = c_u_p_total_weight_processed / package_took
committing_string = 'wrote ' + HydrusData.ConvertIntToPrettyString( c_u_p_num_rows ) + ' rows at ' + HydrusData.ConvertIntToPrettyString( rows_s ) + ' rows/s - now committing to disk'
self._controller.pub( 'splash_set_status_text', committing_string )
job_key.SetVariable( 'popup_text_2', committing_string )
job_key.SetVariable( 'popup_gauge_2', ( c_u_p_num_rows, c_u_p_num_rows ) )
HydrusData.Print( job_key.ToString() )
if previous_journal_mode == 'wal' and not self._fast_big_transaction_wal:
self._Commit()
self._c.execute( 'PRAGMA journal_mode = WAL;' )
self._BeginImmediate()
return ( True, c_u_p_total_weight_processed )
def _ProcessContentUpdates( self, service_keys_to_content_updates, do_pubsubs = True ):
notify_new_downloads = False
@ -7009,7 +7142,14 @@ class DB( HydrusDB.HydrusDB ):
content_update = HydrusSerialisable.CreateFromNetworkString( update_network_string )
self._ProcessRepositoryContentUpdate( job_key, service_id, content_update )
did_whole_update = self._ProcessRepositoryContentUpdate( job_key, service_id, content_update )
( i_paused, should_quit ) = job_key.WaitIfNeeded()
if should_quit or not did_whole_update:
return ( True, False )
self._c.execute( 'UPDATE ' + repository_updates_table_name + ' SET processed = ? WHERE hash_id = ?;', ( True, hash_id ) )
@ -8437,6 +8577,23 @@ class DB( HydrusDB.HydrusDB ):
if version == 250:
duplicate_filter = ClientData.Shortcuts( 'duplicate_filter' )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [ CC.SHORTCUT_MODIFIER_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_not_dupes' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_alternates' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_exactly_the_same' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [ CC.SHORTCUT_MODIFIER_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_UP, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
self._SetJSONDump( duplicate_filter )
self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
@ -8455,10 +8612,6 @@ class DB( HydrusDB.HydrusDB ):
file_service_ids = self._GetServiceIds( HC.AUTOCOMPLETE_CACHE_SPECIFIC_FILE_SERVICES )
# this method grew into a monster that merged deleted, pending and current according to a hierarchy of services
# this cost a lot of CPU time and was extremely difficult to maintain
# now it attempts a simpler union, not letting delete overwrite a current or pending
change_in_num_mappings = 0
change_in_num_deleted_mappings = 0
change_in_num_pending_mappings = 0
@ -9016,8 +9169,7 @@ class DB( HydrusDB.HydrusDB ):
if action == 'analyze': result = self._AnalyzeStaleBigTables( *args, **kwargs )
elif action == 'associate_repository_update_hashes': result = self._AssociateRepositoryUpdateHashes( *args, **kwargs )
elif action == 'backup': result = self._Backup( *args, **kwargs )
elif action == 'content_update_package':result = self._ProcessContentUpdatePackage( *args, **kwargs )
elif action == 'content_updates':result = self._ProcessContentUpdates( *args, **kwargs )
elif action == 'content_updates': result = self._ProcessContentUpdates( *args, **kwargs )
elif action == 'db_integrity': result = self._CheckDBIntegrity( *args, **kwargs )
elif action == 'delete_hydrus_session_key': result = self._DeleteHydrusSessionKey( *args, **kwargs )
elif action == 'delete_imageboard': result = self._DeleteYAMLDump( YAML_DUMP_ID_IMAGEBOARD, *args, **kwargs )
@ -9028,6 +9180,7 @@ class DB( HydrusDB.HydrusDB ):
elif action == 'delete_service_info': result = self._DeleteServiceInfo( *args, **kwargs )
elif action == 'delete_unknown_duplicate_pairs': result = self._CacheSimilarFilesDeleteUnknownDuplicatePairs( *args, **kwargs )
elif action == 'dirty_services': result = self._SaveDirtyServices( *args, **kwargs )
elif action == 'duplicate_pair_status': result = self._CacheSimilarFilesSetDuplicatePairStatus( *args, **kwargs )
elif action == 'export_mappings': result = self._ExportToTagArchive( *args, **kwargs )
elif action == 'file_integrity': result = self._CheckFileIntegrity( *args, **kwargs )
elif action == 'hydrus_session': result = self._AddHydrusSession( *args, **kwargs )

View File

@ -485,6 +485,112 @@ def WaitPolitely( page_key = None ):
HydrusGlobals.client_controller.pub( 'waiting_politely', page_key, False )
class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND
SERIALISABLE_VERSION = 1
def __init__( self, command_type = None, data = None ):
if command_type is None:
command_type = CC.APPLICATION_COMMAND_TYPE_SIMPLE
if data is None:
data = 'archive'
HydrusSerialisable.SerialisableBase.__init__( self )
self._command_type = command_type
self._data = data
def __cmp__( self, other ):
return cmp( self.ToString(), other.ToString() )
def _GetSerialisableInfo( self ):
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
serialisable_data = self._data
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
( service_key, content_type, action, value ) = self._data
serialisable_data = ( service_key.encode( 'hex' ), content_type, action, value )
return ( self._command_type, serialisable_data )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._command_type, serialisable_data ) = serialisable_info
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
self._data = serialisable_data
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
( serialisable_service_key, content_type, action, value ) = serialisable_data
self._data = ( serialisable_service_key.decode( 'hex' ), content_type, action, value )
def GetCommandType( self ):
return self._command_type
def GetData( self ):
return self._data
def ToString( self ):
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
return self._data
elif self._command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
( service_key, content_type, action, value ) = self._data
components = []
components.append( HC.content_update_string_lookup[ action ] )
components.append( HC.content_type_string_lookup[ content_type ] )
components.append( '"' + HydrusData.ToUnicode( value ) + '"' )
components.append( 'for' )
services_manager = HydrusGlobals.client_controller.GetServicesManager()
if services_manager.ServiceExists( service_key ):
service = services_manager.GetService( service_key )
components.append( service.GetName() )
else:
components.append( 'unknown service!' )
return ' '.join( components )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND ] = ApplicationCommand
class Booru( HydrusData.HydrusYAMLBase ):
yaml_tag = u'!Booru'
@ -1483,126 +1589,223 @@ class ImportTagOptions( HydrusSerialisable.SerialisableBase ):
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_IMPORT_TAG_OPTIONS ] = ImportTagOptions
class Shortcut( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT
SERIALISABLE_VERSION = 1
def __init__( self, shortcut_type = None, shortcut_key = None, modifiers = None ):
if shortcut_type is None:
shortcut_type = CC.SHORTCUT_TYPE_KEYBOARD
if shortcut_key is None:
shortcut_key = wx.WXK_F7
if modifiers is None:
modifiers = []
modifiers.sort()
HydrusSerialisable.SerialisableBase.__init__( self )
self._shortcut_type = shortcut_type
self._shortcut_key = shortcut_key
self._modifiers = modifiers
def __cmp__( self, other ):
return cmp( self.ToString(), other.ToString() )
def __eq__( self, other ):
return self.__hash__() == other.__hash__()
def __hash__( self ):
return ( self._shortcut_type, self._shortcut_key, tuple( self._modifiers ) ).__hash__()
def _GetSerialisableInfo( self ):
return ( self._shortcut_type, self._shortcut_key, self._modifiers )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( self._shortcut_type, self._shortcut_key, self._modifiers ) = serialisable_info
def GetShortcutType( self ):
return self._shortcut_type
def ToString( self ):
components = []
if CC.SHORTCUT_MODIFIER_CTRL in self._modifiers:
components.append( 'ctrl' )
if CC.SHORTCUT_MODIFIER_ALT in self._modifiers:
components.append( 'alt' )
if CC.SHORTCUT_MODIFIER_SHIFT in self._modifiers:
components.append( 'shift' )
if self._shortcut_type == CC.SHORTCUT_TYPE_KEYBOARD:
if self._shortcut_key in range( 65, 91 ):
components.append( chr( self._shortcut_key + 32 ) ) # + 32 for converting ascii A -> a
elif self._shortcut_key in range( 97, 123 ):
components.append( chr( self._shortcut_key ) )
else:
components.append( CC.wxk_code_string_lookup[ self._shortcut_key ] )
elif self._shortcut_type == CC.SHORTCUT_TYPE_MOUSE:
components.append( CC.shortcut_mouse_string_lookup[ self._shortcut_key ] )
return '+'.join( components )
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUT ] = Shortcut
class Shortcuts( HydrusSerialisable.SerialisableBaseNamed ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS
SERIALISABLE_VERSION = 1
SERIALISABLE_VERSION = 2
def __init__( self, name ):
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
self._mouse_actions = {}
self._keyboard_actions = {}
# update this to have actions as a separate serialisable class
self._shortcuts_to_commands = {}
def _ConvertActionToSerialisableAction( self, action ):
def __iter__( self ):
( service_key, data ) = action
if service_key is None:
for ( shortcut, command ) in self._shortcuts_to_commands.items():
return [ service_key, data ]
else:
serialisable_service_key = service_key.encode( 'hex' )
return [ serialisable_service_key, data ]
def _ConvertSerialisableActionToAction( self, serialisable_action ):
( serialisable_service_key, data ) = serialisable_action
if serialisable_service_key is None:
return ( serialisable_service_key, data ) # important to return tuple, as serialisable_action is likely a list
else:
service_key = serialisable_service_key.decode( 'hex' )
return ( service_key, data )
yield ( shortcut, command )
def _GetSerialisableInfo( self ):
serialisable_mouse_actions = []
for ( ( modifier, mouse_button ), action ) in self._mouse_actions.items():
serialisable_action = self._ConvertActionToSerialisableAction( action )
serialisable_mouse_actions.append( ( modifier, mouse_button, serialisable_action ) )
serialisable_keyboard_actions = []
for ( ( modifier, key ), action ) in self._keyboard_actions.items():
serialisable_action = self._ConvertActionToSerialisableAction( action )
serialisable_keyboard_actions.append( ( modifier, key, serialisable_action ) )
return ( serialisable_mouse_actions, serialisable_keyboard_actions )
return [ ( shortcut.GetSerialisableTuple(), command.GetSerialisableTuple() ) for ( shortcut, command ) in self._shortcuts_to_commands.items() ]
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
( serialisable_mouse_actions, serialisable_keyboard_actions ) = serialisable_info
self._mouse_actions = {}
for ( modifier, mouse_button, serialisable_action ) in serialisable_mouse_actions:
for ( serialisable_shortcut, serialisable_command ) in serialisable_info:
action = self._ConvertSerialisableActionToAction( serialisable_action )
shortcut = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_shortcut )
command = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_command )
self._mouse_actions[ ( modifier, mouse_button ) ] = action
self._keyboard_actions = {}
for ( modifier, key, serialisable_action ) in serialisable_keyboard_actions:
action = self._ConvertSerialisableActionToAction( serialisable_action )
self._keyboard_actions[ ( modifier, key ) ] = action
self._shortcuts_to_commands[ shortcut ] = command
def ClearActions( self ):
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
self._mouse_actions = {}
self._keyboard_actions = {}
def DeleteKeyboardAction( self, modifier, key ):
if ( modifier, key ) in self._keyboard_actions:
if version == 1:
del self._keyboard_actions[ ( modifier, key ) ]
( serialisable_mouse_actions, serialisable_keyboard_actions ) = old_serialisable_info
shortcuts_to_commands = {}
# this never stored mouse actions, so skip
services_manager = HydrusGlobals.client_controller.GetServicesManager()
for ( modifier, key, ( serialisable_service_key, data ) ) in serialisable_keyboard_actions:
if modifier not in CC.shortcut_wx_to_hydrus_lookup:
modifiers = []
else:
modifiers = [ CC.shortcut_wx_to_hydrus_lookup[ modifier ] ]
shortcut = Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, key, modifiers )
if serialisable_service_key is None:
command = ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, data )
else:
service_key = serialisable_service_key.decode( 'hex' )
if not services_manager.ServiceExists( service_key ):
continue
action = HC.CONTENT_UPDATE_FLIP
value = data
service = services_manager.GetService( service_key )
service_type = service.GetServiceType()
if service_type in HC.TAG_SERVICES:
content_type = HC.CONTENT_TYPE_MAPPINGS
elif service_type in HC.RATINGS_SERVICES:
content_type = HC.CONTENT_TYPE_RATINGS
else:
continue
command = ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( service_key, content_type, action, value ) )
shortcuts_to_commands[ shortcut ] = command
new_serialisable_info = ( ( shortcut.GetSerialisableTuple(), command.GetSerialisableTuple() ) for ( shortcut, command ) in shortcuts_to_commands.items() )
return ( 2, new_serialisable_info )
def DeleteMouseAction( self, modifier, mouse_button ):
def GetCommand( self, shortcut ):
if ( modifier, mouse_button ) in self._mouse_actions:
if shortcut in self._shortcuts_to_commands:
del self._mouse_actions[ ( modifier, mouse_button ) ]
def GetKeyboardAction( self, modifier, key ):
if ( modifier, key ) in self._keyboard_actions:
return self._keyboard_actions[ ( modifier, key ) ]
return self._shortcuts_to_commands[ shortcut ]
else:
@ -1610,41 +1813,44 @@ class Shortcuts( HydrusSerialisable.SerialisableBaseNamed ):
def GetMouseAction( self, modifier, mouse_button ):
def SetCommand( self, shortcut, command ):
if ( modifier, mouse_button ) in self._mouse_actions:
return self._mouse_actions[ ( modifier, mouse_button ) ]
else:
return None
def IterateKeyboardShortcuts( self ):
for ( ( modifier, key ), action ) in self._keyboard_actions.items(): yield ( ( modifier, key ), action )
def IterateMouseShortcuts( self ):
for ( ( modifier, mouse_button ), action ) in self._mouse_actions.items(): yield ( ( modifier, mouse_button ), action )
def SetKeyboardAction( self, modifier, key, action ):
self._keyboard_actions[ ( modifier, key ) ] = action
def SetMouseAction( self, modifier, mouse_button, action ):
self._mouse_actions[ ( modifier, mouse_button ) ] = action
self._shortcuts_to_commands[ shortcut ] = command
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS ] = Shortcuts
def GetShortcutFromEvent( event ):
def ConvertKeyEventToShortcut( event ):
key = event.KeyCode
if key in range( 65, 91 ) or key in CC.wxk_code_string_lookup.keys():
modifiers = []
if event.AltDown():
modifiers.append( CC.SHORTCUT_MODIFIER_ALT )
if event.CmdDown():
modifiers.append( CC.SHORTCUT_MODIFIER_CTRL )
if event.ShiftDown():
modifiers.append( CC.SHORTCUT_MODIFIER_SHIFT )
shortcut = Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, key, modifiers )
return shortcut
return None
def ConvertKeyEventToSimpleTuple( event ):
modifier = wx.ACCEL_NORMAL
@ -1656,3 +1862,54 @@ def GetShortcutFromEvent( event ):
return ( modifier, key )
def ConvertMouseEventToShortcut( event ):
key = None
if event.LeftDown():
key = CC.SHORTCUT_MOUSE_LEFT
elif event.MiddleDown():
key = CC.SHORTCUT_MOUSE_MIDDLE
elif event.RightDown():
key = CC.SHORTCUT_MOUSE_RIGHT
elif event.GetWheelRotation() > 0:
key = CC.SHORTCUT_MOUSE_SCROLL_UP
elif event.GetWheelRotation() < 0:
key = CC.SHORTCUT_MOUSE_SCROLL_DOWN
if key is not None:
modifiers = []
if event.AltDown():
modifiers.append( CC.SHORTCUT_MODIFIER_ALT )
if event.CmdDown():
modifiers.append( CC.SHORTCUT_MODIFIER_CTRL )
if event.ShiftDown():
modifiers.append( CC.SHORTCUT_MODIFIER_SHIFT )
shortcut = Shortcut( CC.SHORTCUT_TYPE_MOUSE, key, modifiers )
return shortcut
return None

View File

@ -572,3 +572,22 @@ def GetDefaultScriptRows():
return script_info
def GetDefaultShortcuts():
shortcuts = []
duplicate_filter = ClientData.Shortcuts( 'duplicate_filter' )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [ wx.ACCEL_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_not_dupes' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_alternates' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_exactly_the_same' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_RIGHT, [ wx.ACCEL_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_this_is_better' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_UP, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
duplicate_filter.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_NUMPAD_UP, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'duplicate_filter_skip' ) )
shortcuts.append( duplicate_filter )
return shortcuts

View File

@ -285,7 +285,7 @@ class AutoCompleteDropdown( wx.Panel ):
HydrusGlobals.client_controller.ResetIdleTimer()
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_INSERT, wx.WXK_NUMPAD_INSERT ):

View File

@ -18,6 +18,7 @@ import ClientTags
import gc
import HydrusImageHandling
import HydrusPaths
import HydrusSerialisable
import HydrusTags
import os
import wx
@ -1033,7 +1034,9 @@ class Canvas( wx.Window ):
self._current_zoom = 1.0
self._canvas_zoom = 1.0
self._drag_begin_coordinates = None
self._last_drag_coordinates = None
self._current_drag_is_touch = False
self._last_motion_coordinates = ( 0, 0 )
self._total_drag_delta = ( 0, 0 )
@ -1248,7 +1251,14 @@ class Canvas( wx.Window ):
if not ( ClientGUICommon.WindowHasFocus( self ) or ClientGUICommon.ChildHasFocus( self ) ):
return True
focus = wx.Window.FindFocus()
focus_is_my_hover_window = focus.GetParent() == self and isinstance( focus, ClientGUIHoverFrames.FullscreenHoverFrame )
if not focus_is_my_hover_window:
return True
if self._current_media is not None and self._current_media.GetMime() == HC.APPLICATION_FLASH:
@ -1543,8 +1553,9 @@ class Canvas( wx.Window ):
( x, y ) = pos
self._drag_begin_coordinates = ( x, y )
self._last_drag_coordinates = ( x, y )
self._current_drag_is_touch = False
def EventEraseBackground( self, event ): pass
@ -2192,12 +2203,15 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
self._media_list = ClientMedia.ListeningMediaList( self._file_service_key, [] )
self._duplicate_filter_shortcuts = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, 'duplicate_filter' )
self._hover_commands.AddCommand( 'this is better', self._CurrentMediaIsBetter )
self._hover_commands.AddCommand( 'exact duplicates', self._MediaAreTheSame )
self._hover_commands.AddCommand( 'alternates', self._MediaAreAlternates )
self._hover_commands.AddCommand( 'not duplicates', self._MediaAreNotDupes )
self._hover_commands.AddCommand( 'custom action', self._DoCustomAction )
self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse )
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
# add support for 'f' to borderless
@ -2210,6 +2224,8 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
HydrusGlobals.client_controller.sub( self, 'SwitchMedia', 'canvas_show_previous' )
HydrusGlobals.client_controller.sub( self, 'ShowNewPair', 'canvas_show_new_pair' )
HydrusGlobals.client_controller.sub( self, 'RefreshShortcuts', 'refresh_shortcuts' )
def _Close( self ):
@ -2220,14 +2236,26 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def _CurrentMediaIsBetter( self ):
pass
other_media = self._media_list.GetNext( self._current_media )
better_hash = self._current_media.GetHash()
worse_hash = other_media.GetHash()
merge_options = self._GetMergeOptions( HC.DUPLICATE_BETTER )
HydrusGlobals.client_controller.WriteSynchronous( 'duplicate_pair_status', HC.DUPLICATE_BETTER, better_hash, worse_hash, merge_options )
self._ShowNewPair()
def _DoCustomAction( self ):
pass
wx.MessageBox( 'This doesn\'t do anything yet!' )
return
# ( duplicate_status, hash_a, hash_b, merge_options ) = panel.getvalue()
# HydrusGlobals.client_controller.WriteSynchronous( 'duplicate_pair_status', duplicate_status, hash_a, hash_b, merge_options )
# launch the dialog to choose exactly what happens
# if OK on that:
@ -2258,20 +2286,122 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def _GetMergeOptions( self, duplicate_status ):
# fetch it from client_options, given a status
return None
def _MediaAreAlternates( self ):
pass
other_media = self._media_list.GetNext( self._current_media )
hash_a = self._current_media.GetHash()
hash_b = other_media.GetHash()
merge_options = self._GetMergeOptions( HC.DUPLICATE_ALTERNATE )
HydrusGlobals.client_controller.WriteSynchronous( 'duplicate_pair_status', HC.DUPLICATE_ALTERNATE, hash_a, hash_b )
self._ShowNewPair()
def _MediaAreNotDupes( self ):
other_media = self._media_list.GetNext( self._current_media )
hash_a = self._current_media.GetHash()
hash_b = other_media.GetHash()
merge_options = self._GetMergeOptions( HC.DUPLICATE_NOT_DUPLICATE )
HydrusGlobals.client_controller.WriteSynchronous( 'duplicate_pair_status', HC.DUPLICATE_NOT_DUPLICATE, hash_a, hash_b )
self._ShowNewPair()
def _MediaAreTheSame( self ):
pass
other_media = self._media_list.GetNext( self._current_media )
hash_a = self._current_media.GetHash()
hash_b = other_media.GetHash()
merge_options = self._GetMergeOptions( HC.DUPLICATE_SAME_FILE )
HydrusGlobals.client_controller.WriteSynchronous( 'duplicate_pair_status', HC.DUPLICATE_SAME_FILE, hash_a, hash_b, merge_options )
self._ShowNewPair()
def _ProcessApplicationCommand( self, command ):
command_processed = True
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
action = data
if action == 'duplicate_filter_this_is_better':
self._CurrentMediaIsBetter()
elif action == 'duplicate_filter_exactly_the_same':
self._MediaAreTheSame()
elif action == 'duplicate_filter_alternates':
self._MediaAreAlternates()
elif action == 'duplicate_filter_not_dupes':
self._MediaAreNotDupes()
elif action == 'duplicate_filter_custom_action':
self._DoCustomAction()
elif action == 'duplicate_filter_skip':
self._ShowNewPair()
else:
command_processed = False
else:
command_processed = False
return command_processed
def _ProcessShortcut( self, shortcut ):
shortcut_processed = False
command = self._duplicate_filter_shortcuts.GetCommand( shortcut )
if command is not None:
command_processed = self._ProcessApplicationCommand( command )
if command_processed:
shortcut_processed = True
return shortcut_processed
def _ShowNewPair( self ):
result = HydrusGlobals.client_controller.Read( 'duplicate_pair', self._file_service_key, HC.DUPLICATE_UNKNOWN )
@ -2300,12 +2430,28 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def EventCharHook( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
shortcut = ClientData.ConvertKeyEventToShortcut( event )
if shortcut is not None:
shortcut_processed = self._ProcessShortcut( shortcut )
if shortcut_processed:
return
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ):
self._Close()
else:
event.Skip()
def EventClose( self, event ):
@ -2313,7 +2459,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
self._Close()
def EventMouseWheel( self, event ):
def EventMouse( self, event ):
if self._HydrusShouldNotProcessInput():
@ -2321,7 +2467,26 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
else:
self._SwitchMedia()
shortcut = ClientData.ConvertMouseEventToShortcut( event )
if shortcut is not None:
shortcut_processed = self._ProcessShortcut( shortcut )
if shortcut_processed:
return
if event.GetWheelRotation() != 0:
self._SwitchMedia()
else:
event.Skip()
@ -2345,6 +2510,11 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
wx.CallLater( 100, catch_up )
def RefreshShortcuts( self ):
self._duplicate_filter_shortcuts = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, 'duplicate_filter' )
def ShowNewPair( self, canvas_key ):
if canvas_key == self._canvas_key:
@ -2592,6 +2762,15 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
if event.Dragging() and self._last_drag_coordinates is not None:
off_starting_point = self._drag_begin_coordinates != self._last_drag_coordinates
hit_same_point_twice = ( x, y ) == self._last_drag_coordinates
# touch drags generate motion events continuously, even when not moving
if off_starting_point and hit_same_point_twice:
self._current_drag_is_touch = True
( old_x, old_y ) = self._last_drag_coordinates
( delta_x, delta_y ) = ( x - old_x, y - old_y )
@ -2600,7 +2779,16 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
show_mouse = False
self.WarpPointer( old_x, old_y )
if not self._current_drag_is_touch:
# touch events obviously don't mix with warping well. the touch just warps it back and again and we get a massive delta!
self.WarpPointer( old_x, old_y )
else:
self._last_drag_coordinates = ( x, y )
else:
@ -2901,7 +3089,7 @@ class CanvasMediaListFilterInbox( CanvasMediaList ):
if self._HydrusShouldNotProcessInput(): event.Skip()
else:
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if modifier == wx.ACCEL_NORMAL and key == wx.WXK_SPACE: self._Keep()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '+' ), wx.WXK_ADD, wx.WXK_NUMPAD_ADD ): self._ZoomIn()
@ -3216,7 +3404,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
else:
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete()
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
@ -3540,15 +3728,17 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
if self._HydrusShouldNotProcessInput(): event.Skip()
else:
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
shortcut = ClientData.ConvertKeyEventToShortcut( event )
action = self._shortcuts.GetKeyboardAction( modifier, key )
command = self._shortcuts.GetCommand( shortcut )
if action is not None:
if command is not None:
( service_key, data ) = action
data = command.GetData()
if service_key is None:
command_type = command.GetCommandType()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
if data == 'archive': self._Archive()
elif data == 'delete': self._Delete()
@ -3571,7 +3761,9 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
elif data == 'previous': self._ShowPrevious()
elif data == 'next': self._ShowNext()
else:
elif command_type == CC.APPLICATION_COMMAND_TYPE_CONTENT:
( service_key, content_type, action, value ) = data
service = HydrusGlobals.client_controller.GetServicesManager().GetService( service_key )
@ -3581,7 +3773,7 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
if service_type in HC.TAG_SERVICES:
tag = data
tag = value
tags_manager = self._current_media.GetTagsManager()
@ -3663,7 +3855,7 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
# maybe this needs to be more complicated, if action is, say, remove the rating?
# ratings needs a good look at anyway
rating = data
rating = value
row = ( rating, hashes )
@ -3675,6 +3867,8 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
else:
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if modifier == wx.ACCEL_NORMAL and key in ( ord( '+' ), wx.WXK_ADD, wx.WXK_NUMPAD_ADD ): self._ZoomIn()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '-' ), wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT ): self._ZoomOut()
elif modifier == wx.ACCEL_NORMAL and key == ord( 'Z' ): self._ZoomSwitch()

View File

@ -3127,7 +3127,7 @@ class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in CC.DELETE_KEYS:
@ -3407,12 +3407,71 @@ class SaneListCtrlForSingleObject( SaneListCtrl ):
self._objects_to_data_indices[ obj ] = data_index
class Shortcut( wx.TextCtrl ):
class Shortcut( wx.Panel ):
def __init__( self, parent, modifier = wx.ACCEL_NORMAL, key = wx.WXK_F7 ):
def __init__( self, parent ):
self._modifier = modifier
self._key = key
wx.Panel.__init__( self, parent )
self._mouse_radio = wx.RadioButton( self, style = wx.RB_GROUP, label = 'mouse' )
self._mouse_shortcut = ShortcutMouse( self, self._mouse_radio )
self._keyboard_radio = wx.RadioButton( self, label = 'keyboard' )
self._keyboard_shortcut = ShortcutKeyboard( self, self._keyboard_radio )
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( wx.StaticText( self, label = 'Mouse events only work for the duplicate filter atm!' ), CC.FLAGS_EXPAND_PERPENDICULAR )
gridbox = wx.FlexGridSizer( 0, 2 )
gridbox.AddGrowableCol( 1, 1 )
gridbox.AddF( self._mouse_radio, CC.FLAGS_VCENTER )
gridbox.AddF( self._mouse_shortcut, CC.FLAGS_EXPAND_BOTH_WAYS )
gridbox.AddF( self._keyboard_radio, CC.FLAGS_VCENTER )
gridbox.AddF( self._keyboard_shortcut, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( gridbox, CC.FLAGS_EXPAND_BOTH_WAYS )
self.SetSizer( vbox )
def GetValue( self ):
if self._mouse_radio.GetValue() == True:
return self._mouse_shortcut.GetValue()
else:
return self._keyboard_shortcut.GetValue()
def SetValue( self, shortcut ):
if shortcut.GetShortcutType() == CC.SHORTCUT_TYPE_MOUSE:
self._mouse_radio.SetValue( True )
self._mouse_shortcut.SetValue( shortcut )
else:
self._keyboard_radio.SetValue( True )
self._keyboard_shortcut.SetValue( shortcut )
class ShortcutKeyboard( wx.TextCtrl ):
def __init__( self, parent, related_radio = None ):
self._shortcut = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F7, [] )
self._related_radio = related_radio
wx.TextCtrl.__init__( self, parent, style = wx.TE_PROCESS_ENTER )
@ -3423,35 +3482,87 @@ class Shortcut( wx.TextCtrl ):
def _SetShortcutString( self ):
display_string = ''
if self._modifier == wx.ACCEL_ALT: display_string += 'alt + '
elif self._modifier == wx.ACCEL_CTRL: display_string += 'ctrl + '
elif self._modifier == wx.ACCEL_SHIFT: display_string += 'shift + '
if self._key in range( 65, 91 ): display_string += chr( self._key + 32 ) # + 32 for converting ascii A -> a
elif self._key in range( 97, 123 ): display_string += chr( self._key )
else: display_string += CC.wxk_code_string_lookup[ self._key ]
display_string = self._shortcut.ToString()
wx.TextCtrl.SetValue( self, display_string )
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
shortcut = ClientData.ConvertKeyEventToShortcut( event )
if key in range( 65, 91 ) or key in CC.wxk_code_string_lookup.keys():
if shortcut is not None:
( self._modifier, self._key ) = ( modifier, key )
self._shortcut = shortcut
if self._related_radio is not None:
self._related_radio.SetValue( True )
self._SetShortcutString()
def GetValue( self ): return ( self._modifier, self._key )
def SetValue( self, modifier, key ):
def GetValue( self ):
( self._modifier, self._key ) = ( modifier, key )
return self._shortcut
def SetValue( self, shortcut ):
self._shortcut = shortcut
self._SetShortcutString()
class ShortcutMouse( wx.Button ):
def __init__( self, parent, related_radio = None ):
self._shortcut = ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] )
self._related_radio = related_radio
wx.Button.__init__( self, parent )
self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse )
self._SetShortcutString()
def _SetShortcutString( self ):
display_string = self._shortcut.ToString()
self.SetLabel( display_string )
def EventMouse( self, event ):
shortcut = ClientData.ConvertMouseEventToShortcut( event )
if shortcut is not None:
self._shortcut = shortcut
if self._related_radio is not None:
self._related_radio.SetValue( True )
self._SetShortcutString()
def GetValue( self ):
return self._shortcut
def SetValue( self, shortcut ):
self._shortcut = shortcut
self._SetShortcutString()

View File

@ -498,13 +498,10 @@ class DialogInputImportTagOptions( Dialog ):
class DialogInputCustomFilterAction( Dialog ):
def __init__( self, parent, modifier = wx.ACCEL_NORMAL, key = wx.WXK_F7, service_key = None, action = 'archive' ):
def __init__( self, parent, shortcut, command ):
Dialog.__init__( self, parent, 'input custom filter action' )
self._service_key = service_key
self._action = action
self._current_ratings_like_service = None
self._current_ratings_numerical_service = None
@ -512,11 +509,14 @@ class DialogInputCustomFilterAction( Dialog ):
self._shortcut_panel = ClientGUICommon.StaticBox( self, 'shortcut' )
self._shortcut = ClientGUICommon.Shortcut( self._shortcut_panel, modifier, key )
self._shortcut = ClientGUICommon.Shortcut( self._shortcut_panel )
self._none_panel = ClientGUICommon.StaticBox( self, 'non-service actions' )
self._none_actions = wx.Choice( self._none_panel, choices = [ 'manage_tags', 'manage_ratings', 'archive', 'inbox', 'delete', 'fullscreen_switch', 'frame_back', 'frame_next', 'next', 'first', 'last', 'open_externally', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'previous', 'remove', 'zoom_in', 'zoom_out', 'zoom_switch' ] )
choices = [ 'manage_tags', 'manage_ratings', 'archive', 'inbox', 'delete', 'fullscreen_switch', 'frame_back', 'frame_next', 'next', 'first', 'last', 'open_externally', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'previous', 'remove', 'zoom_in', 'zoom_out', 'zoom_switch' ]
choices.extend( CC.DUPLICATE_FILTER_ACTIONS )
self._none_actions = wx.Choice( self._none_panel, choices = choices )
self._ok_none = wx.Button( self._none_panel, label = 'ok' )
self._ok_none.Bind( wx.EVT_BUTTON, self.EventOKNone )
@ -573,12 +573,23 @@ class DialogInputCustomFilterAction( Dialog ):
self._SetActions()
if self._service_key is None:
#
self._shortcut.SetValue( shortcut )
command_type = command.GetCommandType()
data = command.GetData()
if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
self._none_actions.SetStringSelection( self._action )
action = data
self._none_actions.SetStringSelection( action )
else:
( service_key, content_type, action, value ) = data
self._service = HydrusGlobals.client_controller.GetServicesManager().GetService( service_key )
service_name = self._service.GetName()
@ -588,7 +599,7 @@ class DialogInputCustomFilterAction( Dialog ):
self._tag_service_keys.SetStringSelection( service_name )
self._tag_value.SetValue( self._action )
self._tag_value.SetValue( value )
elif service_type == HC.LOCAL_RATING_LIKE:
@ -596,9 +607,18 @@ class DialogInputCustomFilterAction( Dialog ):
self._SetActions()
if self._action is None: self._ratings_like_remove.SetValue( True )
elif self._action == True: self._ratings_like_like.SetValue( True )
elif self._action == False: self._ratings_like_dislike.SetValue( True )
if value is None:
self._ratings_like_remove.SetValue( True )
elif value == True:
self._ratings_like_like.SetValue( True )
elif value == False:
self._ratings_like_dislike.SetValue( True )
elif service_type == HC.LOCAL_RATING_NUMERICAL:
@ -606,7 +626,7 @@ class DialogInputCustomFilterAction( Dialog ):
self._SetActions()
if self._action is None:
if value is None:
self._ratings_numerical_remove.SetValue( True )
@ -614,7 +634,7 @@ class DialogInputCustomFilterAction( Dialog ):
num_stars = self._current_ratings_numerical_service.GetNumStars()
slider_value = int( round( self._action * num_stars ) )
slider_value = int( round( value * num_stars ) )
self._ratings_numerical_slider.SetValue( slider_value )
@ -734,9 +754,9 @@ class DialogInputCustomFilterAction( Dialog ):
def EventOKNone( self, event ):
self._service_key = None
self._action = self._none_actions.GetStringSelection()
self._pretty_action = self._action
action = self._none_actions.GetStringSelection()
self._final_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, action )
self.EndModal( wx.ID_OK )
@ -747,27 +767,29 @@ class DialogInputCustomFilterAction( Dialog ):
if selection != wx.NOT_FOUND:
self._service_key = self._ratings_like_service_keys.GetClientData( selection )
service_key = self._ratings_like_service_keys.GetClientData( selection )
if self._ratings_like_like.GetValue():
self._action = 1.0
self._pretty_action = 'like'
value = 1.0
elif self._ratings_like_dislike.GetValue():
self._action = 0.0
self._pretty_action = 'dislike'
value = 0.0
else:
self._action = None
self._pretty_action = 'remove'
value = None
self._final_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( service_key, HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_FLIP, value ) )
self.EndModal( wx.ID_OK )
else: self.EndModal( wx.ID_CANCEL )
else:
self.EndModal( wx.ID_CANCEL )
def EventOKRatingsNumerical( self, event ):
@ -776,53 +798,57 @@ class DialogInputCustomFilterAction( Dialog ):
if selection != wx.NOT_FOUND:
self._service_key = self._ratings_numerical_service_keys.GetClientData( selection )
service_key = self._ratings_numerical_service_keys.GetClientData( selection )
if self._ratings_numerical_remove.GetValue():
self._action = None
self._pretty_action = 'remove'
value = None
else:
value = self._ratings_numerical_slider.GetValue()
self._pretty_action = HydrusData.ToUnicode( value )
num_stars = self._current_ratings_numerical_service.GetNumStars()
allow_zero = self._current_ratings_numerical_service.AllowZero()
if allow_zero:
self._action = float( value ) / num_stars
value = float( value ) / num_stars
else:
self._action = float( value - 1 ) / ( num_stars - 1 )
value = float( value - 1 ) / ( num_stars - 1 )
self._final_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( service_key, HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_FLIP, value ) )
self.EndModal( wx.ID_OK )
else: self.EndModal( wx.ID_CANCEL )
else:
self.EndModal( wx.ID_CANCEL )
def EventOKTag( self, event ):
# this could support multiple tags now
selection = self._tag_service_keys.GetSelection()
if selection != wx.NOT_FOUND:
self._service_key = self._tag_service_keys.GetClientData( selection )
service_key = self._tag_service_keys.GetClientData( selection )
self._action = self._tag_value.GetValue()
self._pretty_action = self._action
value = self._tag_value.GetValue()
self._final_command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( service_key, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_FLIP, value ) )
self.EndModal( wx.ID_OK )
else: self.EndModal( wx.ID_CANCEL )
else:
self.EndModal( wx.ID_CANCEL )
def EventRecalcActions( self, event ):
@ -834,14 +860,9 @@ class DialogInputCustomFilterAction( Dialog ):
def GetInfo( self ):
( modifier, key ) = self._shortcut.GetValue()
shortcut = self._shortcut.GetValue()
if self._service_key is None: pretty_service_key = ''
else: pretty_service_key = HydrusGlobals.client_controller.GetServicesManager().GetService( self._service_key ).GetName()
( pretty_modifier, pretty_key ) = ClientData.ConvertShortcutToPrettyShortcut( modifier, key )
return ( ( pretty_modifier, pretty_key, pretty_service_key, self._pretty_action ), ( modifier, key, self._service_key, self._action ) )
return ( shortcut, self._final_command )
def SetTags( self, tags ):
@ -949,7 +970,7 @@ class DialogInputFileSystemPredicates( Dialog ):
def EventCharHook( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -1704,11 +1725,11 @@ class DialogInputShortcut( Dialog ):
def __init__( self, parent, modifier = wx.ACCEL_NORMAL, key = wx.WXK_F7, action = 'new_page' ):
Dialog.__init__( self, parent, 'configure shortcut' )
Dialog.__init__( self, parent, 'edit shortcut' )
self._action = action
self._shortcut = ClientGUICommon.Shortcut( self, modifier, key )
self._shortcut = ClientGUICommon.Shortcut( self )
self._actions = wx.Choice( self, choices = [ 'archive', 'inbox', 'close_page', 'filter', 'fullscreen_switch', 'frame_back', 'frame_next', 'manage_ratings', 'manage_tags', 'new_page', 'unclose_page', 'refresh', 'set_media_focus', 'set_search_focus', 'show_hide_splitters', 'synchronised_wait_switch', 'next', 'first', 'last', 'undo', 'redo', 'open_externally', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'previous', 'remove', 'zoom_in', 'zoom_out', 'zoom_switch' ] )
@ -1720,6 +1741,18 @@ class DialogInputShortcut( Dialog ):
#
if modifier not in CC.shortcut_wx_to_hydrus_lookup:
modifiers = []
else:
modifiers = [ CC.shortcut_wx_to_hydrus_lookup[ modifier ] ]
shortcut = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, key, modifiers )
self._shortcut.SetValue( shortcut )
self._actions.SetSelection( self._actions.FindString( action ) )
#
@ -1727,7 +1760,7 @@ class DialogInputShortcut( Dialog ):
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._shortcut, CC.FLAGS_VCENTER )
hbox.AddF( self._actions, CC.FLAGS_EXPAND_PERPENDICULAR )
hbox.AddF( self._actions, CC.FLAGS_VCENTER )
b_box = wx.BoxSizer( wx.HORIZONTAL )
b_box.AddF( self._ok, CC.FLAGS_VCENTER )
@ -1749,7 +1782,37 @@ class DialogInputShortcut( Dialog ):
def GetInfo( self ):
( modifier, key ) = self._shortcut.GetValue()
shortcut = self._shortcut.GetValue()
modifiers = shortcut._modifiers
if modifiers == []:
modifier = wx.ACCEL_NORMAL
else:
hydrus_modifier = modifiers[0]
if hydrus_modifier == CC.SHORTCUT_MODIFIER_ALT:
modifier = wx.ACCEL_ALT
elif hydrus_modifier == CC.SHORTCUT_MODIFIER_CTRL:
modifier = wx.ACCEL_CTRL
elif hydrus_modifier == CC.SHORTCUT_MODIFIER_SHIFT:
modifier = wx.ACCEL_SHIFT
else:
modifier = wx.ACCEL_NORMAL
key = shortcut._shortcut_key
return ( modifier, key, self._actions.GetStringSelection() )
@ -2392,7 +2455,7 @@ class DialogPageChooser( Dialog ):
id = None
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key == wx.WXK_UP: id = 8
elif key == wx.WXK_LEFT: id = 4
@ -3525,7 +3588,7 @@ class DialogSelectFromList( Dialog ):
def EventCharHook( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key == wx.WXK_ESCAPE:
@ -3539,7 +3602,7 @@ class DialogSelectFromList( Dialog ):
def EventListKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_SPACE, wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -4040,7 +4103,6 @@ class DialogShortcuts( Dialog ):
self.SetInitialSize( ( x, y ) )
wx.CallAfter( self.EventSelect, None )
wx.CallAfter( self._ok.SetFocus )
@ -4054,16 +4116,30 @@ class DialogShortcuts( Dialog ):
for ( key, action ) in key_dict.items():
if modifier not in CC.shortcut_wx_to_hydrus_lookup:
modifiers = []
else:
modifiers = [ CC.shortcut_wx_to_hydrus_lookup[ modifier ] ]
shortcut = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, key, modifiers )
if action in ( 'manage_tags', 'manage_ratings', 'archive', 'inbox', 'fullscreen_switch', 'frame_back', 'frame_next', 'next', 'first', 'last', 'open_externally', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'previous', 'remove', 'zoom_in', 'zoom_out', 'zoom_switch' ):
service_key = None
command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, action )
default_shortcuts.SetKeyboardAction( modifier, key, ( service_key, action ) )
default_shortcuts.SetCommand( shortcut, command )
default_shortcuts.SetKeyboardAction( wx.ACCEL_NORMAL, wx.WXK_DELETE, ( None, 'delete' ) )
shortcut = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_DELETE, [] )
command = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'delete' )
default_shortcuts.SetCommand( shortcut, command )
return default_shortcuts
@ -4072,7 +4148,7 @@ class DialogShortcuts( Dialog ):
name = self._shortcuts.GetCurrentKey()
if name is None: # 'default'
if name is None or name in CC.SHORTCUTS_RESERVED_NAMES: # None = 'default'
return True
@ -4149,14 +4225,9 @@ class DialogShortcuts( Dialog ):
existing_shortcuts = page.GetShortcuts()
for ( ( modifier, key ), action ) in existing_shortcuts.IterateKeyboardShortcuts():
for ( shortcut, command ) in existing_shortcuts:
shortcuts.SetKeyboardAction( modifier, key, action )
for ( ( modifier, mouse_button ), action ) in existing_shortcuts.IterateMouseShortcuts():
shortcuts.SetKeyboardAction( modifier, mouse_button, action )
shortcuts.SetCommand( shortcut, command )
@ -4194,9 +4265,8 @@ class DialogShortcuts( Dialog ):
wx.Panel.__init__( self, parent )
self._original_shortcuts = shortcuts
self._shortcuts = ClientGUICommon.SaneListCtrl( self, 120, [ ( 'modifier', 150 ), ( 'key', 150 ), ( 'service', 150 ), ( 'action', -1 ) ], delete_key_callback = self.RemoveShortcuts, activation_callback = self.EditShortcuts )
self._name = shortcuts.GetName()
self._shortcuts = ClientGUICommon.SaneListCtrl( self, 120, [ ( 'shortcut', 150 ), ( 'command', -1 ) ], delete_key_callback = self.RemoveShortcuts, activation_callback = self.EditShortcuts )
self._add = wx.Button( self, label = 'add' )
self._add.Bind( wx.EVT_BUTTON, self.EventAdd )
@ -4211,36 +4281,16 @@ class DialogShortcuts( Dialog ):
#
for ( ( modifier, key ), action ) in self._original_shortcuts.IterateKeyboardShortcuts():
for ( shortcut, command ) in shortcuts:
( pretty_modifier, pretty_key ) = ClientData.ConvertShortcutToPrettyShortcut( modifier, key )
sort_tuple = ( shortcut, command )
( service_key, data ) = action
pretty_tuple = self._ConvertSortTupleToPrettyTuple( sort_tuple )
if service_key is None:
pretty_service_key = ''
else:
try:
service = HydrusGlobals.client_controller.GetServicesManager().GetService( service_key )
pretty_service_key = service.GetName()
except HydrusExceptions.DataMissing:
pretty_service_key = 'service not found'
pretty_data = data
self._shortcuts.Append( ( pretty_modifier, pretty_key, pretty_service_key, pretty_data ), ( modifier, key, service_key, data ) )
self._shortcuts.Append( pretty_tuple, sort_tuple )
self._SortListCtrl()
self._shortcuts.SortListItems( 1 )
#
@ -4258,43 +4308,50 @@ class DialogShortcuts( Dialog ):
self.SetSizer( vbox )
def _SortListCtrl( self ):
def _ConvertSortTupleToPrettyTuple( self, ( shortcut, command ) ):
self._shortcuts.SortListItems( 3 )
return ( shortcut.ToString(), command.ToString() )
def EditShortcuts( self ):
for index in self._shortcuts.GetAllSelected():
( modifier, key, service_key, action ) = self._shortcuts.GetClientData( index )
( shortcut, command ) = self._shortcuts.GetClientData( index )
with DialogInputCustomFilterAction( self, modifier = modifier, key = key, service_key = service_key, action = action ) as dlg:
with DialogInputCustomFilterAction( self, shortcut, command ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
( pretty_tuple, sort_tuple ) = dlg.GetInfo()
( shortcut, command ) = dlg.GetInfo()
sort_tuple = ( shortcut, command )
pretty_tuple = self._ConvertSortTupleToPrettyTuple( sort_tuple )
self._shortcuts.UpdateRow( index, pretty_tuple, sort_tuple )
self._SortListCtrl()
def EventAdd( self, event ):
with DialogInputCustomFilterAction( self ) as dlg:
shortcut = ClientData.Shortcut()
command = ClientData.ApplicationCommand()
with DialogInputCustomFilterAction( self, shortcut, command ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
( pretty_tuple, sort_tuple ) = dlg.GetInfo()
( shortcut, command ) = dlg.GetInfo()
sort_tuple = ( shortcut, command )
pretty_tuple = self._ConvertSortTupleToPrettyTuple( sort_tuple )
self._shortcuts.Append( pretty_tuple, sort_tuple )
self._SortListCtrl()
@ -4310,17 +4367,11 @@ class DialogShortcuts( Dialog ):
def GetShortcuts( self ):
name = self._original_shortcuts.GetName()
shortcuts = ClientData.Shortcuts( self._name )
shortcuts = ClientData.Shortcuts( name )
rows = self._shortcuts.GetClientData()
for ( modifier, key, service_key, data ) in rows:
for ( shortcut, command ) in self._shortcuts.GetClientData():
action = ( service_key, data )
shortcuts.SetKeyboardAction( modifier, key, action )
shortcuts.SetCommand( shortcut, command )
return shortcuts

View File

@ -3568,7 +3568,7 @@ class DialogManageTagCensorship( ClientGUIDialogs.Dialog ):
def EventKeyDownTag( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):

View File

@ -2,6 +2,7 @@ import ClientConstants as CC
import ClientData
import ClientGUICanvas
import ClientGUICommon
import ClientGUIDialogs
import ClientGUIListBoxes
import HydrusConstants as HC
import HydrusData
@ -438,11 +439,12 @@ class FullscreenHoverFrameTopDuplicatesFilter( FullscreenHoverFrameTop ):
def _PopulateCenterButtons( self ):
# probably better to just launch a dialog with this stuff as proper panels, yeah
# I'll be making those panels for the custom dialog anyway
# make a merge_options serialisable object to track all this that I can plug into the dialog and obey at the db level
# store two in the options, for better and same, and then the custom just displays/produces one depending on chosen status
# cog icon to control
# cog icon launches a dialog
# file delete on better
# extend these to be per-service!
# rating merging on better (d NO) (these are mutually exclusive, so add a radio menu type or whatever)
# rating moving on better (d YES)
# rating merging on same (d YES)
@ -452,8 +454,8 @@ class FullscreenHoverFrameTopDuplicatesFilter( FullscreenHoverFrameTop ):
menu_items = []
menu_items.append( ( 'normal', 'edit tag/ratings merge options and whether to delete bad files', 'help compute.', self._Archive ) )
menu_items.append( ( 'normal', 'edit shortcuts', 'help compute.', self._Archive ) )
menu_items.append( ( 'normal', 'edit tag/ratings merge options and whether to delete bad files', 'edit what happens when you filter files', self._EditMergeOptions ) )
menu_items.append( ( 'normal', 'edit shortcuts', 'edit how to quickly filter files', self._EditShortcuts ) )
cog_button = ClientGUICommon.MenuBitmapButton( self, CC.GlobalBMPs.cog, menu_items )
@ -462,6 +464,22 @@ class FullscreenHoverFrameTopDuplicatesFilter( FullscreenHoverFrameTop ):
FullscreenHoverFrameTop._PopulateCenterButtons( self )
def _EditMergeOptions( self ):
wx.MessageBox( 'This doesn\'t do anything yet!' )
def _EditShortcuts( self ):
with ClientGUIDialogs.DialogShortcuts( self ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
HydrusGlobals.client_controller.pub( 'refresh_shortcuts' )
def _PopulateLeftButtons( self ):
FullscreenHoverFrameTop._PopulateLeftButtons( self )

View File

@ -1304,7 +1304,6 @@ class ListBoxTagsAC( ListBoxTagsPredicates ):
if len( predicates ) > 0:
self._Hit( False, False, None )
self._Hit( False, False, 0 )
@ -1699,7 +1698,7 @@ class ListBoxTagsStringsAddRemove( ListBoxTagsStrings ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in CC.DELETE_KEYS:
@ -1817,6 +1816,8 @@ class ListBoxTagsSelection( ListBoxTags ):
def _RecalcStrings( self, limit_to_these_tags = None ):
previous_selected_terms = set( self._selected_terms )
if limit_to_these_tags is None:
self._Clear()
@ -1858,6 +1859,14 @@ class ListBoxTagsSelection( ListBoxTags ):
for term in previous_selected_terms:
if term in self._terms:
self._selected_terms.add( term )
self._SortTags()
@ -2197,7 +2206,7 @@ class ListBoxTagsSelectionTagsDialog( ListBoxTagsSelection ):
if len( self._selected_terms ) > 0:
self._add_func( self._selected_terms )
self._add_func( set( self._selected_terms ) )
@ -2205,6 +2214,6 @@ class ListBoxTagsSelectionTagsDialog( ListBoxTagsSelection ):
if len( self._selected_terms ) > 0:
self._delete_func( self._selected_terms )
self._delete_func( set( self._selected_terms ) )

View File

@ -383,7 +383,7 @@ def GenerateDumpMultipartFormDataCTAndBody( fields ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ): self.EventReady( None )
else: event.Skip()
@ -776,6 +776,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._file_domain_button = ClientGUICommon.BetterButton( self._filtering_panel, 'file domain', self._FileDomainButtonHit )
self._num_unknown_duplicates = wx.StaticText( self._filtering_panel )
self._num_better_duplicates = wx.StaticText( self._filtering_panel )
self._num_same_file_duplicates = wx.StaticText( self._filtering_panel )
self._num_alternate_duplicates = wx.StaticText( self._filtering_panel )
self._show_some_dupes = ClientGUICommon.BetterButton( self._filtering_panel, 'show some pairs (prototype!)', self._ShowSomeDupes )
@ -828,6 +829,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._filtering_panel.AddF( self._file_domain_button, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.AddF( self._num_unknown_duplicates, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.AddF( self._num_better_duplicates, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.AddF( self._num_same_file_duplicates, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.AddF( self._num_alternate_duplicates, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.AddF( self._show_some_dupes, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -1146,9 +1148,10 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
num_unknown = duplicate_types_to_count[ HC.DUPLICATE_UNKNOWN ]
self._num_unknown_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( num_unknown ) + ' potential duplicates found.' )
self._num_same_file_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( duplicate_types_to_count[ HC.DUPLICATE_SAME_FILE ] ) + ' same file pairs filtered.' )
self._num_alternate_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( duplicate_types_to_count[ HC.DUPLICATE_ALTERNATE ] ) + ' alternate file pairs filtered.' )
self._num_unknown_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( num_unknown ) + ' potential matches found.' )
self._num_better_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( duplicate_types_to_count[ HC.DUPLICATE_BETTER ] ) + ' better/worse pairs filtered.' )
self._num_same_file_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( duplicate_types_to_count[ HC.DUPLICATE_SAME_FILE ] ) + ' exactly similar pairs filtered.' )
self._num_alternate_duplicates.SetLabelText( HydrusData.ConvertIntToPrettyString( duplicate_types_to_count[ HC.DUPLICATE_ALTERNATE ] ) + ' alternate pairs filtered.' )
if num_unknown > 0:
@ -1533,7 +1536,7 @@ class ManagementPanelGalleryImport( ManagementPanel ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -2064,7 +2067,7 @@ class ManagementPanelPageOfImagesImport( ManagementPanel ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -3076,7 +3079,7 @@ class ManagementPanelThreadWatcherImport( ManagementPanel ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -3341,7 +3344,7 @@ class ManagementPanelURLsImport( ManagementPanel ):
def EventKeyDown( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):

View File

@ -2788,6 +2788,8 @@ class MediaPanelThumbnails( MediaPanel ):
shortcut_names = HydrusGlobals.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
shortcut_names = [ name for name in shortcut_names if name not in CC.SHORTCUTS_RESERVED_NAMES ]
if len( shortcut_names ) > 0:
custom_shortcuts_menu = wx.Menu()

View File

@ -306,8 +306,14 @@ class OptionsPanelTags( OptionsPanel ):
for namespace in namespaces:
if namespace == '': label = 'no namespace'
else: label = namespace
if namespace == '':
label = 'unnamespaced'
else:
label = namespace
namespace_checkbox = wx.CheckBox( self, label = label )

View File

@ -44,6 +44,11 @@ class ReviewServicePanel( wx.Panel ):
subpanels.append( self._ServiceFilePanel( self, service ) )
if self._service.GetServiceKey() == CC.TRASH_SERVICE_KEY:
subpanels.append( self._ServiceTrashPanel( self, service ) )
if service_type in HC.TAG_SERVICES:
subpanels.append( self._ServiceTagPanel( self, service ) )
@ -225,24 +230,6 @@ class ReviewServicePanel( wx.Panel ):
def EventClearTrash( self, event ):
def do_it():
hashes = self._controller.Read( 'trash_hashes' )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )
service_keys_to_content_updates = { CC.TRASH_SERVICE_KEY : [ content_update ] }
self._controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
wx.CallAfter( self._DisplayService )
self._controller.CallToThread( do_it )
def EventCopyExternalShareURL( self, event ):
shares = self._booru_shares.GetSelectedClientData()
@ -414,18 +401,6 @@ class ReviewServicePanel( wx.Panel ):
self.UnpinIPFSDirectories()
def EventServiceWideUpdate( self, event ):
with ClientGUITopLevelWindows.DialogNullipotent( self, 'advanced content update' ) as dlg:
panel = ClientGUIScrolledPanelsReview.AdvancedContentUpdatePanel( dlg, self._service_key )
dlg.SetPanel( panel )
dlg.ShowModal()
def GetServiceKey( self ):
return self._service.GetServiceKey()
@ -521,7 +496,7 @@ class ReviewServicePanel( wx.Panel ):
def _Refresh( self ):
HydrusGlobals.client_controller.CallToThread( self.THREADUpdateTagInfo )
HydrusGlobals.client_controller.CallToThread( self.THREADUpdateFileInfo )
def _UpdateFromThread( self, text ):
@ -546,7 +521,7 @@ class ReviewServicePanel( wx.Panel ):
def THREADUpdateTagInfo( self ):
def THREADUpdateFileInfo( self ):
service_info = HydrusGlobals.client_controller.Read( 'service_info', self._service.GetServiceKey() )
@ -1008,16 +983,28 @@ class ReviewServicePanel( wx.Panel ):
def _SyncNow( self ):
def do_it():
self._service.Sync( False )
self._my_updater.Update()
message = 'This will tell the database to process any outstanding update files.'
message += os.linesep * 2
message += 'This is a big task that usually runs during idle time. It locks the entire database. If you interact significantly with the program while it runs, your gui will hang, and then you will have to wait a long time for the processing to completely finish before you get it back.'
message += os.linesep * 2
message += 'If you are a new user, click \'no\'!'
self._sync_now_button.Disable()
HydrusGlobals.client_controller.CallToThread( do_it )
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
if dlg.ShowModal() == wx.ID_YES:
def do_it():
self._service.Sync( False )
self._my_updater.Update()
self._sync_now_button.Disable()
HydrusGlobals.client_controller.CallToThread( do_it )
def _UpdateFromThread( self, download_text, download_value, processing_text, processing_value, range ):
@ -1027,6 +1014,29 @@ class ReviewServicePanel( wx.Panel ):
self._download_progress.SetValue( download_text, download_value, range )
self._processing_progress.SetValue( processing_text, processing_value, range )
if processing_value == download_value:
self._sync_now_button.Disable()
if download_value == 0:
self._export_updates_button.Disable()
else:
self._export_updates_button.Enable()
if processing_value == 0:
self._reset_button.Disable()
else:
self._reset_button.Enable()
except wx.PyDeadObjectError:
pass
@ -1143,7 +1153,7 @@ class ReviewServicePanel( wx.Panel ):
self._my_updater = ClientGUICommon.ThreadToGUIUpdater( self, self._Refresh )
self._name_and_type = wx.StaticText( self )
self._rating_info_st = wx.StaticText( self )
#
@ -1151,16 +1161,26 @@ class ReviewServicePanel( wx.Panel ):
#
self.AddF( self._name_and_type, CC.FLAGS_EXPAND_PERPENDICULAR )
self.AddF( self._rating_info_st, CC.FLAGS_EXPAND_PERPENDICULAR )
HydrusGlobals.client_controller.sub( self, 'Update', 'service_updated' )
def _Refresh( self ):
# put this fetch on a thread, since it'll have to go to the db
HydrusGlobals.client_controller.CallToThread( self.THREADUpdateRatingInfo )
self._name_and_type.SetLabelText( 'This service has ratings. This box will regain its old information in a later version.' )
def _UpdateFromThread( self, text ):
try:
self._rating_info_st.SetLabelText( text )
except wx.PyDeadObjectError:
pass
def Update( self, service ):
@ -1173,6 +1193,17 @@ class ReviewServicePanel( wx.Panel ):
def THREADUpdateRatingInfo( self ):
service_info = HydrusGlobals.client_controller.Read( 'service_info', self._service.GetServiceKey() )
num_files = service_info[ HC.SERVICE_INFO_NUM_FILES ]
text = HydrusData.ConvertIntToPrettyString( num_files ) + ' files are rated'
wx.CallAfter( self._UpdateFromThread, text )
class _ServiceTagPanel( ClientGUICommon.StaticBox ):
@ -1186,6 +1217,8 @@ class ReviewServicePanel( wx.Panel ):
self._tag_info_st = wx.StaticText( self )
self._advanced_content_update = ClientGUICommon.BetterButton( self, 'advanced service-wide update', self._AdvancedContentUpdate )
#
self._Refresh()
@ -1193,10 +1226,23 @@ class ReviewServicePanel( wx.Panel ):
#
self.AddF( self._tag_info_st, CC.FLAGS_EXPAND_PERPENDICULAR )
self.AddF( self._advanced_content_update, CC.FLAGS_LONE_BUTTON )
HydrusGlobals.client_controller.sub( self, 'Update', 'service_updated' )
def _AdvancedContentUpdate( self ):
with ClientGUITopLevelWindows.DialogNullipotent( self, 'advanced content update' ) as dlg:
panel = ClientGUIScrolledPanelsReview.AdvancedContentUpdatePanel( dlg, self._service.GetServiceKey() )
dlg.SetPanel( panel )
dlg.ShowModal()
def _Refresh( self ):
HydrusGlobals.client_controller.CallToThread( self.THREADUpdateTagInfo )
@ -1245,3 +1291,49 @@ class ReviewServicePanel( wx.Panel ):
class _ServiceTrashPanel( ClientGUICommon.StaticBox ):
def __init__( self, parent, service ):
ClientGUICommon.StaticBox.__init__( self, parent, 'trash' )
self._service = service
self._clear_trash = ClientGUICommon.BetterButton( self, 'clear trash', self._ClearTrash )
#
self.AddF( self._clear_trash, CC.FLAGS_LONE_BUTTON )
def _ClearTrash( self ):
message = 'This will completely clear your trash of all its files, deleting them permanently from the client. This operation cannot be undone.'
message += os.linesep * 2
message += 'If you have many files in your trash, it will take some time to complete and for all the files to eventually be deleted.'
with ClientGUIDialogs.DialogYesNo( self, message, yes_label = 'do it', no_label = 'forget it' ) as dlg_add:
result = dlg_add.ShowModal()
if result == wx.ID_YES:
def do_it():
hashes = HydrusGlobals.client_controller.Read( 'trash_hashes' )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )
service_keys_to_content_updates = { CC.TRASH_SERVICE_KEY : [ content_update ] }
HydrusGlobals.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
HydrusGlobals.client_controller.pub( 'service_updated', self._service )
HydrusGlobals.client_controller.CallToThread( do_it )

View File

@ -1622,7 +1622,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def EventKeyDownNamespace( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -2854,7 +2854,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
shortcuts[ wx.ACCEL_ALT ] = {}
shortcuts[ wx.ACCEL_SHIFT ] = {}
for ( modifier, key, action ) in self._shortcuts.GetClientData(): shortcuts[ modifier ][ key ] = action
for ( modifier, key, action ) in self._shortcuts.GetClientData():
shortcuts[ modifier ][ key ] = action
HC.options[ 'shortcuts' ] = shortcuts
@ -2931,7 +2934,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def EventKeyDownSortBy( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
@ -4421,7 +4424,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
# the char hook event goes up. if it isn't skipped all the way, the subsequent text event will never occur
# however we don't want the char hook going all the way up sometimes!
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if not HC.PLATFORM_LINUX:

View File

@ -668,7 +668,7 @@ class FrameThatTakesScrollablePanel( FrameThatResizes ):
def EventCharHook( self, event ):
( modifier, key ) = ClientData.GetShortcutFromEvent( event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if key == wx.WXK_ESCAPE:

View File

@ -1111,6 +1111,8 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
successful_hashes = set()
i = 0
while True:
path = self._path_cache.GetNextSeed( CC.STATUS_UNKNOWN )
@ -1211,6 +1213,13 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
self._path_cache.UpdateSeedStatus( path, CC.STATUS_FAILED, exception = e )
i += 1
if i % 100 == 0:
self._ActionPaths()
if len( successful_hashes ) > 0:

View File

@ -49,7 +49,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 250
SOFTWARE_VERSION = 251
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -135,6 +135,8 @@ CONTENT_UPDATE_DENY_PEND = 11
CONTENT_UPDATE_DENY_PETITION = 12
CONTENT_UPDATE_ADVANCED = 13
CONTENT_UPDATE_UNDELETE = 14
CONTENT_UPDATE_SET = 15
CONTENT_UPDATE_FLIP = 16
content_update_string_lookup = {}
@ -151,6 +153,8 @@ content_update_string_lookup[ CONTENT_UPDATE_RATING ] = 'rating'
content_update_string_lookup[ CONTENT_UPDATE_DENY_PEND ] = 'deny pend'
content_update_string_lookup[ CONTENT_UPDATE_DENY_PETITION ] = 'deny petition'
content_update_string_lookup[ CONTENT_UPDATE_UNDELETE ] = 'undelete'
content_update_string_lookup[ CONTENT_UPDATE_SET ] = 'set'
content_update_string_lookup[ CONTENT_UPDATE_FLIP ] = 'flip on/off'
DEFINITIONS_TYPE_HASHES = 0
DEFINITIONS_TYPE_TAGS = 1
@ -159,6 +163,10 @@ DUPLICATE_UNKNOWN = 0
DUPLICATE_NOT_DUPLICATE = 1
DUPLICATE_SAME_FILE = 2
DUPLICATE_ALTERNATE = 3
DUPLICATE_BETTER = 4
DUPLICATE_SMALLER_BETTER = 5
DUPLICATE_LARGER_BETTER = 6
DUPLICATE_WORSE = 7
ENCODING_RAW = 0
ENCODING_HEX = 1

View File

@ -320,7 +320,9 @@ def LaunchDirectory( path ):
cmd.append( path )
process = subprocess.Popen( cmd, startupinfo = HydrusData.GetSubprocessStartupInfo() )
# setsid call un-childs this new process
process = subprocess.Popen( cmd, preexec_fn = os.setsid, startupinfo = HydrusData.GetSubprocessStartupInfo() )
process.wait()
@ -349,11 +351,13 @@ def LaunchFile( path ):
cmd.append( path )
process = subprocess.Popen( cmd, startupinfo = HydrusData.GetSubprocessStartupInfo() )
# setsid call un-childs this new process
process = subprocess.Popen( cmd, preexec_fn = os.setsid, startupinfo = HydrusData.GetSubprocessStartupInfo() )
process.wait()
process.communicate()
process.communicate()

View File

@ -43,6 +43,8 @@ SERIALISABLE_TYPE_METADATA = 37
SERIALISABLE_TYPE_BANDWIDTH_RULES = 38
SERIALISABLE_TYPE_BANDWIDTH_TRACKER = 39
SERIALISABLE_TYPE_CLIENT_TO_SERVER_UPDATE = 40
SERIALISABLE_TYPE_SHORTCUT = 41
SERIALISABLE_TYPE_APPLICATION_COMMAND = 42
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}

View File

@ -1171,35 +1171,41 @@ class TestClientDB( unittest.TestCase ):
def test_shortcuts( self ):
num_default = len( ClientDefaults.GetDefaultShortcuts() )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
self.assertEqual( result, [] )
self.assertEqual( len( result ), num_default )
#
shortcuts = ClientData.Shortcuts( 'test' )
shortcuts.SetKeyboardAction( wx.ACCEL_NORMAL, wx.WXK_NUMPAD1, ( HydrusData.GenerateKey(), 'action_data' ) )
shortcuts.SetKeyboardAction( wx.ACCEL_SHIFT, wx.WXK_END, ( None, 'other_action_data' ) )
self._write( 'serialisable', shortcuts )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
self.assertEqual( len( result ), 1 )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, 'test' )
self.assertEqual( result.GetKeyboardAction( wx.ACCEL_NORMAL, wx.WXK_NUMPAD1 ), shortcuts.GetKeyboardAction( wx.ACCEL_NORMAL, wx.WXK_NUMPAD1 ) )
self.assertEqual( result.GetKeyboardAction( wx.ACCEL_SHIFT, wx.WXK_END ), shortcuts.GetKeyboardAction( wx.ACCEL_SHIFT, wx.WXK_END ) )
#
self._write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, 'test' )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
self.assertEqual( result, [] )
for ( i, shortcuts ) in enumerate( ClientDefaults.GetDefaultShortcuts() ):
name = 'shortcuts ' + str( i )
shortcuts.SetName( name )
self._write( 'serialisable', shortcuts )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
self.assertEqual( len( result ), num_default + 1 )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, name )
for ( shortcut, command ) in shortcuts:
self.assertEqual( result.GetCommand( shortcut ).GetData(), command.GetData() )
#
self._write( 'delete_serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS, name )
result = self._read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
self.assertEqual( len( result ), num_default )
class TestServerDB( unittest.TestCase ):

View File

@ -1,5 +1,6 @@
import ClientConstants as CC
import ClientData
import ClientDefaults
import ClientDownloading
import ClientImporting
import ClientSearch
@ -117,53 +118,108 @@ class TestSerialisables( unittest.TestCase ):
self._dump_and_load_and_test( db, test )
def test_SERIALISABLE_TYPE_APPLICATION_COMMAND( self ):
def test( obj, dupe_obj ):
self.assertEqual( obj.GetCommandType(), dupe_obj.GetCommandType() )
self.assertEqual( obj.GetData(), dupe_obj.GetData() )
acs = []
acs.append( ( ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive' ), 'archive' ) )
acs.append( ( ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( HydrusData.GenerateKey(), HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_FLIP, 'test' ) ), 'flip on/off mappings "test" for unknown service!' ) )
acs.append( ( ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( CC.LOCAL_TAG_SERVICE_KEY, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_FLIP, 'test' ) ), 'flip on/off mappings "test" for local tags' ) )
acs.append( ( ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( HydrusData.GenerateKey(), HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_SET, 0.4 ) ), 'set ratings "0.4" for unknown service!' ) )
for ( ac, s ) in acs:
self._dump_and_load_and_test( ac, test )
self.assertEqual( ac.ToString(), s )
def test_SERIALISABLE_TYPE_SHORTCUT( self ):
def test( obj, dupe_obj ):
self.assertEqual( dupe_obj.__hash__(), ( dupe_obj._shortcut_type, dupe_obj._shortcut_key, tuple( dupe_obj._modifiers ) ).__hash__() )
self.assertEqual( obj, dupe_obj )
shortcuts = []
shortcuts.append( ( ClientData.Shortcut(), 'f7' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] ), 'space' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'a' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), 'ctrl+a' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'A' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), 'ctrl+a' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_HOME, [ CC.SHORTCUT_MODIFIER_ALT, CC.SHORTCUT_MODIFIER_CTRL ] ), 'ctrl+alt+home' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] ), 'left-click' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [ CC.SHORTCUT_MODIFIER_CTRL ] ), 'ctrl+middle-click' ) )
shortcuts.append( ( ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_DOWN, [ CC.SHORTCUT_MODIFIER_ALT, CC.SHORTCUT_MODIFIER_SHIFT ] ), 'alt+shift+scroll down' ) )
for ( shortcut, s ) in shortcuts:
self._dump_and_load_and_test( shortcut, test )
self.assertEqual( shortcut.ToString(), s )
def test_SERIALISABLE_TYPE_SHORTCUTS( self ):
def test( obj, dupe_obj ):
for ( ( modifier, key ), action ) in obj.IterateKeyboardShortcuts():
for ( shortcut, command ) in obj:
self.assertEqual( dupe_obj.GetKeyboardAction( modifier, key ), action )
for ( ( modifier, mouse_button ), action ) in obj.IterateMouseShortcuts():
self.assertEqual( dupe_obj.GetMouseAction( modifier, mouse_button ), action )
self.assertEqual( dupe_obj.GetCommand( shortcut ).GetData(), command.GetData() )
action_1 = ( HydrusData.GenerateKey(), u'test unicode tag\u2026' )
action_2 = ( HydrusData.GenerateKey(), 0.5 )
action_3 = ( None, 'archive' )
default_shortcuts = ClientDefaults.GetDefaultShortcuts()
for shortcuts in default_shortcuts:
self._dump_and_load_and_test( shortcuts, test )
command_1 = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'archive' )
command_2 = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( HydrusData.GenerateKey(), HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_FLIP, 'test' ) )
command_3 = ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_CONTENT, ( CC.LOCAL_TAG_SERVICE_KEY, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_FLIP, 'test' ) )
k_shortcut_1 = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_SPACE, [] )
k_shortcut_2 = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'a' ), [ CC.SHORTCUT_MODIFIER_CTRL ] )
k_shortcut_3 = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'A' ), [ CC.SHORTCUT_MODIFIER_CTRL ] )
k_shortcut_4 = ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_HOME, [ CC.SHORTCUT_MODIFIER_ALT, CC.SHORTCUT_MODIFIER_CTRL ] )
m_shortcut_1 = ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_LEFT, [] )
m_shortcut_2 = ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_MIDDLE, [ CC.SHORTCUT_MODIFIER_CTRL ] )
m_shortcut_3 = ClientData.Shortcut( CC.SHORTCUT_TYPE_MOUSE, CC.SHORTCUT_MOUSE_SCROLL_DOWN, [ CC.SHORTCUT_MODIFIER_ALT, CC.SHORTCUT_MODIFIER_SHIFT ] )
shortcuts = ClientData.Shortcuts( 'test' )
shortcuts.SetKeyboardAction( wx.ACCEL_NORMAL, wx.WXK_NUMPAD1, action_1 )
shortcuts.SetKeyboardAction( wx.ACCEL_SHIFT, wx.WXK_END, action_2 )
shortcuts.SetKeyboardAction( wx.ACCEL_CTRL, ord( 'R' ), action_3 )
shortcuts.SetCommand( k_shortcut_1, command_1 )
shortcuts.SetCommand( k_shortcut_2, command_2 )
shortcuts.SetCommand( k_shortcut_3, command_2 )
shortcuts.SetCommand( k_shortcut_4, command_3 )
shortcuts.SetMouseAction( wx.ACCEL_NORMAL, wx.MOUSE_BTN_LEFT, action_1 )
shortcuts.SetMouseAction( wx.ACCEL_SHIFT, wx.MOUSE_BTN_MIDDLE, action_2 )
shortcuts.SetMouseAction( wx.ACCEL_CTRL, wx.MOUSE_BTN_RIGHT, action_3 )
shortcuts.SetMouseAction( wx.ACCEL_ALT, wx.MOUSE_WHEEL_VERTICAL, action_3 )
shortcuts.SetCommand( m_shortcut_1, command_1 )
shortcuts.SetCommand( m_shortcut_2, command_2 )
shortcuts.SetCommand( m_shortcut_3, command_3 )
k_a = [ item for item in shortcuts.IterateKeyboardShortcuts() ]
self._dump_and_load_and_test( shortcuts, test )
self.assertEqual( len( k_a ), 3 )
self.assertEqual( shortcuts.GetCommand( k_shortcut_1 ).GetData(), command_1.GetData() )
for ( ( modifier, key ), action ) in k_a:
self.assertEqual( shortcuts.GetKeyboardAction( modifier, key ), action )
shortcuts.SetCommand( k_shortcut_1, command_3 )
m_a = [ item for item in shortcuts.IterateMouseShortcuts() ]
self.assertEqual( len( m_a ), 4 )
for ( ( modifier, mouse_button ), action ) in m_a:
self.assertEqual( shortcuts.GetMouseAction( modifier, mouse_button ), action )
self.assertEqual( shortcuts.GetCommand( k_shortcut_1 ).GetData(), command_3.GetData() )
def test_SERIALISABLE_TYPE_SUBSCRIPTION( self ):