diff --git a/docs/changelog.md b/docs/changelog.md
index 89ce29539..f9ab58060 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,56 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 597](https://github.com/hydrusnetwork/hydrus/releases/tag/v597)
+
+### misc
+
+* fixed an issue that caused non-empty hard drive file import file logs that were created before v595 (this typically affected import folders that are set to 'leave source alone, do not reattempt it' for any of the result actions) to lose track of their original import objects' unique IDs and thus, when given more items to possibly add (again, usually on an import folder sync), to re-add the same items one time over again and essentially double-up in size one time. this broke the ability to review the file log UI panel too, so users who noticed the behaviour was jank couldn't see what was going on. on update, all the newer duplicate items will be removed and you'll reset to the original 'already in db' etc.. stuff you had before. all file logs now check for and remove newer duplicates whenever they load or change contents. this happened because of the 'make file logs load faster' update in v595--it worked great for downloaders and subs, but local file imports use a slightly different ID system to differentiate separate objects and it was not updated correct
+* the main text-fetching routine that failed to load the list UI in the above case can now recover from null results if this happens again
+* file import objects now have some more safety code to ensure they are identifying themselves correctly on load
+* did some more work on copying tags: the new 'always copy parents with tags' was not as helpful as I expected, so this is no longer the default when you hit Ctrl+C (it goes back to the old behaviour of just copying the top-line rows in your selection). when you open a tag selection 'copy' menu, it now lists as a separate item 'copy 2 selected and 3 parents' kind of thing if you do want parents. also, parents will no longer copy with their indent (wew), and the taglists are now deduped so you will not be inundated with tagspam. futhermore, the 'what tags do we have' taglist in the manage tags dialog, and favourites/suggestions taglists, are now more parent-aware and plugged into this system
+* added Mr Bones to the frame locations list under `options->gui`. if you use him a lot, he'll now remember where he was and how big he was
+* also added `manage_times_dialog`, `manage_urls_dialog`, `manage_notes_dialog`, and `export_files_frame` to the list. they will all remember last size and position by default
+* the client now recovers from a missing frame location entry with a fallback and a note in the log
+* rewrote the way the media viewer hover windows and their sub-controls are updated to the current media object. the old asynchronous pubsub is out, and synchronous Qt signals are in. fingers crossed this truly fixes the rare-but-annoying 'oh the ratings in the top-right hover aren't updating I guess' bug, but we'll see. I had to be stricter about the pipeline here, and I was careful to ensure it would be failsafe, so if you discover a media viewer with hover windows that simply won't switch media (they'd probably be frozen in a null state from viewer open), let me know the details!
+* some built versions of the client seem unable to find their local help, so now, when a user asks to open a help page, if it seems to be missing locally, a little text with the paths involved is now written to the log
+
+### parsing
+
+* all formulae now have a 'name/description' field. this is wholly decorative and simply appears in the single- or multi-line summary of the formula in UI. all formulae start with and will initialise with a blank label
+* the generic 'edit formula' panel (the one where you can change the formula type) now has import/export buttons
+* updated the ZIPPER UI to use a newer single-class 'queue list' widget rather than some ten year old 'still has some wx in it' scatter of gubbins
+* added import/export/duplicate capability to the 'queue list' widget, and added it for ZIPPER formulae
+* also added import/export/duplicate buttons to the 'edit string processor' list!!
+* 'any characters' String Match objects now describe themselves with the 'such as' respective example string, with the new proviso that no String Match will give this string if it is stuck at the 'example string' default. you'll probably most see this in the manage url class dialog for components and parameters
+* cleaned a bunch of this code generally
+
+### client api
+
+* fixed an issue fetching millisecond-precise timestamps in the `file_metadata` call when one of the timestamps had a null value (for instance if the file has no modified date of any kind registered)
+* in the various potential duplicates calls, some simple searches (usually when one/both of two searches are system:everything) are now optimised using the same routine that happens in UI
+* the client api version is now 75
+
+### Win 7 news
+
+* for Win 7 users who run from source, I believe newer the program's newer virtual environments will no longer build in Win 7. it looks like a new version of psd-tools will not compile in python 3.8, and there's also some code in newer versions of the program that 3.8 simply won't run. I think the last version that works for you is v582. we've known this train was coming for a while, so I'm afraid Win 7 guys will have to freeze at that version unless and until they update Windows or move to Linux/macOS
+* I have updated the 'running from source' help to talk about this, including adding the magic git line you need to choose a specific version rather than normal git pull. this is likely the last time I will specifically support Win 7, and I suspect I will sunset pyside2 and PyQt5 testing too
+
+### Windows future build
+
+* I am releasing a future build alongside this release, just for Windows. it has new dlls for SQLite and mpv. advanced users are invited to test it out and tell me if there are any problems booting and playing media, and if there are no issues, I'll fold this into the normal build next week
+* mpv: 2023-08-20 to 2024-10-20
+* SQLite: 3.45.3 to 3.47.0
+* these bring normal optimisations and bug fixes. I expect no huge problems (although I believe the mpv dll strictly no longer supports Win 7, but that is now moot), but please check and we'll see
+
+### boring code cleanup
+
+* in prep for duplicates auto-resolution, the five variables that go into a potential duplicates search (two file searches, the search type, the pixel dupe requirement, and the max hamming distance) are now bundled into one nice clean object that is simpler to handle and will be easier to update in future. everything that touches this stuff--the page manager, the page UI (there's a whole edit panel for the new class), the filter itself, the Client API, the db search code, all the unit tests, and now the duplicates auto-resolution system--all works on this new thing rather than throwing list of variables around
+
+### duplicates auto-resolution
+
+* I pushed this forward in a bunch of ways. nothing actually works yet, still, but if you poke around in the advanced placeholder UI, you'll see the new potential duplicates search context UI, now with side-by-side file search context panels, for the fleshed-out pixel-perfect jpeg/png default
+
## [Version 596](https://github.com/hydrusnetwork/hydrus/releases/tag/v596)
### misc
@@ -379,27 +429,3 @@ title: Changelog
* 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
-
-* I made a second stupid typo last week. it raised an error when trying to open the 'manage tag display and search' dialog! it was fixed thanks to a user
-* the current local file domains of a file (e.g. 'my files') are now simply listed in the top-right hover window, above any remote locations or URLs. I think I'm going to make these checkboxes or something in future so we can have one-click file migrations
-* if you set up a _share->export files_ job and one of the internal files is actually missing, the error message now tells you to go check for missing files using the database file maintenance stuff
-* if an export files job that is set to delete internal files breaks half way through, the routine now makes sure only to delete what was actually successful
-* subscriptions now catch program shutdown signals better. previously, this was being handled as an unknown error and delay times and error texts were being set. it now just closes cleanly, no worries
-* the command palette should now match case-insensitively
-* I _may_ have fixed a false-positive delete-lock report ('could not delete files xyz because of delete lock') that can happen in the duplicate filter. also, the 'unable to delete file' popup that happens in this case now quietly prints the current stack to log, which I would be interested in seeing
-* I believe I have fixed several of the false-positive 'hey it looks like you edited this parser, are you sure you want to cancel?' confirmations in the edit parser dialog
-* the automatic datestring parsing routine should now be more resilient against english datestrings when the locale differs significantly (it seems if the locale requires a 24-hour clock, it may be a problem for AM/PM time strings)
-* cleaned up some ancient-and-terrible sash-sizing code that manages the three resizable panels of each media results page. hopefully I fixed an issue in Docker and other places where the media page could spawn with a 0-pixel-wide thumbnail panel
-* fixed a weird/stupid bug with the new scanbar that would sometimes start giving errors on media transitions because it couldn't find its media parent
-* improved how a core UI job waits on the database to be free. it now uses just a little less CPU/fewer thread switches
-* improved how that same UI job waits on the pubsub system to be free, same deal
-* since they reversed the API click-through requirement, removed the 8chan TOS click-through login script from the defaults. existing users will see it set to non-active. 8chan thread watching should work out the box again
-
-### new list stuff
-
-* I worked on a new multi-column list class that uses a more intelligent data model. I basically finished it, but I will not launch it yet--it needs a bunch more testing and debugging
-* as a side thing, a variety of list display update calls, even on the old list, are now a little faster
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index e67d7b1b8..71835237c 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,45 @@
+ -
+
+
+ misc
+ - fixed an issue that caused non-empty hard drive file import file logs that were created before v595 (this typically affected import folders that are set to 'leave source alone, do not reattempt it' for any of the result actions) to lose track of their original import objects' unique IDs and thus, when given more items to possibly add (again, usually on an import folder sync), to re-add the same items one time over again and essentially double-up in size one time. this broke the ability to review the file log UI panel too, so users who noticed the behaviour was jank couldn't see what was going on. on update, all the newer duplicate items will be removed and you'll reset to the original 'already in db' etc.. stuff you had before. all file logs now check for and remove newer duplicates whenever they load or change contents. this happened because of the 'make file logs load faster' update in v595--it worked great for downloaders and subs, but local file imports use a slightly different ID system to differentiate separate objects and it was not updated correct
+ - the main text-fetching routine that failed to load the list UI in the above case can now recover from null results if this happens again
+ - file import objects now have some more safety code to ensure they are identifying themselves correctly on load
+ - did some more work on copying tags: the new 'always copy parents with tags' was not as helpful as I expected, so this is no longer the default when you hit Ctrl+C (it goes back to the old behaviour of just copying the top-line rows in your selection). when you open a tag selection 'copy' menu, it now lists as a separate item 'copy 2 selected and 3 parents' kind of thing if you do want parents. also, parents will no longer copy with their indent (wew), and the taglists are now deduped so you will not be inundated with tagspam. futhermore, the 'what tags do we have' taglist in the manage tags dialog, and favourites/suggestions taglists, are now more parent-aware and plugged into this system
+ - added Mr Bones to the frame locations list under `options->gui`. if you use him a lot, he'll now remember where he was and how big he was
+ - also added `manage_times_dialog`, `manage_urls_dialog`, `manage_notes_dialog`, and `export_files_frame` to the list. they will all remember last size and position by default
+ - the client now recovers from a missing frame location entry with a fallback and a note in the log
+ - rewrote the way the media viewer hover windows and their sub-controls are updated to the current media object. the old asynchronous pubsub is out, and synchronous Qt signals are in. fingers crossed this truly fixes the rare-but-annoying 'oh the ratings in the top-right hover aren't updating I guess' bug, but we'll see. I had to be stricter about the pipeline here, and I was careful to ensure it would be failsafe, so if you discover a media viewer with hover windows that simply won't switch media (they'd probably be frozen in a null state from viewer open), let me know the details!
+ - some built versions of the client seem unable to find their local help, so now, when a user asks to open a help page, if it seems to be missing locally, a little text with the paths involved is now written to the log
+ parsing
+ - all formulae now have a 'name/description' field. this is wholly decorative and simply appears in the single- or multi-line summary of the formula in UI. all formulae start with and will initialise with a blank label
+ - the generic 'edit formula' panel (the one where you can change the formula type) now has import/export buttons
+ - updated the ZIPPER UI to use a newer single-class 'queue list' widget rather than some ten year old 'still has some wx in it' scatter of gubbins
+ - added import/export/duplicate capability to the 'queue list' widget, and added it for ZIPPER formulae
+ - also added import/export/duplicate buttons to the 'edit string processor' list!!
+ - 'any characters' String Match objects now describe themselves with the 'such as' respective example string, with the new proviso that no String Match will give this string if it is stuck at the 'example string' default. you'll probably most see this in the manage url class dialog for components and parameters
+ - cleaned a bunch of this code generally
+ client api
+ - fixed an issue fetching millisecond-precise timestamps in the `file_metadata` call when one of the timestamps had a null value (for instance if the file has no modified date of any kind registered)
+ - in the various potential duplicates calls, some simple searches (usually when one/both of two searches are system:everything) are now optimised using the same routine that happens in UI
+ - the client api version is now 75
+ Win 7 news
+ - for Win 7 users who run from source, I believe newer the program's newer virtual environments will no longer build in Win 7. it looks like a new version of psd-tools will not compile in python 3.8, and there's also some code in newer versions of the program that 3.8 simply won't run. I think the last version that works for you is v582. we've known this train was coming for a while, so I'm afraid Win 7 guys will have to freeze at that version unless and until they update Windows or move to Linux/macOS
+ - I have updated the 'running from source' help to talk about this, including adding the magic git line you need to choose a specific version rather than normal git pull. this is likely the last time I will specifically support Win 7, and I suspect I will sunset pyside2 and PyQt5 testing too
+ Windows future build
+ - I am releasing a future build alongside this release, just for Windows. it has new dlls for SQLite and mpv. advanced users are invited to test it out and tell me if there are any problems booting and playing media, and if there are no issues, I'll fold this into the normal build next week
+ - mpv: 2023-08-20 to 2024-10-20
+ - SQLite: 3.45.3 to 3.47.0
+ - these bring normal optimisations and bug fixes. I expect no huge problems (although I believe the mpv dll strictly no longer supports Win 7, but that is now moot), but please check and we'll see
+ boring code cleanup
+ - in prep for duplicates auto-resolution, the five variables that go into a potential duplicates search (two file searches, the search type, the pixel dupe requirement, and the max hamming distance) are now bundled into one nice clean object that is simpler to handle and will be easier to update in future. everything that touches this stuff--the page manager, the page UI (there's a whole edit panel for the new class), the filter itself, the Client API, the db search code, all the unit tests, and now the duplicates auto-resolution system--all works on this new thing rather than throwing list of variables around
+ duplicates auto-resolution
+ - I pushed this forward in a bunch of ways. nothing actually works yet, still, but if you poke around in the advanced placeholder UI, you'll see the new potential duplicates search context UI, now with side-by-side file search context panels, for the fleshed-out pixel-perfect jpeg/png default
+
+
-
diff --git a/docs/running_from_source.md b/docs/running_from_source.md
index 5cae29e40..58dc8e584 100644
--- a/docs/running_from_source.md
+++ b/docs/running_from_source.md
@@ -50,7 +50,19 @@ There are now setup scripts that make this easy on Windows and Linux. You do not
Git should now be installed on your system. Any new terminal/command line/powershell window (shift+right-click on any folder and hit something like 'Open in terminal') now has the `git` command!
- Then you will need to install Python. Get 3.10 or 3.11 [here](https://www.python.org/downloads/windows/) (or, if you are Win 7, I think you'll want [this](https://www.python.org/downloads/release/python-3810/)). During the install process, make sure it has something like 'Add Python to PATH' checked. This makes Python available everywhere in Windows.
+ ??? warning "Windows 7"
+ For a long time, I supported Windows 7 via running from source. Unfortunately, as libraries and code inevitably updated, this finally seems to no longer be feasible. Python 3.8 will no longer run the program. I understand v582 is one of the last versions of the program to work.
+
+ First, you will have to install the older [Python 3.8](https://www.python.org/downloads/release/python-3810/), since that is the latest version that you can run.
+
+ Then, later, when you do the `git clone https://github.com/hydrusnetwork/hydrus` line, you will need to run `git checkout tags/v578`, which will rewind you to that point in time.
+
+ You will also need to navigate to `install_dir/static/requirements/advanced` and edit `requirements_core.txt`; remove the 'psd-tools' line before you run setup_venv.
+
+ I can't promise anything though. The requirements.txt isn't perfect, and something else may break in future! You may like to think about setting up a Linux instance.
+
+
+ Then you will need to install Python. Get 3.10 or 3.11 [here](https://www.python.org/downloads/windows/). During the install process, make sure it has something like 'Add Python to PATH' checked. This makes Python available everywhere in Windows.
=== "Linux"
@@ -61,9 +73,9 @@ There are now setup scripts that make this easy on Windows and Linux. You do not
You should already have a fairly new python. Ideally, you want at least 3.9. You can find out what version you have just by opening a new terminal and typing 'python'.
-If you are already on newer python, like 3.12+, that's ok--you might need to select the 'advanced' setup later on and choose the '(t)est' options. If you are stuck on a much older version of python, try the same thing, but with the '(o)lder' options (but I can't promise it will work!).
+If you are already on newer python, like 3.12+, that's ok--you might need to select the 'advanced' setup later on and choose the '(t)est' options. If you are stuck on 3.9, try the same thing, but with the '(o)lder' options (but I can't promise it will work!).
-Then, get the hydrus source. It is best to get it with Git: make a new folder somewhere, open a terminal in it, and then paste:
+**Then, get the hydrus source.** It is best to get it with Git: make a new folder somewhere, open a terminal in it, and then paste:
git clone https://github.com/hydrusnetwork/hydrus
@@ -76,7 +88,7 @@ If Git is not available, then just go to the [latest release](https://github.com
We will call the base extract directory, the one with 'hydrus_client.py' in it, `install_dir`.
-!!! info "Mixed Builds"
+!!! warning "Mixed Builds"
Don't mix and match build extracts and source extracts. The process that runs the code gets confused if there are unexpected extra .dlls in the directory. **If you need to convert between built and source releases, perform a [clean install](getting_started_installing.md#clean_installs).**
If you are converting from one install type to another, make a backup before you start. Then, if it all goes wrong, you'll always have a safe backup to rollback to.
diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py
index b6b11b7aa..50f4972a0 100644
--- a/hydrus/client/ClientOptions.py
+++ b/hydrus/client/ClientOptions.py
@@ -730,6 +730,11 @@ def _InitialiseDefaults( self ):
self._dictionary[ 'frame_locations' ][ 'deeply_nested_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
self._dictionary[ 'frame_locations' ][ 'regular_center_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'center', False, False )
self._dictionary[ 'frame_locations' ][ 'file_history_chart' ] = ( True, True, ( 960, 720 ), None, ( -1, -1 ), 'topleft', False, False )
+ self._dictionary[ 'frame_locations' ][ 'mr_bones' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
+ self._dictionary[ 'frame_locations' ][ 'manage_urls_dialog' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
+ self._dictionary[ 'frame_locations' ][ 'manage_times_dialog' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
+ self._dictionary[ 'frame_locations' ][ 'manage_notes_dialog' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
+ self._dictionary[ 'frame_locations' ][ 'export_files_frame' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
#
@@ -790,7 +795,7 @@ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
loaded_dictionary = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_info )
- for ( key, value ) in list(loaded_dictionary.items()):
+ for ( key, value ) in loaded_dictionary.items():
if key in self._dictionary and isinstance( self._dictionary[ key ], dict ) and isinstance( value, dict ):
@@ -811,7 +816,7 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
if 'media_view' in loaded_dictionary:
- mimes = list(loaded_dictionary[ 'media_view' ].keys())
+ mimes = list( loaded_dictionary[ 'media_view' ].keys() )
for mime in mimes:
@@ -1249,7 +1254,18 @@ def GetFrameLocation( self, frame_key ):
with self._lock:
- return self._dictionary[ 'frame_locations' ][ frame_key ]
+ d = self._dictionary[ 'frame_locations' ]
+
+ if frame_key in d:
+
+ return d[ frame_key ]
+
+ else:
+
+ HydrusData.Print( f'Could not find {frame_key} in the frame locations options!' )
+
+ return ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
+
diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py
index f54bb289d..3cf642b49 100644
--- a/hydrus/client/ClientParsing.py
+++ b/hydrus/client/ClientParsing.py
@@ -790,13 +790,19 @@ def LooksLikeJSON( self ):
class ParseFormula( HydrusSerialisable.SerialisableBase ):
- def __init__( self, string_processor = None ):
+ def __init__( self, name = None, string_processor = None ):
+
+ if name is None:
+
+ name = ''
+
if string_processor is None:
string_processor = ClientStrings.StringProcessor()
+ self._name = name
self._string_processor = string_processor
@@ -820,6 +826,11 @@ def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool
raise NotImplementedError()
+ def GetName( self ):
+
+ return self._name
+
+
def GetStringProcessor( self ):
return self._string_processor
@@ -867,6 +878,11 @@ def ParsesSeparatedContent( self ):
return False
+ def SetName( self, name: str ):
+
+ self._name = name
+
+
def SetStringProcessor( self, string_processor: "ClientStrings.StringProcessor" ):
self._string_processor = string_processor
@@ -887,11 +903,11 @@ class ParseFormulaZipper( ParseFormula ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_ZIPPER
SERIALISABLE_NAME = 'Zipper Parsing Formula'
- SERIALISABLE_VERSION = 2
+ SERIALISABLE_VERSION = 3
- def __init__( self, formulae = None, sub_phrase = None, string_processor = None ):
+ def __init__( self, formulae = None, sub_phrase = None, name = None, string_processor = None ):
- super().__init__( string_processor )
+ super().__init__( name = name, string_processor = string_processor )
if formulae is None:
@@ -915,12 +931,12 @@ def _GetSerialisableInfo( self ):
serialisable_formulae = HydrusSerialisable.SerialisableList( self._formulae ).GetSerialisableTuple()
serialisable_string_processor = self._string_processor.GetSerialisableTuple()
- return ( serialisable_formulae, self._sub_phrase, serialisable_string_processor )
+ return ( serialisable_formulae, self._sub_phrase, self._name, serialisable_string_processor )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( serialisable_formulae, self._sub_phrase, serialisable_string_processor ) = serialisable_info
+ ( serialisable_formulae, self._sub_phrase, self._name, serialisable_string_processor ) = serialisable_info
self._formulae = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_formulae )
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
@@ -1004,6 +1020,17 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 2, new_serialisable_info )
+ if version == 2:
+
+ ( serialisable_formulae, sub_phrase, serialisable_string_processor ) = old_serialisable_info
+
+ name = ''
+
+ new_serialisable_info = ( serialisable_formulae, sub_phrase, name, serialisable_string_processor )
+
+ return ( 3, new_serialisable_info )
+
+
def GetFormulae( self ):
@@ -1017,13 +1044,29 @@ def GetSubstitutionPhrase( self ):
def ToPrettyString( self ):
- return 'ZIPPER with ' + HydrusNumbers.ToHumanInt( len( self._formulae ) ) + ' formulae.'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return f'ZIPPER: {t}with {HydrusNumbers.ToHumanInt( len( self._formulae ) )} formulae.'
def ToPrettyMultilineString( self ):
s = []
+ header = '--ZIPPER--'
+
+ if self._name != '':
+
+ header += '\n' + self._name
+
+
for formula in self._formulae:
s.append( formula.ToPrettyMultilineString() )
@@ -1033,7 +1076,7 @@ def ToPrettyMultilineString( self ):
separator = '\n' * 2
- text = '--ZIPPER--' + '\n' * 2 + separator.join( s )
+ text = header + '\n' * 2 + separator.join( s )
return text
@@ -1045,11 +1088,11 @@ class ParseFormulaContextVariable( ParseFormula ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_CONTEXT_VARIABLE
SERIALISABLE_NAME = 'Context Variable Formula'
- SERIALISABLE_VERSION = 2
+ SERIALISABLE_VERSION = 3
- def __init__( self, variable_name = None, string_processor = None ):
+ def __init__( self, variable_name = None, name = None, string_processor = None ):
- super().__init__( string_processor )
+ super().__init__( name = name, string_processor = string_processor )
if variable_name is None:
@@ -1063,12 +1106,12 @@ def _GetSerialisableInfo( self ):
serialisable_string_processor = self._string_processor.GetSerialisableTuple()
- return ( self._variable_name, serialisable_string_processor )
+ return ( self._variable_name, self._name, serialisable_string_processor )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( self._variable_name, serialisable_string_processor ) = serialisable_info
+ ( self._variable_name, self._name, serialisable_string_processor ) = serialisable_info
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
@@ -1107,6 +1150,17 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 2, new_serialisable_info )
+ if version == 2:
+
+ ( variable_name, serialisable_string_processor ) = old_serialisable_info
+
+ name = ''
+
+ new_serialisable_info = ( variable_name, name, serialisable_string_processor )
+
+ return ( 3, new_serialisable_info )
+
+
def GetVariableName( self ):
@@ -1115,18 +1169,34 @@ def GetVariableName( self ):
def ToPrettyString( self ):
- return 'CONTEXT VARIABLE: ' + self._variable_name
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return f'CONTEXT VARIABLE: {t}{self._variable_name}'
def ToPrettyMultilineString( self ):
s = []
+ header = '--CONTEXT VARIABLE--'
+
+ if self._name != '':
+
+ header += '\n' + self._name
+
+
s.append( 'fetch the "' + self._variable_name + '" variable from the parsing context' )
separator = '\n' * 2
- text = '--CONTEXT VARIABLE--' + '\n' * 2 + separator.join( s )
+ text = header + '\n' * 2 + separator.join( s )
return text
@@ -1141,11 +1211,11 @@ class ParseFormulaHTML( ParseFormula ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_HTML
SERIALISABLE_NAME = 'HTML Parsing Formula'
- SERIALISABLE_VERSION = 7
+ SERIALISABLE_VERSION = 8
- def __init__( self, tag_rules = None, content_to_fetch = None, attribute_to_fetch = None, string_processor = None ):
+ def __init__( self, tag_rules = None, content_to_fetch = None, attribute_to_fetch = None, name = None, string_processor = None ):
- super().__init__( string_processor )
+ super().__init__( name = name, string_processor = string_processor )
if tag_rules is None:
@@ -1273,12 +1343,12 @@ def _GetSerialisableInfo( self ):
serialisable_string_processor = self._string_processor.GetSerialisableTuple()
- return ( serialisable_tag_rules, self._content_to_fetch, self._attribute_to_fetch, serialisable_string_processor )
+ return ( serialisable_tag_rules, self._content_to_fetch, self._attribute_to_fetch, self._name, serialisable_string_processor )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( serialisable_tag_rules, self._content_to_fetch, self._attribute_to_fetch, serialisable_string_processor ) = serialisable_info
+ ( serialisable_tag_rules, self._content_to_fetch, self._attribute_to_fetch, self._name, serialisable_string_processor ) = serialisable_info
self._tag_rules = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_rules )
@@ -1433,6 +1503,17 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 7, new_serialisable_info )
+ if version == 7:
+
+ ( serialisable_new_tag_rules, content_to_fetch, attribute_to_fetch, serialisable_string_processor ) = old_serialisable_info
+
+ name = ''
+
+ new_serialisable_info = ( serialisable_new_tag_rules, content_to_fetch, attribute_to_fetch, name, serialisable_string_processor )
+
+ return ( 8, new_serialisable_info )
+
+
def GetAttributeToFetch( self ):
@@ -1456,12 +1537,30 @@ def ParsesSeparatedContent( self ):
def ToPrettyString( self ):
- return 'HTML with ' + HydrusNumbers.ToHumanInt( len( self._tag_rules ) ) + ' tag rules.'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return f'HTML: {t}with {HydrusNumbers.ToHumanInt( len( self._tag_rules ) )} tag rules.'
def ToPrettyMultilineString( self ):
- pretty_strings = [ t_r.ToString() for t_r in self._tag_rules ]
+ pretty_strings = []
+
+ header = '--HTML--'
+
+ if self._name != '':
+
+ header += '\n' + self._name
+
+
+ pretty_strings.extend( [ t_r.ToString() for t_r in self._tag_rules ] )
if self._content_to_fetch == HTML_CONTENT_ATTRIBUTE:
@@ -1480,7 +1579,7 @@ def ToPrettyMultilineString( self ):
separator = '\n' + 'and then '
- pretty_multiline_string = '--HTML--' + '\n' + separator.join( pretty_strings )
+ pretty_multiline_string = header + '\n' + separator.join( pretty_strings )
return pretty_multiline_string
@@ -1763,11 +1862,11 @@ class ParseFormulaJSON( ParseFormula ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_JSON
SERIALISABLE_NAME = 'JSON Parsing Formula'
- SERIALISABLE_VERSION = 3
+ SERIALISABLE_VERSION = 4
- def __init__( self, parse_rules = None, content_to_fetch = None, string_processor = None ):
+ def __init__( self, parse_rules = None, content_to_fetch = None, name = None, string_processor = None ):
- super().__init__( string_processor )
+ super().__init__( name = name, string_processor = string_processor )
if parse_rules is None:
@@ -1988,12 +2087,12 @@ def _GetSerialisableInfo( self ):
serialisable_parse_rules = [ ( parse_rule_type, parse_rule.GetSerialisableTuple() ) if parse_rule_type == JSON_PARSE_RULE_TYPE_DICT_KEY else ( parse_rule_type, parse_rule ) for ( parse_rule_type, parse_rule ) in self._parse_rules ]
serialisable_string_processor = self._string_processor.GetSerialisableTuple()
- return ( serialisable_parse_rules, self._content_to_fetch, serialisable_string_processor )
+ return ( serialisable_parse_rules, self._content_to_fetch, self._name, serialisable_string_processor )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( serialisable_parse_rules, self._content_to_fetch, serialisable_string_processor ) = serialisable_info
+ ( serialisable_parse_rules, self._content_to_fetch, self._name, serialisable_string_processor ) = serialisable_info
self._parse_rules = [ ( parse_rule_type, HydrusSerialisable.CreateFromSerialisableTuple( serialisable_parse_rule ) ) if parse_rule_type == JSON_PARSE_RULE_TYPE_DICT_KEY else ( parse_rule_type, serialisable_parse_rule ) for ( parse_rule_type, serialisable_parse_rule ) in serialisable_parse_rules ]
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
@@ -2079,6 +2178,17 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 3, new_serialisable_info )
+ if version == 3:
+
+ ( serialisable_parse_rules, content_to_fetch, serialisable_string_processor ) = old_serialisable_info
+
+ name = ''
+
+ new_serialisable_info = ( serialisable_parse_rules, content_to_fetch, name, serialisable_string_processor )
+
+ return ( 4, new_serialisable_info )
+
+
def GetContentToFetch( self ):
@@ -2097,11 +2207,27 @@ def ParsesSeparatedContent( self ):
def ToPrettyString( self ):
- return 'JSON with ' + HydrusNumbers.ToHumanInt( len( self._parse_rules ) ) + ' parse rules.'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return f'JSON: {t}with {HydrusNumbers.ToHumanInt( len( self._parse_rules ) )} parse rules.'
def ToPrettyMultilineString( self ):
+ header = '--JSON--'
+
+ if self._name != '':
+
+ header += '\n' + self._name
+
+
pretty_strings = [ RenderJSONParseRule( p_r ) for p_r in self._parse_rules ]
if self._content_to_fetch == JSON_CONTENT_STRING:
@@ -2121,7 +2247,7 @@ def ToPrettyMultilineString( self ):
separator = '\n' + 'and then '
- pretty_multiline_string = '--JSON--' + '\n' + separator.join( pretty_strings )
+ pretty_multiline_string = header + '\n' + separator.join( pretty_strings )
return pretty_multiline_string
@@ -2133,11 +2259,11 @@ class ParseFormulaNested( ParseFormula ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_NESTED
SERIALISABLE_NAME = 'Nested Parsing Formula'
- SERIALISABLE_VERSION = 1
+ SERIALISABLE_VERSION = 2
- def __init__( self, main_formula = None, sub_formula = None, string_processor = None ):
+ def __init__( self, main_formula = None, sub_formula = None, name = None, string_processor = None ):
- super().__init__( string_processor )
+ super().__init__( name = name, string_processor = string_processor )
if main_formula is None:
@@ -2159,12 +2285,12 @@ def _GetSerialisableInfo( self ):
serialisable_sub_formula = self._sub_formula.GetSerialisableTuple()
serialisable_string_processor = self._string_processor.GetSerialisableTuple()
- return ( serialisable_main_formula, serialisable_sub_formula, serialisable_string_processor )
+ return ( serialisable_main_formula, serialisable_sub_formula, self._name, serialisable_string_processor )
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
- ( serialisable_main_formula, serialisable_sub_formula, serialisable_string_processor ) = serialisable_info
+ ( serialisable_main_formula, serialisable_sub_formula, self._name, serialisable_string_processor ) = serialisable_info
self._main_formula = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_main_formula )
self._sub_formula = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_sub_formula )
@@ -2187,6 +2313,20 @@ def _ParseRawTexts( self, parsing_context, parsing_text: str, collapse_newlines:
return all_sub_parsed_texts
+ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
+
+ if version == 1:
+
+ ( serialisable_main_formula, serialisable_sub_formula, serialisable_string_processor ) = old_serialisable_info
+
+ name = ''
+
+ new_serialisable_info = ( serialisable_main_formula, serialisable_sub_formula, name, serialisable_string_processor )
+
+ return ( 2, new_serialisable_info )
+
+
+
def GetMainFormula( self ):
return self._main_formula
@@ -2199,12 +2339,28 @@ def GetSubFormula( self ):
def ToPrettyString( self ):
- return f'NESTED formulae, taking from "{self._main_formula.ToPrettyString()}" and sending to "{self._sub_formula.ToPrettyString()}".'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return f'NESTED formulae: {t}taking from "{self._main_formula.ToPrettyString()}" and sending to "{self._sub_formula.ToPrettyString()}".'
def ToPrettyMultilineString( self ):
- text = '--NESTED--' + '\n' * 2 + self._main_formula.ToPrettyMultilineString() + '\n->\n' + self._sub_formula.ToPrettyMultilineString()
+ header = '--NESTED--'
+
+ if self._name != '':
+
+ header += '\n' + self._name
+
+
+ text = header + '\n' * 2 + self._main_formula.ToPrettyMultilineString() + '\n->\n' + self._sub_formula.ToPrettyMultilineString()
return text
diff --git a/hydrus/client/ClientStrings.py b/hydrus/client/ClientStrings.py
index 810e836fe..c9a6a1050 100644
--- a/hydrus/client/ClientStrings.py
+++ b/hydrus/client/ClientStrings.py
@@ -704,13 +704,15 @@ def ToString( self, simple = False, with_type = False ) -> str:
FLEXIBLE_MATCH_HEX = 3
FLEXIBLE_MATCH_BASE64 = 4
+DEFAULT_EXAMPLE_STRING = 'example string'
+
class StringMatch( StringProcessingStep ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH
SERIALISABLE_NAME = 'String Match'
SERIALISABLE_VERSION = 1
- def __init__( self, match_type = STRING_MATCH_ANY, match_value = '', min_chars = None, max_chars = None, example_string = 'example string' ):
+ def __init__( self, match_type = STRING_MATCH_ANY, match_value = '', min_chars = None, max_chars = None, example_string = DEFAULT_EXAMPLE_STRING ):
super().__init__()
@@ -912,8 +914,6 @@ def ToString( self, simple = False, with_type = False ) -> str:
result += 'characters'
- show_example = False
-
elif self._match_type == STRING_MATCH_FIXED:
result = self._match_value
@@ -954,7 +954,10 @@ def ToString( self, simple = False, with_type = False ) -> str:
if show_example:
- result += ', such as "' + self._example_string + '"'
+ if self._example_string != DEFAULT_EXAMPLE_STRING:
+
+ result += ', such as "' + self._example_string + '"'
+
if with_type:
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index e6c121f70..84d364f32 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -72,6 +72,7 @@
from hydrus.client.db import ClientDBTagSuggestions
from hydrus.client.db import ClientDBURLMap
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.importing import ClientImportFiles
from hydrus.client.media import ClientMediaManagers
from hydrus.client.media import ClientMediaResult
@@ -1810,14 +1811,17 @@ def _DoAfterJobWork( self ):
HydrusDB.HydrusDB._DoAfterJobWork( self )
- def _DuplicatesGetRandomPotentialDuplicateHashes(
- self,
- file_search_context_1: ClientSearchFileSearchContext.FileSearchContext,
- file_search_context_2: ClientSearchFileSearchContext.FileSearchContext,
- dupe_search_type: int,
- pixel_dupes_preference,
- max_hamming_distance
- ) -> typing.List[ bytes ]:
+ def _DuplicatesGetRandomPotentialDuplicateHashes( self, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext ) -> typing.List[ bytes ]:
+
+ potential_duplicates_search_context = potential_duplicates_search_context.Duplicate()
+
+ potential_duplicates_search_context.OptimiseForSearch()
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ dupe_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_dupes_preference = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
db_location_context = self.modules_files_storage.GetDBLocationContext( file_search_context_1.GetLocationContext() )
@@ -1947,13 +1951,23 @@ def _DuplicatesGetRandomPotentialDuplicateHashes(
- def _DuplicatesGetPotentialDuplicatePairsForFiltering( self, file_search_context_1: ClientSearchFileSearchContext.FileSearchContext, file_search_context_2: ClientSearchFileSearchContext.FileSearchContext, dupe_search_type: int, pixel_dupes_preference, max_hamming_distance, max_num_pairs: typing.Optional[ int ] = None ):
+ def _DuplicatesGetPotentialDuplicatePairsForFiltering( self, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext, max_num_pairs: typing.Optional[ int ] = None ):
if max_num_pairs is None:
max_num_pairs = CG.client_controller.new_options.GetInteger( 'duplicate_filter_max_batch_size' )
+ potential_duplicates_search_context = potential_duplicates_search_context.Duplicate()
+
+ potential_duplicates_search_context.OptimiseForSearch()
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ dupe_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_dupes_preference = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
+
# we need to batch non-intersecting decisions here to keep it simple at the gui-level
# we also want to maximise per-decision value
@@ -2150,7 +2164,17 @@ def _DuplicatesGetPotentialDuplicatePairsForFiltering( self, file_search_context
return batch_of_pairs_of_media_results
- def _DuplicatesGetPotentialDuplicatesCount( self, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ):
+ def _DuplicatesGetPotentialDuplicatesCount( self, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext ):
+
+ potential_duplicates_search_context = potential_duplicates_search_context.Duplicate()
+
+ potential_duplicates_search_context.OptimiseForSearch()
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ dupe_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_dupes_preference = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
db_location_context = self.modules_files_storage.GetDBLocationContext( file_search_context_1.GetLocationContext() )
diff --git a/hydrus/client/duplicates/ClientDuplicatesAutoResolution.py b/hydrus/client/duplicates/ClientDuplicatesAutoResolution.py
index 231096ecb..107910f85 100644
--- a/hydrus/client/duplicates/ClientDuplicatesAutoResolution.py
+++ b/hydrus/client/duplicates/ClientDuplicatesAutoResolution.py
@@ -2,16 +2,23 @@
import threading
import typing
+from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusThreading
from hydrus.core import HydrusTime
+from hydrus.client import ClientConstants as CC
from hydrus.client import ClientDaemons
from hydrus.client import ClientGlobals as CG
+from hydrus.client import ClientLocation
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.metadata import ClientMetadataConditional
+from hydrus.client.search import ClientNumberTest
+from hydrus.client.search import ClientSearchPredicate
+from hydrus.client.search import ClientSearchFileSearchContext
# in the database I guess we'll assign these in a new table to all outstanding pairs that match a search
DUPLICATE_STATUS_DOES_NOT_MATCH_SEARCH = 0
@@ -122,7 +129,7 @@ class PairSelectorAndComparator( HydrusSerialisable.SerialisableBase ):
def __init__( self ):
"""
- This guy holds a bunch of rules. It is given a pair of media and it tests all the rules both ways around. If the files pass all the rules, we have a match and thus a confirmed better file.
+ This guy holds a bunch of comparators. It is given a pair of media and it tests all the rules both ways around. If the files pass all the rules, we have a match and thus a confirmed better file.
A potential future expansion here is to attach scores to the rules and have a score threshold, but let's not get ahead of ourselves.
"""
@@ -177,19 +184,11 @@ def __init__( self, name ):
# the id here will be for the database to match up rules to cached pair statuses. slightly wewmode, but we'll see
self._id = -1
- # TODO: Yes, do this before we get too excited here
- # maybe make this search part into its own object? in ClientDuplicates
- # could wangle duplicate pages and client api dupe stuff to work in the same guy, great idea
- # duplicate search, too, rather than passing around a bunch of params
- self._file_search_context_1 = None
- self._file_search_context_2 = None
- self._dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
- self._pixel_dupes_preference = ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED
- self._max_hamming_distance = 4
+ self._paused = False
- self._selector_and_comparator = None
+ self._potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
- self._paused = False
+ self._selector_and_comparator = None
# action info
# set as better
@@ -222,6 +221,11 @@ def GetComparatorSummary( self ) -> str:
return 'if A is jpeg and B is png'
+ def GetPotentialDuplicatesSearchContext( self ) -> ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext:
+
+ return self._potential_duplicates_search_context
+
+
def GetRuleSummary( self ) -> str:
return 'system:filetype is jpeg & system:filetype is png, pixel duplicates'
@@ -242,6 +246,16 @@ def SetId( self, value: int ):
self._id = value
+ def SetPaused( self, value: bool ):
+
+ self._paused = value
+
+
+ def SetPotentialDuplicatesSearchContext( self, value: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext ):
+
+ self._potential_duplicates_search_context = value
+
+
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_DUPLICATES_AUTO_RESOLUTION_RULE ] = DuplicatesAutoResolutionRule
@@ -251,8 +265,41 @@ def GetDefaultRuleSuggestions() -> typing.List[ DuplicatesAutoResolutionRule ]:
#
+ location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
+
+ predicates = [
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_MIME, value = ( HC.IMAGE_JPEG, ) ),
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_HEIGHT, value = ClientNumberTest.NumberTest.STATICCreateFromCharacters( '>', 128 ) ),
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_WIDTH, value = ClientNumberTest.NumberTest.STATICCreateFromCharacters( '>', 128 ) )
+ ]
+
+ file_search_context_1 = ClientSearchFileSearchContext.FileSearchContext(
+ location_context = location_context,
+ predicates = predicates
+ )
+
+ predicates = [
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_MIME, value = ( HC.IMAGE_PNG, ) ),
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_HEIGHT, value = ClientNumberTest.NumberTest.STATICCreateFromCharacters( '>', 128 ) ),
+ ClientSearchPredicate.Predicate( predicate_type = ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_WIDTH, value = ClientNumberTest.NumberTest.STATICCreateFromCharacters( '>', 128 ) )
+ ]
+
+ file_search_context_2 = ClientSearchFileSearchContext.FileSearchContext(
+ location_context = location_context,
+ predicates = predicates
+ )
+
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
+ potential_duplicates_search_context.SetPixelDupesPreference( ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_REQUIRED )
+
duplicates_auto_resolution_rule = DuplicatesAutoResolutionRule( 'pixel-perfect jpegs vs pngs' )
+ duplicates_auto_resolution_rule.SetPotentialDuplicatesSearchContext( potential_duplicates_search_context )
+
suggested_rules.append( duplicates_auto_resolution_rule )
# add on a thing here about resolution. one(both) files need to be like at least 128x128
diff --git a/hydrus/client/duplicates/ClientPotentialDuplicatesSearchContext.py b/hydrus/client/duplicates/ClientPotentialDuplicatesSearchContext.py
new file mode 100644
index 000000000..b59cf6b5b
--- /dev/null
+++ b/hydrus/client/duplicates/ClientPotentialDuplicatesSearchContext.py
@@ -0,0 +1,137 @@
+import typing
+
+from hydrus.core import HydrusSerialisable
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
+from hydrus.client import ClientLocation
+from hydrus.client.duplicates import ClientDuplicates
+
+from hydrus.client.search import ClientSearchFileSearchContext
+from hydrus.client.search import ClientSearchPredicate
+
+class PotentialDuplicatesSearchContext( HydrusSerialisable.SerialisableBase ):
+
+ SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_POTENTIAL_DUPLICATES_SEARCH_CONTEXT
+ SERIALISABLE_NAME = 'Potential Duplicates Search Context'
+ SERIALISABLE_VERSION = 1
+
+ def __init__( self, location_context: typing.Optional[ ClientLocation.LocationContext ] = None, initial_predicates = None ):
+
+ if location_context is None:
+
+ try:
+
+ location_context = CG.client_controller.new_options.GetDefaultLocalLocationContext()
+
+ except:
+
+ location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
+
+
+
+ if initial_predicates is None:
+
+ initial_predicates = [ ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_SYSTEM_EVERYTHING ) ]
+
+
+ file_search_context = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, predicates = initial_predicates )
+
+ self._file_search_context_1 = file_search_context
+ self._file_search_context_2 = file_search_context.Duplicate()
+ self._dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
+ self._pixel_dupes_preference = ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED
+ self._max_hamming_distance = 4
+
+
+ def _GetSerialisableInfo( self ):
+
+ serialisable_file_search_context_1 = self._file_search_context_1.GetSerialisableTuple()
+ serialisable_file_search_context_2 = self._file_search_context_2.GetSerialisableTuple()
+
+ return ( serialisable_file_search_context_1, serialisable_file_search_context_2, self._dupe_search_type, self._pixel_dupes_preference, self._max_hamming_distance )
+
+
+ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+
+ ( serialisable_file_search_context_1, serialisable_file_search_context_2, self._dupe_search_type, self._pixel_dupes_preference, self._max_hamming_distance ) = serialisable_info
+
+ self._file_search_context_1 = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context_1 )
+ self._file_search_context_2 = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_search_context_2 )
+
+
+ def GetDupeSearchType( self ) -> int:
+
+ return self._dupe_search_type
+
+
+ def GetFileSearchContext1( self ) -> ClientSearchFileSearchContext:
+
+ return self._file_search_context_1
+
+
+ def GetFileSearchContext2( self ) -> ClientSearchFileSearchContext:
+
+ return self._file_search_context_2
+
+
+ def GetMaxHammingDistance( self ) -> int:
+
+ return self._max_hamming_distance
+
+
+ def GetPixelDupesPreference( self ) -> int:
+
+ return self._pixel_dupes_preference
+
+
+ def OptimiseForSearch( self ):
+
+ if self._dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH and ( self._file_search_context_1.IsJustSystemEverything() or self._file_search_context_1.HasNoPredicates() ):
+
+ self._dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
+
+ elif self._dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES:
+
+ if self._file_search_context_1.IsJustSystemEverything() or self._file_search_context_1.HasNoPredicates():
+
+ f = self._file_search_context_1
+ self._file_search_context_1 = self._file_search_context_2
+ self._file_search_context_2 = f
+
+ self._dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
+
+ elif self._file_search_context_2.IsJustSystemEverything() or self._file_search_context_2.HasNoPredicates():
+
+ self._dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
+
+
+
+
+ def SetDupeSearchType( self, value: int ):
+
+ self._dupe_search_type = value
+
+
+ def SetFileSearchContext1( self, value: ClientSearchFileSearchContext ):
+
+ self._file_search_context_1 = value
+
+
+ def SetFileSearchContext2( self, value : ClientSearchFileSearchContext ):
+
+ self._file_search_context_2 = value
+
+
+ def SetMaxHammingDistance( self, value : int ):
+
+ self._max_hamming_distance = value
+
+
+ def SetPixelDupesPreference( self, value : int ):
+
+ self._pixel_dupes_preference = value
+
+
+
+HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_POTENTIAL_DUPLICATES_SEARCH_CONTEXT ] = PotentialDuplicatesSearchContext
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 9052b5382..66eabce7d 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -2467,7 +2467,7 @@ def work_callable( args ):
def publish_callable( result ):
- frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'review your fate' )
+ frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'review your fate', frame_key = 'mr_bones' )
panel = ClientGUIScrolledPanelsReview.ReviewHowBonedAmI( frame )
diff --git a/hydrus/client/gui/ClientGUIDialogsQuick.py b/hydrus/client/gui/ClientGUIDialogsQuick.py
index 1c9087615..dcc40531c 100644
--- a/hydrus/client/gui/ClientGUIDialogsQuick.py
+++ b/hydrus/client/gui/ClientGUIDialogsQuick.py
@@ -142,6 +142,8 @@ def OpenDocumentation( win: QW.QWidget, documentation_path: str ):
else:
+ HydrusData.Print( f'Was asked to open "{documentation_path}", which appeared to be "{local_path}" locally, but it did not seem to exist!' )
+
message = 'You do not have a local help! Are you running from source? Would you like to open the online help or see a guide on how to build your own?'
yes_tuples = []
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index 2605a3c97..38eb92e29 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -590,6 +590,7 @@ def GetValue( self ) -> ClientNetworkingGUG.NestedGalleryURLGenerator:
return ngug
+
class EditGUGsPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent: QW.QWidget, gugs ):
diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py
index 7365f1b6d..62bd7ccd5 100644
--- a/hydrus/client/gui/ClientGUIStringPanels.py
+++ b/hydrus/client/gui/ClientGUIStringPanels.py
@@ -314,6 +314,7 @@ def __init__( self, parent: QW.QWidget, string_converter: ClientStrings.StringCo
model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, self._ConvertConversionToDisplayTuple, self._ConvertConversionToSortTuple )
+ # TODO: Yo, if I converted the conversion steps to their own serialisable object, this guy could have import/export/duplicate buttons nice and easy
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 )
@@ -2168,6 +2169,7 @@ def __init__( self, parent, string_processor: ClientStrings.StringProcessor, tes
self._controls_panel = ClientGUICommon.StaticBox( self, 'processing steps' )
self._processing_steps = ClientGUIListBoxes.QueueListBox( self, 8, self._ConvertDataToListBoxString, add_callable = self._Add, edit_callable = self._Edit )
+ self._processing_steps.AddImportExportButtons( ( ClientStrings.StringProcessingStep, ) )
#
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index 547826820..f4fb1ab7f 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -18,6 +18,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsManage
@@ -316,6 +317,9 @@ class Canvas( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
CANVAS_TYPE = CC.CANVAS_MEDIA_VIEWER
+ mediaCleared = QC.Signal()
+ mediaChanged = QC.Signal( ClientMedia.MediaSingleton )
+
def __init__( self, parent, location_context: ClientLocation.LocationContext ):
self._qss_colours = {
@@ -558,7 +562,7 @@ def _ManageURLs( self ):
title = 'manage known urls'
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg:
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, title, frame_key = 'manage_urls_dialog' ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditURLsPanel( dlg, ( self._current_media, ) )
@@ -1295,6 +1299,15 @@ def SetMedia( self, media: typing.Optional[ ClientMedia.MediaSingleton ] ):
+ if self._current_media is None:
+
+ self.mediaCleared.emit()
+
+ elif isinstance( self._current_media, ClientMedia.MediaSingleton ): # just to be safe on the delicate type def requirements here
+
+ self.mediaChanged.emit( self._current_media )
+
+
CG.client_controller.pub( 'canvas_new_display_media', self._canvas_key, self._current_media )
CG.client_controller.pub( 'canvas_new_index_string', self._canvas_key, self._GetIndexString() )
@@ -2113,6 +2126,9 @@ def __init__( self, parent, location_context ):
top_hover = self._GenerateHoverTopFrame()
+ self.mediaChanged.connect( top_hover.SetMedia )
+ self.mediaCleared.connect( top_hover.ClearMedia )
+
top_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
self._media_container.zoomChanged.connect( top_hover.SetCurrentZoom )
@@ -2123,6 +2139,9 @@ def __init__( self, parent, location_context ):
tags_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameTags( self, self, top_hover, self._canvas_key, self._location_context )
+ self.mediaChanged.connect( tags_hover.SetMedia )
+ self.mediaCleared.connect( tags_hover.ClearMedia )
+
tags_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
self._hovers.append( tags_hover )
@@ -2131,6 +2150,9 @@ def __init__( self, parent, location_context ):
top_right_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameTopRight( self, self, top_hover, self._canvas_key )
+ self.mediaChanged.connect( top_right_hover.SetMedia )
+ self.mediaCleared.connect( top_right_hover.ClearMedia )
+
top_right_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
self._hovers.append( top_right_hover )
@@ -2139,6 +2161,9 @@ def __init__( self, parent, location_context ):
self._right_notes_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameRightNotes( self, self, top_right_hover, self._canvas_key )
+ self.mediaChanged.connect( self._right_notes_hover.SetMedia )
+ self.mediaCleared.connect( self._right_notes_hover.ClearMedia )
+
self._right_notes_hover.sendApplicationCommand.connect( self.ProcessApplicationCommand )
self._hovers.append( self._right_notes_hover )
@@ -2408,14 +2433,19 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
showPairInPage = QC.Signal( list )
- def __init__( self, parent, file_search_context_1: ClientSearchFileSearchContext.FileSearchContext, file_search_context_2: ClientSearchFileSearchContext.FileSearchContext, dupe_search_type, pixel_dupes_preference, max_hamming_distance ):
+ def __init__( self, parent, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext ):
+
+ self._potential_duplicates_search_context = potential_duplicates_search_context
- location_context = file_search_context_1.GetLocationContext()
+ location_context = self._potential_duplicates_search_context.GetFileSearchContext1().GetLocationContext()
super().__init__( parent, location_context )
self._duplicates_right_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameRightDuplicates( self, self, self._canvas_key )
+ self.mediaChanged.connect( self._duplicates_right_hover.SetMedia )
+ self.mediaCleared.connect( self._duplicates_right_hover.ClearMedia )
+
self._right_notes_hover.AddHoverThatCanBeOnTop( self._duplicates_right_hover )
self._duplicates_right_hover.showPairInPage.connect( self._ShowPairInPage )
@@ -2429,12 +2459,6 @@ def __init__( self, parent, file_search_context_1: ClientSearchFileSearchContext
self._my_shortcuts_handler.AddWindowToFilter( self._duplicates_right_hover )
- self._file_search_context_1 = file_search_context_1
- self._file_search_context_2 = file_search_context_2
- self._dupe_search_type = dupe_search_type
- self._pixel_dupes_preference = pixel_dupes_preference
- self._max_hamming_distance = max_hamming_distance
-
self._maintain_pan_and_zoom = True
self._currently_fetching_pairs = False
@@ -2794,7 +2818,7 @@ def _LoadNextBatchOfPairs( self ):
self._currently_fetching_pairs = True
- CG.client_controller.CallToThread( self.THREADFetchPairs, self._file_search_context_1, self._file_search_context_2, self._dupe_search_type, self._pixel_dupes_preference, self._max_hamming_distance )
+ CG.client_controller.CallToThread( self.THREADFetchPairs, self._potential_duplicates_search_context )
self.update()
@@ -3434,7 +3458,7 @@ def Undelete( self, canvas_key ):
- def THREADFetchPairs( self, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ):
+ def THREADFetchPairs( self, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext ):
def qt_close():
@@ -3463,7 +3487,7 @@ def qt_continue( unprocessed_pairs ):
self._ShowCurrentPair()
- result = CG.client_controller.Read( 'duplicate_pairs_for_filtering', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ result = CG.client_controller.Read( 'duplicate_pairs_for_filtering', potential_duplicates_search_context )
if len( result ) == 0:
@@ -3475,6 +3499,7 @@ def qt_continue( unprocessed_pairs ):
+
class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
exitFocusMedia = QC.Signal( ClientMedia.Media )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
index b480a21a8..fe98168e0 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
@@ -33,6 +33,7 @@
from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIMenuButton
+from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientRatings
@@ -50,7 +51,6 @@ def __init__( self, parent, service_key, canvas_key ):
self._hashes = set()
CG.client_controller.sub( self, 'ProcessContentUpdatePackage', 'content_updates_gui' )
- CG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
def _Draw( self, painter ):
@@ -77,6 +77,11 @@ def _SetRating( self, rating ):
+ def ClearMedia( self ):
+
+ self.SetMedia( None )
+
+
def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ):
if self._current_media is not None:
@@ -107,30 +112,27 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
+ self._current_media = media
+
+ if self._current_media is None:
- self._current_media = media
+ self._hashes = set()
- if self._current_media is None:
-
- self._hashes = set()
-
- self._rating_state = None
- self._rating = None
-
- else:
-
- self._hashes = self._current_media.GetHashes()
-
- ( self._rating_state, self._rating ) = ClientRatings.GetIncDecStateFromMedia( ( self._current_media, ), self._service_key )
-
+ self._rating_state = None
+ self._rating = None
- self.update()
+ else:
- self._UpdateTooltip()
+ self._hashes = self._current_media.GetHashes()
+ ( self._rating_state, self._rating ) = ClientRatings.GetIncDecStateFromMedia( ( self._current_media, ), self._service_key )
+
+
+ self.update()
+
+ self._UpdateTooltip()
@@ -145,7 +147,6 @@ def __init__( self, parent, service_key, canvas_key ):
self._hashes = set()
CG.client_controller.sub( self, 'ProcessContentUpdatePackage', 'content_updates_gui' )
- CG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
def _Draw( self, painter ):
@@ -160,6 +161,25 @@ def _Draw( self, painter ):
+ def _SetRatingFromCurrentMedia( self ):
+
+ if self._current_media is None:
+
+ rating_state = ClientRatings.NULL
+
+ else:
+
+ rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), self._service_key )
+
+
+ self._SetRating( rating_state )
+
+
+ def ClearMedia( self ):
+
+ self.SetMedia( None )
+
+
def EventLeftDown( self, event ):
if self._current_media is not None:
@@ -214,41 +234,25 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def _SetRatingFromCurrentMedia( self ):
+ def SetMedia( self, media ):
+
+ self._current_media = media
if self._current_media is None:
- rating_state = ClientRatings.NULL
+ self._hashes = set()
else:
- rating_state = ClientRatings.GetLikeStateFromMedia( ( self._current_media, ), self._service_key )
+ self._hashes = self._current_media.GetHashes()
- self._SetRating( rating_state )
+ self._SetRatingFromCurrentMedia()
-
- def SetDisplayMedia( self, canvas_key, media ):
-
- if canvas_key == self._canvas_key:
-
- self._current_media = media
-
- if self._current_media is None:
-
- self._hashes = set()
-
- else:
-
- self._hashes = self._current_media.GetHashes()
-
-
- self._SetRatingFromCurrentMedia()
-
- self.update()
-
+ self.update()
+
class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ):
def __init__( self, parent, service_key, canvas_key ):
@@ -263,7 +267,6 @@ def __init__( self, parent, service_key, canvas_key ):
self._hashes = set()
CG.client_controller.sub( self, 'ProcessContentUpdatePackage', 'content_updates_gui' )
- CG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
def _ClearRating( self ):
@@ -306,6 +309,11 @@ def _SetRating( self, rating ):
+ def ClearMedia( self ):
+
+ self.SetMedia( None )
+
+
def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ):
if self._current_media is not None:
@@ -336,26 +344,23 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
-
- self._current_media = media
+ self._current_media = media
+
+ if self._current_media is None:
- if self._current_media is None:
-
- self._hashes = set()
-
- else:
-
- self._hashes = self._current_media.GetHashes()
-
+ self._hashes = set()
- self.update()
+ else:
- self._UpdateTooltip()
+ self._hashes = self._current_media.GetHashes()
+ self.update()
+
+ self._UpdateTooltip()
+
# Note that I go setFocusPolicy( QC.Qt.TabFocus ) on all the icon buttons in the hover windows
@@ -370,6 +375,9 @@ class CanvasHoverFrame( QW.QFrame ):
sendApplicationCommand = QC.Signal( CAC.ApplicationCommand )
+ mediaCleared = QC.Signal()
+ mediaChanged = QC.Signal( ClientMedia.MediaSingleton )
+
def __init__( self, parent: QW.QWidget, my_canvas, canvas_key ):
# TODO: Clean up old references to window stuff, decide on lower/hide/show/raise options
@@ -404,8 +412,6 @@ def __init__( self, parent: QW.QWidget, my_canvas, canvas_key ):
parent.installEventFilter( self )
- CG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
-
def _GetIdealSizeAndPosition( self ):
@@ -522,6 +528,11 @@ def AddHoverThatCanBeOnTop( self, win: "CanvasHoverFrame" ):
self._hover_panels_that_can_be_on_top_of_us.append( win )
+ def ClearMedia( self ):
+
+ self.SetMedia( None )
+
+
def DoRegularHideShow( self ):
if not self._position_initialised:
@@ -655,14 +666,21 @@ def PositionIsInitialised( self ):
return self._position_initialised
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
+ self._current_media = media
+
+ if self._current_media is None:
+
+ self.mediaCleared.emit()
- self._current_media = media
+ elif isinstance( self._current_media, ClientMedia.MediaSingleton ): # just to be safe on the delicate type def requirements here
+
+ self.mediaChanged.emit( self._current_media )
+
class CanvasHoverFrameTop( CanvasHoverFrame ):
def __init__( self, parent, my_canvas, canvas_key ):
@@ -1120,21 +1138,18 @@ def SetCurrentZoom( self, zoom: float ):
self._zoom_text.setText( label )
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
-
- CanvasHoverFrame.SetDisplayMedia( self, canvas_key, media )
-
- self._ResetText()
-
- self._ResetButtons()
-
- # minimumsize is not immediately updated without this
- self.layout().activate()
-
- self._SizeAndPosition( force = True )
-
+ super().SetMedia( media )
+
+ self._ResetText()
+
+ self._ResetButtons()
+
+ # minimumsize is not immediately updated without this
+ self.layout().activate()
+
+ self._SizeAndPosition( force = True )
def SetIndexString( self, canvas_key, text ):
@@ -1286,6 +1301,9 @@ def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_ke
control = RatingLikeCanvas( self, service_key, canvas_key )
+ self.mediaChanged.connect( control.SetMedia )
+ self.mediaCleared.connect( control.ClearMedia )
+
QP.AddToLayout( like_hbox, control, CC.FLAGS_NONE )
@@ -1301,6 +1319,9 @@ def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_ke
control = RatingNumericalCanvas( self, service_key, canvas_key )
+ self.mediaChanged.connect( control.SetMedia )
+ self.mediaCleared.connect( control.ClearMedia )
+
QP.AddToLayout( vbox, control, CC.FLAGS_NONE )
vbox.setAlignment( control, QC.Qt.AlignRight )
@@ -1323,6 +1344,9 @@ def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_ke
control = RatingIncDecCanvas( self, service_key, canvas_key )
+ self.mediaChanged.connect( control.SetMedia )
+ self.mediaCleared.connect( control.ClearMedia )
+
QP.AddToLayout( incdec_hbox, control, CC.FLAGS_NONE )
@@ -1506,19 +1530,16 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
-
- CanvasHoverFrame.SetDisplayMedia( self, canvas_key, media )
-
- self._ResetData()
-
- # size is not immediately updated without this
- self.layout().activate()
-
- self._SizeAndPosition( force = True )
-
+ super().SetMedia( media )
+
+ self._ResetData()
+
+ # size is not immediately updated without this
+ self.layout().activate()
+
+ self._SizeAndPosition( force = True )
class NotePanel( QW.QWidget ):
@@ -1818,25 +1839,22 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
+ super().SetMedia( media )
+
+ if self._is_currently_up:
- CanvasHoverFrame.SetDisplayMedia( self, canvas_key, media )
+ # magical refresh that makes the labels look correct and not be hidden???
+ self._LowerHover()
+ self._ResetNotes()
+ self._RaiseHover()
- if self._is_currently_up:
-
- # magical refresh that makes the labels look correct and not be hidden???
- self._LowerHover()
- self._ResetNotes()
- self._RaiseHover()
-
- else:
-
- self._ResetNotes()
-
- self._position_initialised = False
-
+ else:
+
+ self._ResetNotes()
+
+ self._position_initialised = False
@@ -2238,14 +2256,11 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
- def SetDisplayMedia( self, canvas_key, media ):
+ def SetMedia( self, media ):
- if canvas_key == self._canvas_key:
-
- CanvasHoverFrame.SetDisplayMedia( self, canvas_key, media )
-
- self._ResetTags()
-
+ super().SetMedia( media )
+
+ self._ResetTags()
def wheelEvent( self, event ):
diff --git a/hydrus/client/gui/duplicates/ClientGUIDuplicatesAutoResolution.py b/hydrus/client/gui/duplicates/ClientGUIDuplicatesAutoResolution.py
index d46852266..20203dff8 100644
--- a/hydrus/client/gui/duplicates/ClientGUIDuplicatesAutoResolution.py
+++ b/hydrus/client/gui/duplicates/ClientGUIDuplicatesAutoResolution.py
@@ -12,6 +12,7 @@
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.duplicates import ClientGUIPotentialDuplicatesSearchContext
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.panels import ClientGUIScrolledPanels
@@ -139,7 +140,7 @@ def _Edit( self ):
return
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit export folder' ) as dlg:
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit duplicates auto-resolution rule' ) as dlg:
panel = EditDuplicatesAutoResolutionRulePanel( dlg, duplicates_auto_resolution_rule )
@@ -193,24 +194,41 @@ def __init__( self, parent, duplicates_auto_resolution_rule: ClientDuplicatesAut
self._name = QW.QLineEdit( self._rule_panel )
- # paused
- # search gubbins
+ self._paused = QW.QCheckBox( self._rule_panel )
+
+ self._main_notebook = ClientGUICommon.BetterNotebook( self )
+
+ potential_duplicates_search_context = duplicates_auto_resolution_rule.GetPotentialDuplicatesSearchContext()
+
+ self._potential_duplicates_search_context = ClientGUIPotentialDuplicatesSearchContext.EditPotentialDuplicatesSearchContextPanel( self._main_notebook, potential_duplicates_search_context, put_searches_side_by_side = True )
+
+ self._potential_duplicates_search_context.setEnabled( False )
+
# comparator gubbins
# some way to test-run searches and see pair counts, and, eventually, a way to preview some pairs and the auto-choices we'd see
#
self._name.setText( self._duplicates_auto_resolution_rule.GetName() )
+ self._paused.setChecked( self._duplicates_auto_resolution_rule.IsPaused() )
+
+ #
+
+ self._main_notebook.addTab( self._potential_duplicates_search_context, 'search' )
+ self._main_notebook.addTab( QW.QWidget( self._main_notebook ), 'test' )
+ self._main_notebook.addTab( QW.QWidget( self._main_notebook ), 'action' )
#
rows = []
rows.append( ( 'name: ', self._name ) )
+ rows.append( ( 'paused: ', self._paused ) )
gridbox = ClientGUICommon.WrapInGrid( self._rule_panel, rows )
self._rule_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ self._rule_panel.Add( self._main_notebook, CC.FLAGS_EXPAND_PERPENDICULAR )
#
@@ -232,9 +250,15 @@ def GetValue( self ):
duplicates_auto_resolution_rule = ClientDuplicatesAutoResolution.DuplicatesAutoResolutionRule( name )
- # paused and search gubbins, everything else
+ duplicates_auto_resolution_rule.SetPaused( self._paused.isChecked() )
+
+ duplicates_auto_resolution_rule.SetPotentialDuplicatesSearchContext( self._potential_duplicates_search_context.GetValue() )
+
+ # everything else
+
+ duplicates_auto_resolution_rule.SetId( self._duplicates_auto_resolution_rule.GetId() )
- # TODO: transfer any cached search data, including what we may have re-fetched in this panel's work, to the new folder
+ # TODO: transfer any cached search data, including what we may have re-fetched in this panel's work, to the new rule
return duplicates_auto_resolution_rule
diff --git a/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py b/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py
new file mode 100644
index 000000000..d21f02602
--- /dev/null
+++ b/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py
@@ -0,0 +1,204 @@
+from qtpy import QtCore as QC
+from qtpy import QtWidgets as QW
+
+from hydrus.core import HydrusData
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
+from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.panels import ClientGUIScrolledPanels
+from hydrus.client.gui.search import ClientGUIACDropdown
+from hydrus.client.gui.widgets import ClientGUICommon
+from hydrus.client.search import ClientSearchFileSearchContext
+
+class EditPotentialDuplicatesSearchContextPanel( QW.QWidget ):
+
+ valueChanged = QC.Signal()
+
+ def __init__( self, parent: QW.QWidget, potential_duplicates_search_context: ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext, synchronised = True, page_key = None, put_searches_side_by_side = False ):
+
+ super().__init__( parent )
+
+ #
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+
+ file_search_context_1.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
+ file_search_context_2.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
+
+ if page_key is None:
+
+ page_key = HydrusData.GenerateKey()
+
+
+ self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_1, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
+ self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_2, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
+
+ self._dupe_search_type = ClientGUICommon.BetterChoice( self )
+
+ self._dupe_search_type.addItem( 'at least one file matches the search', ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH )
+ self._dupe_search_type.addItem( 'both files match the search', ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH )
+ self._dupe_search_type.addItem( 'both files match different searches', ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
+
+ self._pixel_dupes_preference = ClientGUICommon.BetterChoice( self )
+
+ for p in ( ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_REQUIRED, ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED, ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_EXCLUDED ):
+
+ self._pixel_dupes_preference.addItem( ClientDuplicates.similar_files_pixel_dupes_string_lookup[ p ], p )
+
+
+ self._max_hamming_distance = ClientGUICommon.BetterSpinBox( self, min = 0, max = 64 )
+ self._max_hamming_distance.setSingleStep( 2 )
+
+ #
+
+ self._dupe_search_type.SetValue( potential_duplicates_search_context.GetDupeSearchType() )
+ self._pixel_dupes_preference.SetValue( potential_duplicates_search_context.GetPixelDupesPreference() )
+ self._max_hamming_distance.setValue( potential_duplicates_search_context.GetMaxHammingDistance() )
+
+ #
+
+ self._UpdateControls()
+
+ #
+
+ vbox = QP.VBoxLayout()
+
+ QP.AddToLayout( vbox, self._dupe_search_type, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ if put_searches_side_by_side:
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._tag_autocomplete_1, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( hbox, self._tag_autocomplete_2, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ else:
+
+ QP.AddToLayout( vbox, self._tag_autocomplete_1, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._tag_autocomplete_2, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+
+ rows = []
+
+ rows.append( ( 'maximum search distance of pair: ', self._max_hamming_distance ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( self, rows )
+
+ QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._pixel_dupes_preference, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ self.setLayout( vbox )
+
+ self._tag_autocomplete_1.searchChanged.connect( self.Search1Changed )
+ self._tag_autocomplete_2.searchChanged.connect( self.Search2Changed )
+
+ self._dupe_search_type.currentIndexChanged.connect( self.DupeSearchTypeChanged )
+ self._pixel_dupes_preference.currentIndexChanged.connect( self.PixelDupesPreferenceChanged )
+ self._max_hamming_distance.valueChanged.connect( self.MaxHammingDistanceChanged )
+
+
+ def _UpdateControls( self ):
+
+ dupe_search_type = self._dupe_search_type.GetValue()
+
+ self._tag_autocomplete_2.setVisible( dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
+
+ pixel_dupes_preference = self._pixel_dupes_preference.GetValue()
+
+ self._max_hamming_distance.setEnabled( pixel_dupes_preference != ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_REQUIRED )
+
+
+ def DupeSearchTypeChanged( self ):
+
+ self._UpdateControls()
+
+ self.valueChanged.emit()
+
+
+ def GetValue( self, optimise_for_search = True ) -> ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext:
+
+ file_search_context_1 = self._tag_autocomplete_1.GetFileSearchContext()
+ file_search_context_2 = self._tag_autocomplete_2.GetFileSearchContext()
+
+ dupe_search_type = self._dupe_search_type.GetValue()
+
+ pixel_dupes_preference = self._pixel_dupes_preference.GetValue()
+
+ max_hamming_distance = self._max_hamming_distance.value()
+
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ return potential_duplicates_search_context
+
+
+ def IsSynchronised( self ) -> bool:
+
+ return self._tag_autocomplete_1.IsSynchronised()
+
+
+ def MaxHammingDistanceChanged( self ):
+
+ self.valueChanged.emit()
+
+
+ def PageHidden( self ):
+
+ self._tag_autocomplete_1.SetForceDropdownHide( True )
+ self._tag_autocomplete_2.SetForceDropdownHide( True )
+
+
+ def PageShown( self ):
+
+ self._tag_autocomplete_1.SetForceDropdownHide( False )
+ self._tag_autocomplete_2.SetForceDropdownHide( False )
+
+
+ def PixelDupesPreferenceChanged( self ):
+
+ self._UpdateControls()
+
+ self.valueChanged.emit()
+
+
+ def REPEATINGPageUpdate( self ):
+
+ self._tag_autocomplete_1.REPEATINGPageUpdate()
+ self._tag_autocomplete_2.REPEATINGPageUpdate()
+
+
+ def Search1Changed( self ):
+
+ self._tag_autocomplete_2.blockSignals( True )
+
+ self._tag_autocomplete_2.SetLocationContext( self._tag_autocomplete_1.GetLocationContext() )
+ self._tag_autocomplete_2.SetSynchronised( self._tag_autocomplete_1.IsSynchronised() )
+
+ self._tag_autocomplete_2.blockSignals( False )
+
+ self.valueChanged.emit()
+
+
+ def Search2Changed( self ):
+
+ self._tag_autocomplete_1.blockSignals( True )
+
+ self._tag_autocomplete_1.SetLocationContext( self._tag_autocomplete_2.GetLocationContext() )
+ self._tag_autocomplete_1.SetSynchronised( self._tag_autocomplete_2.IsSynchronised() )
+
+ self._tag_autocomplete_1.blockSignals( False )
+
+ self.valueChanged.emit()
+
+
diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py
index a06788a11..a16a12643 100644
--- a/hydrus/client/gui/lists/ClientGUIListBoxes.py
+++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py
@@ -850,6 +850,9 @@ def _SetNonDupeName( self, obj ):
HydrusSerialisable.SetNonDupeName( obj, disallowed_names )
+
+# TODO: We must be able to unify this guy with AddEditDeleteListBox mate. This is basically just that guy but with (duplicates allowed?) and different sort
+# failing that, we must be able to merge a bunch of this to a base superclass
class QueueListBox( QW.QWidget ):
listBoxChanged = QC.Signal()
@@ -862,6 +865,8 @@ def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_calla
super().__init__( parent )
+ self._permitted_object_types = tuple()
+
self._listbox = BetterQListWidget( self )
self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection )
@@ -874,6 +879,8 @@ def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_calla
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
+ self._enabled_only_on_selection_buttons = []
+
if self._add_callable is None:
self._add_button.hide()
@@ -899,13 +906,13 @@ def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_calla
QP.AddToLayout( hbox, self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, buttons_vbox, CC.FLAGS_CENTER_PERPENDICULAR )
- buttons_hbox = QP.HBoxLayout()
+ self._buttons_hbox = QP.HBoxLayout()
- QP.AddToLayout( buttons_hbox, self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( buttons_hbox, self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( self._buttons_hbox, self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( self._buttons_hbox, self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( vbox, buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, self._buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
@@ -955,6 +962,11 @@ def _AddData( self, data ):
self._listbox.Append( pretty_data, data )
+ def _CheckImportObjectCustom( self, obj ):
+
+ pass
+
+
def _Delete( self ):
num_selected = self._listbox.GetNumSelected()
@@ -983,6 +995,18 @@ def _Down( self ):
self.listBoxChanged.emit()
+ def _Duplicate( self ):
+
+ dupe_data = self._GetExportObject()
+
+ if dupe_data is not None:
+
+ dupe_data = dupe_data.Duplicate()
+
+ self._ImportObject( dupe_data )
+
+
+
def _Edit( self ):
for list_widget_item in self._listbox.selectedItems():
@@ -1007,30 +1031,262 @@ def _Edit( self ):
self.listBoxChanged.emit()
- def _Up( self ):
+ def _ExportToClipboard( self ):
- self._listbox.MoveSelected( -1 )
+ export_object = self._GetExportObject()
- self.listBoxChanged.emit()
+ if export_object is not None:
+
+ json = export_object.DumpToString()
+
+ CG.client_controller.pub( 'clipboard', 'text', json )
+
- def _UpdateButtons( self ):
+ def _ExportToPNG( self ):
- if self._listbox.GetNumSelected() == 0:
+ export_object = self._GetExportObject()
+
+ if export_object is not None:
- self._up_button.setEnabled( False )
- self._delete_button.setEnabled( False )
- self._down_button.setEnabled( False )
+ from hydrus.client.gui import ClientGUITopLevelWindowsPanels
+ from hydrus.client.gui import ClientGUISerialisable
- self._edit_button.setEnabled( False )
+ with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
+
+ panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
+
+ dlg.SetPanel( panel )
+
+ dlg.exec()
+
+
+
+
+ def _ExportToPNGs( self ):
+
+ export_object = self._GetExportObject()
+
+ if export_object is None:
+
+ return
+
+
+ if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
+
+ self._ExportToPNG()
+
+ return
+
+
+ from hydrus.client.gui import ClientGUITopLevelWindowsPanels
+ from hydrus.client.gui import ClientGUISerialisable
+
+ with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to pngs' ) as dlg:
+
+ panel = ClientGUISerialisable.PNGsExportPanel( dlg, export_object )
+
+ dlg.SetPanel( panel )
+
+ dlg.exec()
+
+
+
+
+ def _GetExportObject( self ):
+
+ to_export = HydrusSerialisable.SerialisableList()
+
+ for obj in self.GetData( only_selected = True ):
+
+ to_export.append( obj )
+
+
+ if len( to_export ) == 0:
+
+ return None
+
+ elif len( to_export ) == 1:
+
+ return to_export[0]
else:
- self._up_button.setEnabled( True )
- self._delete_button.setEnabled( True )
- self._down_button.setEnabled( True )
+ return to_export
- self._edit_button.setEnabled( True )
+
+
+ def _ImportFromClipboard( self ):
+
+ try:
+
+ raw_text = CG.client_controller.GetClipboardText()
+
+ except HydrusExceptions.DataMissing as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem pasting!', str(e) )
+
+ return
+
+
+ try:
+
+ obj = HydrusSerialisable.CreateFromString( raw_text )
+
+ self._ImportObject( obj )
+
+ except Exception as e:
+
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e )
+
+
+
+ def _ImportFromPNG( self ):
+
+ with QP.FileDialog( self, 'select the png or pngs with the encoded data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'PNG (*.png)|*.png' ) as dlg:
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ for path in dlg.GetPaths():
+
+ try:
+
+ payload = ClientSerialisable.LoadFromPNG( path )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem importing!', str(e) )
+
+ return
+
+
+ try:
+
+ obj = HydrusSerialisable.CreateFromNetworkBytes( payload )
+
+ self._ImportObject( obj )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem importing!', 'I could not understand what was encoded in the png!' )
+
+ return
+
+
+
+
+
+
+ def _ImportObject( self, obj, can_present_messages = True ):
+
+ num_added = 0
+ bad_object_type_names = set()
+ other_bad_errors = set()
+
+ if isinstance( obj, HydrusSerialisable.SerialisableList ):
+
+ for sub_obj in obj:
+
+ ( sub_num_added, sub_bad_object_type_names, sub_other_bad_errors ) = self._ImportObject( sub_obj, can_present_messages = False )
+
+ num_added += sub_num_added
+ bad_object_type_names.update( sub_bad_object_type_names )
+ other_bad_errors.update( sub_other_bad_errors )
+
+
+ else:
+
+ if isinstance( obj, self._permitted_object_types ):
+
+ import_ok = True
+
+ try:
+
+ self._CheckImportObjectCustom( obj )
+
+ except HydrusExceptions.VetoException as e:
+
+ import_ok = False
+
+ other_bad_errors.add( str( e ) )
+
+
+ if import_ok:
+
+ self._AddData( obj )
+
+ num_added += 1
+
+
+ else:
+
+ bad_object_type_names.add( HydrusData.GetTypeName( type( obj ) ) )
+
+
+
+ if can_present_messages:
+
+ if len( bad_object_type_names ) > 0:
+
+ message = 'The imported objects included these types:'
+ message += '\n' * 2
+ message += '\n'.join( bad_object_type_names )
+ message += '\n' * 2
+ message += 'Whereas this control only allows:'
+ message += '\n' * 2
+ message += '\n'.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
+
+ ClientGUIDialogsMessage.ShowWarning( self, message )
+
+
+ if len( other_bad_errors ) > 0:
+
+ message = 'The imported objects were wrong for this control:'
+ message += '\n' * 2
+ message += '\n'.join( other_bad_errors )
+
+ ClientGUIDialogsMessage.ShowWarning( self, message )
+
+
+ if num_added > 0:
+
+ message = '{} objects added!'.format( HydrusNumbers.ToHumanInt( num_added ) )
+
+ ClientGUIDialogsMessage.ShowInformation( self, message )
+
+
+
+ self.listBoxChanged.emit()
+
+ return ( num_added, bad_object_type_names, other_bad_errors )
+
+
+ def _Up( self ):
+
+ self._listbox.MoveSelected( -1 )
+
+ self.listBoxChanged.emit()
+
+
+ def _UpdateButtons( self ):
+
+ we_have_selection = self._listbox.GetNumSelected() > 0
+
+ self._up_button.setEnabled( we_have_selection )
+ self._delete_button.setEnabled( we_have_selection )
+ self._down_button.setEnabled( we_have_selection )
+
+ self._edit_button.setEnabled( we_have_selection )
+
+ for button in self._enabled_only_on_selection_buttons:
+
+ button.setEnabled( we_have_selection )
@@ -1044,6 +1300,41 @@ def AddDatas( self, datas ):
self.listBoxChanged.emit()
+ def AddImportExportButtons( self, permitted_object_types ):
+
+ self._permitted_object_types = tuple( permitted_object_types )
+
+ export_menu_items = []
+
+ export_menu_items.append( ( 'normal', 'to clipboard', 'Serialise the selected data and put it on your clipboard.', self._ExportToClipboard ) )
+ export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPNG ) )
+
+ all_objs_are_named = False not in ( issubclass( o, HydrusSerialisable.SerialisableBaseNamed ) for o in self._permitted_object_types )
+
+ if all_objs_are_named:
+
+ export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPNGs ) )
+
+
+ import_menu_items = []
+
+ import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
+ import_menu_items.append( ( 'normal', 'from pngs', 'Load a data from an encoded png.', self._ImportFromPNG ) )
+
+ button = ClientGUIMenuButton.MenuButton( self, 'export', export_menu_items )
+ QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ self._enabled_only_on_selection_buttons.append( button )
+
+ button = ClientGUIMenuButton.MenuButton( self, 'import', import_menu_items )
+ QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ button = ClientGUICommon.BetterButton( self, 'duplicate', self._Duplicate )
+ QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ self._enabled_only_on_selection_buttons.append( button )
+
+ self._UpdateButtons()
+
+
def Clear( self ):
self._listbox.clear()
@@ -1069,6 +1360,7 @@ def Pop( self ):
return self._listbox.PopData( 0 )
+
class ListBox( QW.QScrollArea ):
listBoxChanged = QC.Signal()
@@ -2526,7 +2818,7 @@ def __init__( self, parent, *args, tag_display_type: int = ClientTags.TAG_DISPLA
CG.client_controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
- def _GetCopyableTagStrings( self, command ):
+ def _GetCopyableTagStrings( self, command, include_parents = False ):
only_selected = command in ( COPY_SELECTED_TAGS, COPY_SELECTED_TAGS_WITH_COUNTS, COPY_SELECTED_SUBTAGS, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
with_counts = command in ( COPY_ALL_TAGS_WITH_COUNTS, COPY_ALL_SUBTAGS_WITH_COUNTS, COPY_SELECTED_TAGS_WITH_COUNTS, COPY_SELECTED_SUBTAGS_WITH_COUNTS )
@@ -2550,23 +2842,23 @@ def _GetCopyableTagStrings( self, command ):
terms = self._ordered_terms
- copyable_tag_strings = HydrusLists.MassExtend( [ term.GetCopyableTexts( with_counts = with_counts ) for term in terms ] )
+ copyable_tag_strings = HydrusLists.MassExtend( [ term.GetCopyableTexts( with_counts = with_counts, include_parents = include_parents ) for term in terms ] )
if only_subtags:
copyable_tag_strings = [ HydrusTags.SplitTag( tag_string )[1] for tag_string in copyable_tag_strings ]
- if not with_counts:
-
- copyable_tag_strings = HydrusData.DedupeList( copyable_tag_strings )
-
-
if '' in copyable_tag_strings:
copyable_tag_strings.remove( '' )
+ if not with_counts:
+
+ copyable_tag_strings = HydrusData.DedupeList( copyable_tag_strings )
+
+
return copyable_tag_strings
@@ -2688,9 +2980,9 @@ def _NewSearchPages( self, pages_of_predicates ):
- def _ProcessMenuCopyEvent( self, command ):
+ def _ProcessMenuCopyEvent( self, command, include_parents = False ):
- texts = self._GetCopyableTagStrings( command )
+ texts = self._GetCopyableTagStrings( command, include_parents = include_parents )
if len( texts ) > 0:
@@ -2838,7 +3130,7 @@ def eventFilter( self, watched, event ):
if shift_down and or_predicate is not None:
- predicates = (or_predicate,)
+ predicates = ( or_predicate, )
self._NewSearchPages( [ predicates ] )
@@ -2942,6 +3234,8 @@ def ShowMenu( self ):
selected_copyable_tag_strings = self._GetCopyableTagStrings( COPY_SELECTED_TAGS )
selected_copyable_subtag_strings = self._GetCopyableTagStrings( COPY_SELECTED_SUBTAGS )
+ selected_copyable_tag_strings_with_parents = self._GetCopyableTagStrings( COPY_SELECTED_TAGS, include_parents = True )
+
if len( selected_copyable_tag_strings ) > 0:
if len( selected_copyable_tag_strings ) == 1:
@@ -2989,6 +3283,26 @@ def ShowMenu( self ):
+ num_parents = len( selected_copyable_tag_strings_with_parents ) - len( selected_copyable_tag_strings )
+
+ if num_parents > 0:
+
+ ClientGUIMenus.AppendSeparator( copy_menu )
+
+ if len( selected_copyable_tag_strings ) == 1:
+
+ ( selection_string, ) = selected_copyable_tag_strings
+
+ else:
+
+ selection_string = '{} selected'.format( HydrusNumbers.ToHumanInt( len( selected_copyable_tag_strings ) ) )
+
+
+ selection_string += f' and {HydrusNumbers.ToHumanInt( num_parents )} parents'
+
+ ClientGUIMenus.AppendMenuItem( copy_menu, selection_string, 'Copy the selected tags and their (deduplicated) parents to your clipboard.', self._ProcessMenuCopyEvent, COPY_SELECTED_TAGS, include_parents = True )
+
+
copy_all_is_appropriate = len( self._ordered_terms ) > len( self._selected_terms )
@@ -4138,6 +4452,7 @@ def SetTags( self, tags ):
self._DataHasChanged()
+
class ListBoxTagsStringsAddRemove( ListBoxTagsStrings ):
tagsAdded = QC.Signal()
diff --git a/hydrus/client/gui/lists/ClientGUIListBoxesData.py b/hydrus/client/gui/lists/ClientGUIListBoxesData.py
index 60f316aa6..d00f341d1 100644
--- a/hydrus/client/gui/lists/ClientGUIListBoxesData.py
+++ b/hydrus/client/gui/lists/ClientGUIListBoxesData.py
@@ -40,7 +40,7 @@ def CanFadeColours( self ):
return False
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
raise NotImplementedError()
@@ -84,7 +84,7 @@ def __hash__( self ):
return self._tag_slice.__hash__()
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
return [ self._tag_slice ]
@@ -136,7 +136,7 @@ def __hash__( self ):
return self._namespace.__hash__()
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
if self._namespace is None:
@@ -249,9 +249,16 @@ def GetBestTag( self ) -> str:
return self._ideal_tag
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
- return [ self._tag ]
+ rows = [ self._tag ]
+
+ if include_parents and self._parent_tags is not None:
+
+ rows.extend( self._parent_tags )
+
+
+ return rows
def GetSearchPredicates( self ) -> typing.List[ ClientSearchPredicate.Predicate ]:
@@ -328,6 +335,7 @@ def SetParents( self, parents ):
self._parent_tags = parents
+
class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
def __init__( self, tag: str, current_count: int, deleted_count: int, pending_count: int, petitioned_count: int, include_actual_counts: bool ):
@@ -358,7 +366,7 @@ def __lt__( self, other ):
return NotImplemented
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
if with_counts:
@@ -376,7 +384,14 @@ def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
else:
- return [ self._tag ]
+ rows = [ self._tag ]
+
+ if include_parents and self._parent_tags is not None:
+
+ rows.extend( self._parent_tags )
+
+
+ return rows
@@ -498,7 +513,7 @@ def CanFadeColours( self ):
return not self._predicate.IsORPredicate()
- def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
+ def GetCopyableTexts( self, with_counts: bool = False, include_parents = False ) -> typing.List[ str ]:
if self._predicate.IsORPredicate():
@@ -525,7 +540,7 @@ def GetCopyableTexts( self, with_counts: bool = False ) -> typing.List[ str ]:
texts = [ self._predicate.ToString( with_count = with_counts, for_parsable_export = True ) ]
- if self._predicate.HasParentPredicates():
+ if include_parents and self._predicate.HasParentPredicates():
texts.extend( [ parent.ToString( with_count = with_counts, for_parsable_export = True ) for parent in self._predicate.GetParentPredicates() ] )
diff --git a/hydrus/client/gui/media/ClientGUIMediaModalActions.py b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
index 30737474b..ba2dfadde 100644
--- a/hydrus/client/gui/media/ClientGUIMediaModalActions.py
+++ b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
@@ -376,7 +376,7 @@ def EditFileNotes( win: QW.QWidget, media: ClientMedia.MediaSingleton, name_to_s
title = 'manage notes'
- with ClientGUITopLevelWindowsPanels.DialogEdit( win, title ) as dlg:
+ with ClientGUITopLevelWindowsPanels.DialogEdit( win, title, frame_key = 'manage_notes_dialog' ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditFileNotesPanel( dlg, names_to_notes, name_to_start_on = name_to_start_on )
@@ -402,7 +402,7 @@ def EditFileTimestamps( win: QW.QWidget, ordered_medias: typing.List[ ClientMedi
title = 'manage times'
- with ClientGUITopLevelWindowsPanels.DialogEdit( win, title ) as dlg:
+ with ClientGUITopLevelWindowsPanels.DialogEdit( win, title, frame_key = 'manage_times_dialog' ) as dlg:
panel = ClientGUIEditTimestamps.EditFileTimestampsPanel( dlg, ordered_medias )
@@ -477,7 +477,7 @@ def ExportFiles( win: QW.QWidget, medias: typing.Collection[ ClientMedia.Media ]
if len( flat_media ) > 0:
- frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( win, 'export files' )
+ frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( win, 'export files', frame_key = 'export_files_frame' )
panel = ClientGUIExport.ReviewExportFilesPanel( frame, flat_media, do_export_and_then_quit = do_export_and_then_quit )
diff --git a/hydrus/client/gui/pages/ClientGUIManagementController.py b/hydrus/client/gui/pages/ClientGUIManagementController.py
index d3eb51f18..fc2734bed 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementController.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementController.py
@@ -10,6 +10,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.importing import ClientImportGallery
from hydrus.client.importing import ClientImportLocal
from hydrus.client.importing import ClientImportSimpleURLs
@@ -68,17 +69,13 @@ def CreateManagementControllerDuplicateFilter(
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_DUPLICATE_FILTER )
- file_search_context = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, predicates = initial_predicates )
-
synchronised = CG.client_controller.new_options.GetBoolean( 'default_search_synchronised' )
management_controller.SetVariable( 'synchronised', synchronised )
- management_controller.SetVariable( 'file_search_context_1', file_search_context )
- management_controller.SetVariable( 'file_search_context_2', file_search_context.Duplicate() )
- management_controller.SetVariable( 'dupe_search_type', ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH )
- management_controller.SetVariable( 'pixel_dupes_preference', ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED )
- management_controller.SetVariable( 'max_hamming_distance', 4 )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext( location_context = location_context, initial_predicates = initial_predicates )
+
+ management_controller.SetVariable( 'potential_duplicates_search_context', potential_duplicates_search_context )
return management_controller
@@ -196,7 +193,7 @@ class ManagementController( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_MANAGEMENT_CONTROLLER
SERIALISABLE_NAME = 'Client Page Management Controller'
- SERIALISABLE_VERSION = 13
+ SERIALISABLE_VERSION = 14
def __init__( self, page_name = 'page' ):
@@ -565,6 +562,56 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
return ( 13, new_serialisable_info )
+ if version == 13:
+
+ ( page_name, management_type, serialisable_variables ) = old_serialisable_info
+
+ if management_type == MANAGEMENT_TYPE_DUPLICATE_FILTER:
+
+ variables: HydrusSerialisable.SerialisableDictionary = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_variables )
+
+ file_search_context_1 = variables[ 'file_search_context_1' ]
+ file_search_context_2 = variables[ 'file_search_context_2' ]
+ dupe_search_type = variables.get( 'dupe_search_type', ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH )
+ pixel_dupes_preference = variables.get( 'pixel_dupes_preference', ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED )
+ max_hamming_distance = variables.get( 'max_hamming_distance', 4 )
+
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ variables[ 'potential_duplicates_search_context' ] = potential_duplicates_search_context
+
+ del variables[ 'file_search_context_1' ]
+ del variables[ 'file_search_context_2' ]
+
+ if 'dupe_search_type' in variables:
+
+ del variables[ 'dupe_search_type' ]
+
+
+ if 'pixel_dupes_preference' in variables:
+
+ del variables[ 'pixel_dupes_preference' ]
+
+
+ if 'max_hamming_distance' in variables:
+
+ del variables[ 'max_hamming_distance' ]
+
+
+ serialisable_variables = variables.GetSerialisableTuple()
+
+
+ new_serialisable_info = ( page_name, management_type, serialisable_variables )
+
+ return ( 14, new_serialisable_info )
+
+
def GetAPIInfoDict( self, simple ):
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index 397e56360..4c1d4cb77 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -1,5 +1,4 @@
import collections
-import os
import random
import threading
import typing
@@ -38,6 +37,7 @@
from hydrus.client.gui.canvas import ClientGUICanvas
from hydrus.client.gui.canvas import ClientGUICanvasFrame
from hydrus.client.gui.duplicates import ClientGUIDuplicatesAutoResolution
+from hydrus.client.gui.duplicates import ClientGUIPotentialDuplicatesSearchContext
from hydrus.client.gui.importing import ClientGUIFileSeedCache
from hydrus.client.gui.importing import ClientGUIGallerySeedLog
from hydrus.client.gui.importing import ClientGUIImport
@@ -565,11 +565,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._filtering_panel = ClientGUICommon.StaticBox( self._main_right_panel, 'duplicate filter' )
- file_search_context_1 = management_controller.GetVariable( 'file_search_context_1' )
- file_search_context_2 = management_controller.GetVariable( 'file_search_context_2' )
-
- file_search_context_1.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
- file_search_context_2.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
+ potential_duplicates_search_context = management_controller.GetVariable( 'potential_duplicates_search_context' )
if self._management_controller.HasVariable( 'synchronised' ):
@@ -580,24 +576,9 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
synchronised = True
- self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_1, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
- self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_2, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
-
- self._dupe_search_type = ClientGUICommon.BetterChoice( self._filtering_panel )
-
- self._dupe_search_type.addItem( 'at least one file matches the search', ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH )
- self._dupe_search_type.addItem( 'both files match the search', ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH )
- self._dupe_search_type.addItem( 'both files match different searches', ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
+ self._potential_duplicates_search_context = ClientGUIPotentialDuplicatesSearchContext.EditPotentialDuplicatesSearchContextPanel( self._filtering_panel, potential_duplicates_search_context, synchronised = synchronised, page_key = self._page_key )
- self._pixel_dupes_preference = ClientGUICommon.BetterChoice( self._filtering_panel )
-
- for p in ( ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_REQUIRED, ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED, ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_EXCLUDED ):
-
- self._pixel_dupes_preference.addItem( ClientDuplicates.similar_files_pixel_dupes_string_lookup[ p ], p )
-
-
- self._max_hamming_distance_for_filter = ClientGUICommon.BetterSpinBox( self._filtering_panel, min = 0, max = 64 )
- self._max_hamming_distance_for_filter.setSingleStep( 2 )
+ self._potential_duplicates_search_context.valueChanged.connect( self._PotentialDuplicatesSearchContextChanged )
self._num_potential_duplicates = ClientGUICommon.BetterStaticText( self._filtering_panel, ellipsize_end = True )
self._refresh_dupe_counts_button = ClientGUICommon.BetterBitmapButton( self._filtering_panel, CC.global_pixmaps().refresh, self.RefreshDuplicateNumbers )
@@ -632,34 +613,6 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
#
- self._max_hamming_distance_for_potential_discovery_spinctrl.setValue( new_options.GetInteger( 'similar_files_duplicate_pairs_search_distance' ) )
-
- self._dupe_search_type.SetValue( management_controller.GetVariable( 'dupe_search_type' ) )
-
- if not management_controller.HasVariable( 'pixel_dupes_preference' ):
-
- management_controller.SetVariable( 'pixel_dupes_preference', ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED )
-
-
- self._pixel_dupes_preference.SetValue( management_controller.GetVariable( 'pixel_dupes_preference' ) )
-
- self._pixel_dupes_preference.currentIndexChanged.connect( self.FilterSearchDomainChanged )
-
- if not management_controller.HasVariable( 'max_hamming_distance' ):
-
- management_controller.SetVariable( 'max_hamming_distance', 4 )
-
-
- self._max_hamming_distance_for_filter.setValue( management_controller.GetVariable( 'max_hamming_distance' ) )
-
- self._max_hamming_distance_for_filter.valueChanged.connect( self.FilterSearchDomainChanged )
-
- #
-
- self._UpdateFilterSearchControls()
-
- #
-
self._media_sort_widget.hide()
distance_hbox = QP.HBoxLayout()
@@ -703,17 +656,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
QP.AddToLayout( text_and_button_hbox, self._num_potential_duplicates, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
QP.AddToLayout( text_and_button_hbox, self._refresh_dupe_counts_button, CC.FLAGS_CENTER_PERPENDICULAR )
- rows = []
-
- rows.append( ( 'maximum search distance of pair: ', self._max_hamming_distance_for_filter ) )
-
- gridbox = ClientGUICommon.WrapInGrid( self._filtering_panel, rows )
-
- self._filtering_panel.Add( self._dupe_search_type, CC.FLAGS_EXPAND_PERPENDICULAR )
- self._filtering_panel.Add( self._tag_autocomplete_1, CC.FLAGS_EXPAND_PERPENDICULAR )
- self._filtering_panel.Add( self._tag_autocomplete_2, CC.FLAGS_EXPAND_PERPENDICULAR )
- self._filtering_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- self._filtering_panel.Add( self._pixel_dupes_preference, CC.FLAGS_EXPAND_PERPENDICULAR )
+ self._filtering_panel.Add( self._potential_duplicates_search_context, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.Add( text_and_button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
self._filtering_panel.Add( self._launch_filter, CC.FLAGS_EXPAND_PERPENDICULAR )
@@ -742,11 +685,6 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
self._controller.sub( self, 'NotifyNewMaintenanceNumbers', 'new_similar_files_maintenance_numbers' )
self._controller.sub( self, 'NotifyNewPotentialsSearchNumbers', 'new_similar_files_potentials_search_numbers' )
- self._tag_autocomplete_1.searchChanged.connect( self.Search1Changed )
- self._tag_autocomplete_2.searchChanged.connect( self.Search2Changed )
-
- self._dupe_search_type.currentIndexChanged.connect( self.FilterDupeSearchTypeChanged )
-
self._max_hamming_distance_for_potential_discovery_spinctrl.valueChanged.connect( self.MaxHammingDistanceForPotentialDiscoveryChanged )
@@ -771,80 +709,36 @@ def _EditMergeOptions( self, duplicate_type ):
- def _FilterSearchDomainUpdated( self ):
+ def _LaunchFilter( self ):
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData( optimise_for_search = False )
+ potential_duplicates_search_context = self._potential_duplicates_search_context.GetValue()
- self._management_controller.SetVariable( 'file_search_context_1', file_search_context_1 )
- self._management_controller.SetVariable( 'file_search_context_2', file_search_context_2 )
+ canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() )
- synchronised = self._tag_autocomplete_1.IsSynchronised()
+ canvas_window = ClientGUICanvas.CanvasFilterDuplicates( canvas_frame, potential_duplicates_search_context )
- self._management_controller.SetVariable( 'synchronised', synchronised )
+ canvas_window.showPairInPage.connect( self._ShowPairInPage )
- self._management_controller.SetVariable( 'dupe_search_type', dupe_search_type )
- self._management_controller.SetVariable( 'pixel_dupes_preference', pixel_dupes_preference )
- self._management_controller.SetVariable( 'max_hamming_distance', max_hamming_distance )
+ canvas_frame.SetCanvas( canvas_window )
- self._SetLocationContext( file_search_context_1.GetLocationContext() )
+
+ def _PotentialDuplicatesSearchContextChanged( self ):
- self._UpdateFilterSearchControls()
+ potential_duplicates_search_context = self._potential_duplicates_search_context.GetValue()
- if self._tag_autocomplete_1.IsSynchronised():
-
- self._dupe_count_numbers_dirty = True
-
+ self._management_controller.SetVariable( 'potential_duplicates_search_context', potential_duplicates_search_context )
-
- def _GetDuplicateFileSearchData( self, optimise_for_search = True ) -> typing.Tuple[ ClientSearchFileSearchContext.FileSearchContext, ClientSearchFileSearchContext.FileSearchContext, int, int, int ]:
+ synchronised = self._potential_duplicates_search_context.IsSynchronised()
- file_search_context_1 = self._tag_autocomplete_1.GetFileSearchContext()
- file_search_context_2 = self._tag_autocomplete_2.GetFileSearchContext()
+ self._management_controller.SetVariable( 'synchronised', synchronised )
- dupe_search_type = self._dupe_search_type.GetValue()
+ self._SetLocationContext( potential_duplicates_search_context.GetFileSearchContext1().GetLocationContext() )
- if optimise_for_search:
+ if synchronised:
- if dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH and ( file_search_context_1.IsJustSystemEverything() or file_search_context_1.HasNoPredicates() ):
-
- dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
-
- elif dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES:
-
- if file_search_context_1.IsJustSystemEverything() or file_search_context_1.HasNoPredicates():
-
- f = file_search_context_1
- file_search_context_1 = file_search_context_2
- file_search_context_2 = f
-
- dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
-
- elif file_search_context_2.IsJustSystemEverything() or file_search_context_2.HasNoPredicates():
-
- dupe_search_type = ClientDuplicates.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
-
-
+ self._dupe_count_numbers_dirty = True
- pixel_dupes_preference = self._pixel_dupes_preference.GetValue()
-
- max_hamming_distance = self._max_hamming_distance_for_filter.value()
-
- return ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
-
-
- def _LaunchFilter( self ):
-
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
-
- canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() )
-
- canvas_window = ClientGUICanvas.CanvasFilterDuplicates( canvas_frame, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
-
- canvas_window.showPairInPage.connect( self._ShowPairInPage )
-
- canvas_frame.SetCanvas( canvas_window )
-
def _RefreshDuplicateCounts( self ):
@@ -864,9 +758,9 @@ def qt_code( potential_duplicates_count ):
self._UpdatePotentialDuplicatesCount( potential_duplicates_count )
- def thread_do_it( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ):
+ def thread_do_it( potential_duplicates_search_context ):
- potential_duplicates_count = CG.client_controller.Read( 'potential_duplicates_count', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_count = CG.client_controller.Read( 'potential_duplicates_count', potential_duplicates_search_context )
QP.CallAfter( qt_code, potential_duplicates_count )
@@ -879,9 +773,9 @@ def thread_do_it( file_search_context_1, file_search_context_2, dupe_search_type
self._num_potential_duplicates.setText( 'updating' + HC.UNICODE_ELLIPSIS )
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
+ potential_duplicates_search_context = self._potential_duplicates_search_context.GetValue()
- CG.client_controller.CallToThread( thread_do_it, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ CG.client_controller.CallToThread( thread_do_it, potential_duplicates_search_context )
@@ -938,9 +832,10 @@ def _ShowPairInPage( self, media: typing.Collection[ ClientMedia.MediaSingleton
def _ShowPotentialDupes( self, hashes ):
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
+ potential_duplicates_search_context = self._potential_duplicates_search_context.GetValue()
- location_context = file_search_context_1.GetLocationContext()
+ # I think this forces the matter if we are not currently synchronised, maybe it is redundant though, not sure
+ location_context = potential_duplicates_search_context.GetFileSearchContext1().GetLocationContext()
self._SetLocationContext( location_context )
@@ -964,11 +859,11 @@ def _ShowPotentialDupes( self, hashes ):
def _ShowRandomPotentialDupes( self ):
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
+ potential_duplicates_search_context = self._potential_duplicates_search_context.GetValue()
self._page_state = CC.PAGE_STATE_SEARCHING
- hashes = self._controller.Read( 'random_potential_duplicate_hashes', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ hashes = self._controller.Read( 'random_potential_duplicate_hashes', potential_duplicates_search_context )
if len( hashes ) == 0:
@@ -1069,25 +964,6 @@ def _UpdatePotentialDuplicatesCount( self, potential_duplicates_count ):
- def _UpdateFilterSearchControls( self ):
-
- ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData( optimise_for_search = False )
-
- self._tag_autocomplete_2.setVisible( dupe_search_type == ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
-
- self._max_hamming_distance_for_filter.setEnabled( self._pixel_dupes_preference.GetValue() != ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_REQUIRED )
-
-
- def FilterDupeSearchTypeChanged( self ):
-
- self._FilterSearchDomainUpdated()
-
-
- def FilterSearchDomainChanged( self ):
-
- self._FilterSearchDomainUpdated()
-
-
def MaxHammingDistanceForPotentialDiscoveryChanged( self ):
search_distance = self._max_hamming_distance_for_potential_discovery_spinctrl.value()
@@ -1113,14 +989,14 @@ def PageHidden( self ):
ManagementPanel.PageHidden( self )
- self._tag_autocomplete_1.SetForceDropdownHide( True )
+ self._potential_duplicates_search_context.PageHidden()
def PageShown( self ):
ManagementPanel.PageShown( self )
- self._tag_autocomplete_1.SetForceDropdownHide( False )
+ self._potential_duplicates_search_context.PageShown()
def RefreshDuplicateNumbers( self ):
@@ -1130,7 +1006,7 @@ def RefreshDuplicateNumbers( self ):
def RefreshQuery( self ):
- self._FilterSearchDomainUpdated()
+ self._PotentialDuplicatesSearchContextChanged()
def REPEATINGPageUpdate( self ):
@@ -1149,32 +1025,7 @@ def REPEATINGPageUpdate( self ):
self._RefreshDuplicateCounts()
- self._tag_autocomplete_1.REPEATINGPageUpdate()
- self._tag_autocomplete_2.REPEATINGPageUpdate()
-
-
- def Search1Changed( self, file_search_context: ClientSearchFileSearchContext.FileSearchContext ):
-
- self._tag_autocomplete_2.blockSignals( True )
-
- self._tag_autocomplete_2.SetLocationContext( self._tag_autocomplete_1.GetLocationContext() )
- self._tag_autocomplete_2.SetSynchronised( self._tag_autocomplete_1.IsSynchronised() )
-
- self._tag_autocomplete_2.blockSignals( False )
-
- self._FilterSearchDomainUpdated()
-
-
- def Search2Changed( self, file_search_context: ClientSearchFileSearchContext.FileSearchContext ):
-
- self._tag_autocomplete_1.blockSignals( True )
-
- self._tag_autocomplete_1.SetLocationContext( self._tag_autocomplete_2.GetLocationContext() )
- self._tag_autocomplete_1.SetSynchronised( self._tag_autocomplete_2.IsSynchronised() )
-
- self._tag_autocomplete_1.blockSignals( False )
-
- self._FilterSearchDomainUpdated()
+ self._potential_duplicates_search_context.REPEATINGPageUpdate()
diff --git a/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py
index 8416d8eed..04ce878f8 100644
--- a/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py
+++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py
@@ -932,7 +932,7 @@ def _ManageURLs( self ):
title = 'manage urls for {} files'.format( num_files )
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg:
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, title, frame_key = 'manage_urls_dialog' ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditURLsPanel( dlg, flat_media )
diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
index c9a9aa912..d73a13aa6 100644
--- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
@@ -2289,6 +2289,15 @@ def __init__( self, parent: QW.QWidget, info ):
text = 'Setting frame location info for ' + name + '.'
+ if name == 'manage_tags_dialog':
+
+ text += '\n\nThis is the manage tags dialog launched off the thumbnail grid.'
+
+ elif name == 'manage_tags_frame':
+
+ text += '\n\nThis is the manage tags dialog launched off the media viewer.'
+
+
QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,text), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._remember_size, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._remember_position, CC.FLAGS_EXPAND_PERPENDICULAR )
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
index 338f8021d..d254aee41 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py
@@ -7,13 +7,17 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
+from hydrus.core import HydrusSerialisable
from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientParsing
+from hydrus.client import ClientSerialisable
from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
+from hydrus.client.gui import ClientGUISerialisable
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import ClientGUIStringPanels
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@@ -39,251 +43,6 @@ def GetValue( self ):
-class EditZipperFormulaPanel( EditSpecificFormulaPanel ):
-
- def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaZipper, test_data: ClientParsing.ParsingTestData ):
-
- super().__init__( parent, collapse_newlines )
-
- #
-
- menu_items = []
-
- page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_ZIPPER_FORMULA )
-
- menu_items.append( ( 'normal', 'open the zipper formula help', 'Open the help page for zipper formulae in your web browser.', page_func ) )
-
- help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )
-
- help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', object_name = 'HydrusIndeterminate' )
-
- #
-
- test_panel = ClientGUICommon.StaticBox( self, 'test' )
-
- self._test_panel = ClientGUIParsingTest.TestPanelFormula( test_panel, self.GetValue, test_data = test_data )
-
- self._test_panel.SetCollapseNewlines( self._collapse_newlines )
-
- #
-
- edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
-
- # TODO: get rid of all the GetClientData for this guy, below. replace with newer stuff, add methods to BetterQListWidget (ReplaceData?) as needed
- self._formulae = ClientGUIListBoxes.BetterQListWidget( edit_panel )
- self._formulae.setSelectionMode( QW.QAbstractItemView.SingleSelection )
- self._formulae.itemDoubleClicked.connect( self.Edit )
-
- self._add_formula = ClientGUICommon.BetterButton( edit_panel, 'add', self.Add )
-
- self._edit_formula = ClientGUICommon.BetterButton( edit_panel, 'edit', self.Edit )
-
- self._move_formula_up = ClientGUICommon.BetterButton( edit_panel, '\u2191', self.MoveUp )
-
- self._delete_formula = ClientGUICommon.BetterButton( edit_panel, 'X', self.Delete )
-
- self._move_formula_down = ClientGUICommon.BetterButton( edit_panel, '\u2193', self.MoveDown )
-
- self._sub_phrase = QW.QLineEdit( edit_panel )
-
- formulae = formula.GetFormulae()
- sub_phrase = formula.GetSubstitutionPhrase()
- string_processor = formula.GetStringProcessor()
-
- self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
-
- #
-
- for formula in formulae:
-
- pretty_formula = formula.ToPrettyString()
-
- item = QW.QListWidgetItem()
- item.setText( pretty_formula )
- item.setData( QC.Qt.UserRole, formula )
- self._formulae.addItem( item )
-
-
- self._sub_phrase.setText( sub_phrase )
-
- #
-
- udd_button_vbox = QP.VBoxLayout()
-
- udd_button_vbox.addStretch( 1 )
- QP.AddToLayout( udd_button_vbox, self._move_formula_up, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( udd_button_vbox, self._delete_formula, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( udd_button_vbox, self._move_formula_down, CC.FLAGS_CENTER_PERPENDICULAR )
- udd_button_vbox.addStretch( 1 )
-
- formulae_hbox = QP.HBoxLayout()
-
- QP.AddToLayout( formulae_hbox, self._formulae, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( formulae_hbox, udd_button_vbox, CC.FLAGS_CENTER_PERPENDICULAR )
-
- ae_button_hbox = QP.HBoxLayout()
-
- QP.AddToLayout( ae_button_hbox, self._add_formula, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( ae_button_hbox, self._edit_formula, CC.FLAGS_CENTER_PERPENDICULAR )
-
- rows = []
-
- rows.append( ( 'substitution phrase:', self._sub_phrase ) )
-
- gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
-
- edit_panel.Add( formulae_hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
- edit_panel.Add( ae_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
-
- if collapse_newlines:
-
- label = 'Newlines are removed from parsed strings right after parsing, before string processing.'
-
- else:
-
- label = 'Newlines are not collapsed here (probably a note parser)'
-
-
- edit_panel.Add( ClientGUICommon.BetterStaticText( edit_panel, label, ellipsize_end = True ), CC.FLAGS_EXPAND_PERPENDICULAR )
- edit_panel.Add( self._string_processor_button, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- #
-
- test_panel.Add( self._test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
-
- #
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( hbox, test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
-
- vbox = QP.VBoxLayout()
-
- QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
-
- self.widget().setLayout( vbox )
-
-
- def Add( self ):
-
- existing_formula = ClientParsing.ParseFormulaHTML()
-
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
-
- panel = EditFormulaPanel( dlg, existing_formula, self._test_panel.GetTestDataForChild )
-
- dlg.SetPanel( panel )
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- new_formula = panel.GetValue()
-
- pretty_formula = new_formula.ToPrettyString()
-
- item = QW.QListWidgetItem()
- item.setText( pretty_formula )
- item.setData( QC.Qt.UserRole, new_formula )
- self._formulae.addItem( item )
-
-
-
-
- def Delete( self ):
-
- selection = QP.ListWidgetGetSelection( self._formulae )
-
- if selection != -1:
-
- if self._formulae.count() == 1:
-
- ClientGUIDialogsMessage.ShowWarning( self, 'A zipper formula needs at least one sub-formula!' )
-
- else:
-
- QP.ListWidgetDelete( self._formulae, selection )
-
-
-
-
- def Edit( self ):
-
- selection = QP.ListWidgetGetSelection( self._formulae )
-
- if selection != -1:
-
- old_formula = QP.GetClientData( self._formulae, selection )
-
- with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
-
- panel = EditFormulaPanel( dlg, old_formula, self._test_panel.GetTestDataForChild )
-
- dlg.SetPanel( panel )
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- new_formula = panel.GetValue()
-
- pretty_formula = new_formula.ToPrettyString()
-
- self._formulae.item( selection ).setText( pretty_formula )
- self._formulae.item( selection ).setData( QC.Qt.UserRole, new_formula )
-
-
-
-
-
- def GetValue( self ):
-
- formulae = self._formulae.GetData()
-
- sub_phrase = self._sub_phrase.text()
-
- string_processor = self._string_processor_button.GetValue()
-
- formula = ClientParsing.ParseFormulaZipper( formulae, sub_phrase, string_processor )
-
- return formula
-
-
- def MoveDown( self ):
-
- selection = QP.ListWidgetGetSelection( self._formulae )
-
- if selection != -1 and selection + 1 < self._formulae.count():
-
- pretty_rule = self._formulae.item( selection ).text()
- rule = QP.GetClientData( self._formulae, selection )
-
- QP.ListWidgetDelete( self._formulae, selection )
-
- item = QW.QListWidgetItem()
- item.setText( pretty_rule )
- item.setData( QC.Qt.UserRole, rule )
- self._formulae.insertItem( selection + 1, item )
-
-
-
- def MoveUp( self ):
-
- selection = QP.ListWidgetGetSelection( self._formulae )
-
- if selection != -1 and selection > 0:
-
- pretty_rule = self._formulae.item( selection ).text()
- rule = QP.GetClientData( self._formulae, selection )
-
- QP.ListWidgetDelete( self._formulae, selection )
-
- item = QW.QListWidgetItem()
- item.setText( pretty_rule )
- item.setData( QC.Qt.UserRole, rule )
- self._formulae.insertItem( selection - 1, item )
-
-
-
class EditContextVariableFormulaPanel( EditSpecificFormulaPanel ):
def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaContextVariable, test_data: ClientParsing.ParsingTestData ):
@@ -317,8 +76,13 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
self._variable_name = QW.QLineEdit( edit_panel )
variable_name = formula.GetVariableName()
+ name = formula.GetName()
string_processor = formula.GetStringProcessor()
+ self._name = QW.QLineEdit( edit_panel )
+ self._name.setText( name )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'Optional, and decorative only. Leave blank to set nothing.' ) )
+
self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
#
@@ -329,6 +93,7 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
rows = []
+ rows.append( ( 'name/description:', self._name ) )
rows.append( ( 'variable name:', self._variable_name ) )
gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
@@ -370,9 +135,11 @@ def GetValue( self ):
variable_name = self._variable_name.text()
+ name = self._name.text()
+
string_processor = self._string_processor_button.GetValue()
- formula = ClientParsing.ParseFormulaContextVariable( variable_name, string_processor )
+ formula = ClientParsing.ParseFormulaContextVariable( variable_name = variable_name, name = name, string_processor = string_processor )
return formula
@@ -405,6 +172,20 @@ def __init__( self, parent: QW.QWidget, formula: ClientParsing.ParseFormula, tes
self._change_formula_type = ClientGUICommon.BetterButton( my_panel, 'change formula type', self._ChangeFormulaType )
+ menu_items = []
+
+ menu_items.append( ( 'normal', 'to clipboard', 'Serialise the formula and put it on your clipboard.', self.ExportToClipboard ) )
+ menu_items.append( ( 'normal', 'to png', 'Serialise the formula and encode it to an image file you can easily share with other hydrus users.', self.ExportToPNG ) )
+
+ self._export_button = ClientGUIMenuButton.MenuButton( self, 'export', menu_items )
+
+ menu_items = []
+
+ menu_items.append( ( 'normal', 'from clipboard', 'Load a formula from text in your clipboard.', self.ImportFromClipboard ) )
+ menu_items.append( ( 'normal', 'from png', 'Load a formula from an encoded png.', self.ImportFromPNG ) )
+
+ self._import_button = ClientGUIMenuButton.MenuButton( self, 'import', menu_items )
+
#
self._UpdateControls()
@@ -415,6 +196,8 @@ def __init__( self, parent: QW.QWidget, formula: ClientParsing.ParseFormula, tes
QP.AddToLayout( button_hbox, self._edit_formula, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( button_hbox, self._change_formula_type, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( button_hbox, self._export_button, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( button_hbox, self._import_button, CC.FLAGS_EXPAND_BOTH_WAYS )
my_panel.Add( self._formula_description, CC.FLAGS_EXPAND_BOTH_WAYS )
my_panel.Add( button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@@ -530,6 +313,120 @@ def _EditFormula( self ):
+ def _GetExportObject( self ):
+
+ return self._current_formula
+
+
+ def _ImportObject( self, obj ):
+
+ if isinstance( obj, ClientParsing.ParseFormula ):
+
+ formula = obj
+
+ self._current_formula = formula
+
+ self._UpdateControls()
+
+ else:
+
+ ClientGUIDialogsMessage.ShowWarning( self, f'That was not a formula--it was a: {type(obj).__name__}' )
+
+
+
+ def ExportToClipboard( self ):
+
+ export_object = self._GetExportObject()
+
+ if export_object is not None:
+
+ json = export_object.DumpToString()
+
+ CG.client_controller.pub( 'clipboard', 'text', json )
+
+
+
+ def ExportToPNG( self ):
+
+ export_object = self._GetExportObject()
+
+ if export_object is not None:
+
+ with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
+
+ panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
+
+ dlg.SetPanel( panel )
+
+ dlg.exec()
+
+
+
+
+ def ImportFromClipboard( self ):
+
+ try:
+
+ raw_text = CG.client_controller.GetClipboardText()
+
+ except HydrusExceptions.DataMissing as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem importing!', str(e) )
+
+ return
+
+
+ try:
+
+ obj = HydrusSerialisable.CreateFromString( raw_text )
+
+ self._ImportObject( obj )
+
+ except Exception as e:
+
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Formula', e )
+
+
+
+ def ImportFromPNG( self ):
+
+ with QP.FileDialog( self, 'select the png with the encoded formula', wildcard = 'PNG (*.png)' ) as dlg:
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ path = dlg.GetPath()
+
+ try:
+
+ payload = ClientSerialisable.LoadFromPNG( path )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem loading!', str(e) )
+
+ return
+
+
+ try:
+
+ obj = HydrusSerialisable.CreateFromNetworkBytes( payload )
+
+ self._ImportObject( obj )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ ClientGUIDialogsMessage.ShowCritical( self, 'Problem loading!', 'I could not understand what was encoded in the png!' )
+
+
+
+
+
def _UpdateControls( self ):
if self._current_formula is None:
@@ -810,8 +707,13 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
tag_rules = formula.GetTagRules()
content_to_fetch = formula.GetContentToFetch()
attribute_to_fetch = formula.GetAttributeToFetch()
+ name = formula.GetName()
string_processor = formula.GetStringProcessor()
+ self._name = QW.QLineEdit( edit_panel )
+ self._name.setText( name )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'Optional, and decorative only. Leave blank to set nothing.' ) )
+
self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
#
@@ -854,13 +756,22 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
rows = []
- rows.append( ( 'content to fetch:', self._content_to_fetch ) )
- rows.append( ( 'attribute to fetch: ', self._attribute_to_fetch ) )
+ rows.append( ( 'name/description:', self._name ) )
gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+ edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
edit_panel.Add( tag_rules_hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
edit_panel.Add( ae_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+ rows = []
+
+ rows.append( ( 'content to fetch:', self._content_to_fetch ) )
+ rows.append( ( 'attribute to fetch: ', self._attribute_to_fetch ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+
edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
if collapse_newlines:
@@ -973,7 +884,7 @@ def Edit( self ):
def GetValue( self ):
- tags_rules = [ QP.GetClientData( self._tag_rules, i ) for i in range( self._tag_rules.count() ) ]
+ tag_rules = [ QP.GetClientData( self._tag_rules, i ) for i in range( self._tag_rules.count() ) ]
content_to_fetch = self._content_to_fetch.GetValue()
@@ -984,9 +895,17 @@ def GetValue( self ):
raise HydrusExceptions.VetoException( 'Please enter an attribute to fetch!' )
+ name = self._name.text()
+
string_processor = self._string_processor_button.GetValue()
- formula = ClientParsing.ParseFormulaHTML( tags_rules, content_to_fetch, attribute_to_fetch, string_processor )
+ formula = ClientParsing.ParseFormulaHTML(
+ tag_rules = tag_rules,
+ content_to_fetch = content_to_fetch,
+ attribute_to_fetch = attribute_to_fetch,
+ name = name,
+ string_processor = string_processor
+ )
return formula
@@ -1186,8 +1105,13 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
parse_rules = formula.GetParseRules()
content_to_fetch = formula.GetContentToFetch()
+ name = formula.GetName()
string_processor = formula.GetStringProcessor()
+ self._name = QW.QLineEdit( edit_panel )
+ self._name.setText( name )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'Optional, and decorative only. Leave blank to set nothing.' ) )
+
self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
#
@@ -1226,12 +1150,21 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
rows = []
- rows.append( ( 'content to fetch:', self._content_to_fetch ) )
+ rows.append( ( 'name/description:', self._name ) )
gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+ edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
edit_panel.Add( parse_rules_hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
edit_panel.Add( ae_button_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+ rows = []
+
+ rows.append( ( 'content to fetch:', self._content_to_fetch ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+
edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
if collapse_newlines:
@@ -1336,9 +1269,11 @@ def GetValue( self ):
content_to_fetch = self._content_to_fetch.GetValue()
+ name = self._name.text()
+
string_processor = self._string_processor_button.GetValue()
- formula = ClientParsing.ParseFormulaJSON( parse_rules, content_to_fetch, string_processor )
+ formula = ClientParsing.ParseFormulaJSON( parse_rules = parse_rules, content_to_fetch = content_to_fetch, name = name, string_processor = string_processor )
return formula
@@ -1421,8 +1356,13 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
self._sub_formula_panel = EditFormulaPanel( sub_panel, sub_formula, test_data_callable = self._GetSubTestData )
+ name = formula.GetName()
string_processor = formula.GetStringProcessor()
+ self._name = QW.QLineEdit( edit_panel )
+ self._name.setText( name )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'Optional, and decorative only. Leave blank to set nothing.' ) )
+
self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
#
@@ -1437,6 +1377,15 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client
st.setWordWrap( True )
edit_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ rows = []
+
+ rows.append( ( 'name/description:', self._name ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+
+ edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
edit_panel.Add( main_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
edit_panel.Add( sub_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
@@ -1505,9 +1454,184 @@ def GetValue( self ):
main_formula = self._main_formula_panel.GetValue()
sub_formula = self._sub_formula_panel.GetValue()
+ name = self._name.text()
+ string_processor = self._string_processor_button.GetValue()
+
+ formula = ClientParsing.ParseFormulaNested( main_formula = main_formula, sub_formula = sub_formula, name = name, string_processor = string_processor )
+
+ return formula
+
+
+
+class EditZipperFormulaPanel( EditSpecificFormulaPanel ):
+
+ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaZipper, test_data: ClientParsing.ParsingTestData ):
+
+ super().__init__( parent, collapse_newlines )
+
+ #
+
+ menu_items = []
+
+ page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_ZIPPER_FORMULA )
+
+ menu_items.append( ( 'normal', 'open the zipper formula help', 'Open the help page for zipper formulae in your web browser.', page_func ) )
+
+ help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )
+
+ help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', object_name = 'HydrusIndeterminate' )
+
+ #
+
+ test_panel = ClientGUICommon.StaticBox( self, 'test' )
+
+ self._test_panel = ClientGUIParsingTest.TestPanelFormula( test_panel, self.GetValue, test_data = test_data )
+
+ self._test_panel.SetCollapseNewlines( self._collapse_newlines )
+
+ #
+
+ edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
+
+ self._formulae = ClientGUIListBoxes.QueueListBox( edit_panel, 6, self._ConvertFormulaToListString, add_callable = self._Add, edit_callable = self._Edit )
+
+ self._formulae.AddImportExportButtons( ( ClientParsing.ParseFormula, ) )
+
+ self._sub_phrase = QW.QLineEdit( edit_panel )
+
+ formulae = formula.GetFormulae()
+ sub_phrase = formula.GetSubstitutionPhrase()
+ name = formula.GetName()
+ string_processor = formula.GetStringProcessor()
+
+ self._name = QW.QLineEdit( edit_panel )
+ self._name.setText( name )
+ self._name.setToolTip( ClientGUIFunctions.WrapToolTip( 'Optional, and decorative only. Leave blank to set nothing.' ) )
+
+ self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor )
+
+ #
+
+ self._formulae.AddDatas( formulae )
+
+ self._sub_phrase.setText( sub_phrase )
+
+ #
+
+ rows = []
+
+ rows.append( ( 'name/description:', self._name ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+
+ edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+ edit_panel.Add( self._formulae, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ rows = []
+
+ rows.append( ( 'substitution phrase:', self._sub_phrase ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( edit_panel, rows )
+
+ edit_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+ if collapse_newlines:
+
+ label = 'Newlines are removed from parsed strings right after parsing, before string processing.'
+
+ else:
+
+ label = 'Newlines are not collapsed here (probably a note parser)'
+
+
+ edit_panel.Add( ClientGUICommon.BetterStaticText( edit_panel, label, ellipsize_end = True ), CC.FLAGS_EXPAND_PERPENDICULAR )
+ edit_panel.Add( self._string_processor_button, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ #
+
+ test_panel.Add( self._test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ #
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( hbox, test_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ vbox = QP.VBoxLayout()
+
+ QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT )
+ QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+
+ self.widget().setLayout( vbox )
+
+
+ def _Add( self ):
+
+ existing_formula = ClientParsing.ParseFormulaHTML()
+
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
+
+ panel = EditFormulaPanel( dlg, existing_formula, self._test_panel.GetTestDataForChild )
+
+ dlg.SetPanel( panel )
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ new_formula = panel.GetValue()
+
+ return new_formula
+
+ else:
+
+ raise HydrusExceptions.VetoException()
+
+
+
+
+ def _ConvertFormulaToListString( self, formula: ClientParsing.ParseFormula ) -> str:
+
+ return formula.ToPrettyString()
+
+
+ def _Edit( self, old_formula ):
+
+ with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
+
+ panel = EditFormulaPanel( dlg, old_formula, self._test_panel.GetTestDataForChild )
+
+ dlg.SetPanel( panel )
+
+ if dlg.exec() == QW.QDialog.Accepted:
+
+ new_formula = panel.GetValue()
+
+ return new_formula
+
+ else:
+
+ raise HydrusExceptions.VetoException()
+
+
+
+
+ def GetValue( self ):
+
+ formulae = self._formulae.GetData()
+
+ sub_phrase = self._sub_phrase.text()
+
+ name = self._name.text()
+
string_processor = self._string_processor_button.GetValue()
- formula = ClientParsing.ParseFormulaNested( main_formula = main_formula, sub_formula = sub_formula, string_processor = string_processor )
+ formula = ClientParsing.ParseFormulaZipper(
+ formulae = formulae,
+ sub_phrase = sub_phrase,
+ name = name,
+ string_processor = string_processor
+ )
return formula
diff --git a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
index 186154f5d..c1cba835a 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py
@@ -1196,6 +1196,7 @@ def ImportFromPNG( self ):
+
class ScriptManagementControl( QW.QWidget ):
def __init__( self, parent ):
diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py
index 4696cbe41..68380e904 100644
--- a/hydrus/client/gui/widgets/ClientGUICommon.py
+++ b/hydrus/client/gui/widgets/ClientGUICommon.py
@@ -493,6 +493,7 @@ def SetValue( self, data ):
+
class BetterNotebook( QW.QTabWidget ):
def _ShiftSelection( self, delta ):
@@ -550,6 +551,7 @@ def SelectRight( self ):
self._ShiftSelection( 1 )
+
class BetterSpinBox( QW.QSpinBox ):
def __init__( self, parent: QW.QWidget, initial = None, min = None, max = None, width = None ):
diff --git a/hydrus/client/importing/ClientImportFileSeeds.py b/hydrus/client/importing/ClientImportFileSeeds.py
index 2994c346c..48953db9a 100644
--- a/hydrus/client/importing/ClientImportFileSeeds.py
+++ b/hydrus/client/importing/ClientImportFileSeeds.py
@@ -12,6 +12,7 @@
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
+from hydrus.core import HydrusLists
from hydrus.core import HydrusNumbers
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
@@ -133,6 +134,8 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_NAME = 'File Import'
SERIALISABLE_VERSION = 8
+ top_wew_default = 'https://big-guys.4u/monica_lewinsky_hott.tiff.exe.vbs'
+
def __init__( self, file_seed_type: int = None, file_seed_data: str = None ):
if file_seed_type is None:
@@ -140,11 +143,9 @@ def __init__( self, file_seed_type: int = None, file_seed_data: str = None ):
file_seed_type = FILE_SEED_TYPE_URL
- top_wew_default = 'https://big-guys.4u/monica_lewinsky_hott.tiff.exe.vbs'
-
if file_seed_data is None:
- file_seed_data = top_wew_default
+ file_seed_data = self.top_wew_default
super().__init__()
@@ -153,7 +154,7 @@ def __init__( self, file_seed_type: int = None, file_seed_data: str = None ):
self.file_seed_data = file_seed_data
self.file_seed_data_for_comparison = file_seed_data
- if self.file_seed_data != top_wew_default:
+ if self.file_seed_data != self.top_wew_default:
self.Normalise() # this fixes the comparison file seed data and fails safely
@@ -191,6 +192,11 @@ def __eq__( self, other ):
def __hash__( self ):
+ if self.file_seed_data_for_comparison is None:
+
+ self.file_seed_data_for_comparison = self.file_seed_data
+
+
return ( self.file_seed_type, self.file_seed_data_for_comparison ).__hash__()
@@ -328,6 +334,12 @@ def _InitialiseFromSerialisableInfo( self, serialisable_info ):
self._external_filterable_tags = set( serialisable_external_filterable_tags )
self._external_additional_service_keys_to_tags = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_external_additional_service_keys_to_tags )
+ # fixing a problem when updating to v8 of this object, originally I accidentally reset this to None for local path guys
+ if self.file_seed_data_for_comparison is None:
+
+ self.file_seed_data_for_comparison = self.file_seed_data
+
+
self._primary_urls = set( serialisable_primary_urls )
self._source_urls = set( serialisable_source_urls )
self._tags = set( serialisable_tags )
@@ -593,6 +605,10 @@ def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
pass
+ else:
+
+ file_seed_data_for_comparison = file_seed_data
+
new_serialisable_info = (
file_seed_type,
@@ -2370,6 +2386,18 @@ def _GetFileSeedsToIndices( self ) -> typing.Dict[ FileSeed, int ]:
self._file_seeds_to_indices = { file_seed : index for ( index, file_seed ) in enumerate( self._file_seeds ) }
+ if len( self._file_seeds_to_indices ) != len( self._file_seeds ):
+
+ # woah, we have some dupes! maybe url classes changed and renormalisation happened, maybe hydev fixed some bad dupe file paths or something
+ # let's correct ourselves now we have the chance; this guy simply cannot handle dupes atm
+
+ self._file_seeds = HydrusData.DedupeList( self._file_seeds )
+
+ self._file_seeds_to_indices = { file_seed : index for ( index, file_seed ) in enumerate( self._file_seeds ) }
+
+ self._statuses_to_file_seeds_dirty = True
+
+
self._file_seeds_to_indices_dirty = False
diff --git a/hydrus/client/networking/api/ClientLocalServerCore.py b/hydrus/client/networking/api/ClientLocalServerCore.py
index 72251e35d..bff3e592e 100644
--- a/hydrus/client/networking/api/ClientLocalServerCore.py
+++ b/hydrus/client/networking/api/ClientLocalServerCore.py
@@ -27,6 +27,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.metadata import ClientRatings
from hydrus.client.search import ClientSearchFileSearchContext
from hydrus.client.search import ClientSearchParseSystemPredicates
@@ -653,7 +654,7 @@ def ParseClientAPISearchPredicates( request ) -> typing.List[ ClientSearchPredic
return predicates
-def ParseDuplicateSearch( request: HydrusServerRequest.HydrusRequest ):
+def ParsePotentialDuplicatesSearchContext( request: HydrusServerRequest.HydrusRequest ) -> ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext:
location_context = ParseLocationContext( request, ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) )
@@ -687,7 +688,6 @@ def ParseDuplicateSearch( request: HydrusServerRequest.HydrusRequest ):
predicates_2 = ConvertTagListToPredicates( request, tags_2, do_permission_check = False )
-
file_search_context_1 = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, tag_context = tag_context_1, predicates = predicates_1 )
file_search_context_2 = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, tag_context = tag_context_2, predicates = predicates_2 )
@@ -695,13 +695,15 @@ def ParseDuplicateSearch( request: HydrusServerRequest.HydrusRequest ):
pixel_dupes_preference = request.parsed_request_args.GetValue( 'pixel_duplicates', int, default_value = ClientDuplicates.SIMILAR_FILES_PIXEL_DUPES_ALLOWED )
max_hamming_distance = request.parsed_request_args.GetValue( 'max_hamming_distance', int, default_value = 4 )
- return (
- file_search_context_1,
- file_search_context_2,
- dupe_search_type,
- pixel_dupes_preference,
- max_hamming_distance
- )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ return potential_duplicates_search_context
def ParseLocationContext( request: HydrusServerRequest.HydrusRequest, default: ClientLocation.LocationContext, deleted_allowed = True ):
diff --git a/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py b/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py
index 706125b07..148b2d5a8 100644
--- a/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py
+++ b/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py
@@ -433,7 +433,7 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
if include_milliseconds:
- time_converter = lambda t: t / 1000
+ time_converter = lambda t: t / 1000 if t is not None else None
else:
diff --git a/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py b/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
index 120b2f713..3c067c39a 100644
--- a/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
+++ b/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
@@ -48,15 +48,9 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsGetPotentialsCount
def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
- (
- file_search_context_1,
- file_search_context_2,
- dupe_search_type,
- pixel_dupes_preference,
- max_hamming_distance
- ) = ClientLocalServerCore.ParseDuplicateSearch( request )
+ potential_duplicates_search_context = ClientLocalServerCore.ParsePotentialDuplicatesSearchContext( request )
- count = CG.client_controller.Read( 'potential_duplicates_count', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ count = CG.client_controller.Read( 'potential_duplicates_count', potential_duplicates_search_context )
body_dict = { 'potential_duplicates_count' : count }
@@ -72,17 +66,11 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsGetPotentialPairs(
def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
- (
- file_search_context_1,
- file_search_context_2,
- dupe_search_type,
- pixel_dupes_preference,
- max_hamming_distance
- ) = ClientLocalServerCore.ParseDuplicateSearch( request )
+ potential_duplicates_search_context = ClientLocalServerCore.ParsePotentialDuplicatesSearchContext( request )
max_num_pairs = request.parsed_request_args.GetValue( 'max_num_pairs', int, default_value = CG.client_controller.new_options.GetInteger( 'duplicate_filter_max_batch_size' ) )
- filtering_pairs_media_results = CG.client_controller.Read( 'duplicate_pairs_for_filtering', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance, max_num_pairs = max_num_pairs )
+ filtering_pairs_media_results = CG.client_controller.Read( 'duplicate_pairs_for_filtering', potential_duplicates_search_context, max_num_pairs = max_num_pairs )
filtering_pairs_hashes = [ ( m1.GetHash().hex(), m2.GetHash().hex() ) for ( m1, m2 ) in filtering_pairs_media_results ]
@@ -100,15 +88,9 @@ class HydrusResourceClientAPIRestrictedManageFileRelationshipsGetRandomPotential
def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
- (
- file_search_context_1,
- file_search_context_2,
- dupe_search_type,
- pixel_dupes_preference,
- max_hamming_distance
- ) = ClientLocalServerCore.ParseDuplicateSearch( request )
+ potential_duplicates_search_context = ClientLocalServerCore.ParsePotentialDuplicatesSearchContext( request )
- hashes = CG.client_controller.Read( 'random_potential_duplicate_hashes', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ hashes = CG.client_controller.Read( 'random_potential_duplicate_hashes', potential_duplicates_search_context )
body_dict = { 'random_potential_duplicate_hashes' : [ hash.hex() for hash in hashes ] }
diff --git a/hydrus/client/search/ClientSearchPredicate.py b/hydrus/client/search/ClientSearchPredicate.py
index 309171413..41152cda9 100644
--- a/hydrus/client/search/ClientSearchPredicate.py
+++ b/hydrus/client/search/ClientSearchPredicate.py
@@ -1943,7 +1943,14 @@ def ToString( self, with_count: bool = True, render_for_user: bool = False, or_u
elif self._predicate_type == PREDICATE_TYPE_PARENT:
- base = ' '
+ if for_parsable_export:
+
+ base = ''
+
+ else:
+
+ base = ' '
+
tag = self._value
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index c2b38c804..ce2e5b576 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,8 +105,8 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 596
-CLIENT_API_VERSION = 74
+SOFTWARE_VERSION = 597
+CLIENT_API_VERSION = 75
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py
index 3a0b7a5eb..7e636426d 100644
--- a/hydrus/core/HydrusSerialisable.py
+++ b/hydrus/core/HydrusSerialisable.py
@@ -148,6 +148,7 @@
SERIALISABLE_TYPE_DUPLICATES_AUTO_RESOLUTION_PAIR_COMPARATOR_TWO_FILES_RELATIVE = 131
SERIALISABLE_TYPE_METADATA_CONDITIONAL = 132
SERIALISABLE_TYPE_PARSE_FORMULA_NESTED = 133
+SERIALISABLE_TYPE_POTENTIAL_DUPLICATES_SEARCH_CONTEXT = 134
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}
diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py
index 3ef08675f..4b7839355 100644
--- a/hydrus/core/HydrusText.py
+++ b/hydrus/core/HydrusText.py
@@ -224,7 +224,12 @@ def ElideText( text, max_length, elide_center = False ):
return text
-def GetFirstLine( text: str ) -> str:
+def GetFirstLine( text: typing.Optional[ str ] ) -> str:
+
+ if text is None:
+
+ return 'unknown'
+
if len( text ) > 0:
diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py
index a3a5ad1e2..79742ecf8 100644
--- a/hydrus/test/TestClientAPI.py
+++ b/hydrus/test/TestClientAPI.py
@@ -4514,7 +4514,13 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'potential_duplicates_count' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
self.assertEqual( file_search_context_1.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
@@ -4552,7 +4558,13 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'potential_duplicates_count' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
self.assertEqual( file_search_context_1.GetSerialisableTuple(), test_file_search_context_1.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), test_file_search_context_2.GetSerialisableTuple() )
@@ -4589,10 +4601,16 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'duplicate_pairs_for_filtering' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
max_num_pairs = kwargs[ 'max_num_pairs' ]
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
+
self.assertEqual( file_search_context_1.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
self.assertEqual( potentials_search_type, default_potentials_search_type )
@@ -4631,10 +4649,16 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'duplicate_pairs_for_filtering' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
max_num_pairs = kwargs[ 'max_num_pairs' ]
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
+
self.assertEqual( file_search_context_1.GetSerialisableTuple(), test_file_search_context_1.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), test_file_search_context_2.GetSerialisableTuple() )
self.assertEqual( potentials_search_type, test_potentials_search_type )
@@ -4667,7 +4691,13 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'random_potential_duplicate_hashes' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
self.assertEqual( file_search_context_1.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), default_file_search_context.GetSerialisableTuple() )
@@ -4705,7 +4735,13 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
[ ( args, kwargs ) ] = TG.test_controller.GetRead( 'random_potential_duplicate_hashes' )
- ( file_search_context_1, file_search_context_2, potentials_search_type, pixel_duplicates, max_hamming_distance ) = args
+ ( potential_duplicates_search_context, ) = args
+
+ file_search_context_1 = potential_duplicates_search_context.GetFileSearchContext1()
+ file_search_context_2 = potential_duplicates_search_context.GetFileSearchContext2()
+ potentials_search_type = potential_duplicates_search_context.GetDupeSearchType()
+ pixel_duplicates = potential_duplicates_search_context.GetPixelDupesPreference()
+ max_hamming_distance = potential_duplicates_search_context.GetMaxHammingDistance()
self.assertEqual( file_search_context_1.GetSerialisableTuple(), test_file_search_context_1.GetSerialisableTuple() )
self.assertEqual( file_search_context_2.GetSerialisableTuple(), test_file_search_context_2.GetSerialisableTuple() )
diff --git a/hydrus/test/TestClientDBDuplicates.py b/hydrus/test/TestClientDBDuplicates.py
index 086b2e74e..81874fb55 100644
--- a/hydrus/test/TestClientDBDuplicates.py
+++ b/hydrus/test/TestClientDBDuplicates.py
@@ -11,6 +11,7 @@
from hydrus.client import ClientLocation
from hydrus.client.db import ClientDB
from hydrus.client.duplicates import ClientDuplicates
+from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.importing import ClientImportFiles
from hydrus.client.importing.options import FileImportOptions
from hydrus.client.metadata import ClientContentUpdates
@@ -129,17 +130,25 @@ def _test_initial_state( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertEqual( num_potentials, self._expected_num_potentials )
- result = self._read( 'random_potential_duplicate_hashes', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ result = self._read( 'random_potential_duplicate_hashes', potential_duplicates_search_context )
self.assertEqual( len( result ), len( self._all_hashes ) )
self.assertEqual( set( result ), self._all_hashes )
- filtering_pairs = self._read( 'duplicate_pairs_for_filtering', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ filtering_pairs = self._read( 'duplicate_pairs_for_filtering', potential_duplicates_search_context )
for ( a, b ) in filtering_pairs:
@@ -182,7 +191,15 @@ def _test_initial_better_worse( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self._num_free_agents -= 1
@@ -270,7 +287,15 @@ def _test_initial_king_usurp( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self._num_free_agents -= 1
@@ -337,7 +362,15 @@ def _test_initial_same_quality( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self._num_free_agents -= 1
@@ -515,7 +548,15 @@ def _test_poach_same( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -599,7 +640,15 @@ def _test_group_merge( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -643,7 +692,15 @@ def _test_establish_false_positive_group( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -663,7 +720,15 @@ def _test_false_positive( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -719,7 +784,15 @@ def _test_establish_alt_group( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -739,7 +812,15 @@ def _test_alt( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -801,7 +882,15 @@ def _test_expand_false_positive( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
@@ -864,7 +953,15 @@ def _test_expand_alt( self ):
max_hamming_distance = 4
dupe_search_type = ClientDuplicates.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH
- num_potentials = self._read( 'potential_duplicates_count', self._file_search_context_1, self._file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
+ potential_duplicates_search_context = ClientPotentialDuplicatesSearchContext.PotentialDuplicatesSearchContext()
+
+ potential_duplicates_search_context.SetFileSearchContext1( self._file_search_context_1 )
+ potential_duplicates_search_context.SetFileSearchContext2( self._file_search_context_2 )
+ potential_duplicates_search_context.SetDupeSearchType( dupe_search_type )
+ potential_duplicates_search_context.SetPixelDupesPreference( pixel_dupes_preference )
+ potential_duplicates_search_context.SetMaxHammingDistance( max_hamming_distance )
+
+ num_potentials = self._read( 'potential_duplicates_count', potential_duplicates_search_context )
self.assertLess( num_potentials, self._expected_num_potentials )
diff --git a/hydrus/test/TestClientParsing.py b/hydrus/test/TestClientParsing.py
index 11e2e482a..0f0cbd2d7 100644
--- a/hydrus/test/TestClientParsing.py
+++ b/hydrus/test/TestClientParsing.py
@@ -14,7 +14,7 @@ class DummyFormula( ClientParsing.ParseFormula ):
def __init__( self, result: typing.List[ str ] ):
- super().__init__()
+ super().__init__( name = 'dummy formula' )
self._result = result
@@ -36,12 +36,30 @@ def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool
def ToPrettyString( self ):
- return 'test dummy formula'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return t + 'test'
def ToPrettyMultilineString( self ):
- return 'test dummy formula' + '\n' + 'returns what you give it'
+ if self._name == '':
+
+ t = ''
+
+ else:
+
+ t = f'{self._name}: '
+
+
+ return t + 'test' + '\n' + 'returns what you give it'