diff --git a/docs/changelog.md b/docs/changelog.md
index ac5258a69..53fe035a5 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,36 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 588](https://github.com/hydrusnetwork/hydrus/releases/tag/v588)
+
+### fast new lists
+
+* tl;dr: big lists faster now. you do not need to do anything
+* every multi-column list in the program (there's about 75 of them) now works on a more sophisticated model (specifically, we are updating from QTreeWidget to QTreeView). instead of the list storing and regenerating display labels for every single row of a table, only the rows that are currently in view are generally consulted. sort events are similarly extremely fast, with off-screen updates virtualised and deferred
+* in my tests, a list with 170,000 rows now sorts in about four seconds. my code is still connected to a non-optimised part of the old system, so I hope to improve gains with background cleanup work in coming months. I believe I can make it work at least twice as fast in places, particularly in initialisation
+* multi-column lists are much better about initialising/migrating the selection 'focus' (the little, usually dotted-line-border box that says where keyboard focus is) through programmatic insertions and deletes and sorts
+* column headers now show the up/down 'sort' arrows using native style. everything is a bit more Qt-native and closer to C++ instead of my old custom garbage
+* none of this changes anything to do with single-column lists across the program, which are still using somewhat jank old code. my taglist in particular is an entirely custom object that is neat in some ways but stuck in place by my brittle design. the above rewrite was tricky in a couple of annoying ways but overall very worth doing, so I expect to replicate it elsewhere. another open choice is rewriting the similarly entirely custom thumbnail canvas to a proper Qt widget with a QLayout and such. we'll see how future work goes
+
+### misc
+
+* fixed the 'show' part of 'pages->sidebar and preview panels->show/hide sidebar and preview panel', which was busted last week in the page relayout cleanup
+* I think I fixed the frame of flicker (usually a moment of page-wide autocomplete input) you would sometimes get when clicking a 'show these files' popup message files button
+* fixed the new shimmie parser (by adding a simpler dupe and wangling the example urls around) to correctly parse r34h tags
+* I think I may have fixed some deadlocks and/or mega-pauses in the manage tag parents/siblings dialogs when entering pairs causes a dialog (a yes/no confirmation, or the 'enter a reason' input) to pop up
+* I think I have fixed the 'switch between fullscreen borderless and regular framed window' command when set to the 'media_viewer' shortcut set. some command-processing stuff wasn't wired up fully after I cleared out some old hacks a while ago
+* the manage tag parents dialog has some less janky layout as it is expanded/shrunk
+* if similar files search tree maintenance fails to regenerate a branch, the user is now told to try running the full regen
+* the full similar files search tree regen now filters out nodes with invalid phashes (i.e. due to database damage), deleting those nodes fully and printing all pertinent info to the log, and tells the user what to do next
+* you can now regen the similar files search tree on an empty database without error, lol
+* while I was poking around lists, I fixed a bit of bad error handling when you try to import a broken serialised data png to a multi-column list
+
+### client api
+
+* the `/get_files/search_files` command now supports `include_current_tags` and `include_pending_tags`, mirroring the buttons on the normal search interface (issue #1577)
+* updated the help and unit tests to check these new params
+* client api version is now 69
+
## [Version 587](https://github.com/hydrusnetwork/hydrus/releases/tag/v587)
### all misc this week
@@ -348,30 +378,3 @@ title: Changelog
* cleaned up some regex ui code
* cleaned up some garbage in the string panel ui code
* fixed some weird vertical stretch in some single-control dialogs
-
-## [Version 578](https://github.com/hydrusnetwork/hydrus/releases/tag/v578)
-
-### animated webp
-
-* we now have animated webp support! despite many libraries having trouble with this, it turns out that modern PIL can decode and render them. I have figured out a solution using my old native gif renderer, so webps will now play in the program
-* I'm going the same route as gifs and (a)pngs--the program now tracks 'webp' vs 'animated webp' as different filetypes. all your image webps will be queued for a scan on update, and any with animation will become the new type, with num frames and duration, and will be fully viewable in the media viewer
-* I don't know when PIL added this tech, so if you are a source user and haven't rebuilt your venv in a while, this is probably a good time!
-
-### misc
-
-* the five new 'draw hover-window text in the background of media viewer' options are now copied to the media viewer itself, under a new 'eye' icon menu button. I'll be hanging more stuff off here, like 'always on top' in future!
-* the 'known urls' media submenu is now just 'urls', and it now has A) the 'manage' command, moved from the manage menu and B) 'open in a new page' for the focused file's specific URLs or 'any of them' (i.e. it opens a new search page with 'system:known url=blah', so if you need to find which files share a URL, it is now just one click
-* fixed the gelbooru 0.2.5 post parser's fetching of multiple source urls. it was not splitting them correctly due to a (recent?) change on gelbooru's end and adding unhelpful `https://gelbooru.com/%7C` gumpf as an additional source url
-* added a 'network report mode (silent)' to the `help->debug` menu, which does everything the network report mode does but with silent logging rather than a million popups. should help with longer-term debugging
-* fixed an issue with fetching gif variable framerate timings in the native renderer
-* added variable framerate tech to the native renderer for all animation types PIL can figure out except apng (previously it could only do it for gif)
-* tightened up some of the file metadata checks. apng is now scanned for exif data, the HEIF types are now scanned for transparency. these jobs are also queued up on update
-* the media viewer's top hover's center buttons (usually inbox/delete stuff) are finally centered correctly, aligned with the text below. apologies for the delay; it took several years of under-waterfall mediation to gather enough chi, but I finally have a beginner's understanding of `QSizePolicy.Expanding`
-
-### boring cleanup
-
-* fixed some more long tooltips to wrap into nice newlines
-* converted the 'manage urls' dialog to the decoupled 'edit' paradigm
-* refactored most of the 'scrolling panel' code to a new `gui.panels` module
-* broke up some of the bloated scrolling panel code into smaller files, moved Migrate Tags and Edit Timestamps to new files in `gui.metadata`, and replaced/deleted some old code
-* refactored `ClientGUITime` to `gui.metadata`, `ClientGUILogin` to `gui.networking`
diff --git a/docs/developer_api.md b/docs/developer_api.md
index bde63dd7b..47a283d8b 100644
--- a/docs/developer_api.md
+++ b/docs/developer_api.md
@@ -1466,6 +1466,8 @@ Arguments (in percent-encoded JSON):
* `tags`: (a list of tags you wish to search for)
* [file domain](#parameters_file_domain) (optional, defaults to _all my files_)
* `tag_service_key`: (optional, hexadecimal, the tag domain on which to search, defaults to _all my files_)
+ * `include_current_tags`: (optional, bool, whether to search 'current' tags, defaults to `true`)
+ * `include_pending_tags`: (optional, bool, whether to search 'pending' tags, defaults to `true`)
* `file_sort_type`: (optional, integer, the results sort method, defaults to `2` for `import time`)
* `file_sort_asc`: true or false (optional, default `true`, the results sort order)
* `return_file_ids`: true or false (optional, default `true`, returns file id results)
@@ -1590,7 +1592,9 @@ Makes:
* samus aran OR lara croft
* system:height > 1000
-The file and tag services are for search domain selection, just like clicking the buttons in the client. They are optional--default is 'all my files' and 'all known tags'.
+The file and tag services are for search domain selection, just like clicking the buttons in the client. They are optional--default is 'all my files' and 'all known tags'.
+
+`include_current_tags` and `include_pending_tags` do the same as the buttons on the normal search interface. They alter the search of normal tags and tag-related system predicates like 'system:number of tags', including or discluding that type of tag from whatever the search is doing. If you set both of these to `false`, you'll often get no results.
File searches occur in the `display` `tag_display_type`. If you want to pair autocomplete tag lookup from [/search_tags](#add_tags_search_tags) to this file search (e.g. for making a standard booru search interface), then make sure you are searching `display` tags there.
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index 8732c8723..fbb885cb9 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,33 @@
+ -
+
+
+ fast new lists
+ - tl;dr: big lists faster now. you do not need to do anything
+ - every multi-column list in the program (there's about 75 of them) now works on a more sophisticated model (specifically, we are updating from QTreeWidget to QTreeView). instead of the list storing and regenerating display labels for every single row of a table, only the rows that are currently in view are generally consulted. sort events are similarly extremely fast, with off-screen updates virtualised and deferred
+ - in my tests, a list with 170,000 rows now sorts in about four seconds. my code is still connected to a non-optimised part of the old system, so I hope to improve gains with background cleanup work in coming months. I believe I can make it work at least twice as fast in places, particularly in initialisation
+ - multi-column lists are much better about initialising/migrating the selection 'focus' (the little, usually dotted-line-border box that says where keyboard focus is) through programmatic insertions and deletes and sorts
+ - column headers now show the up/down 'sort' arrows using native style. everything is a bit more Qt-native and closer to C++ instead of my old custom garbage
+ - none of this changes anything to do with single-column lists across the program, which are still using somewhat jank old code. my taglist in particular is an entirely custom object that is neat in some ways but stuck in place by my brittle design. the above rewrite was tricky in a couple of annoying ways but overall very worth doing, so I expect to replicate it elsewhere. another open choice is rewriting the similarly entirely custom thumbnail canvas to a proper Qt widget with a QLayout and such. we'll see how future work goes
+ misc
+ - fixed the 'show' part of 'pages->sidebar and preview panels->show/hide sidebar and preview panel', which was busted last week in the page relayout cleanup
+ - I think I fixed the frame of flicker (usually a moment of page-wide autocomplete input) you would sometimes get when clicking a 'show these files' popup message files button
+ - fixed the new shimmie parser (by adding a simpler dupe and wangling the example urls around) to correctly parse r34h tags
+ - I think I may have fixed some deadlocks and/or mega-pauses in the manage tag parents/siblings dialogs when entering pairs causes a dialog (a yes/no confirmation, or the 'enter a reason' input) to pop up
+ - I think I have fixed the 'switch between fullscreen borderless and regular framed window' command when set to the 'media_viewer' shortcut set. some command-processing stuff wasn't wired up fully after I cleared out some old hacks a while ago
+ - the manage tag parents dialog has some less janky layout as it is expanded/shrunk
+ - if similar files search tree maintenance fails to regenerate a branch, the user is now told to try running the full regen
+ - the full similar files search tree regen now filters out nodes with invalid phashes (i.e. due to database damage), deleting those nodes fully and printing all pertinent info to the log, and tells the user what to do next
+ - you can now regen the similar files search tree on an empty database without error, lol
+ - while I was poking around lists, I fixed a bit of bad error handling when you try to import a broken serialised data png to a multi-column list
+ client api
+ - the `/get_files/search_files` command now supports `include_current_tags` and `include_pending_tags`, mirroring the buttons on the normal search interface (issue #1577)
+ - updated the help and unit tests to check these new params
+ - client api version is now 69
+
+
-
diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py
index a078d8a2e..ddc77112e 100644
--- a/hydrus/client/ClientFiles.py
+++ b/hydrus/client/ClientFiles.py
@@ -2760,7 +2760,7 @@ def _RegenSimilarFilesMetadata( self, media_result ):
self._controller.WriteSynchronous( 'file_maintenance_add_jobs_hashes', { hash }, REGENERATE_FILE_DATA_JOB_CHECK_SIMILAR_FILES_MEMBERSHIP )
- return None
+ return []
try:
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 39e56af12..b1f2a681c 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -5249,7 +5249,7 @@ def _LoadModules( self ):
#
- self.modules_similar_files = ClientDBSimilarFiles.ClientDBSimilarFiles( self._c, self.modules_services, self.modules_files_storage )
+ self.modules_similar_files = ClientDBSimilarFiles.ClientDBSimilarFiles( self._c, self.modules_services, self.modules_hashes, self.modules_files_storage )
self._modules.append( self.modules_similar_files )
@@ -10665,6 +10665,40 @@ def ask_what_to_do_zip_docx_scan():
+ if version == 587:
+
+ try:
+
+ domain_manager: ClientNetworkingDomain.NetworkDomainManager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
+
+ domain_manager.Initialise()
+
+ # new example urls suggest new links, this should force the detach and re-link we want
+ domain_manager.DeleteParsers( 'shimmie file page parser' )
+
+ domain_manager.OverwriteDefaultParsers( [
+ 'shimmie file page parser',
+ 'shimmie file page parser - simple tags'
+ ] )
+
+ #
+
+ domain_manager.TryToLinkURLClassesAndParsers()
+
+ #
+
+ self.modules_serialisable.SetJSONDump( domain_manager )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Trying to update some downloaders failed! Please let hydrus dev know!'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusNumbers.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/db/ClientDBSimilarFiles.py b/hydrus/client/db/ClientDBSimilarFiles.py
index eaf92d004..cdbc63a9d 100644
--- a/hydrus/client/db/ClientDBSimilarFiles.py
+++ b/hydrus/client/db/ClientDBSimilarFiles.py
@@ -15,13 +15,15 @@
from hydrus.client import ClientThreading
from hydrus.client.db import ClientDBFilesStorage
from hydrus.client.db import ClientDBModule
+from hydrus.client.db import ClientDBMaster
from hydrus.client.db import ClientDBServices
class ClientDBSimilarFiles( ClientDBModule.ClientDBModule ):
- def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.ClientDBMasterServices, modules_files_storage: ClientDBFilesStorage.ClientDBFilesStorage ):
+ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.ClientDBMasterServices, modules_hashes: ClientDBMaster.ClientDBMasterHashes, modules_files_storage: ClientDBFilesStorage.ClientDBFilesStorage ):
self.modules_services = modules_services
+ self.modules_hashes = modules_hashes
self.modules_files_storage = modules_files_storage
self._reported_on_a_broken_branch = False
@@ -733,7 +735,16 @@ def MaintainTree( self, maintenance_mode = HC.MAINTENANCE_FORCED, job_status = N
- self._RegenerateBranch( job_status, biggest_perceptual_hash_id )
+ try:
+
+ self._RegenerateBranch( job_status, biggest_perceptual_hash_id )
+
+ except:
+
+ HydrusData.ShowText( 'It looks like similar files maintenance had trouble regenerating a branch of the search tree! You should try _database->regenerate->similar files search tree_, and if that still produces errors, let hydev know.' )
+
+ raise
+
rebalance_perceptual_hash_ids = self._STL( self._Execute( 'SELECT phash_id FROM shape_maintenance_branch_regen;' ) )
@@ -793,6 +804,75 @@ def RegenerateTree( self ):
all_nodes = self._Execute( 'SELECT phash_id, phash FROM shape_perceptual_hashes;' ).fetchall()
+ good_nodes = []
+ bad_nodes = []
+
+ for ( phash_id, phash ) in all_nodes:
+
+ if isinstance( phash, bytes ) and len( phash ) == 8:
+
+ good_nodes.append( ( phash_id, phash ) )
+
+ else:
+
+ bad_nodes.append( ( phash_id, phash ) )
+
+
+
+ all_nodes = good_nodes
+
+ if len( all_nodes ) == 0:
+
+ return
+
+
+ if len( bad_nodes ) > 0:
+
+ bad_phash_ids = { phash_id for ( phash_id, phash ) in bad_nodes }
+
+ self._ExecuteMany( 'DELETE FROM shape_perceptual_hashes WHERE phash_id = ?;', ( ( phash_id, ) for phash_id in bad_phash_ids ) )
+
+ with self._MakeTemporaryIntegerTable( bad_phash_ids, 'phash_id' ) as temp_table_name:
+
+ affected_hash_ids = self._STS( self._Execute( f'SELECT hash_id FROM {temp_table_name} CROSS JOIN shape_perceptual_hash_map USING ( phash_id );' ) )
+
+
+ self._ExecuteMany( 'DELETE FROM shape_perceptual_hash_map WHERE phash_id = ?;', ( ( phash_id, ) for phash_id in bad_phash_ids ) )
+
+ message = 'Discovered some bad nodes in your similar files search tree! The nodes have been deleted.'
+ message += '\n'
+ message += 'More details have been written to log, including the file list for affected hashes. You may wish to manually schedule a "similar files regen" job for all the affected file hashes. You should also check out "help my db is broke.txt" in your install_dir/db folder, since there are no clean ways these bad nodes got into your database.'
+
+ HydrusData.ShowText( message )
+
+ HydrusData.Print( 'The bad nodes:' )
+
+ for ( phash_id, phash ) in bad_nodes:
+
+ if isinstance( phash, bytes ):
+
+ HydrusData.Print( f'phash_id: {phash_id}, presumably incorrect-length phash: "{phash.hex()}"' )
+
+ else:
+
+ HydrusData.Print( f'phash_id: {phash_id}, presumably corrupt phash: "{phash}"' )
+
+
+
+ HydrusData.Print( 'The affected hashes:' )
+
+ try:
+
+ hash_ids_to_hashes = self.modules_hashes.GetHashIdsToHashes( hash_ids = affected_hash_ids )
+
+ HydrusData.Print( '\n'.join( ( hash.hex() for hash in hash_ids_to_hashes.values() ) ) )
+
+ except:
+
+ HydrusData.Print( 'Could not print affected hashes (might be your regular hashes are busted too)' )
+
+
+
job_status.SetStatusText( HydrusNumbers.ToHumanInt( len( all_nodes ) ) + ' leaves found, now regenerating' )
( root_id, root_perceptual_hash ) = self._PopBestRootNode( all_nodes ) #HydrusData.RandomPop( all_nodes )
diff --git a/hydrus/client/db/ClientDBTagSiblings.py b/hydrus/client/db/ClientDBTagSiblings.py
index 337a0300e..686edf73f 100644
--- a/hydrus/client/db/ClientDBTagSiblings.py
+++ b/hydrus/client/db/ClientDBTagSiblings.py
@@ -1050,8 +1050,6 @@ def RegenChains( self, tag_service_ids, tag_ids ):
for applicable_tag_service_id in applicable_tag_service_ids:
- service_key = self.modules_services.GetService( applicable_tag_service_id ).GetServiceKey()
-
statuses_to_pair_ids = self.GetTagSiblingsIdsChains( applicable_tag_service_id, tag_ids_to_clear_and_regen )
petitioned_fast_lookup = set( statuses_to_pair_ids[ HC.CONTENT_STATUS_PETITIONED ] )
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 5ceaaee5c..914a80f8c 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -1597,6 +1597,18 @@ def _DebugMakeSomePopups( self ):
CG.client_controller.pub( 'message', job_status )
+ #
+
+ job_status = ClientThreading.JobStatus()
+
+ job_status.SetStatusText( 'client api test file popup' )
+
+ hashes = [ bytes.fromhex( '78f92ba4a786225ee2a1236efa6b7dc81dd729faf4af99f96f3e20bad6d8b538' ) ]
+
+ job_status.SetFiles( hashes, 'go' )
+
+ self._controller.pub( 'message', job_status )
+
#
job_status = ClientThreading.JobStatus()
diff --git a/hydrus/client/gui/ClientGUIDialogsManage.py b/hydrus/client/gui/ClientGUIDialogsManage.py
index 5abe215cd..5fea2121b 100644
--- a/hydrus/client/gui/ClientGUIDialogsManage.py
+++ b/hydrus/client/gui/ClientGUIDialogsManage.py
@@ -607,7 +607,9 @@ def __init__( self, parent ):
self._mappings_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._mappings_list = ClientGUIListCtrl.BetterListCtrl( self._mappings_listctrl_panel, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, 12, self._ConvertDataToListCtrlTuples, delete_key_callback = self._Remove, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._mappings_list = ClientGUIListCtrl.BetterListCtrlTreeView( self._mappings_listctrl_panel, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, 12, model, delete_key_callback = self._Remove, activation_callback = self._Edit )
self._mappings_listctrl_panel.SetListCtrl( self._mappings_list )
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index 256fe08ca..3c7487e78 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -2,6 +2,7 @@
import typing
import urllib.parse
+from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
@@ -57,7 +58,9 @@ def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_displa
self._gug_display_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook )
- self._gug_display_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._gug_display_list_ctrl_panel, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, 15, self._ConvertGUGDisplayDataToListCtrlTuples, activation_callback = self._EditGUGDisplay )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, self._ConvertGUGDisplayDataToListCtrlTuples )
+
+ self._gug_display_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_display_list_ctrl_panel, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, 15, model, activation_callback = self._EditGUGDisplay )
self._gug_display_list_ctrl_panel.SetListCtrl( self._gug_display_list_ctrl )
@@ -69,7 +72,9 @@ def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_displa
self._url_display_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( media_viewer_urls_panel )
- self._url_display_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._url_display_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, 15, self._ConvertURLDisplayDataToListCtrlTuples, activation_callback = self._EditURLDisplay )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, self._ConvertURLDisplayDataToListCtrlTuples )
+
+ self._url_display_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._url_display_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, 15, model, activation_callback = self._EditURLDisplay )
self._url_display_list_ctrl_panel.SetListCtrl( self._url_display_list_ctrl )
@@ -440,7 +445,9 @@ def __init__( self, parent: QW.QWidget, ngug: ClientNetworkingGUG.NestedGalleryU
self._gug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUG_GUGS.ID, 30, self._ConvertGUGDataToListCtrlTuples, use_simple_delete = True )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NGUG_GUGS.ID, self._ConvertGUGDataToListCtrlTuples )
+
+ self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUG_GUGS.ID, 30, model, use_simple_delete = True )
self._gug_list_ctrl_panel.SetListCtrl( self._gug_list_ctrl )
@@ -580,7 +587,9 @@ def __init__( self, parent: QW.QWidget, gugs ):
self._gug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook )
- self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_GUGS.ID, 30, self._ConvertGUGToListCtrlTuples, delete_key_callback = self._DeleteGUG, activation_callback = self._EditGUG )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GUGS.ID, self._ConvertGUGToListCtrlTuples )
+
+ self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_GUGS.ID, 30, model, delete_key_callback = self._DeleteGUG, activation_callback = self._EditGUG )
self._gug_list_ctrl_panel.SetListCtrl( self._gug_list_ctrl )
@@ -596,7 +605,9 @@ def __init__( self, parent: QW.QWidget, gugs ):
self._ngug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook )
- self._ngug_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._ngug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUGS.ID, 20, self._ConvertNGUGToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditNGUG )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NGUGS.ID, self._ConvertNGUGToListCtrlTuples )
+
+ self._ngug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._ngug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUGS.ID, 20, model, use_simple_delete = True, activation_callback = self._EditNGUG )
self._ngug_list_ctrl_panel.SetListCtrl( self._ngug_list_ctrl )
@@ -1339,7 +1350,9 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
parameters_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( parameters_panel )
- self._parameters = ClientGUIListCtrl.BetterListCtrl( parameters_listctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.ID, 5, self._ConvertParameterToListCtrlTuples, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_PARAMETERS.ID, self._ConvertParameterToListCtrlTuples )
+
+ self._parameters = ClientGUIListCtrl.BetterListCtrlTreeView( parameters_listctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_PARAMETERS.ID, 5, model, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )
parameters_listctrl_panel.SetListCtrl( self._parameters )
@@ -2286,7 +2299,9 @@ def __init__( self, parent: QW.QWidget, url_classes: typing.Iterable[ ClientNetw
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASSES.ID, 15, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASSES.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASSES.ID, 15, model, use_simple_delete = True, activation_callback = self._Edit )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -2300,9 +2315,7 @@ def __init__( self, parent: QW.QWidget, url_classes: typing.Iterable[ ClientNetw
#
- self._list_ctrl.AddDatas( url_classes )
-
- self._list_ctrl.Sort()
+ self._list_ctrl.SetData( url_classes )
#
@@ -2519,13 +2532,17 @@ def __init__( self, parent: QW.QWidget, network_engine, url_classes, parsers, ur
#
- self._api_pairs_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._notebook, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, 10, self._ConvertAPIPairDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, self._ConvertAPIPairDataToListCtrlTuples )
+
+ self._api_pairs_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._notebook, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, 10, model )
#
self._parser_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook )
- self._parser_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._parser_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, 24, self._ConvertParserDataToListCtrlTuples, activation_callback = self._EditParser )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, self._ConvertParserDataToListCtrlTuples )
+
+ self._parser_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._parser_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, 24, model, activation_callback = self._EditParser )
self._parser_list_ctrl_panel.SetListCtrl( self._parser_list_ctrl )
diff --git a/hydrus/client/gui/ClientGUIShortcutControls.py b/hydrus/client/gui/ClientGUIShortcutControls.py
index a2a4e5ce9..046d24c5b 100644
--- a/hydrus/client/gui/ClientGUIShortcutControls.py
+++ b/hydrus/client/gui/ClientGUIShortcutControls.py
@@ -111,7 +111,9 @@ def __init__( self, parent, shortcuts: ClientGUIShortcuts.ShortcutSet ):
self._shortcuts_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._shortcuts = ClientGUIListCtrl.BetterListCtrl( self._shortcuts_panel, CGLC.COLUMN_LIST_SHORTCUTS.ID, 20, data_to_tuples_func = self._ConvertSortTupleToPrettyTuple, delete_key_callback = self.RemoveShortcuts, activation_callback = self.EditShortcuts )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUTS.ID, self._ConvertSortTupleToPrettyTuple )
+
+ self._shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( self._shortcuts_panel, CGLC.COLUMN_LIST_SHORTCUTS.ID, 20, model, delete_key_callback = self.RemoveShortcuts, activation_callback = self.EditShortcuts )
self._shortcuts_panel.SetListCtrl( self._shortcuts )
@@ -384,7 +386,9 @@ def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merg
reserved_panel = ClientGUICommon.StaticBox( self, 'built-in hydrus shortcut sets' )
- self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrl( reserved_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, data_to_tuples_func = self._GetTuples, activation_callback = self._EditReserved )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetTuples )
+
+ self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( reserved_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, model, activation_callback = self._EditReserved )
self._reserved_shortcuts.setMinimumSize( QC.QSize( 320, 200 ) )
@@ -395,7 +399,9 @@ def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merg
custom_panel = ClientGUICommon.StaticBox( self, 'custom user sets' )
- self._custom_shortcuts = ClientGUIListCtrl.BetterListCtrl( custom_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, data_to_tuples_func = self._GetTuples, delete_key_callback = self._Delete, activation_callback = self._EditCustom )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetTuples )
+
+ self._custom_shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( custom_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, model, delete_key_callback = self._Delete, activation_callback = self._EditCustom )
self._add_button = ClientGUICommon.BetterButton( custom_panel, 'add', self._Add )
self._edit_custom_button = ClientGUICommon.BetterButton( custom_panel, 'edit', self._EditCustom )
diff --git a/hydrus/client/gui/ClientGUIStringControls.py b/hydrus/client/gui/ClientGUIStringControls.py
index 4de5c98bf..e289d5784 100644
--- a/hydrus/client/gui/ClientGUIStringControls.py
+++ b/hydrus/client/gui/ClientGUIStringControls.py
@@ -212,7 +212,9 @@ def __init__( self, parent, initial_dict: typing.Dict[ ClientStrings.StringMatch
column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.KEY : self._key_name }
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, model, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
listctrl_panel.SetListCtrl( self._listctrl )
@@ -399,7 +401,9 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, str ], min_height =
column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_VALUE.KEY : self._key_name, CGLC.COLUMN_LIST_KEY_TO_VALUE.VALUE : self._value_name }
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, min_height, self._ConvertDataToListCtrlTuples, use_simple_delete = use_simple_delete, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, self._ConvertDataToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, min_height, model, use_simple_delete = use_simple_delete, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
self._listctrl.columnListContentsChanged.connect( self.columnListContentsChanged )
listctrl_panel.SetListCtrl( self._listctrl )
@@ -552,6 +556,7 @@ def SetValue( self, str_to_str_dict: typing.Dict[ str, str ] ):
self._SetValue( str_to_str_dict )
+
class StringToStringMatchDictControl( QW.QWidget ):
def __init__( self, parent, initial_dict: typing.Dict[ str, ClientStrings.StringMatch ], min_height = 10, key_name = 'key' ):
@@ -564,7 +569,9 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, ClientStrings.String
column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.KEY : self._key_name }
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, model, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides )
listctrl_panel.SetListCtrl( self._listctrl )
diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py
index df418e189..bd1f16418 100644
--- a/hydrus/client/gui/ClientGUIStringPanels.py
+++ b/hydrus/client/gui/ClientGUIStringPanels.py
@@ -312,7 +312,9 @@ def __init__( self, parent: QW.QWidget, string_converter: ClientStrings.StringCo
conversions_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._conversions = ClientGUIListCtrl.BetterListCtrl( conversions_panel, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, 7, self._ConvertConversionToListCtrlTuples, delete_key_callback = self._DeleteConversion, activation_callback = self._EditConversion )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, self._ConvertConversionToListCtrlTuples )
+
+ self._conversions = ClientGUIListCtrl.BetterListCtrlTreeView( conversions_panel, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, 7, model, delete_key_callback = self._DeleteConversion, activation_callback = self._EditConversion )
conversions_panel.SetListCtrl( self._conversions )
@@ -402,7 +404,7 @@ def _AddConversion( self ):
if dlg.exec() == QW.QDialog.Accepted:
- number = self._conversions.topLevelItemCount() + 1
+ number = self._conversions.count() + 1
( conversion_type, data ) = panel.GetValue()
@@ -427,7 +429,7 @@ def _CanMoveDown( self ):
( number, conversion_type, data ) = selected_data[0]
- if number < self._conversions.topLevelItemCount():
+ if number < self._conversions.count():
return True
@@ -493,7 +495,7 @@ def _DeleteConversion( self ):
# now we need to shuffle up any missing numbers
- num_rows = self._conversions.topLevelItemCount()
+ num_rows = self._conversions.count()
i = 1
search_i = i
diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py
index df2705602..6b888e01c 100644
--- a/hydrus/client/gui/ClientGUISubscriptions.py
+++ b/hydrus/client/gui/ClientGUISubscriptions.py
@@ -187,7 +187,9 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions.
queries_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._query_panel )
- self._query_headers = ClientGUIListCtrl.BetterListCtrl( queries_panel, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, 10, self._ConvertQueryHeaderToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditQuery )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, self._ConvertQueryHeaderToListCtrlTuples )
+
+ self._query_headers = ClientGUIListCtrl.BetterListCtrlTreeView( queries_panel, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, 10, model, use_simple_delete = True, activation_callback = self._EditQuery )
queries_panel.SetListCtrl( self._query_headers )
@@ -1322,7 +1324,9 @@ def __init__( self, parent: QW.QWidget, subscriptions: typing.Collection[ Client
self._subscriptions_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._subscriptions = ClientGUIListCtrl.BetterListCtrl( self._subscriptions_panel, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, 12, self._ConvertSubscriptionToListCtrlTuples, use_simple_delete = True, activation_callback = self.Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, self._ConvertSubscriptionToListCtrlTuples )
+
+ self._subscriptions = ClientGUIListCtrl.BetterListCtrlTreeView( self._subscriptions_panel, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, 12, model, use_simple_delete = True, activation_callback = self.Edit )
self._subscriptions_panel.SetListCtrl( self._subscriptions )
diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py
index 15d4fb236..efa72ab02 100644
--- a/hydrus/client/gui/ClientGUITags.py
+++ b/hydrus/client/gui/ClientGUITags.py
@@ -3573,7 +3573,9 @@ def __init__( self, parent, service_key, tags = None ):
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._tag_parents = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 6, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_PARENTS.ID, self._ConvertPairToListCtrlTuples )
+
+ self._tag_parents = ClientGUIListCtrl.BetterListCtrlTreeView( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 6, model, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
self._listctrl_panel.SetListCtrl( self._tag_parents )
@@ -3656,7 +3658,7 @@ def __init__( self, parent, service_key, tags = None ):
QP.AddToLayout( vbox, ClientGUICommon.WrapInText( self._pursue_whole_chain, self, 'show whole chains' ), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, workspace_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( vbox, tags_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+ QP.AddToLayout( vbox, tags_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, input_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self.setLayout( vbox )
@@ -4319,7 +4321,9 @@ def __init__( self, parent, service_key, tags = None ):
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 14, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, self._ConvertPairToListCtrlTuples )
+
+ self._tag_siblings = ClientGUIListCtrl.BetterListCtrlTreeView( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 14, model, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
self._listctrl_panel.SetListCtrl( self._tag_siblings )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index cc7941aba..422c11e68 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -2354,6 +2354,10 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
self._TryToCloseWindow()
+ elif action == CAC.SIMPLE_SWITCH_BETWEEN_FULLSCREEN_BORDERLESS_AND_REGULAR_FRAMED_WINDOW:
+
+ self.parentWidget().FullscreenSwitch()
+
else:
command_processed = False
@@ -3640,7 +3644,7 @@ def AddMediaResults( self, page_key, media_results ):
def EventFullscreenSwitch( self, event ):
- self.parentWidget().FullscreenSwitch()
+ self.ProcessApplicationCommand( CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SWITCH_BETWEEN_FULLSCREEN_BORDERLESS_AND_REGULAR_FRAMED_WINDOW ) )
def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ):
@@ -4588,11 +4592,11 @@ def ShowMenu( self ):
if self.parentWidget().isFullScreen():
- ClientGUIMenus.AppendMenuItem( menu, 'exit fullscreen', 'Make this media viewer a regular window with borders.', self.parentWidget().FullscreenSwitch )
+ ClientGUIMenus.AppendMenuItem( menu, 'exit fullscreen', 'Make this media viewer a regular window with borders.', self.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SWITCH_BETWEEN_FULLSCREEN_BORDERLESS_AND_REGULAR_FRAMED_WINDOW ) )
else:
- ClientGUIMenus.AppendMenuItem( menu, 'go fullscreen', 'Make this media viewer a fullscreen window without borders.', self.parentWidget().FullscreenSwitch )
+ ClientGUIMenus.AppendMenuItem( menu, 'go fullscreen', 'Make this media viewer a fullscreen window without borders.', self.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_SWITCH_BETWEEN_FULLSCREEN_BORDERLESS_AND_REGULAR_FRAMED_WINDOW ) )
slideshow = ClientGUIMenus.GenerateMenu( menu )
diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py
index 242140aeb..272047b3e 100644
--- a/hydrus/client/gui/exporting/ClientGUIExport.py
+++ b/hydrus/client/gui/exporting/ClientGUIExport.py
@@ -51,7 +51,9 @@ def __init__( self, parent, export_folders ):
self._export_folders_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._export_folders = ClientGUIListCtrl.BetterListCtrl( self._export_folders_panel, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, 6, self._ConvertExportFolderToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, self._ConvertExportFolderToListCtrlTuples )
+
+ self._export_folders = ClientGUIListCtrl.BetterListCtrlTreeView( self._export_folders_panel, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, 6, model, use_simple_delete = True, activation_callback = self._Edit )
self._export_folders_panel.SetListCtrl( self._export_folders )
@@ -587,7 +589,9 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
self._tags_box.setMinimumSize( QC.QSize( 220, 300 ) )
- self._paths = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, 24, self._ConvertDataToListCtrlTuples, delete_key_callback = self._DeletePaths )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, self._ConvertDataToListCtrlTuples )
+
+ self._paths = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, 24, model, delete_key_callback = self._DeletePaths )
self._paths.Sort()
@@ -688,7 +692,7 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
ClientGUIFunctions.SetFocusLater( self._export )
- self._paths.itemSelectionChanged.connect( self._RefreshTags )
+ self._paths.selectionModel().selectionChanged.connect( self._RefreshTags )
self._metadata_routers_button.valueChanged.connect( self._MetadataRoutersUpdated )
if do_export_and_then_quit:
diff --git a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py
index 314becef5..daf836a12 100644
--- a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py
+++ b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py
@@ -336,7 +336,9 @@ def __init__( self, parent, controller, file_seed_cache: ClientImportFileSeeds.F
# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on file_seed add/remove
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, 30, self._ConvertFileSeedToListCtrlTuples, activation_callback = self._ShowSelectionInNewPage, delete_key_callback = self._DeleteSelected )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, self._ConvertFileSeedToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, 30, model, activation_callback = self._ShowSelectionInNewPage, delete_key_callback = self._DeleteSelected )
#
diff --git a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py
index ff3c5ab1e..cf3dcc480 100644
--- a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py
+++ b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py
@@ -258,7 +258,9 @@ def __init__( self, parent, controller, read_only: bool, can_generate_more_pages
# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on gallery_seed add/remove
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, 30, self._ConvertGallerySeedToListCtrlTuples, delete_key_callback = self._DeleteSelected )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, self._ConvertGallerySeedToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, 30, model, delete_key_callback = self._DeleteSelected )
#
diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py
index 6d05d5c09..c6f59348c 100644
--- a/hydrus/client/gui/importing/ClientGUIImport.py
+++ b/hydrus/client/gui/importing/ClientGUIImport.py
@@ -251,7 +251,9 @@ def __init__( self, parent, service_key, filename_tagging_options, present_for_a
quick_namespaces_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._quick_namespaces_panel )
- self._quick_namespaces_list = ClientGUIListCtrl.BetterListCtrl( quick_namespaces_listctrl_panel, CGLC.COLUMN_LIST_QUICK_NAMESPACES.ID, 4, self._ConvertQuickRegexDataToListCtrlTuples, use_simple_delete = True, activation_callback = self.EditQuickNamespaces )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_QUICK_NAMESPACES.ID, self._ConvertQuickRegexDataToListCtrlTuples )
+
+ self._quick_namespaces_list = ClientGUIListCtrl.BetterListCtrlTreeView( quick_namespaces_listctrl_panel, CGLC.COLUMN_LIST_QUICK_NAMESPACES.ID, 4, model, use_simple_delete = True, activation_callback = self.EditQuickNamespaces )
quick_namespaces_listctrl_panel.SetListCtrl( self._quick_namespaces_list )
@@ -959,9 +961,11 @@ def __init__( self, parent, service_key, paths ):
self._service_key = service_key
self._paths = paths
- self._paths_list = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, self._ConvertDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._paths_list = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, model )
- self._paths_list.itemSelectionChanged.connect( self.EventItemSelected )
+ self._paths_list.selectionModel().selectionChanged.connect( self.EventItemSelected )
#
@@ -1069,7 +1073,9 @@ def __init__( self, parent, metadata_routers, paths ):
self._paths = paths
- self._paths_list = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, self._ConvertDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._paths_list = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, model )
allowed_importer_classes = [ ClientMetadataMigrationImporters.SingleFileMetadataImporterTXT, ClientMetadataMigrationImporters.SingleFileMetadataImporterJSON ]
allowed_exporter_classes = [ ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTags, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaNotes, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaURLs, ClientMetadataMigrationExporters.SingleFileMetadataExporterMediaTimestamps ]
diff --git a/hydrus/client/gui/importing/ClientGUIImportFolders.py b/hydrus/client/gui/importing/ClientGUIImportFolders.py
index 6625f0d34..784fdc6c8 100644
--- a/hydrus/client/gui/importing/ClientGUIImportFolders.py
+++ b/hydrus/client/gui/importing/ClientGUIImportFolders.py
@@ -38,7 +38,9 @@ def __init__( self, parent, import_folders ):
import_folders_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._import_folders = ClientGUIListCtrl.BetterListCtrl( import_folders_panel, CGLC.COLUMN_LIST_IMPORT_FOLDERS.ID, 8, self._ConvertImportFolderToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_IMPORT_FOLDERS.ID, self._ConvertImportFolderToListCtrlTuples )
+
+ self._import_folders = ClientGUIListCtrl.BetterListCtrlTreeView( import_folders_panel, CGLC.COLUMN_LIST_IMPORT_FOLDERS.ID, 8, model, use_simple_delete = True, activation_callback = self._Edit )
import_folders_panel.SetListCtrl( self._import_folders )
@@ -249,7 +251,9 @@ def create_choice():
filename_tagging_options_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._filename_tagging_options_box )
- self._filename_tagging_options = ClientGUIListCtrl.BetterListCtrl( filename_tagging_options_panel, CGLC.COLUMN_LIST_FILENAME_TAGGING_OPTIONS.ID, 5, self._ConvertFilenameTaggingOptionsToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditFilenameTaggingOptions )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FILENAME_TAGGING_OPTIONS.ID, self._ConvertFilenameTaggingOptionsToListCtrlTuples )
+
+ self._filename_tagging_options = ClientGUIListCtrl.BetterListCtrlTreeView( filename_tagging_options_panel, CGLC.COLUMN_LIST_FILENAME_TAGGING_OPTIONS.ID, 5, model, use_simple_delete = True, activation_callback = self._EditFilenameTaggingOptions )
filename_tagging_options_panel.SetListCtrl( self._filename_tagging_options )
diff --git a/hydrus/client/gui/lists/ClientGUIListConstants.py b/hydrus/client/gui/lists/ClientGUIListConstants.py
index a4a03d27b..fbdf3164c 100644
--- a/hydrus/client/gui/lists/ClientGUIListConstants.py
+++ b/hydrus/client/gui/lists/ClientGUIListConstants.py
@@ -827,19 +827,19 @@ class COLUMN_LIST_REGEX_FAVOURITES( COLUMN_LIST_DEFINITION ):
#
-class COLUMN_LIST_URL_CLASS_PATH_COMPONENTS( COLUMN_LIST_DEFINITION ):
+class COLUMN_LIST_URL_CLASS_PARAMETERS( COLUMN_LIST_DEFINITION ):
ID = 36
KEY = 0
VALUE = 1
-column_list_type_name_lookup[ COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.ID ] = 'url class path components'
+column_list_type_name_lookup[ COLUMN_LIST_URL_CLASS_PARAMETERS.ID ] = 'url class parameters'
-register_column_type( COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.ID, COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.KEY, 'name', False, 14, True )
-register_column_type( COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.ID, COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.VALUE, 'value', False, 45, True )
+register_column_type( COLUMN_LIST_URL_CLASS_PARAMETERS.ID, COLUMN_LIST_URL_CLASS_PARAMETERS.KEY, 'name', False, 14, True )
+register_column_type( COLUMN_LIST_URL_CLASS_PARAMETERS.ID, COLUMN_LIST_URL_CLASS_PARAMETERS.VALUE, 'value', False, 45, True )
-default_column_list_sort_lookup[ COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.ID ] = ( COLUMN_LIST_URL_CLASS_PATH_COMPONENTS.KEY, True )
+default_column_list_sort_lookup[ COLUMN_LIST_URL_CLASS_PARAMETERS.ID ] = ( COLUMN_LIST_URL_CLASS_PARAMETERS.KEY, True )
#
diff --git a/hydrus/client/gui/lists/ClientGUIListCtrl.py b/hydrus/client/gui/lists/ClientGUIListCtrl.py
index e62b494de..dee2797d0 100644
--- a/hydrus/client/gui/lists/ClientGUIListCtrl.py
+++ b/hydrus/client/gui/lists/ClientGUIListCtrl.py
@@ -2,6 +2,7 @@
import typing
from qtpy import QtCore as QC
+from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from hydrus.core import HydrusData
@@ -40,7 +41,7 @@ def SafeNoneStr( value ):
# note that this AbstractItemModel can support nested folder stuff, with some work. we'd prob want to move to a data storage system that actuall was a tree, rather than this indices-to-data stuff
class HydrusListItemModel( QC.QAbstractItemModel ):
- def __init__( self, parent: QW.QWidget, column_list_type, data_to_display_tuple_func: typing.Callable, data_to_sort_tuple_func: typing.Callable, column_types_to_name_overrides = None ):
+ def __init__( self, parent: QW.QWidget, column_list_type: int, data_to_display_tuple_func: typing.Callable, data_to_sort_tuple_func: typing.Callable, column_types_to_name_overrides = None ):
QC.QAbstractItemModel.__init__( self, parent )
@@ -138,7 +139,17 @@ def data( self, index: QC.QModelIndex, role = QC.Qt.DisplayRole ):
self._data_to_display_tuples[ data ] = display_tuple
- return self._data_to_display_tuples[ data ][ column_type ]
+ text = self._data_to_display_tuples[ data ][ column_type ]
+
+ if role == QC.Qt.ToolTipRole:
+
+ return ClientGUIFunctions.WrapToolTip( text )
+
+ else:
+
+ return text
+
+
elif role == QC.Qt.UserRole:
@@ -197,7 +208,7 @@ def flags( self, index: QC.QModelIndex ):
return QC.Qt.NoItemFlags
- return QC.Qt.ItemIsEnabled | QC.Qt.ItemIsSelectable
+ return QC.Qt.ItemIsEnabled | QC.Qt.ItemIsSelectable | QC.Qt.ItemNeverHasChildren
def GetData( self, indices: typing.Optional[ typing.Collection[ int ] ] = None ):
@@ -212,13 +223,29 @@ def GetData( self, indices: typing.Optional[ typing.Collection[ int ] ] = None )
+ def GetEarliestData( self, datas ):
+
+ if len( datas ) == 0:
+
+ return None
+
+
+ matching_tuples = sorted( ( ( self._data_to_indices[ data ], data ) for data in datas if data in self._data_to_indices ) )
+
+ if len( matching_tuples ) == 0:
+
+ return None
+
+
+ return matching_tuples[0][1]
+
def GetModelIndexFromData( self, data: object ):
if data in self._data_to_indices:
index = self._data_to_indices[ data ]
- return self.createIndex( index, 0, QC.QModelIndex() )
+ return self.createIndex( index, 0 )
else:
@@ -233,7 +260,7 @@ def HasData( self, data: object ):
def headerData( self, section: int, orientation: QC.Qt.Orientation, role = QC.Qt.DisplayRole ):
- if orientation == QC.Qt.Vertical:
+ if orientation != QC.Qt.Orientation.Horizontal:
return None
@@ -259,7 +286,7 @@ def headerData( self, section: int, orientation: QC.Qt.Orientation, role = QC.Qt
else:
- return None
+ return QC.QAbstractItemModel.headerData( self, section, orientation, role = role )
@@ -270,7 +297,11 @@ def index( self, row: int, column: int, parent = QC.QModelIndex() ):
return QC.QModelIndex()
- return self.createIndex( row, column, parent )
+ # WOOP WOOP, TWO MAN-HOURS DIED HERE
+ # do not return self.createIndex( row, column, parent ), it causes the >0 columns to not repaint or respond to mouse clicks
+ # I guess somehow the default parent here was like the 0, 0 index or -1, -1 or something crazy since I'm not setting up a root 'properly' or something
+ # anyway it broke the whole thing. just doing row, column fixes the selection and repaint issues
+ return self.createIndex( row, column )
def parent( self, index = QC.QModelIndex() ):
@@ -312,6 +343,13 @@ def SetData( self, datas ):
def sort( self, column: int, order: QC.Qt.SortOrder = QC.Qt.AscendingOrder ):
+ self.layoutAboutToBeChanged.emit()
+
+ # TODO: OK, so I understand we can upgrade this and extend to allowing filter behaviour by inserting a QSortFilterProxyModel
+ # that dude would handle sort, which I guess means it would do the data_to_sort_tuples stuff too, and would do so by overriding a 'lessThan' method in a subclass
+ # it would also allow quick filtering
+ # note, important, however, that you need to be careful in the view or whatever to do mapFromSource and mapToSource when handling indices since they'll jump around via the proxy's sort/filtering
+
self._sort_column_type = self._column_list_status.GetColumnTypeFromIndex( column )
asc = order == QC.Qt.AscendingOrder
@@ -360,6 +398,24 @@ def master_sort_key( data ):
self.layoutChanged.emit()
+ def ReplaceDatas( self, replacement_tuples ):
+
+ for ( old_data, new_data ) in replacement_tuples:
+
+ index = self._data_to_indices[ old_data ]
+
+ del self._data_to_indices[ old_data ]
+
+ self._data_to_indices[ new_data ] = index
+ self._indices_to_data[ index ] = new_data
+
+
+ new_datas = [ new_data for ( old_data, new_data ) in replacement_tuples ]
+
+ # tell the View the display strings have updated
+ self.UpdateDatas( new_datas )
+
+
def UpdateDatas( self, datas, check_for_changed_sort_data = False ):
sort_data_has_changed = False
@@ -431,12 +487,38 @@ def UpdateDatas( self, datas, check_for_changed_sort_data = False ):
+class HydrusListItemModelBridge( HydrusListItemModel ):
+ """
+ This guy is the 'temporary' bridge between the old model, which spits out both display and sort tuples in one go, and the future view, which intends to do this separately.
+ It is inefficient, but it is easy to deploy.
+ """
+ def __init__( self, parent: QW.QWidget, column_list_type: int, data_to_tuples_func: typing.Callable, column_types_to_name_overrides = None ):
+
+ self._data_to_tuples_func = data_to_tuples_func
+
+ def data_to_display_tuple_func( data ):
+
+ ( display_tuple, sort_tuple ) = self._data_to_tuples_func( data )
+
+ return display_tuple
+
+
+ def data_to_sort_tuple_func( data ):
+
+ ( display_tuple, sort_tuple ) = self._data_to_tuples_func( data )
+
+ return sort_tuple
+
+
+ HydrusListItemModel.__init__( self, parent, column_list_type, data_to_display_tuple_func, data_to_sort_tuple_func, column_types_to_name_overrides = column_types_to_name_overrides )
+
+
+
class BetterListCtrlTreeView( QW.QTreeView ):
columnListContentsChanged = QC.Signal()
- columnListStatusChanged = QC.Signal()
- def __init__( self, parent, column_list_type, height_num_chars, model: HydrusListItemModel, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, style = None, column_types_to_name_overrides = None ):
+ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusListItemModel, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, column_types_to_name_overrides = None ):
QW.QTreeView.__init__( self, parent )
@@ -444,15 +526,21 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis
self._creation_time = HydrusTime.GetNow()
+ # TODO: pull this from the model m8
self._column_list_type = column_list_type
self._column_list_status: ClientGUIListStatus.ColumnListStatus = CG.client_controller.column_list_manager.GetStatus( self._column_list_type )
self._original_column_list_status = self._column_list_status
+ self._temp_selected_data_record = []
+
+ self.setUniformRowHeights( True )
self.setAlternatingRowColors( True )
self.setSortingEnabled( True )
self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
+ self.setSelectionBehavior( QW.QTreeView.SelectRows )
self.setRootIsDecorated( False )
+ self.setEditTriggers( QW.QTreeView.NoEditTriggers )
self._initial_height_num_chars = height_num_chars
self._forced_height_num_chars = None
@@ -471,44 +559,9 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis
# This sets header data, so we can now do header-section-sizing gubbins
self.setModel( model )
- # old way
- '''
- #sizing_column_initial_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width()
- total_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width()
-
- resize_column = 1
-
- for ( i, ( name, width_num_chars ) ) in enumerate( columns ):
-
- if width_num_chars == -1:
-
- width = -1
-
- resize_column = i + 1
-
- else:
-
- width = self.fontMetrics().boundingRect( 'x' * width_num_chars ).width()
-
- total_width += width
-
-
- self.headerItem().setText( i, name )
-
- self.setColumnWidth( i, width )
-
-
- # Technically this is the previous behavior, but the two commented lines might work better in some cases (?)
- self.header().setStretchLastSection( False )
- self.header().setSectionResizeMode( resize_column - 1 , QW.QHeaderView.Stretch )
- #self.setColumnWidth( resize_column - 1, sizing_column_initial_width )
- #self.header().setStretchLastSection( True )
-
- self.setMinimumWidth( total_width )
- '''
-
main_tlw = CG.client_controller.GetMainTLW()
+ # Note: now (2024-08) we moved to TreeView, I have no idea what the status of this stuff is
# if last section is set too low, for instance 3, the column seems unable to ever shrink from initial (expanded to fill space) size
# _ _ ___ _ _ __ __ ___
# ( \/\/ )( _)( \/\/ ) ( ) ( ) ( \
@@ -529,20 +582,6 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis
for ( i, column_type ) in enumerate( self._column_list_status.GetColumnTypes() ):
- self.headerItem().setData( i, QC.Qt.UserRole, column_type )
-
- if column_types_to_name_overrides is not None and column_type in column_types_to_name_overrides:
-
- name = column_types_to_name_overrides[ column_type ]
-
- else:
-
- name = CGLC.column_list_column_name_lookup[ self._column_list_type ][ column_type ]
-
-
- self.headerItem().setText( i, name )
- self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) )
-
if i == last_column_index:
width_chars = MIN_SECTION_SIZE_CHARS
@@ -554,7 +593,7 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis
width_chars = max( width_chars, MIN_SECTION_SIZE_CHARS )
- # ok this is a pain in the neck issue, but fontmetrics changes afte widget init. I guess font gets styled on top afterwards
+ # ok this is a pain in the neck issue, but fontmetrics changes after widget init. I guess font gets styled on top afterwards
# this means that if I use this window's fontmetrics here, in init, then it is different later on, and we get creeping growing columns lmao
# several other places in the client are likely affected in different ways by this also!
width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars )
@@ -570,16 +609,20 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown )
- self.itemDoubleClicked.connect( self.ProcessActivateAction )
-
self.header().setSectionsMovable( False ) # can only turn this on when we move from data/sort tuples
# self.header().setFirstSectionMovable( True ) # same
- #self.header().setSectionsClickable( True )
- #self.header().sectionClicked.connect( self.EventColumnClick )
+ self.header().setSectionsClickable( True )
#self.header().sectionMoved.connect( self._DoStatusChanged ) # same
self.header().sectionResized.connect( self._SectionsResized )
+ self.model().layoutAboutToBeChanged.connect( self._PreserveSelectionStore )
+ self.model().rowsAboutToBeInserted.connect( self._PreserveSelectionStore )
+ self.model().rowsAboutToBeRemoved.connect( self._PreserveSelectionStore )
+ self.model().layoutChanged.connect( self._PreserveSelectionRestoreFromSort )
+ self.model().rowsInserted.connect( self._PreserveSelectionRestore )
+ self.model().rowsRemoved.connect( self._PreserveSelectionRestore )
+
self.header().setContextMenuPolicy( QC.Qt.CustomContextMenu )
self.header().customContextMenuRequested.connect( self._ShowHeaderMenu )
@@ -656,7 +699,8 @@ def _GenerateCurrentStatus( self ) -> ClientGUIListStatus.ColumnListStatus:
logical_index = header.logicalIndex( visual_index )
- column_type = self.header().data( logical_index, QC.Qt.UserRole )
+ column_type = self.model().headerData( logical_index, QC.Qt.Orientation.Horizontal, QC.Qt.UserRole )
+
width_pixels = header.sectionSize( logical_index )
shown = not header.isSectionHidden( logical_index )
@@ -736,9 +780,12 @@ def _GetDisplayAndSortTuples( self, data ):
def _GetRowHeightEstimate( self ):
- if self.topLevelItemCount() > 0:
+ # this straight-up returns 0 during dialog init wew, I guess when I ask during init the text isn't initialised or whatever
+ if self.model().rowCount() > 0 and False:
- height = self.rowHeight( self.indexFromItem( self.topLevelItem( 0 ) ) )
+ model_index = self.model().index( 0, 0 )
+
+ height = self.rowHeight( model_index )
else:
@@ -750,55 +797,27 @@ def _GetRowHeightEstimate( self ):
def _GetSelectedIndices( self ) -> typing.List[ int ]:
- return sorted( ( index.row() for index in self.selectionModel().selectedIndexes() ) )
+ return sorted( ( index.row() for index in self.selectionModel().selectedRows() ) )
- def _IterateTopLevelItems( self ) -> typing.Iterator[ QW.QTreeWidgetItem ]:
+ def _PreserveSelectionRestore( self ):
- for i in range( self.topLevelItemCount() ):
-
- yield self.topLevelItem( i )
-
+ self.SelectDatas( self._temp_selected_data_record, deselect_others = True )
-
- def _RecalculateIndicesAfterDelete( self ):
+ self._temp_selected_data_record = []
- indices_and_data_info = sorted( self._indices_to_data_info.items() )
+
+ def _PreserveSelectionRestoreFromSort( self ):
- self._indices_to_data_info = {}
- self._data_to_indices = {}
+ self._PreserveSelectionRestore()
- for ( index, ( old_index, data_info ) ) in enumerate( indices_and_data_info ):
-
- ( data, display_tuple, sort_tuple ) = data_info
-
- self._data_to_indices[ data ] = index
- self._indices_to_data_info[ index ] = data_info
-
+ # save that a sort just happened
+ self._DoStatusChanged()
- def _RefreshHeaderNames( self ):
+ def _PreserveSelectionStore( self ):
- for i in range( self.header().count() ):
-
- column_type = self.headerItem().data( i, QC.Qt.UserRole )
-
- name = CGLC.column_list_column_name_lookup[ self._column_list_type ][ column_type ]
-
- if column_type == self._sort_column_type:
-
- char = '\u25B2' if self._sort_asc else '\u25BC'
-
- name_for_title = '{} {}'.format( name, char )
-
- else:
-
- name_for_title = name
-
-
- self.headerItem().setText( i, name_for_title )
- self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) )
-
+ self._temp_selected_data_record = self.GetData( only_selected = True )
def _SectionsResized( self, logical_index, old_size, new_size ):
@@ -841,23 +860,6 @@ def _ShowRowsMenu( self ):
CGC.core().PopupMenu( self, menu )
- def _UpdateRow( self, index, display_tuple ):
-
- for ( column_index, value ) in enumerate( display_tuple ):
-
- tree_widget_item = self.topLevelItem( index )
-
- first_line = HydrusText.GetFirstLine( value )
- existing_value = tree_widget_item.text( column_index )
-
- if existing_value != first_line:
-
- tree_widget_item.setText( column_index, first_line )
- tree_widget_item.setToolTip( column_index, ClientGUIFunctions.WrapToolTip( value ) )
-
-
-
-
def AddDatas( self, datas: typing.Iterable[ object ], select_sort_and_scroll = False ):
datas = list( datas )
@@ -877,7 +879,7 @@ def AddDatas( self, datas: typing.Iterable[ object ], select_sort_and_scroll = F
self.Sort()
- first_data = sorted( ( ( self._data_to_indices[ data ], data ) for data in datas ) )[0][1]
+ first_data = self.model().GetEarliestData( datas )
self.ScrollToData( first_data )
@@ -911,21 +913,6 @@ def DeleteSelected( self ):
self.DeleteDatas( deletee_datas )
- def EventItemActivated( self, item, column ):
-
- if self._activation_callback is not None:
-
- try:
-
- self._activation_callback()
-
- except Exception as e:
-
- HydrusData.ShowException( e )
-
-
-
-
def EventKeyDown( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
@@ -988,6 +975,11 @@ def ForceHeight( self, rows ):
#QP.SetMinClientSize( self, ( existing_min_width, ideal_client_height ) )
+ def count( self ):
+
+ return self.model().rowCount()
+
+
def GetData( self, only_selected = False ) -> list:
if only_selected:
@@ -1035,12 +1027,64 @@ def HasDoneDeletes( self ):
def HasOneSelected( self ):
- return len( self.selectionModel().selectedIndexes() ) == 1
+ return len( self.selectionModel().selectedRows() ) == 1
def HasSelected( self ):
- return len( self.selectionModel().selectedIndexes() ) > 0
+ return len( self.selectionModel().selectedRows() ) > 0
+
+
+ def minimumSizeHint( self ):
+
+ width = 0
+
+ for i in range( self.model().columnCount() - 1 ):
+
+ width += self.columnWidth( i )
+
+
+ width += self._min_section_width # the last column
+
+ width += self.frameWidth() * 2
+
+ if self._forced_height_num_chars is None:
+
+ min_num_rows = 4
+
+ else:
+
+ min_num_rows = self._forced_height_num_chars
+
+
+ header_size = self.header().sizeHint() # this is better than min size hint for some reason ?( 69, 69 )?
+
+ data_area_height = self._GetRowHeightEstimate() * min_num_rows
+
+ PADDING = 10
+
+ min_size_hint = QC.QSize( width, header_size.height() + data_area_height + PADDING )
+
+ return min_size_hint
+
+
+ def mouseDoubleClickEvent( self, event: QG.QMouseEvent ):
+
+ if event.button() == QC.Qt.LeftButton:
+
+ index = self.indexAt(event.pos()) # Get the index of the item clicked
+
+ if index.isValid():
+
+ self.ProcessActivateAction()
+
+ event.accept()
+
+ return
+
+
+
+ QW.QTreeView.mouseDoubleClickEvent( self, event )
def NotifySettingsUpdated( self, column_list_type = None ):
@@ -1058,10 +1102,6 @@ def NotifySettingsUpdated( self, column_list_type = None ):
#
- ( self._sort_column_type, self._sort_asc ) = self._column_list_status.GetSort()
-
- #
-
main_tlw = CG.client_controller.GetMainTLW()
MIN_SECTION_SIZE_CHARS = 3
@@ -1129,115 +1169,37 @@ def ProcessDeleteAction( self ):
- def ScrollToData( self, data: object ):
-
- data = QP.ListsToTuples( data )
-
- model_index = self.model().GetModelIndexFromData( data )
-
- if model_index.isValid():
-
- self.scrollTo( model_index, hint = QW.QAbstractItemView.ScrollHint.PositionAtCenter )
-
- self.setFocus( QC.Qt.OtherFocusReason )
-
-
-
- def SelectDatas( self, datas: typing.Iterable[ object ], deselect_others = False ):
-
- selectee_datas = { QP.ListsToTuples( data ) for data in datas }
-
- current_selection = self.GetData( only_selected = True )
-
- model = self.model()
- selection_model = self.selectionModel()
-
- if deselect_others:
-
- deselectee_datas = set( current_selection ).difference( selectee_datas )
-
- for data in deselectee_datas:
-
- model_index = model.GetModelIndexFromData( data )
-
- selection_model.select( model_index, QC.QItemSelectionModel.Clear | QC.QItemSelectionModel.Rows )
-
-
-
- selectee_datas.difference_update( current_selection )
-
- for data in selectee_datas:
-
- model_index = model.GetModelIndexFromData( data )
-
- selection_model.select( model_index, QC.QItemSelectionModel.Select | QC.QItemSelectionModel.Rows )
-
-
-
- def SetCopyRowsCallable( self, copy_rows_callable ):
-
- self._copy_rows_callable = copy_rows_callable
-
-
- def SetData( self, datas: typing.Iterable[ object ] ):
-
- datas = [ QP.ListsToTuples( data ) for data in datas ]
-
- self.model().SetData( datas )
-
- self.Sort()
-
- self.columnListContentsChanged.emit()
-
-
- def ShowDeleteSelectedDialog( self ):
-
- from hydrus.client.gui import ClientGUIDialogsQuick
-
- result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
+ def ReplaceData( self, old_data: object, new_data: object, sort_and_scroll = False ):
- if result == QW.QDialog.Accepted:
-
- self.DeleteSelected()
-
+ self.ReplaceDatas( [ ( old_data, new_data ) ], sort_and_scroll = sort_and_scroll )
- def minimumSizeHint( self ):
-
- width = 0
+ def ReplaceDatas( self, replacement_tuples, sort_and_scroll = False ):
- for i in range( self.columnCount() - 1 ):
+ if len( replacement_tuples ) == 0:
- width += self.columnWidth( i )
+ return
- width += self._min_section_width # the last column
+ replacement_tuples = [ ( QP.ListsToTuples( old_data ), QP.ListsToTuples( new_data ) ) for ( old_data, new_data ) in replacement_tuples ]
- width += self.frameWidth() * 2
+ self.model().ReplaceDatas( replacement_tuples )
- if self._forced_height_num_chars is None:
+ if sort_and_scroll:
- min_num_rows = 4
+ self.Sort()
- else:
+ first_new_data = self.model().GetEarliestData( [ new_data for ( old_data, new_data ) in replacement_tuples ] )
- min_num_rows = self._forced_height_num_chars
+ self.ScrollToData( first_new_data )
- header_size = self.header().sizeHint() # this is better than min size hint for some reason ?( 69, 69 )?
-
- data_area_height = self._GetRowHeightEstimate() * min_num_rows
-
- PADDING = 10
-
- min_size_hint = QC.QSize( width, header_size.height() + data_area_height + PADDING )
-
- return min_size_hint
+ self.columnListContentsChanged.emit()
def resizeEvent( self, event ):
- result = QW.QTreeWidget.resizeEvent( self, event )
+ result = QW.QTreeView.resizeEvent( self, event )
# do not touch this! weird hack that fixed a new bug in 6.6.1 where all columns would reset on load to 100px wide!
if self._has_initialised_size:
@@ -1310,64 +1272,76 @@ def sizeHint( self ):
return size_hint
- def sortByColumn( self, column: int, order: QC.Qt.SortOrder ):
-
- selected_data = self.GetData( only_selected = True )
+ def ScrollToData( self, data: object ):
- # probably don't have to do the clear here, but it feels neater to do it like this rather than let existing selections go bananas, even briefly
- self.selectionModel().clearSelection()
+ data = QP.ListsToTuples( data )
- self.model().sort( column, order )
+ model_index = self.model().GetModelIndexFromData( data )
- self.SelectDatas( selected_data )
+ if model_index.isValid():
+
+ self.scrollTo( model_index, hint = QW.QAbstractItemView.ScrollHint.PositionAtCenter )
+
+ self.setFocus( QC.Qt.OtherFocusReason )
+
- self.columnListContentsChanged.emit()
+
+ def SelectDatas( self, datas: typing.Iterable[ object ], deselect_others = False ):
- self._DoStatusChanged()
+ selectee_datas = { QP.ListsToTuples( data ) for data in datas }
-
- def Sort( self, sort_column_type = None, sort_asc = None ):
+ current_selection = self.GetData( only_selected = True )
- ( default_sort_column_type, default_sort_asc ) = self._column_list_status.GetSort()
+ model = self.model()
+ selection_model = self.selectionModel()
- if sort_column_type is not None:
+ if deselect_others:
- sort_column_type = default_sort_column_type
+ deselectee_datas = set( current_selection ).difference( selectee_datas )
+
+ for data in deselectee_datas:
+
+ model_index = model.GetModelIndexFromData( data )
+
+ selection_model.select( model_index, QC.QItemSelectionModel.Deselect | QC.QItemSelectionModel.Rows )
+
- if sort_asc is not None:
+ selectee_datas.difference_update( current_selection )
+
+ for data in selectee_datas:
- sort_asc = default_sort_asc
+ model_index = model.GetModelIndexFromData( data )
+
+ selection_model.select( model_index, QC.QItemSelectionModel.Select | QC.QItemSelectionModel.Rows )
- column = self._column_list_status.GetColumnIndexFromType( sort_column_type )
- ord = QC.Qt.AscendingOrder if sort_asc else QC.Qt.DescendingOrder
-
- self.sortByColumn( column, ord )
+ if len( selectee_datas ) > 0:
+
+ data = model.GetEarliestData( selectee_datas )
+
+ model_index = model.GetModelIndexFromData( data )
+
+ selection_model.setCurrentIndex( model_index, QC.QItemSelectionModel.Current )
+
- self.columnListContentsChanged.emit()
+
+ def SetCopyRowsCallable( self, copy_rows_callable ):
- self._DoStatusChanged()
+ self._copy_rows_callable = copy_rows_callable
- def UpdateDatas( self, datas: typing.Optional[ typing.Iterable[ object ] ] = None, check_for_changed_sort_data = False ):
+ def SetData( self, datas: typing.Iterable[ object ] ):
- if datas is not None:
-
- datas = [ QP.ListsToTuples( data ) for data in datas ]
-
- else:
-
- datas = self.GetData()
-
+ datas = [ QP.ListsToTuples( data ) for data in datas ]
- sort_data_has_changed = self.model().UpdateDatas( datas, check_for_changed_sort_data = check_for_changed_sort_data )
+ self.model().SetData( datas )
- self.columnListContentsChanged.emit()
+ self.Sort()
- return sort_data_has_changed
+ self.columnListContentsChanged.emit()
-
+
def SetNonDupeName( self, obj: object ):
current_names = { o.GetName() for o in self.GetData() if o is not obj }
@@ -1375,60 +1349,63 @@ def SetNonDupeName( self, obj: object ):
HydrusSerialisable.SetNonDupeName( obj, current_names )
- def ReplaceData( self, old_data: object, new_data: object, sort_and_scroll = False ):
+ def ShowDeleteSelectedDialog( self ):
- self.ReplaceDatas( [ ( old_data, new_data ) ], sort_and_scroll = sort_and_scroll )
+ from hydrus.client.gui import ClientGUIDialogsQuick
-
- def ReplaceDatas( self, replacement_tuples, sort_and_scroll = False ):
+ result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
- if len( replacement_tuples ) == 0:
+ if result == QW.QDialog.Accepted:
- return
+ self.DeleteSelected()
- first_new_data = None
+
+ def Sort( self, sort_column_type = None, sort_asc = None ):
- for ( old_data, new_data ) in replacement_tuples:
-
- old_data = QP.ListsToTuples( old_data )
- new_data = QP.ListsToTuples( new_data )
-
- if first_new_data is None:
-
- first_new_data = new_data
-
+ ( default_sort_column_type, default_sort_asc ) = self._column_list_status.GetSort()
+
+ if sort_column_type is None:
- data_index = self._data_to_indices[ old_data ]
+ sort_column_type = default_sort_column_type
- ( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( new_data )
+
+ if sort_asc is None:
- data_info = ( new_data, display_tuple, sort_tuple )
+ sort_asc = default_sort_asc
- self._indices_to_data_info[ data_index ] = data_info
+
+ column = self._column_list_status.GetColumnIndexFromType( sort_column_type )
+ ord = QC.Qt.AscendingOrder if sort_asc else QC.Qt.DescendingOrder
+
+ # do not call model().sort directly, it does not update the header arrow gubbins
+ self.sortByColumn( column, ord )
+
+
+ def UpdateDatas( self, datas: typing.Optional[ typing.Iterable[ object ] ] = None, check_for_changed_sort_data = False ):
+
+ if datas is not None:
- del self._data_to_indices[ old_data ]
+ datas = [ QP.ListsToTuples( data ) for data in datas ]
- self._data_to_indices[ new_data ] = data_index
+ else:
- self._UpdateRow( data_index, display_tuple )
+ datas = self.GetData()
- if sort_and_scroll and first_new_data is not None:
-
- self.Sort()
-
- self.ScrollToData( first_new_data )
-
+ sort_data_has_changed = self.model().UpdateDatas( datas, check_for_changed_sort_data = check_for_changed_sort_data )
+
+ self.columnListContentsChanged.emit()
+
+ return sort_data_has_changed
class BetterListCtrl( QW.QTreeWidget ):
columnListContentsChanged = QC.Signal()
- columnListStatusChanged = QC.Signal()
- def __init__( self, parent, column_list_type, height_num_chars, data_to_tuples_func, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, style = None, column_types_to_name_overrides = None ):
+ def __init__( self, parent, column_list_type, height_num_chars, data_to_tuples_func, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, column_types_to_name_overrides = None ):
QW.QTreeWidget.__init__( self, parent )
@@ -2060,21 +2037,6 @@ def EventColumnClick( self, col ):
self._DoStatusChanged()
- def EventItemActivated( self, item, column ):
-
- if self._activation_callback is not None:
-
- try:
-
- self._activation_callback()
-
- except Exception as e:
-
- HydrusData.ShowException( e )
-
-
-
-
def EventKeyDown( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
@@ -3148,7 +3110,7 @@ def _ImportPNGs( self, paths ):
have_shown_load_error = True
- except:
+ except Exception as e:
HydrusData.PrintException( e )
@@ -3350,7 +3312,7 @@ def SetListCtrl( self, listctrl ):
self.setLayout( self._vbox )
- self._listctrl.itemSelectionChanged.connect( self.EventSelectionChanged )
+ self._listctrl.selectionModel().selectionChanged.connect( self.EventSelectionChanged )
self._listctrl.model().rowsInserted.connect( self.EventContentChanged )
self._listctrl.model().rowsRemoved.connect( self.EventContentChanged )
diff --git a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
index 4c9b623d6..165d3e5a1 100644
--- a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
+++ b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
@@ -59,7 +59,9 @@ def __init__( self, parent: QW.QWidget, ordered_medias: typing.List[ ClientMedia
self._domain_modified_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( domain_box )
- self._domain_modified_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._domain_modified_list_ctrl_panel, CGLC.COLUMN_LIST_DOMAIN_MODIFIED_TIMESTAMPS.ID, 8, self._ConvertDomainToDomainModifiedListCtrlTuples, use_simple_delete = True, activation_callback = self._EditDomainModifiedTimestamp )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DOMAIN_MODIFIED_TIMESTAMPS.ID, self._ConvertDomainToDomainModifiedListCtrlTuples )
+
+ self._domain_modified_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._domain_modified_list_ctrl_panel, CGLC.COLUMN_LIST_DOMAIN_MODIFIED_TIMESTAMPS.ID, 8, model, use_simple_delete = True, activation_callback = self._EditDomainModifiedTimestamp )
self._domain_modified_list_ctrl_panel.SetListCtrl( self._domain_modified_list_ctrl )
@@ -73,7 +75,9 @@ def __init__( self, parent: QW.QWidget, ordered_medias: typing.List[ ClientMedia
self._file_services_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( file_services_box )
- self._file_services_list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._file_services_list_ctrl_panel, CGLC.COLUMN_LIST_FILE_SERVICE_TIMESTAMPS.ID, 8, self._ConvertDataRowToFileServiceListCtrlTuples, activation_callback = self._EditFileServiceTimestamp )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FILE_SERVICE_TIMESTAMPS.ID, self._ConvertDataRowToFileServiceListCtrlTuples )
+
+ self._file_services_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._file_services_list_ctrl_panel, CGLC.COLUMN_LIST_FILE_SERVICE_TIMESTAMPS.ID, 8, model, activation_callback = self._EditFileServiceTimestamp )
self._file_services_list_ctrl_panel.SetListCtrl( self._file_services_list_ctrl )
diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
index 3cb076d23..0e47b4cc2 100644
--- a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
+++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py
@@ -202,8 +202,9 @@ def _UpdateTestPanel( self ):
if self._test_notebook.count() < i + 1:
- # make this our new listctrl
- list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._test_notebook, CGLC.COLUMN_LIST_METADATA_ROUTER_TEST_RESULTS.ID, 11, self._ConvertTestRowToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_METADATA_ROUTER_TEST_RESULTS.ID, self._ConvertTestRowToListCtrlTuples )
+
+ list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._test_notebook, CGLC.COLUMN_LIST_METADATA_ROUTER_TEST_RESULTS.ID, 11, model )
self._test_notebook.addTab( list_ctrl, 'init' )
diff --git a/hydrus/client/gui/metadata/ClientGUITagActions.py b/hydrus/client/gui/metadata/ClientGUITagActions.py
index 8717e3d41..fbfee8b54 100644
--- a/hydrus/client/gui/metadata/ClientGUITagActions.py
+++ b/hydrus/client/gui/metadata/ClientGUITagActions.py
@@ -64,14 +64,47 @@ def _AddStatusesToPairsToCache( self, statuses_to_pairs ):
- def _AutoPetitionConflicts( self, widget, pairs ):
+ def _FetchStatusToPairs( self, tags = None, where_chain_includes_pending_or_petitioned = False ):
+
+ raise NotImplementedError()
+
+
+ def _GetFixedPendSuggestions( self ) -> typing.Collection[ str ]:
+
+ raise NotImplementedError()
+
+
+ def _GetFixedPetitionSuggestions( self ) -> typing.Collection[ str ]:
raise NotImplementedError()
- def _AutoPetitionLoops( self, widget, pairs ):
+ def _GetMyContentType( self ) -> int:
- current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+ raise NotImplementedError()
+
+
+ def _GetTagsToFetch( self, tags: typing.Collection[ str ] ) -> typing.Set[ str ]:
+
+ if self._have_fetched_all:
+
+ return set()
+
+
+ return set( tags ).difference( self._tags_done_fetched ).difference( self._tags_being_fetched )
+
+
+ def AutoPetitionConflicts( self, widget, pairs ):
+
+ raise NotImplementedError()
+
+
+ def AutoPetitionLoops( self, widget, pairs ):
+
+ with self._lock:
+
+ current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+
as_to_bs = HydrusData.BuildKeyToListDict( current_pairs )
@@ -110,9 +143,12 @@ def _AutoPetitionLoops( self, widget, pairs ):
# so, let's repeal the final link that would cause a loop
pairs_to_auto_petition = [ ( tag_we_are_checking, next_tag_from_this_tag_we_are_checking ) ]
- self._EnterCleanedPairs( widget, pairs_to_auto_petition, only_remove = True, forced_reason = AUTO_PETITION_REASON )
+ self.EnterCleanedPairs( widget, pairs_to_auto_petition, only_remove = True, forced_reason = AUTO_PETITION_REASON )
- current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+ with self._lock:
+
+ current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+
as_to_bs = HydrusData.BuildKeyToListDict( current_pairs )
@@ -141,9 +177,12 @@ def _AutoPetitionLoops( self, widget, pairs ):
- def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_remove = False, forced_reason = None ):
+ def EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_remove = False, forced_reason = None ):
"""Can only call this guy when all the respective tags have been loaded and, when allowing adds, all conflicts and loops have been sorted."""
+ # Note this dude now handles lockgubbins granularly. It used to have an atomic lock wrapped around it, along with other stuff, but the dialogs can cause new Qt events to fire, leading to deadlock hell!
+ # it isn't that big a deal since the threads and stuff don't _edit_ content, they pretty much just ever expand it
+
all_tags = set()
for ( a, b ) in pairs:
@@ -152,10 +191,15 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
all_tags.add( b )
- if not all_tags.issubset( self._tags_done_fetched ) and not self._have_fetched_all:
+ with self._lock:
+
+ problem = not all_tags.issubset( self._tags_done_fetched ) and not self._have_fetched_all
missing_tags = all_tags.difference( self._tags_done_fetched )
+
+ if problem:
+
message = 'Hey, somehow the "Enter some Pairs" routine was called before the related underlying pairs\' groups were loaded. This should not happen! Please tell hydev about this.'
message += '\n'
message += f'I have queued up the needed fetch. Please write down what you were doing to get into this state and see if trying it again works. The missing tags were: {HydrusText.ConvertManyStringsToNiceInsertableHumanSummary( missing_tags, no_trailing_whitespace = True )}'
@@ -176,34 +220,37 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
pairs_to_petition_rescind = []
pairs_to_pend_rescind = []
- for pair in pairs:
+ with self._lock:
- if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
+ for pair in pairs:
- if not only_add:
+ if pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ]:
- pairs_to_pend_rescind.append( pair )
+ if not only_add:
+
+ pairs_to_pend_rescind.append( pair )
+
-
- elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
-
- if not only_remove:
+ elif pair in self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ]:
- pairs_to_petition_rescind.append( pair )
+ if not only_remove:
+
+ pairs_to_petition_rescind.append( pair )
+
-
- elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
-
- if not only_add:
+ elif pair in self._original_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ]:
- pairs_to_petition.append( pair )
+ if not only_add:
+
+ pairs_to_petition.append( pair )
+
-
- else:
-
- if not only_remove:
+ else:
- pairs_to_pend.append( pair )
+ if not only_remove:
+
+ pairs_to_pend.append( pair )
+
@@ -268,37 +315,40 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
if do_it:
- we_are_autopetitioning_somewhere = AUTO_PETITION_REASON in self._pairs_to_reasons.values()
-
- if we_are_autopetitioning_somewhere:
+ with self._lock:
- if self._i_am_local_tag_service:
+ we_are_autopetitioning_somewhere = AUTO_PETITION_REASON in self._pairs_to_reasons.values()
+
+ if we_are_autopetitioning_somewhere:
- reason = 'REPLACEMENT: by user'
+ if self._i_am_local_tag_service:
+
+ reason = 'REPLACEMENT: by user'
+
+ else:
+
+ reason = 'REPLACEMENT: {}'.format( reason )
+
- else:
+
+ for pair in pairs_to_pend:
- reason = 'REPLACEMENT: {}'.format( reason )
+ self._pairs_to_reasons[ pair ] = reason
-
- for pair in pairs_to_pend:
-
- self._pairs_to_reasons[ pair ] = reason
-
-
- if we_are_autopetitioning_somewhere:
-
- for ( p, r ) in list( self._pairs_to_reasons.items() ):
+ if we_are_autopetitioning_somewhere:
- if r == AUTO_PETITION_REASON:
+ for ( p, r ) in list( self._pairs_to_reasons.items() ):
- self._pairs_to_reasons[ p ] = reason
+ if r == AUTO_PETITION_REASON:
+
+ self._pairs_to_reasons[ p ] = reason
+
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].update( pairs_to_pend )
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].update( pairs_to_pend )
+
@@ -364,37 +414,40 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
if do_it:
- we_are_autopetitioning_somewhere = AUTO_PETITION_REASON in self._pairs_to_reasons.values()
-
- if we_are_autopetitioning_somewhere:
+ with self._lock:
- if self._i_am_local_tag_service:
+ we_are_autopetitioning_somewhere = AUTO_PETITION_REASON in self._pairs_to_reasons.values()
+
+ if we_are_autopetitioning_somewhere:
- reason = 'REPLACEMENT: by user'
+ if self._i_am_local_tag_service:
+
+ reason = 'REPLACEMENT: by user'
+
+ else:
+
+ reason = 'REPLACEMENT: {}'.format( reason )
+
- else:
+
+ for pair in pairs_to_petition:
- reason = 'REPLACEMENT: {}'.format( reason )
+ self._pairs_to_reasons[ pair ] = reason
-
- for pair in pairs_to_petition:
-
- self._pairs_to_reasons[ pair ] = reason
-
-
- if we_are_autopetitioning_somewhere:
-
- for ( p, r ) in list( self._pairs_to_reasons.items() ):
+ if we_are_autopetitioning_somewhere:
- if r == AUTO_PETITION_REASON:
+ for ( p, r ) in list( self._pairs_to_reasons.items() ):
- self._pairs_to_reasons[ p ] = reason
+ if r == AUTO_PETITION_REASON:
+
+ self._pairs_to_reasons[ p ] = reason
+
-
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( pairs_to_petition )
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].update( pairs_to_petition )
+
@@ -422,7 +475,10 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
if result == QW.QDialog.Accepted:
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pairs_to_pend_rescind )
+ with self._lock:
+
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ].difference_update( pairs_to_pend_rescind )
+
@@ -450,48 +506,23 @@ def _EnterCleanedPairs( self, widget: QW.QWidget, pairs, only_add = False, only_
if result == QW.QDialog.Accepted:
- self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( pairs_to_petition_rescind )
+ with self._lock:
+
+ self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ].difference_update( pairs_to_petition_rescind )
+
- for ( win, c ) in self._notify_callables:
-
- CG.client_controller.CallAfterQtSafe( win, 'showing pair updates', c )
+ with self._lock:
-
- self._notify_new_tags_info.set()
-
-
-
- def _FetchStatusToPairs( self, tags = None, where_chain_includes_pending_or_petitioned = False ):
-
- raise NotImplementedError()
-
-
- def _GetFixedPendSuggestions( self ) -> typing.Collection[ str ]:
-
- raise NotImplementedError()
-
-
- def _GetFixedPetitionSuggestions( self ) -> typing.Collection[ str ]:
-
- raise NotImplementedError()
-
-
- def _GetMyContentType( self ) -> int:
-
- raise NotImplementedError()
-
-
- def _GetTagsToFetch( self, tags: typing.Collection[ str ] ) -> typing.Set[ str ]:
-
- if self._have_fetched_all:
+ for ( win, c ) in self._notify_callables:
+
+ CG.client_controller.CallAfterQtSafe( win, 'showing pair updates', c )
+
- return set()
+ self._notify_new_tags_info.set()
- return set( tags ).difference( self._tags_done_fetched ).difference( self._tags_being_fetched )
-
def EnterPairs( self, widget: QW.QWidget, pairs, only_add = False ):
@@ -509,19 +540,21 @@ def wait_for_preload():
self._notify_new_tags_info.clear()
- CG.client_controller.CallAfterQtSafe( widget, 'add tag pairs (after preload)', do_it_qt_and_lock )
+ CG.client_controller.CallAfterQtSafe( widget, 'add tag pairs (after preload)', do_it_qt )
- def do_it_qt_and_lock():
+ def do_it_qt():
- with self._lock:
-
- self._AutoPetitionConflicts( widget, pairs )
-
- self._AutoPetitionLoops( widget, pairs )
-
- self._EnterCleanedPairs( widget, pairs, only_add = only_add )
-
+ # ok we used to wrap this guy in a big lock to make it atomic
+ # HOWEVER since EnterCleanedPairs can spawn a couple dialogs, which would then exit the Qt event loop and start processing other stuff, I'm pretty sure we could get an UI deadlock (e.g. some list row updating), hooray
+ # thus we need to promote these guys and do more granular locking and simply trust that the various fetchers and stuff around here aren't making any _edit_ changes to this stuff, only ever _additions_
+ # further, it shouldn't be possible to enterpairs twice at once, so the main guy who is editing stuff here doesn't care about atomicity--that _is_ enforced serialised, fingers-crossed
+
+ self.AutoPetitionConflicts( widget, pairs )
+
+ self.AutoPetitionLoops( widget, pairs )
+
+ self.EnterCleanedPairs( widget, pairs, only_add = only_add )
all_tags = set()
@@ -894,12 +927,6 @@ def RegisterQtUpdateCall( self, widget: QW.QWidget, c: typing.Callable ):
class ParentActionContext( TagPairActionContext ):
- def _AutoPetitionConflicts( self, widget, pairs ):
-
- # no conflicts for parents!
- pass
-
-
def _FetchStatusToPairs( self, tags = None, where_chain_includes_pending_or_petitioned = False ):
try:
@@ -934,12 +961,21 @@ def _GetMyContentType( self ) -> int:
return HC.CONTENT_TYPE_TAG_PARENTS
+ def AutoPetitionConflicts( self, widget, pairs ):
+
+ # no conflicts for parents!
+ pass
+
+
class SiblingActionContext( TagPairActionContext ):
- def _AutoPetitionConflicts( self, widget, pairs ):
+ def AutoPetitionConflicts( self, widget, pairs ):
- current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+ with self._lock:
+
+ current_pairs = self._current_statuses_to_pairs[ HC.CONTENT_STATUS_CURRENT ].union( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PENDING ] ).difference( self._current_statuses_to_pairs[ HC.CONTENT_STATUS_PETITIONED ] )
+
current_olds_to_news = dict( current_pairs )
@@ -966,7 +1002,7 @@ def _AutoPetitionConflicts( self, widget, pairs ):
pairs_to_auto_petition = list( pairs_to_auto_petition )
- self._EnterCleanedPairs( widget, pairs_to_auto_petition, only_remove = True, forced_reason = AUTO_PETITION_REASON )
+ self.EnterCleanedPairs( widget, pairs_to_auto_petition, only_remove = True, forced_reason = AUTO_PETITION_REASON )
diff --git a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
index 77260b59a..5209b3ee4 100644
--- a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
+++ b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py
@@ -219,7 +219,9 @@ def __init__( self, parent, service_type, account_types ):
self._deletee_account_type_keys_to_new_account_type_keys = {}
- self._account_types_listctrl = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_ACCOUNT_TYPES.ID, 20, self._ConvertAccountTypeToTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_ACCOUNT_TYPES.ID, self._ConvertAccountTypeToTuples )
+
+ self._account_types_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_ACCOUNT_TYPES.ID, 20, model, delete_key_callback = self._Delete, activation_callback = self._Edit )
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
diff --git a/hydrus/client/gui/networking/ClientGUILogin.py b/hydrus/client/gui/networking/ClientGUILogin.py
index a134510d3..fa89b397c 100644
--- a/hydrus/client/gui/networking/ClientGUILogin.py
+++ b/hydrus/client/gui/networking/ClientGUILogin.py
@@ -261,7 +261,9 @@ def __init__( self, parent, engine, login_scripts, domains_to_login_info ):
self._domains_and_login_info_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._domains_and_login_info = ClientGUIListCtrl.BetterListCtrl( self._domains_and_login_info_panel, CGLC.COLUMN_LIST_DOMAINS_TO_LOGIN_INFO.ID, 16, self._ConvertDomainAndLoginInfoListCtrlTuples, use_simple_delete = True, activation_callback = self._EditCredentials )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DOMAINS_TO_LOGIN_INFO.ID, self._ConvertDomainAndLoginInfoListCtrlTuples )
+
+ self._domains_and_login_info = ClientGUIListCtrl.BetterListCtrlTreeView( self._domains_and_login_info_panel, CGLC.COLUMN_LIST_DOMAINS_TO_LOGIN_INFO.ID, 16, model, use_simple_delete = True, activation_callback = self._EditCredentials )
self._domains_and_login_info_panel.SetListCtrl( self._domains_and_login_info )
@@ -1306,7 +1308,9 @@ def __init__( self, parent, login_script ):
credential_definitions_panel = ClientGUIListCtrl.BetterListCtrlPanel( credential_definitions_box_panel )
- self._credential_definitions = ClientGUIListCtrl.BetterListCtrl( credential_definitions_panel, CGLC.COLUMN_LIST_CREDENTIAL_DEFINITIONS.ID, 4, self._ConvertCredentialDefinitionToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditCredentialDefinitions )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_CREDENTIAL_DEFINITIONS.ID, self._ConvertCredentialDefinitionToListCtrlTuples )
+
+ self._credential_definitions = ClientGUIListCtrl.BetterListCtrlTreeView( credential_definitions_panel, CGLC.COLUMN_LIST_CREDENTIAL_DEFINITIONS.ID, 4, model, use_simple_delete = True, activation_callback = self._EditCredentialDefinitions )
credential_definitions_panel.SetListCtrl( self._credential_definitions )
@@ -1332,7 +1336,9 @@ def __init__( self, parent, login_script ):
example_domains_info_panel = ClientGUIListCtrl.BetterListCtrlPanel( example_domains_info_box_panel )
- self._example_domains_info = ClientGUIListCtrl.BetterListCtrl( example_domains_info_panel, CGLC.COLUMN_LIST_EXAMPLE_DOMAINS_INFO.ID, 6, self._ConvertExampleDomainInfoToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditExampleDomainsInfo )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXAMPLE_DOMAINS_INFO.ID, self._ConvertExampleDomainInfoToListCtrlTuples )
+
+ self._example_domains_info = ClientGUIListCtrl.BetterListCtrlTreeView( example_domains_info_panel, CGLC.COLUMN_LIST_EXAMPLE_DOMAINS_INFO.ID, 6, model, use_simple_delete = True, activation_callback = self._EditExampleDomainsInfo )
example_domains_info_panel.SetListCtrl( self._example_domains_info )
@@ -1350,7 +1356,9 @@ def __init__( self, parent, login_script ):
test_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( test_panel )
- self._test_listctrl = ClientGUIListCtrl.BetterListCtrl( test_listctrl_panel, CGLC.COLUMN_LIST_LOGIN_SCRIPT_TEST_RESULTS.ID, 6, self._ConvertTestResultToListCtrlTuples, activation_callback = self._ReviewTestResult )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_LOGIN_SCRIPT_TEST_RESULTS.ID, self._ConvertTestResultToListCtrlTuples )
+
+ self._test_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( test_listctrl_panel, CGLC.COLUMN_LIST_LOGIN_SCRIPT_TEST_RESULTS.ID, 6, model, activation_callback = self._ReviewTestResult )
test_listctrl_panel.SetListCtrl( self._test_listctrl )
@@ -1897,7 +1905,9 @@ def __init__( self, parent, login_scripts ):
login_scripts_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._login_scripts = ClientGUIListCtrl.BetterListCtrl( login_scripts_panel, CGLC.COLUMN_LIST_LOGIN_SCRIPTS.ID, 20, self._ConvertLoginScriptToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_LOGIN_SCRIPTS.ID, self._ConvertLoginScriptToListCtrlTuples )
+
+ self._login_scripts = ClientGUIListCtrl.BetterListCtrlTreeView( login_scripts_panel, CGLC.COLUMN_LIST_LOGIN_SCRIPTS.ID, 20, model, use_simple_delete = True, activation_callback = self._Edit )
login_scripts_panel.SetListCtrl( self._login_scripts )
diff --git a/hydrus/client/gui/networking/ClientGUINetwork.py b/hydrus/client/gui/networking/ClientGUINetwork.py
index 77fcd9475..8aeac3b1f 100644
--- a/hydrus/client/gui/networking/ClientGUINetwork.py
+++ b/hydrus/client/gui/networking/ClientGUINetwork.py
@@ -316,7 +316,9 @@ def __init__( self, parent: QW.QWidget, network_contexts_to_custom_header_dicts
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_NETWORK_CONTEXTS_CUSTOM_HEADERS.ID, 15, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NETWORK_CONTEXTS_CUSTOM_HEADERS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_NETWORK_CONTEXTS_CUSTOM_HEADERS.ID, 15, model, use_simple_delete = True, activation_callback = self._Edit )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -569,7 +571,9 @@ def __init__( self, parent, controller ):
self._history_time_delta_none = QW.QCheckBox( 'show all', self )
self._history_time_delta_none.clicked.connect( self.EventTimeDeltaChanged )
- self._bandwidths = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_BANDWIDTH_REVIEW.ID, 20, self._ConvertNetworkContextsToListCtrlTuples, activation_callback = self.ShowNetworkContext )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_BANDWIDTH_REVIEW.ID, self._ConvertNetworkContextsToListCtrlTuples )
+
+ self._bandwidths = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_BANDWIDTH_REVIEW.ID, 20, model, activation_callback = self.ShowNetworkContext )
self._edit_default_bandwidth_rules_button = ClientGUICommon.BetterButton( self, 'edit default bandwidth rules', self._EditDefaultBandwidthRules )
@@ -1102,7 +1106,9 @@ def __init__( self, parent, controller ):
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_NETWORK_JOBS_REVIEW.ID, 20, self._ConvertDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NETWORK_JOBS_REVIEW.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_NETWORK_JOBS_REVIEW.ID, 20, model )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -1218,7 +1224,9 @@ def __init__( self, parent, session_manager ):
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSIONS.ID, 32, self._ConvertNetworkContextToListCtrlTuples, delete_key_callback = self._Clear, activation_callback = self._Review )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSIONS.ID, self._ConvertNetworkContextToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSIONS.ID, 32, model, delete_key_callback = self._Clear, activation_callback = self._Review )
self._listctrl.Sort()
@@ -1446,7 +1454,9 @@ def __init__( self, parent, session_manager, network_context ):
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSION.ID, 8, self._ConvertCookieToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSION.ID, self._ConvertCookieToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_REVIEW_NETWORK_SESSION.ID, 8, model, delete_key_callback = self._Delete, activation_callback = self._Edit )
self._listctrl.Sort()
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index d520cd5f9..0ed03f076 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -1289,7 +1289,9 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._gallery_importers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._gallery_downloader_panel )
- self._gallery_importers_listctrl = ClientGUIListCtrl.BetterListCtrl( self._gallery_importers_listctrl_panel, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, 4, self._ConvertDataToListCtrlTuples, delete_key_callback = self._RemoveGalleryImports, activation_callback = self._HighlightSelectedGalleryImport )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._gallery_importers_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gallery_importers_listctrl_panel, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, 4, model, delete_key_callback = self._RemoveGalleryImports, activation_callback = self._HighlightSelectedGalleryImport )
self._gallery_importers_listctrl_panel.SetListCtrl( self._gallery_importers_listctrl )
@@ -2344,7 +2346,9 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._watchers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._watchers_panel )
- self._watchers_listctrl = ClientGUIListCtrl.BetterListCtrl( self._watchers_listctrl_panel, CGLC.COLUMN_LIST_WATCHERS.ID, 4, self._ConvertDataToListCtrlTuples, delete_key_callback = self._RemoveWatchers, activation_callback = self._HighlightSelectedWatcher )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_WATCHERS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._watchers_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._watchers_listctrl_panel, CGLC.COLUMN_LIST_WATCHERS.ID, 4, model, delete_key_callback = self._RemoveWatchers, activation_callback = self._HighlightSelectedWatcher )
self._watchers_listctrl_panel.SetListCtrl( self._watchers_listctrl )
@@ -4237,7 +4241,9 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._petitions_summary_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._petitions_panel )
- self._petitions_summary_list = ClientGUIListCtrl.BetterListCtrl( self._petitions_summary_list_panel, CGLC.COLUMN_LIST_PETITIONS_SUMMARY.ID, 12, self._ConvertDataToListCtrlTuples, activation_callback = self._ActivateToHighlightPetition )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PETITIONS_SUMMARY.ID, self._ConvertDataToListCtrlTuples )
+
+ self._petitions_summary_list = ClientGUIListCtrl.BetterListCtrlTreeView( self._petitions_summary_list_panel, CGLC.COLUMN_LIST_PETITIONS_SUMMARY.ID, 12, model, activation_callback = self._ActivateToHighlightPetition )
self._petitions_summary_list_panel.SetListCtrl( self._petitions_summary_list )
diff --git a/hydrus/client/gui/pages/ClientGUIPages.py b/hydrus/client/gui/pages/ClientGUIPages.py
index cf9d21c70..45444d1fc 100644
--- a/hydrus/client/gui/pages/ClientGUIPages.py
+++ b/hydrus/client/gui/pages/ClientGUIPages.py
@@ -1,5 +1,6 @@
import collections
import os
+import time
import typing
from qtpy import QtCore as QC
@@ -471,6 +472,7 @@ def __init__( self, parent, controller, management_controller: ClientGUIManageme
self._management_panel.locationChanged.connect( self._preview_canvas.SetLocationContext )
+ # this is the only place we _do_ want to set the split as the parent of the thumbnail panel. doing it on init avoids init flicker
self._media_panel = self._management_panel.GetDefaultEmptyMediaPanel( self._management_media_split )
self._management_media_split.addWidget( self._search_preview_split )
@@ -555,6 +557,10 @@ def _SetPrettyStatus( self, status: str ):
def _SwapMediaPanel( self, new_panel: ClientGUIResults.MediaPanel ):
+ """
+ Yo, it is important that the new_panel here starts with a parent _other_ than the splitter! The page itself is usually fine.
+ If we give it the splitter as parent, you can get a frame of unusual layout flicker, usually a page-wide autocomplete input. Re-parent it here and we are fine.
+ """
previous_sizes = self._management_media_split.sizes()
@@ -591,6 +597,8 @@ def _SwapMediaPanel( self, new_panel: ClientGUIResults.MediaPanel ):
if new_panel.parentWidget() == self._management_media_split:
+ # ideally, this does not occur. we always want to replace and reduce flicker
+
old_panel.setParent( None )
old_panel.setVisible( False )
@@ -1061,14 +1069,8 @@ def SetSplitterPositions( self, hpos = None, vpos = None ):
return
- if vpos < 0:
-
- self._search_preview_split.setSizes( [ total_sum + vpos, -vpos ] )
-
- elif vpos > 0:
-
- self._search_preview_split.setSizes( [ vpos, total_sum - vpos ] )
-
+ # handle if it was hidden before
+ self._search_preview_split.setVisible( True )
total_sum = sum( self._management_media_split.sizes() )
@@ -1081,6 +1083,18 @@ def SetSplitterPositions( self, hpos = None, vpos = None ):
self._management_media_split.setSizes( [ hpos, total_sum - hpos ] )
+ # handle if it was hidden before
+ self._preview_panel.setVisible( True )
+
+ if vpos < 0:
+
+ self._search_preview_split.setSizes( [ total_sum + vpos, -vpos ] )
+
+ elif vpos > 0:
+
+ self._search_preview_split.setSizes( [ vpos, total_sum - vpos ] )
+
+
if HC.options[ 'hide_preview' ]:
QP.CallAfter( QP.Unsplit, self._search_preview_split, self._preview_panel )
@@ -1146,7 +1160,7 @@ def publish_callable( media_results ):
self._SetPrettyStatus( '' )
- media_panel = ClientGUIResults.MediaPanelThumbnails( self._management_media_split, self._page_key, self._management_controller, media_results )
+ media_panel = ClientGUIResults.MediaPanelThumbnails( self, self._page_key, self._management_controller, media_results )
self._SwapMediaPanel( media_panel )
diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
index 59d9134d1..e54ed471f 100644
--- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
@@ -88,7 +88,9 @@ def __init__(
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_DEFAULT_TAG_IMPORT_OPTIONS.ID, 15, self._ConvertDataToListCtrlTuples, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DEFAULT_TAG_IMPORT_OPTIONS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_DEFAULT_TAG_IMPORT_OPTIONS.ID, 15, model, activation_callback = self._Edit )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -1183,7 +1185,9 @@ def __init__( self, parent: QW.QWidget, duplicate_action, duplicate_content_merg
tag_services_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( tag_services_panel )
- self._tag_service_actions = ClientGUIListCtrl.BetterListCtrl( tag_services_listctrl_panel, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_TAG_SERVICES.ID, 5, self._ConvertTagDataToListCtrlTuple, delete_key_callback = self._DeleteTag, activation_callback = self._EditTag )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_TAG_SERVICES.ID, self._ConvertTagDataToListCtrlTuple )
+
+ self._tag_service_actions = ClientGUIListCtrl.BetterListCtrlTreeView( tag_services_listctrl_panel, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_TAG_SERVICES.ID, 5, model, delete_key_callback = self._DeleteTag, activation_callback = self._EditTag )
tag_services_listctrl_panel.SetListCtrl( self._tag_service_actions )
@@ -1197,7 +1201,9 @@ def __init__( self, parent: QW.QWidget, duplicate_action, duplicate_content_merg
rating_services_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( rating_services_panel )
- self._rating_service_actions = ClientGUIListCtrl.BetterListCtrl( rating_services_listctrl_panel, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_RATING_SERVICES.ID, 5, self._ConvertRatingDataToListCtrlTuple, delete_key_callback = self._DeleteRating, activation_callback = self._EditRating )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_RATING_SERVICES.ID, self._ConvertRatingDataToListCtrlTuple )
+
+ self._rating_service_actions = ClientGUIListCtrl.BetterListCtrlTreeView( rating_services_listctrl_panel, CGLC.COLUMN_LIST_DUPLICATE_CONTENT_MERGE_OPTIONS_RATING_SERVICES.ID, 5, model, delete_key_callback = self._DeleteRating, activation_callback = self._EditRating )
rating_services_listctrl_panel.SetListCtrl( self._rating_service_actions )
@@ -2621,7 +2627,9 @@ def __init__( self, parent: QW.QWidget, regex_favourites ):
regex_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._regexes = ClientGUIListCtrl.BetterListCtrl( regex_listctrl_panel, CGLC.COLUMN_LIST_REGEX_FAVOURITES.ID, 8, self._ConvertDataToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REGEX_FAVOURITES.ID, self._ConvertDataToListCtrlTuples )
+
+ self._regexes = ClientGUIListCtrl.BetterListCtrlTreeView( regex_listctrl_panel, CGLC.COLUMN_LIST_REGEX_FAVOURITES.ID, 8, model, use_simple_delete = True, activation_callback = self._Edit )
regex_listctrl_panel.SetListCtrl( self._regexes )
diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py
index c25f85c46..937d44340 100644
--- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py
@@ -923,7 +923,9 @@ def __init__( self, parent ):
mime_panel = ClientGUICommon.StaticBox( self, '\'open externally\' launch paths' )
- self._mime_launch_listctrl = ClientGUIListCtrl.BetterListCtrl( mime_panel, CGLC.COLUMN_LIST_EXTERNAL_PROGRAMS.ID, 15, self._ConvertMimeToListCtrlTuples, activation_callback = self._EditMimeLaunch )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXTERNAL_PROGRAMS.ID, self._ConvertMimeToListCtrlTuples )
+
+ self._mime_launch_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( mime_panel, CGLC.COLUMN_LIST_EXTERNAL_PROGRAMS.ID, 15, model, activation_callback = self._EditMimeLaunch )
for mime in HC.SEARCHABLE_MIMES:
@@ -1472,7 +1474,9 @@ def __init__( self, parent ):
self._disable_get_safe_position_test = QW.QCheckBox( self._misc_panel )
self._disable_get_safe_position_test.setToolTip( ClientGUIFunctions.WrapToolTip( 'If your windows keep getting \'rescued\' despite being in a good location, try this.' ) )
- self._frame_locations = ClientGUIListCtrl.BetterListCtrl( frame_locations_panel, CGLC.COLUMN_LIST_FRAME_LOCATIONS.ID, 15, data_to_tuples_func = lambda x: (self._GetPrettyFrameLocationInfo( x ), self._GetPrettyFrameLocationInfo( x )), activation_callback = self.EditFrameLocations )
+ model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_FRAME_LOCATIONS.ID, self._GetPrettyFrameLocationInfo, self._GetPrettyFrameLocationInfo )
+
+ self._frame_locations = ClientGUIListCtrl.BetterListCtrlTreeView( frame_locations_panel, CGLC.COLUMN_LIST_FRAME_LOCATIONS.ID, 15, model, activation_callback = self.EditFrameLocations )
self._frame_locations_edit_button = QW.QPushButton( 'edit', frame_locations_panel )
self._frame_locations_edit_button.clicked.connect( self.EditFrameLocations )
@@ -2828,7 +2832,9 @@ def __init__( self, parent ):
media_viewer_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( filetype_handling_panel )
- self._filetype_handling_listctrl = ClientGUIListCtrl.BetterListCtrl( media_viewer_list_panel, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, 20, data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, self._GetListCtrlData )
+
+ self._filetype_handling_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( media_viewer_list_panel, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, 20, model, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
media_viewer_list_panel.SetListCtrl( self._filetype_handling_listctrl )
@@ -4799,7 +4805,9 @@ def __init__( self, parent, new_options ):
search_tag_slices_weight_panel = ClientGUIListCtrl.BetterListCtrlPanel( search_tag_slices_weight_box )
- self._search_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( search_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditSearchTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteSearchTagSliceWeight )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, self._ConvertTagSliceAndWeightToListCtrlTuples )
+
+ self._search_tag_slices_weights = ClientGUIListCtrl.BetterListCtrlTreeView( search_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, model, activation_callback = self._EditSearchTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteSearchTagSliceWeight )
tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by the tag type that searched for them. Set to 0 to not search with that type of tag.'
self._search_tag_slices_weights.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
@@ -4815,7 +4823,9 @@ def __init__( self, parent, new_options ):
result_tag_slices_weight_panel = ClientGUIListCtrl.BetterListCtrlPanel( result_tag_slices_weight_box )
- self._result_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( result_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditResultTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteResultTagSliceWeight )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, self._ConvertTagSliceAndWeightToListCtrlTuples )
+
+ self._result_tag_slices_weights = ClientGUIListCtrl.BetterListCtrlTreeView( result_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, model, activation_callback = self._EditResultTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteResultTagSliceWeight )
tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by their tag type. Set to 0 to not suggest that type of tag at all.'
self._result_tag_slices_weights.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
@@ -5462,7 +5472,9 @@ def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPh
st = ClientGUICommon.BetterStaticText( self, text )
st.setWordWrap( True )
- self._locations = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, 12, self._ConvertPrefixToListCtrlTuples, activation_callback = self._SetLocations )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, self._ConvertPrefixToListCtrlTuples )
+
+ self._locations = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, 12, model, activation_callback = self._SetLocations )
self._set_button = ClientGUICommon.BetterButton( self, 'set correct location', self._SetLocations )
self._add_button = ClientGUICommon.BetterButton( self, 'add a possibly correct location (let the client figure out what it contains)', self._AddLocation )
@@ -5471,9 +5483,7 @@ def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPh
#
- self._locations.AddDatas( missing_subfolders )
-
- self._locations.Sort()
+ self._locations.SetData( missing_subfolders )
#
diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py
index f87c68934..3fac9f642 100644
--- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py
+++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py
@@ -167,7 +167,9 @@ def __init__( self, parent, controller ):
current_media_base_locations_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( file_locations_panel )
- self._current_media_base_locations_listctrl = ClientGUIListCtrl.BetterListCtrl( current_media_base_locations_listctrl_panel, CGLC.COLUMN_LIST_DB_MIGRATION_LOCATIONS.ID, 8, self._ConvertLocationToListCtrlTuples, activation_callback = self._SetMaxNumBytes )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DB_MIGRATION_LOCATIONS.ID, self._ConvertLocationToListCtrlTuples )
+
+ self._current_media_base_locations_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( current_media_base_locations_listctrl_panel, CGLC.COLUMN_LIST_DB_MIGRATION_LOCATIONS.ID, 8, model, activation_callback = self._SetMaxNumBytes )
self._current_media_base_locations_listctrl.setSelectionMode( QW.QAbstractItemView.SingleSelection )
self._current_media_base_locations_listctrl.Sort()
@@ -1483,7 +1485,9 @@ def __init__( self, parent, mime: int, exif_dict: typing.Optional[ dict ], file_
exif_panel = ClientGUICommon.StaticBox( self, 'EXIF' )
- self._exif_listctrl = ClientGUIListCtrl.BetterListCtrl( exif_panel, CGLC.COLUMN_LIST_EXIF_DATA.ID, 16, self._ConvertEXIFToListCtrlTuples, activation_callback = self._CopyRow )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXIF_DATA.ID, self._ConvertEXIFToListCtrlTuples )
+
+ self._exif_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( exif_panel, CGLC.COLUMN_LIST_EXIF_DATA.ID, 16, model, activation_callback = self._CopyRow )
label = 'Double-click a row to copy its value to clipboard.'
@@ -1839,7 +1843,9 @@ def __init__( self, parent, stats ):
jobs_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._current_work_panel )
- self._jobs_listctrl = ClientGUIListCtrl.BetterListCtrl( jobs_listctrl_panel, CGLC.COLUMN_LIST_FILE_MAINTENANCE_JOBS.ID, 8, self._ConvertJobTypeToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FILE_MAINTENANCE_JOBS.ID, self._ConvertJobTypeToListCtrlTuples )
+
+ self._jobs_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( jobs_listctrl_panel, CGLC.COLUMN_LIST_FILE_MAINTENANCE_JOBS.ID, 8, model )
jobs_listctrl_panel.SetListCtrl( self._jobs_listctrl )
@@ -2803,7 +2809,9 @@ def __init__( self, parent, paths = None ):
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._paths_list = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_INPUT_LOCAL_FILES.ID, 12, self._ConvertListCtrlDataToTuple, delete_key_callback = self.RemovePaths )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_INPUT_LOCAL_FILES.ID, self._ConvertListCtrlDataToTuple )
+
+ self._paths_list = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_INPUT_LOCAL_FILES.ID, 12, model, delete_key_callback = self.RemovePaths )
listctrl_panel.SetListCtrl( self._paths_list )
@@ -3409,7 +3417,9 @@ def __init__( self, parent, controller, scheduler_name ):
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_JOB_SCHEDULER_REVIEW.ID, 20, self._ConvertDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_JOB_SCHEDULER_REVIEW.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_JOB_SCHEDULER_REVIEW.ID, 20, model )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -3465,7 +3475,9 @@ def __init__( self, parent, controller ):
self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self._list_ctrl_panel, CGLC.COLUMN_LIST_THREADS_REVIEW.ID, 20, self._ConvertDataToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_THREADS_REVIEW.ID, self._ConvertDataToListCtrlTuples )
+
+ self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_THREADS_REVIEW.ID, 20, model )
self._list_ctrl_panel.SetListCtrl( self._list_ctrl )
@@ -3532,7 +3544,9 @@ def __init__( self, parent, controller ):
self._names_to_num_rows = {}
- self._deferred_delete_listctrl = ClientGUIListCtrl.BetterListCtrl( deferred_delete_listctrl_panel, CGLC.COLUMN_LIST_DEFERRED_DELETE_TABLE_DATA.ID, 24, self._ConvertRowToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DEFERRED_DELETE_TABLE_DATA.ID, self._ConvertRowToListCtrlTuples )
+
+ self._deferred_delete_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( deferred_delete_listctrl_panel, CGLC.COLUMN_LIST_DEFERRED_DELETE_TABLE_DATA.ID, 24, model )
deferred_delete_listctrl_panel.SetListCtrl( self._deferred_delete_listctrl )
@@ -3723,7 +3737,9 @@ def __init__( self, parent, controller, vacuum_data ):
vacuum_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._vacuum_listctrl = ClientGUIListCtrl.BetterListCtrl( vacuum_listctrl_panel, CGLC.COLUMN_LIST_VACUUM_DATA.ID, 6, self._ConvertNameToListCtrlTuples )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_VACUUM_DATA.ID, self._ConvertNameToListCtrlTuples )
+
+ self._vacuum_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( vacuum_listctrl_panel, CGLC.COLUMN_LIST_VACUUM_DATA.ID, 6, model )
vacuum_listctrl_panel.SetListCtrl( self._vacuum_listctrl )
diff --git a/hydrus/client/gui/parsing/ClientGUIParsing.py b/hydrus/client/gui/parsing/ClientGUIParsing.py
index aea7fe224..3d82a9127 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsing.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsing.py
@@ -62,7 +62,9 @@ def __init__( self, parent, network_engine ):
listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, 14, self._ConvertContentToListCtrlTuples, use_simple_delete = True )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, self._ConvertContentToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, 14, model, use_simple_delete = True )
self._listctrl.Sort()
@@ -1032,7 +1034,9 @@ def __init__( self, parent: QW.QWidget, test_data_callable: typing.Callable[ [],
content_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._content_parsers = ClientGUIListCtrl.BetterListCtrl( content_parsers_panel, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, 6, self._ConvertContentParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, self._ConvertContentParserToListCtrlTuples )
+
+ self._content_parsers = ClientGUIListCtrl.BetterListCtrlTreeView( content_parsers_panel, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, 6, model, use_simple_delete = True, activation_callback = self._Edit )
content_parsers_panel.SetListCtrl( self._content_parsers )
@@ -1257,7 +1261,9 @@ def __init__( self, parent, parser: ClientParsing.PageParser, formula = None, te
sub_page_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( sub_page_parsers_notebook_panel )
- self._sub_page_parsers = ClientGUIListCtrl.BetterListCtrl( sub_page_parsers_panel, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, 4, self._ConvertSubPageParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditSubPageParser )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, self._ConvertSubPageParserToListCtrlTuples )
+
+ self._sub_page_parsers = ClientGUIListCtrl.BetterListCtrlTreeView( sub_page_parsers_panel, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, 4, model, use_simple_delete = True, activation_callback = self._EditSubPageParser )
sub_page_parsers_panel.SetListCtrl( self._sub_page_parsers )
@@ -1656,7 +1662,9 @@ def __init__( self, parent, parsers ):
parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._parsers = ClientGUIListCtrl.BetterListCtrl( parsers_panel, CGLC.COLUMN_LIST_PARSERS.ID, 20, self._ConvertParserToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PARSERS.ID, self._ConvertParserToListCtrlTuples )
+
+ self._parsers = ClientGUIListCtrl.BetterListCtrlTreeView( parsers_panel, CGLC.COLUMN_LIST_PARSERS.ID, 20, model, use_simple_delete = True, activation_callback = self._Edit )
parsers_panel.SetListCtrl( self._parsers )
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
index e554ede7c..3064af35e 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
@@ -44,7 +44,9 @@ def __init__( self, parent, nodes, referral_url_callable, example_data_callable
self._referral_url_callable = referral_url_callable
self._example_data_callable = example_data_callable
- self._nodes = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_NODES.ID, 20, self._ConvertNodeToTuples, delete_key_callback = self.Delete, activation_callback = self.Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NODES.ID, self._ConvertNodeToTuples )
+
+ self._nodes = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_NODES.ID, 20, model, delete_key_callback = self.Delete, activation_callback = self.Edit )
menu_items = []
@@ -870,7 +872,9 @@ def __init__( self, parent ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
- self._scripts = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_PARSING_SCRIPTS.ID, 20, self._ConvertScriptToTuples, delete_key_callback = self.Delete, activation_callback = self.Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PARSING_SCRIPTS.ID, self._ConvertScriptToTuples )
+
+ self._scripts = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_PARSING_SCRIPTS.ID, 20, model, delete_key_callback = self.Delete, activation_callback = self.Edit )
menu_items = []
diff --git a/hydrus/client/gui/search/ClientGUISearchPanels.py b/hydrus/client/gui/search/ClientGUISearchPanels.py
index 14ed11736..2a8488174 100644
--- a/hydrus/client/gui/search/ClientGUISearchPanels.py
+++ b/hydrus/client/gui/search/ClientGUISearchPanels.py
@@ -177,7 +177,9 @@ def __init__( self, parent, favourite_searches_rows, initial_search_row_to_edit
self._favourite_searches_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._favourite_searches = ClientGUIListCtrl.BetterListCtrl( self._favourite_searches_panel, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, 20, self._ConvertRowToListCtrlTuples, use_simple_delete = True, activation_callback = self._EditFavouriteSearch )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, self._ConvertRowToListCtrlTuples )
+
+ self._favourite_searches = ClientGUIListCtrl.BetterListCtrlTreeView( self._favourite_searches_panel, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, 20, model, use_simple_delete = True, activation_callback = self._EditFavouriteSearch )
self._favourite_searches_panel.SetListCtrl( self._favourite_searches )
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index b4edd9d00..7173c1788 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -56,7 +56,9 @@ def __init__( self, parent, auto_account_creation_service_key = None ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_MANAGE_SERVICES.ID, 25, self._ConvertServiceToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit)
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_MANAGE_SERVICES.ID, self._ConvertServiceToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_MANAGE_SERVICES.ID, 25, model, delete_key_callback = self._Delete, activation_callback = self._Edit)
menu_items = []
@@ -1891,7 +1893,9 @@ def __init__( self, parent, service ):
permissions_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._permissions_list = ClientGUIListCtrl.BetterListCtrl( permissions_list_panel, CGLC.COLUMN_LIST_CLIENT_API_PERMISSIONS.ID, 10, self._ConvertDataToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_CLIENT_API_PERMISSIONS.ID, self._ConvertDataToListCtrlTuples )
+
+ self._permissions_list = ClientGUIListCtrl.BetterListCtrlTreeView( permissions_list_panel, CGLC.COLUMN_LIST_CLIENT_API_PERMISSIONS.ID, 10, model, delete_key_callback = self._Delete, activation_callback = self._Edit )
permissions_list_panel.SetListCtrl( self._permissions_list )
@@ -3463,7 +3467,9 @@ def __init__( self, parent, service ):
self._ipfs_shares_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._ipfs_shares = ClientGUIListCtrl.BetterListCtrl( self._ipfs_shares_panel, CGLC.COLUMN_LIST_IPFS_SHARES.ID, 6, self._ConvertDataToListCtrlTuple, delete_key_callback = self._Unpin, activation_callback = self._SetNotes )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_IPFS_SHARES.ID, self._ConvertDataToListCtrlTuple )
+
+ self._ipfs_shares = ClientGUIListCtrl.BetterListCtrlTreeView( self._ipfs_shares_panel, CGLC.COLUMN_LIST_IPFS_SHARES.ID, 6, model, delete_key_callback = self._Unpin, activation_callback = self._SetNotes )
self._ipfs_shares_panel.SetListCtrl( self._ipfs_shares )
diff --git a/hydrus/client/gui/services/ClientGUIServersideServices.py b/hydrus/client/gui/services/ClientGUIServersideServices.py
index ea10d495d..44f7166f0 100644
--- a/hydrus/client/gui/services/ClientGUIServersideServices.py
+++ b/hydrus/client/gui/services/ClientGUIServersideServices.py
@@ -258,7 +258,9 @@ def __init__( self, parent, service_key ):
self._deletee_service_keys = []
- self._services_listctrl = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_SERVICES.ID, 20, data_to_tuples_func = self._ConvertServiceToTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SERVICES.ID, self._ConvertServiceToTuples )
+
+ self._services_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_SERVICES.ID, 20, model, delete_key_callback = self._Delete, activation_callback = self._Edit )
menu_items = []
diff --git a/hydrus/client/gui/widgets/ClientGUIBandwidth.py b/hydrus/client/gui/widgets/ClientGUIBandwidth.py
index cb5555c2e..4a9691bf8 100644
--- a/hydrus/client/gui/widgets/ClientGUIBandwidth.py
+++ b/hydrus/client/gui/widgets/ClientGUIBandwidth.py
@@ -66,7 +66,9 @@ def display_call( desired_columns, rule ):
'''
- self._listctrl = ClientGUIListCtrl.BetterListCtrl( listctrl_panel, CGLC.COLUMN_LIST_BANDWIDTH_RULES.ID, 8, self._ConvertRuleToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )
+ model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_BANDWIDTH_RULES.ID, self._ConvertRuleToListCtrlTuples )
+
+ self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_BANDWIDTH_RULES.ID, 8, model, use_simple_delete = True, activation_callback = self._Edit )
listctrl_panel.SetListCtrl( self._listctrl )
diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py
index 774bff087..d01d8acca 100644
--- a/hydrus/client/networking/ClientLocalServerResources.py
+++ b/hydrus/client/networking/ClientLocalServerResources.py
@@ -69,7 +69,7 @@
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs', 'width', 'height', 'render_format', 'render_quality' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'service_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'file_service_key', 'deleted_file_service_key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2', 'rating_service_key', 'job_status_key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' }
-CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'download', 'only_return_identifiers', 'only_return_basic_information', 'include_blurhash', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_milliseconds', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system', 'only_in_view' }
+CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'download', 'only_return_identifiers', 'only_return_basic_information', 'include_blurhash', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_milliseconds', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system', 'only_in_view', 'include_current_tags', 'include_pending_tags' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'file_service_keys', 'deleted_file_service_keys', 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@@ -2818,7 +2818,10 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
raise HydrusExceptions.BadRequestException( 'Sorry, search for all known tags over all known files is not supported!' )
- tag_context = ClientSearch.TagContext( service_key = tag_service_key )
+ include_current_tags = request.parsed_request_args.GetValue( 'include_current_tags', bool, default_value = True )
+ include_pending_tags = request.parsed_request_args.GetValue( 'include_pending_tags', bool, default_value = True )
+
+ tag_context = ClientSearch.TagContext( service_key = tag_service_key, include_current_tags = include_current_tags, include_pending_tags = include_pending_tags )
predicates = ParseClientAPISearchPredicates( request )
return_hashes = False
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 201f0fe9f..9d475a06f 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,8 +105,8 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 587
-CLIENT_API_VERSION = 68
+SOFTWARE_VERSION = 588
+CLIENT_API_VERSION = 69
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py
index 7b8697e56..90c13c277 100644
--- a/hydrus/test/TestClientAPI.py
+++ b/hydrus/test/TestClientAPI.py
@@ -5168,6 +5168,8 @@ def _test_search_files( self, connection, set_up_permissions ):
self.assertEqual( file_search_context.GetLocationContext().current_service_keys, { CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY } )
self.assertEqual( file_search_context.GetTagContext().service_key, CC.COMBINED_TAG_SERVICE_KEY )
+ self.assertEqual( file_search_context.GetTagContext().include_current_tags, True )
+ self.assertEqual( file_search_context.GetTagContext().include_pending_tags, True )
self.assertEqual( set( file_search_context.GetPredicates() ), { ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, tag ) for tag in tags } )
self.assertIn( 'sort_by', kwargs )
@@ -5181,6 +5183,72 @@ def _test_search_files( self, connection, set_up_permissions ):
self.assertEqual( kwargs[ 'apply_implicit_limit' ], False )
+ # include current/pending
+
+ HG.test_controller.ClearReads( 'file_query_ids' )
+
+ sample_hash_ids = set( random.sample( list( hash_ids ), 3 ) )
+
+ HG.test_controller.SetRead( 'file_query_ids', set( sample_hash_ids ) )
+
+ tags = [ 'kino', 'green' ]
+
+ path = '/get_files/search_files?tags={}&include_current_tags=false'.format( urllib.parse.quote( json.dumps( tags ) ) )
+
+ connection.request( 'GET', path, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ text = str( data, 'utf-8' )
+
+ self.assertEqual( response.status, 200 )
+
+ d = json.loads( text )
+
+ expected_result = { 'file_ids' : list( sample_hash_ids ) }
+
+ wash_example_json_response( expected_result )
+
+ self.assertEqual( d, expected_result )
+
+ [ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
+
+ ( file_search_context, ) = args
+
+ self.assertEqual( file_search_context.GetTagContext().service_key, CC.COMBINED_TAG_SERVICE_KEY )
+ self.assertEqual( file_search_context.GetTagContext().include_current_tags, False )
+ self.assertEqual( file_search_context.GetTagContext().include_pending_tags, True )
+
+ path = '/get_files/search_files?tags={}&include_pending_tags=false'.format( urllib.parse.quote( json.dumps( tags ) ) )
+
+ connection.request( 'GET', path, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ text = str( data, 'utf-8' )
+
+ self.assertEqual( response.status, 200 )
+
+ d = json.loads( text )
+
+ expected_result = { 'file_ids' : list( sample_hash_ids ) }
+
+ wash_example_json_response( expected_result )
+
+ self.assertEqual( d, expected_result )
+
+ [ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
+
+ ( file_search_context, ) = args
+
+ self.assertEqual( file_search_context.GetTagContext().service_key, CC.COMBINED_TAG_SERVICE_KEY )
+ self.assertEqual( file_search_context.GetTagContext().include_current_tags, True )
+ self.assertEqual( file_search_context.GetTagContext().include_pending_tags, False )
+
# search files and get hashes
HG.test_controller.ClearReads( 'file_query_ids' )
diff --git a/static/default/parsers/shimmie file page parser - simple tags.png b/static/default/parsers/shimmie file page parser - simple tags.png
new file mode 100644
index 000000000..df2f7bcff
Binary files /dev/null and b/static/default/parsers/shimmie file page parser - simple tags.png differ
diff --git a/static/default/parsers/shimmie file page parser.png b/static/default/parsers/shimmie file page parser.png
index 2404b033f..b5637339d 100644
Binary files a/static/default/parsers/shimmie file page parser.png and b/static/default/parsers/shimmie file page parser.png differ