Version 532

closes #1375, closes #947
This commit is contained in:
Hydrus Network Developer 2023-06-21 14:50:13 -05:00
parent e0798b235b
commit aafaa65b12
No known key found for this signature in database
GPG Key ID: 76249F053212133C
46 changed files with 1182 additions and 576 deletions

View File

@ -7,6 +7,40 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 532](https://github.com/hydrusnetwork/hydrus/releases/tag/v532)
### misc
* whenever you say 'show these files in a new page', the new page now has a search interface. it starts with a 'system:hash' pre-populated with the files' hashes, so you can now easily narrow down or return to the stuff you are playing with! original file sort order is preserved until you alter or refresh the search
* tags' `right-click->search` menu now has a 'open in a new duplicate filter' for quick spawning of duplicate filters for specific searches
* the duplicate filter no longer flicks to the 'preparation' tab if there is work to do on the first numbers fetch. this thing has been driving me nuts, I don't know why I wrote it that way to begin with
* improved the reliability of certain session object saving--I believe some situations where the 'searching immediately' and 'this search was completed' status where not being saved for some page queries. this _may_ solve a long time bug where some pages would refresh on load
* all search pages that load with files now explicitly reaffirm internally that they are starting with a completed search, which should reduce some related edge case buggy behaviour here
* the 'string to string' edit control now tries to compensate if it is incorrectly given non-string data. somewhere in the html parsing formula UI this happened, an integer sneaking in the key/value of the tag rule, maybe by manual human JSON editing, but I'm not really sure. should be handled correctly now though. let me know if you are into this and discover anything
* every 'eventFilter' in the program now catches Exceptions ruthlessly. it turns out Qt can't handle an Exception escaping one of these, and this _may_ be the cause of some >=v530 crashing on macOS related to multi-column list interaction under issue #1379. it is probably the cause of some other crashes that I haven't been able to figure out--these will now give normal popup errors, so let me know if you see anything. if you have had crazy crashes in macOS recently and these changes don't fix you, reverting back to v529 is apparently ok! there have been no big database updates in that time, so you should be able to just install v529 on your existing install and be off
* the routine that purges files from the trash now uses fewer database queries to find eligable files. some Linux guys have been working with me on memory explosions possibly in this area--let me know if you notice any difference
* the 'clear trash' command in review services is politer to your database, breaking up a large amount of trash into smaller groups
* the program no longer moans to the log when it physically deletes a file and files no accompanying thumbnail to delete--this is true for several situations, and not worth the logspam
* fixed a typo error in the `url class links` 'try to fill in the gaps' command
### pixiv downloader
* I reworked the pixiv parser changes from a couple weeks ago. as background, what happened is pixiv said if you aren't logged in, you can't get the 'original' quality of the file any more. my first fix was to say 'ok, if the user is not logged in, get the lower quality', but this was the wrong decision. the parser now vetoes, causing an 'ignored' result and telling you the problem in the import note. if you _do_ want to get the lower quality image and not log in, this is now selectable as an alternate parser under _network->downloader components->manage url class links_
* also, a variety of old pixiv objects and other experiments are deleted and merged today. the parsers that worked on the old html format, `pixiv manga page parser`, `pixiv manga_big page parser`, `pixiv single file page parser - new layout`, and `pixiv tag search gallery page parser` will be deleted from your client, and the old gallery url class, `pixiv tag search gallery page` meets a similar fate. `pixiv manga_big page` and `pixiv manga page` are removed and their urls merged into a more accomodating `pixiv file page`, which stays to hold all the legacy pixiv URLs, which on the site are automatically redirected to the new format. thanks to a user for helping me with what here was cruft (issue #947)
### mpv logging and emergency halt
* a user sent me a cool truncated twitter video download that, when loaded into mpv, would crash the program after a click or two around the player. this sent me on an odyssey into the mpv logging system and event loop and some really bizarre behaviour under the hood, and, long story short, mpv will notice this particular problem class in future and immediately unload the file and present the user with a dialog explaining the issue. it also won't let you load that file again that boot
* to recognise this error class, I broaden what is logged and scan the lines as they come in. I've been careful in how I filter, but it may produce some false positives. let me know if this thing triggers for any files that seem fine in an external player
* errors of unknown severity are now printed silently to the log with a little intro text saying which file it was and so on. there are a bunch of these with the sorts of files we deal with, stuff like missing chapter marks or borked header data. I expect I'll work on silencing the ones we confirm are no big deal, but if you encounter a ton of them, particularly if you know some cause crashes, please now check your log and let me know what you see
* if you have two mpv players playing media at the same time, this reporting system will report the info for both files--sorry, I had to hack this gubbins! future versions of mpv or python-mpv may open some doors here
### client api
* the `/get_files/file` command now has a `download=true` parameter which converts the `Content-Disposition` from `inline` (show the file) to `attachment` (auto-download or open save-as dialog) (issue #1375)
* added help and a unit test for the above
* client api version is now 47
## [Version 531](https://github.com/hydrusnetwork/hydrus/releases/tag/v531)
### misc
@ -339,49 +373,3 @@ title: Changelog
* moved the main file viewing stats fetching routine for MediaResult building down to the file viewing stats module
* updated the old custom gridbox layout to handle multiple-column-spanning controls
* went through all the bash scripts and fixed some issues my IDE linter was moaning about. -r on reads, quotes around variable names, 4-space indenting, and neater testing of program return states
## [Version 522](https://github.com/hydrusnetwork/hydrus/releases/tag/v522)
### notes in sidecars
* the sidecars system now supports notes!
* my sidecars only support univariate rows atm (a list of strings, rather than, say, a list of pairs of strings), so I had to make a decision how to handle note names. if I reworked the pipeline to handle multivariate data, it would take weeks; if I incorporated explicit names into the sidecar object, it would have made 'get/export all my notes' awkward or impossible and not solved the storage problem; so I have compromised in this first version by choosing to import/export everything and merging the name and text into the same row. it expects/says 'name: text' for input and output. let me know what you think. I may revisit this, depending on how it goes
* I added a note to the sidecars help about this special 'name: text' rule along with a couple ideas for tricky situations
### misc
* added 'system:framerate' and 'system:number of frames' to the system predicate parser!
* I am undoing two changes to tag logic from last week: you can now have as many colons at the start of a tag as you like, and the content parser no longer tries to stop double-stacked namespaces. both of these were more trouble than they were worth. in related news, '::' is now a valid tag again, displaying as ':', and you can create ':blush:'-style tags by typing '::blush:'. I'm pretty sure these tags will autocomplete search awfully, so if you end up using something like this legit, let me know how it goes
* if you change the 'media/preview viewer uses its own volume' setting, the client now updates the UI sliders for this immediately, it doesn't need a client restart. the actual volume on the video also changes immediately
* when an mpv window is called to play media that has 'no audio', the mpv window is now explicitly muted. we'll see if this fixes an interesting issue where on one system, videos that have an audio channel with no sound, which hydrus detects as 'no audio', were causing cracks and pops and bursts of hellnoise in mpv (we suspect some sort of normalisation gain error)
### file safety with duplicate symlinked directory entries
* the main hydrus function that merges/mirrors files and directories now checks if the source and destination are the same location but with two different representations (e.g. a mapped drive and its network location). if so, to act as a final safety backstop, the mirror skips work and the merge throws an error. previously, if you wangled two entries for the same location into 'migrate database' and started a migration, it could cause file deletions!
* I've also updated my database migration routines to recognise and handle this situation explicitly. it now skips all file operations and just updates the location record instantly. it is now safe to have the same location twice in the dialog using different names, and to migrate from one to the other. the only bizzaro thing is if you look in the directory, it of course has boths' contents. as always though, I'll say make backups regularly, and sync them before you do any big changes like a migration--then if something goes wrong, you always have an up-to-date backup to roll back to
* the 'migrate database' dialog no longer chases the real path of what you give it. if you want to give it the mapped drive Z:, it'll take and remember it
* some related 'this is in the wrong place' recovery code handles these symlink situations better as well
### advanced new parsing tricks
* thanks to a clever user doing the heavy lifting, there are two neat but advanced additions to the downloader system
* first, the parsing system has a new content parser type, 'http headers', which lets you parse http headers to be used on subsequent downloads created by the parsing downloader object (e.g. next gallery page urls, file downloads from post pages, multi-file posts that split off to single post page urls). should be possible to wangle tokenized gallery searches and file downloads and some hacky login systems
* second, the string converter system now lets you calculate the normal hydrus hashes--md5, sha1, sha256, sha512--of any string (decoding it by utf-8), outputting hexadecimal
### http headers on the client api
* the client api now lets you see and edit the http headers (as under _network->data->review http headers_) for the global network context and specific domains. the commands are `/manage_headers/get_headers` and `/manage_headers/set_headers`
* if you have the 'Make a short-lived popup on cookie updates through the Client API' option set (under 'popups' options page), this now applies to these header changes too
* also debuting on the side is a 'network context' object in the `get_headers` response, confirming the domain you set for. this is an internal object that does domain location stuff all over. it isn't important here, but as we do more network domain setting editing, I expect we'll see more of this guy
* I added some some documentation for all this, as normal, to the client api help
* the labels and help around 'manage cookies' permission are now 'manage cookies and headers'
* the client api version is now 43
* the old `/manage_headers/set_user_agent` still works. ideally, please move to `set_headers`, since it isn't that complex, but no rush. I've made a job to delete it in a year
* while I was doing this, I realised get/set_cookies is pretty bad. I hate their old 'just spam tuples' approach. I've slowly been replacing this stuff with nicer named JSON Objects as is more typical in APIs and is easier to update, so I expect I'll overhaul them at some point
### boring cleanup
* gave the about window a pass. it now runs on the newer scrolling panel system using my hydrus UI objects (so e.g. the hyperlink now opens on a custom browser command, if you need it), says what platform you are on and whether you are source/build/app, and the version info lines are cleaned a little
* fixed/cleaned some bad code all around http header management
* wrote some unit tests for http headers in the client api
* wrote some unit tests for notes in sidecars

View File

@ -1619,21 +1619,23 @@ Required Headers: n/a
Arguments :
:
* `file_id`: (numerical file id for the file)
* `hash`: (a hexadecimal SHA256 hash for the file)
* `file_id`: (selective, numerical file id for the file)
* `hash`: (selective, a hexadecimal SHA256 hash for the file)
* `download`: (optional, boolean, default `false`)
Only use one. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.
Only use one of file_id or hash. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.
``` title="Example request"
/get_files/file?file_id=452158
```
``` title="Example request"
/get_files/file?hash=7f30c113810985b69014957c93bc25e8eb4cf3355dae36d8b9d011d8b0cf623a
```
``` title="Example request"
/get_files/file?file_id=452158
```
``` title="Example request"
/get_files/file?hash=7f30c113810985b69014957c93bc25e8eb4cf3355dae36d8b9d011d8b0cf623a&download=true
```
Response:
: The file itself. You should get the correct mime type as the Content-Type header.
By default, this will set the `Content-Disposition` header to `inline`, which causes a web browser to show the file. If you set `download=true`, it will set it to `attachment`, which triggers the browser to automatically download it (or open the 'save as' dialog) instead.
### **GET `/get_files/thumbnail`** { id="get_files_thumbnail" }
@ -1646,8 +1648,8 @@ Required Headers: n/a
Arguments:
:
* `file_id`: (numerical file id for the file)
* `hash`: (a hexadecimal SHA256 hash for the file)
* `file_id`: (selective, numerical file id for the file)
* `hash`: (selective, a hexadecimal SHA256 hash for the file)
Only use one. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.

View File

@ -34,6 +34,35 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_532"><a href="#version_532">version 532</a></h2>
<ul>
<li><h3>misc</h3></li>
<li>whenever you say 'show these files in a new page', the new page now has a search interface. it starts with a 'system:hash' pre-populated with the files' hashes, so you can now easily narrow down or return to the stuff you are playing with! original file sort order is preserved until you alter or refresh the search</li>
<li>tags' `right-click->search` menu now has a 'open in a new duplicate filter' for quick spawning of duplicate filters for specific searches</li>
<li>the duplicate filter no longer flicks to the 'preparation' tab if there is work to do on the first numbers fetch. this thing has been driving me nuts, I don't know why I wrote it that way to begin with</li>
<li>improved the reliability of certain session object saving--I believe some situations where the 'searching immediately' and 'this search was completed' status where not being saved for some page queries. this _may_ solve a long time bug where some pages would refresh on load</li>
<li>all search pages that load with files now explicitly reaffirm internally that they are starting with a completed search, which should reduce some related edge case buggy behaviour here</li>
<li>the 'string to string' edit control now tries to compensate if it is incorrectly given non-string data. somewhere in the html parsing formula UI this happened, an integer sneaking in the key/value of the tag rule, maybe by manual human JSON editing, but I'm not really sure. should be handled correctly now though. let me know if you are into this and discover anything</li>
<li>every 'eventFilter' in the program now catches Exceptions ruthlessly. it turns out Qt can't handle an Exception escaping one of these, and this _may_ be the cause of some >=v530 crashing on macOS related to multi-column list interaction under issue #1379. it is probably the cause of some other crashes that I haven't been able to figure out--these will now give normal popup errors, so let me know if you see anything. if you have had crazy crashes in macOS recently and these changes don't fix you, reverting back to v529 is apparently ok! there have been no big database updates in that time, so you should be able to just install v529 on your existing install and be off</li>
<li>the routine that purges files from the trash now uses fewer database queries to find eligable files. some Linux guys have been working with me on memory explosions possibly in this area--let me know if you notice any difference</li>
<li>the 'clear trash' command in review services is politer to your database, breaking up a large amount of trash into smaller groups</li>
<li>the program no longer moans to the log when it physically deletes a file and files no accompanying thumbnail to delete--this is true for several situations, and not worth the logspam</li>
<li>fixed a typo error in the `url class links` 'try to fill in the gaps' command</li>
<li><h3>pixiv downloader</h3></li>
<li>I reworked the pixiv parser changes from a couple weeks ago. as background, what happened is pixiv said if you aren't logged in, you can't get the 'original' quality of the file any more. my first fix was to say 'ok, if the user is not logged in, get the lower quality', but this was the wrong decision. the parser now vetoes, causing an 'ignored' result and telling you the problem in the import note. if you _do_ want to get the lower quality image and not log in, this is now selectable as an alternate parser under _network->downloader components->manage url class links_</li>
<li>also, a variety of old pixiv objects and other experiments are deleted and merged today. the parsers that worked on the old html format, `pixiv manga page parser`, `pixiv manga_big page parser`, `pixiv single file page parser - new layout`, and `pixiv tag search gallery page parser` will be deleted from your client, and the old gallery url class, `pixiv tag search gallery page` meets a similar fate. `pixiv manga_big page` and `pixiv manga page` are removed and their urls merged into a more accomodating `pixiv file page`, which stays to hold all the legacy pixiv URLs, which on the site are automatically redirected to the new format. thanks to a user for helping me with what here was cruft (issue #947)</li>
<li><h3>mpv logging and emergency halt</h3></li>
<li>a user sent me a cool truncated twitter video download that, when loaded into mpv, would crash the program after a click or two around the player. this sent me on an odyssey into the mpv logging system and event loop and some really bizarre behaviour under the hood, and, long story short, mpv will notice this particular problem class in future and immediately unload the file and present the user with a dialog explaining the issue. it also won't let you load that file again that boot</li>
<li>to recognise this error class, I broaden what is logged and scan the lines as they come in. I've been careful in how I filter, but it may produce some false positives. let me know if this thing triggers for any files that seem fine in an external player</li>
<li>errors of unknown severity are now printed silently to the log with a little intro text saying which file it was and so on. there are a bunch of these with the sorts of files we deal with, stuff like missing chapter marks or borked header data. I expect I'll work on silencing the ones we confirm are no big deal, but if you encounter a ton of them, particularly if you know some cause crashes, please now check your log and let me know what you see</li>
<li>if you have two mpv players playing media at the same time, this reporting system will report the info for both files--sorry, I had to hack this gubbins! future versions of mpv or python-mpv may open some doors here</li>
<li><h3>client api</h3></li>
<li>the `/get_files/file` command now has a `download=true` parameter which converts the `Content-Disposition` from `inline` (show the file) to `attachment` (auto-download or open save-as dialog) (issue #1375)</li>
<li>added help and a unit test for the above</li>
<li>client api version is now 47</li>
</ul>
</li>
<li>
<h2 id="version_531"><a href="#version_531">version 531</a></h2>
<ul>

View File

@ -83,14 +83,23 @@ class PubSubEventCatcher( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == PubSubEventType and isinstance( event, PubSubEvent ):
try:
if self._pubsub.WorkToDo():
if event.type() == PubSubEventType and isinstance( event, PubSubEvent ):
self._pubsub.Process()
if self._pubsub.WorkToDo():
self._pubsub.Process()
event.accept()
return True
event.accept()
except Exception as e:
HydrusData.ShowException( e )
return True

View File

@ -71,6 +71,9 @@ def DAEMONCheckImportFolders():
def DAEMONMaintainTrash():
# TODO: Looking at it, this whole thing is whack
# rewrite it to be a database command that returns 'more work to do' and then just spam it until done
controller = HG.client_controller
if HC.options[ 'trash_max_size' ] is not None:
@ -81,25 +84,35 @@ def DAEMONMaintainTrash():
while service_info[ HC.SERVICE_INFO_TOTAL_SIZE ] > max_size:
if HydrusThreading.IsThreadShuttingDown():
return
hashes = controller.Read( 'trash_hashes', limit = 10 )
hashes = controller.Read( 'trash_hashes', limit = 256 )
if len( hashes ) == 0:
return
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
service_info = controller.Read( 'service_info', CC.TRASH_SERVICE_KEY )
for group_of_hashes in HydrusData.SplitIteratorIntoChunks( hashes, 8 ):
if HydrusThreading.IsThreadShuttingDown():
return
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, group_of_hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
time.sleep( 0.01 )
service_info = controller.Read( 'service_info', CC.TRASH_SERVICE_KEY )
if service_info[ HC.SERVICE_INFO_TOTAL_SIZE ] <= max_size:
break
time.sleep( 2 )
@ -109,22 +122,27 @@ def DAEMONMaintainTrash():
max_age = HC.options[ 'trash_max_age' ] * 3600
hashes = controller.Read( 'trash_hashes', limit = 10, minimum_age = max_age )
hashes = controller.Read( 'trash_hashes', limit = 256, minimum_age = max_age )
while len( hashes ) > 0:
if HydrusThreading.IsThreadShuttingDown():
for group_of_hashes in HydrusData.SplitIteratorIntoChunks( hashes, 8 ):
return
if HydrusThreading.IsThreadShuttingDown():
return
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, group_of_hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
time.sleep( 0.01 )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
hashes = controller.Read( 'trash_hashes', limit = 10, minimum_age = max_age )
hashes = controller.Read( 'trash_hashes', limit = 256, minimum_age = max_age )
time.sleep( 2 )

View File

@ -1274,10 +1274,6 @@ class ClientFilesManager( object ):
num_thumbnails_deleted += 1
else:
HydrusData.Print( 'Wanted to physically delete the "{}" thumbnail, but it was not found!'.format( file_hash.hex() ) )
self._controller.WriteSynchronous( 'clear_deferred_physical_delete', file_hash = file_hash, thumbnail_hash = thumbnail_hash )

View File

@ -171,6 +171,7 @@ class GIFRenderer( object ):
self._pil_global_palette = self._pil_image.palette
# TODO: u wot mate, why is this 'and False'? I assume this is preservation/transferrance of frame/image metadata, but it's been off for years probably, so...
if self._pil_global_palette is not None and False:
self._pil_dirty = self._pil_image.palette.dirty

View File

@ -9392,6 +9392,56 @@ class DB( HydrusDB.HydrusDB ):
if version == 531:
try:
domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
domain_manager.Initialise()
#
domain_manager.OverwriteDefaultParsers( [
'pixiv file page api parser',
'pixiv file page api parser (gets lower quality image if not logged in)'
] )
domain_manager.OverwriteDefaultURLClasses( [
'pixiv file page'
] )
domain_manager.DeleteParsers( [
'pixiv manga page parser',
'pixiv manga_big page parser',
'pixiv single file page parser - new layout',
'pixiv tag search gallery page parser'
] )
domain_manager.DeleteURLClasses( [
'pixiv manga page',
'pixiv manga_big page',
'pixiv tag search gallery page'
] )
#
domain_manager.TryToLinkURLClassesAndParsers()
#
self.modules_serialisable.SetJSONDump( domain_manager )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )

View File

@ -100,6 +100,7 @@ from hydrus.client.gui.services import ClientGUIServersideServices
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.media import ClientMediaResult
from hydrus.client.metadata import ClientTags
from hydrus.client.search import ClientSearch
MENU_ORDER = [ 'file', 'undo', 'pages', 'database', 'network', 'services', 'tags', 'pending', 'help' ]
@ -533,6 +534,7 @@ class FrameGUI( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindows.M
self._controller.sub( self, 'DeleteOldClosedPages', 'delete_old_closed_pages' )
self._controller.sub( self, 'DoFileStorageRebalance', 'do_file_storage_rebalance' )
self._controller.sub( self, 'MaintainMemory', 'memory_maintenance_pulse' )
self._controller.sub( self, 'NewPageDuplicates', 'new_page_duplicates' )
self._controller.sub( self, 'NewPageImportHDD', 'new_hdd_import' )
self._controller.sub( self, 'NewPageQuery', 'new_page_query' )
self._controller.sub( self, 'NotifyAdvancedMode', 'notify_advanced_mode' )
@ -7099,32 +7101,41 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def eventFilter( self, watched, event ):
if watched == self:
try:
if event.type() == QC.QEvent.WindowStateChange:
if watched == self:
was_minimised = event.oldState() == QC.Qt.WindowMinimized
is_minimised = self.isMinimized()
if was_minimised != is_minimised:
if event.type() == QC.QEvent.WindowStateChange:
if self._have_system_tray_icon:
self._system_tray_icon.SetUIIsCurrentlyMinimised( is_minimised )
was_minimised = event.oldState() == QC.Qt.WindowMinimized
is_minimised = self.isMinimized()
if is_minimised:
if was_minimised != is_minimised:
self._was_maximised = event.oldState() == QC.Qt.WindowMaximized
if not self._currently_minimised_to_system_tray and self._controller.new_options.GetBoolean( 'minimise_client_to_system_tray' ):
if self._have_system_tray_icon:
self._FlipShowHideWholeUI()
self._system_tray_icon.SetUIIsCurrentlyMinimised( is_minimised )
if is_minimised:
self._was_maximised = event.oldState() == QC.Qt.WindowMaximized
if not self._currently_minimised_to_system_tray and self._controller.new_options.GetBoolean( 'minimise_client_to_system_tray' ):
self._FlipShowHideWholeUI()
except Exception as e:
HydrusData.ShowException( e )
return True
return False
@ -7437,6 +7448,29 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._menu_updater_database.update()
def NewPageDuplicates(
self,
location_context: ClientLocation.LocationContext,
initial_predicates = None,
page_name = None,
select_page = True,
activate_window = False
):
self._notebook.NewPageDuplicateFilter(
location_context,
initial_predicates = initial_predicates,
page_name = page_name,
on_deepest_notebook = True,
select_page = select_page
)
if activate_window and not self.isActiveWindow():
self.activateWindow()
def NewPageImportHDD( self, paths, file_import_options, metadata_routers, paths_to_additional_service_keys_to_tags, delete_after_success ):
management_controller = ClientGUIManagementController.CreateManagementControllerImportHDD( paths, file_import_options, metadata_routers, paths_to_additional_service_keys_to_tags, delete_after_success )
@ -7457,16 +7491,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
activate_window = False
):
if initial_hashes is None:
initial_hashes = []
if initial_predicates is None:
initial_predicates = []
self._notebook.NewPageQuery(
location_context,
initial_hashes = initial_hashes,

View File

@ -2460,7 +2460,7 @@ class EditURLClassLinksPanel( ClientGUIScrolledPanels.EditPanel ):
self._parser_list_ctrl.AddDatas( new_datas )
self._parser_list_ctrl_listctrl.SelectDatas( new_datas )
self._parser_list_ctrl.SelectDatas( new_datas )
self._parser_list_ctrl.Sort()

View File

@ -191,21 +191,31 @@ class FileDropTarget( QC.QObject ):
self._media_callable = media_callable
def eventFilter( self, object, event ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.Drop:
try:
if self.OnDrop( event.position().toPoint().x(), event.position().toPoint().y() ):
if event.type() == QC.QEvent.Drop:
event.setDropAction( self.OnData( event.mimeData(), event.proposedAction() ) )
if self.OnDrop( event.position().toPoint().x(), event.position().toPoint().y() ):
event.setDropAction( self.OnData( event.mimeData(), event.proposedAction() ) )
event.accept()
elif event.type() == QC.QEvent.DragEnter:
event.accept()
elif event.type() == QC.QEvent.DragEnter:
except Exception as e:
event.accept()
HydrusData.ShowException( e )
return True
return False

View File

@ -221,9 +221,18 @@ class StatusBarRedirectFilter( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.StatusTip:
try:
QW.QApplication.instance().sendEvent( HG.client_controller.gui, event )
if event.type() == QC.QEvent.StatusTip:
QW.QApplication.instance().sendEvent( HG.client_controller.gui, event )
return True
except Exception as e:
HydrusData.ShowException( e )
return True

View File

@ -1047,16 +1047,25 @@ class PopupMessageManager( QW.QFrame ):
def eventFilter( self, watched, event ):
if watched == self.parentWidget():
try:
if event.type() in ( QC.QEvent.Resize, QC.QEvent.Move, QC.QEvent.WindowStateChange ):
if watched == self.parentWidget():
if self._OKToAlterUI():
if event.type() in ( QC.QEvent.Resize, QC.QEvent.Move, QC.QEvent.WindowStateChange ):
self._SizeAndPositionAndShow()
if self._OKToAlterUI():
self._SizeAndPositionAndShow()
except Exception as e:
HydrusData.ShowException( e )
return True
return False

View File

@ -16,29 +16,38 @@ class ResizingEventFilter( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.Resize:
try:
parent = self.parent()
if event.type() == QC.QEvent.Resize:
parent = self.parent()
if isinstance( parent, ResizingScrolledPanel ):
# weird hack fix for a guy who was getting QPaintEvents in here
if not hasattr( event, 'oldSize' ):
return False
old_size = event.oldSize()
size = event.size()
width_larger = size.width() > old_size.width() and size.height() >= old_size.height()
height_larger = size.width() >= old_size.width() and size.height() > old_size.height()
if width_larger or height_larger:
QP.CallAfter( parent.WidgetJustSized, width_larger, height_larger )
if isinstance( parent, ResizingScrolledPanel ):
# weird hack fix for a guy who was getting QPaintEvents in here
if not hasattr( event, 'oldSize' ):
return False
old_size = event.oldSize()
size = event.size()
width_larger = size.width() > old_size.width() and size.height() >= old_size.height()
height_larger = size.width() >= old_size.width() and size.height() > old_size.height()
if width_larger or height_larger:
QP.CallAfter( parent.WidgetJustSized, width_larger, height_larger )
except Exception as e:
HydrusData.ShowException( e )
return True
return False

View File

@ -1418,94 +1418,19 @@ class ShortcutsHandler( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.KeyPress:
try:
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._filter_target, watched, event = event )
shortcut = ConvertKeyEventToShortcut( event )
if shortcut is not None:
if HG.shortcut_report_mode:
message = 'Key shortcut "{}" passing through {}.'.format( shortcut.ToString(), repr( self._parent ) )
if i_should_catch_shortcut_event:
message += ' I am in a state to catch it.'
else:
message += ' I am not in a state to catch it.'
HydrusData.ShowText( message )
if i_should_catch_shortcut_event:
shortcut_processed = self._ProcessShortcut( shortcut )
if shortcut_processed:
event.accept()
return True
elif self._catch_mouse:
if event.type() in ( QC.QEvent.MouseButtonPress, QC.QEvent.MouseButtonRelease, QC.QEvent.MouseButtonDblClick, QC.QEvent.Wheel ):
global CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH
if event.type() == QC.QEvent.MouseButtonPress:
self._last_click_down_position = event.globalPosition().toPoint()
CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH = 0
#if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not HydrusTime.TimeHasPassedPrecise( self._frame_activated_time + 0.017 ):
if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not self._parent_currently_activated:
if event.type() == QC.QEvent.MouseButtonRelease and self._activating_wait_job is not None:
# first completed click in the time window sets us active instantly
self._activating_wait_job.Cancel()
self._parent_currently_activated = True
return False
if event.type() == QC.QEvent.MouseButtonRelease:
release_press_pos = event.globalPosition().toPoint()
delta = release_press_pos - self._last_click_down_position
approx_distance = delta.manhattanLength() + CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH
# if mouse release is some distance from mouse down (i.e. we are ending a drag), then don't fire off a release command
if approx_distance > 20:
return False
if event.type() == QC.QEvent.KeyPress:
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._filter_target, watched, event = event )
shortcut = ConvertMouseEventToShortcut( event )
shortcut = ConvertKeyEventToShortcut( event )
if shortcut is not None:
if HG.shortcut_report_mode:
message = 'Mouse Press shortcut "' + shortcut.ToString() + '" passing through ' + repr( self._parent ) + '.'
message = 'Key shortcut "{}" passing through {}.'.format( shortcut.ToString(), repr( self._parent ) )
if i_should_catch_shortcut_event:
@ -1532,6 +1457,90 @@ class ShortcutsHandler( QC.QObject ):
elif self._catch_mouse:
if event.type() in ( QC.QEvent.MouseButtonPress, QC.QEvent.MouseButtonRelease, QC.QEvent.MouseButtonDblClick, QC.QEvent.Wheel ):
global CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH
if event.type() == QC.QEvent.MouseButtonPress:
self._last_click_down_position = event.globalPosition().toPoint()
CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH = 0
#if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not HydrusTime.TimeHasPassedPrecise( self._frame_activated_time + 0.017 ):
if event.type() != QC.QEvent.Wheel and self._ignore_activating_mouse_click and not self._parent_currently_activated:
if event.type() == QC.QEvent.MouseButtonRelease and self._activating_wait_job is not None:
# first completed click in the time window sets us active instantly
self._activating_wait_job.Cancel()
self._parent_currently_activated = True
return False
if event.type() == QC.QEvent.MouseButtonRelease:
release_press_pos = event.globalPosition().toPoint()
delta = release_press_pos - self._last_click_down_position
approx_distance = delta.manhattanLength() + CUMULATIVE_MOUSEWARP_MANHATTAN_LENGTH
# if mouse release is some distance from mouse down (i.e. we are ending a drag), then don't fire off a release command
if approx_distance > 20:
return False
i_should_catch_shortcut_event = IShouldCatchShortcutEvent( self._filter_target, watched, event = event )
shortcut = ConvertMouseEventToShortcut( event )
if shortcut is not None:
if HG.shortcut_report_mode:
message = 'Mouse Press shortcut "' + shortcut.ToString() + '" passing through ' + repr( self._parent ) + '.'
if i_should_catch_shortcut_event:
message += ' I am in a state to catch it.'
else:
message += ' I am not in a state to catch it.'
HydrusData.ShowText( message )
if i_should_catch_shortcut_event:
shortcut_processed = self._ProcessShortcut( shortcut )
if shortcut_processed:
event.accept()
return True
except Exception as e:
HydrusData.ShowException( e )
return True
return False
@ -1638,13 +1647,22 @@ class ShortcutsDeactivationCatcher( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.WindowActivate:
try:
self._shortcuts_handler.FrameActivated()
if event.type() == QC.QEvent.WindowActivate:
self._shortcuts_handler.FrameActivated()
elif event.type() == QC.QEvent.WindowDeactivate:
self._shortcuts_handler.FrameDeactivated()
elif event.type() == QC.QEvent.WindowDeactivate:
except Exception as e:
self._shortcuts_handler.FrameDeactivated()
HydrusData.ShowException( e )
return True
return False

View File

@ -538,7 +538,7 @@ class StringToStringDictControl( QW.QWidget ):
self.Clear()
self._listctrl.AddDatas( list( str_to_str_dict.items() ) )
self._listctrl.AddDatas( [ ( str( key ), str( value ) ) for ( key, value ) in str_to_str_dict.items() ] )
self._listctrl.Sort()

View File

@ -58,9 +58,19 @@ class FocusEventFilter(QC.QObject):
def __init__(self, parent = None):
super().__init__(parent)
def eventFilter(self, object, event) -> bool:
if event.type() == QC.QEvent.FocusIn:
self.focused.emit()
def eventFilter(self, watched, event) -> bool:
try:
if event.type() == QC.QEvent.FocusIn:
self.focused.emit()
except Exception as e:
return True
return False
class QLocatorSearchResult:

View File

@ -16,7 +16,6 @@ from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusProfiling
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import QtInit
@ -1287,20 +1286,29 @@ class CallAfterEventCatcher( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == CallAfterEventType and isinstance( event, CallAfterEvent ):
try:
if HG.profile_mode:
if event.type() == CallAfterEventType and isinstance( event, CallAfterEvent ):
summary = 'Profiling CallAfter Event: {}'.format( event._fn )
if HG.profile_mode:
summary = 'Profiling CallAfter Event: {}'.format( event._fn )
HydrusProfiling.Profile( summary, 'event.Execute()', globals(), locals(), min_duration_ms = HG.callto_profile_min_job_time_ms )
else:
event.Execute()
HydrusProfiling.Profile( summary, 'event.Execute()', globals(), locals(), min_duration_ms = HG.callto_profile_min_job_time_ms )
event.accept()
else:
event.Execute()
return True
event.accept()
except Exception as e:
HydrusData.ShowException( e )
return True
@ -2221,101 +2229,111 @@ class WidgetEventFilter ( QC.QObject ):
def eventFilter( self, watched, event ):
# Once somehow this got called with no _parent_widget set - which is probably fixed now but leaving the check just in case, wew
# Might be worth debugging this later if it still occurs - the only way I found to reproduce it is to run the help > debug > initialize server command
if not hasattr( self, '_parent_widget') or not isValid( self._parent_widget ): return False
type = event.type()
event_killed = False
if type == QC.QEvent.KeyPress:
try:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KEY_DOWN', event )
# Once somehow this got called with no _parent_widget set - which is probably fixed now but leaving the check just in case, wew
# Might be worth debugging this later if it still occurs - the only way I found to reproduce it is to run the help > debug > initialize server command
if not hasattr( self, '_parent_widget') or not isValid( self._parent_widget ): return False
elif type == QC.QEvent.WindowStateChange:
type = event.type()
if isValid( self._parent_widget ):
event_killed = False
if type == QC.QEvent.KeyPress:
if self._parent_widget.isMaximized() or (event.oldState() & QC.Qt.WindowMaximized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MAXIMIZE', event )
elif type == QC.QEvent.MouseMove:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DCLICK', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KEY_DOWN', event )
elif event.button() == QC.Qt.RightButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DCLICK', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonPress:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DOWN', event )
if event.buttons() & QC.Qt.MiddleButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MIDDLE_DOWN', event )
if event.buttons() & QC.Qt.RightButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DOWN', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonRelease:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_UP', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Wheel:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSEWHEEL', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Scroll:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SCROLLWIN', event )
elif type == QC.QEvent.Move:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE', event )
if isValid( self._parent_widget ) and self._parent_widget.isVisible():
elif type == QC.QEvent.WindowStateChange:
self._user_moved_window = True
if isValid( self._parent_widget ):
if self._parent_widget.isMaximized() or (event.oldState() & QC.Qt.WindowMaximized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MAXIMIZE', event )
elif type == QC.QEvent.Resize:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SIZE', event )
elif type == QC.QEvent.NonClientAreaMouseButtonPress:
self._user_moved_window = False
elif type == QC.QEvent.NonClientAreaMouseButtonRelease:
if self._user_moved_window:
elif type == QC.QEvent.MouseMove:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE_END', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DCLICK', event )
elif event.button() == QC.Qt.RightButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DCLICK', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonPress:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DOWN', event )
if event.buttons() & QC.Qt.MiddleButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MIDDLE_DOWN', event )
if event.buttons() & QC.Qt.RightButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DOWN', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonRelease:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_UP', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Wheel:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSEWHEEL', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Scroll:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SCROLLWIN', event )
elif type == QC.QEvent.Move:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE', event )
if isValid( self._parent_widget ) and self._parent_widget.isVisible():
self._user_moved_window = True
elif type == QC.QEvent.Resize:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SIZE', event )
elif type == QC.QEvent.NonClientAreaMouseButtonPress:
self._user_moved_window = False
elif type == QC.QEvent.NonClientAreaMouseButtonRelease:
if self._user_moved_window:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE_END', event )
self._user_moved_window = False
if event_killed:
if event_killed:
event.accept()
return True
event.accept()
except Exception as e:
HydrusData.ShowException( e )
return True
return False
def _AddCallback( self, evt_name, callback ):

View File

@ -290,7 +290,16 @@ class LayoutEventSilencer( QC.QObject ):
def eventFilter( self, watched, event ):
if watched == self.parent() and event.type() == QC.QEvent.LayoutRequest:
try:
if watched == self.parent() and event.type() == QC.QEvent.LayoutRequest:
return True
except Exception as e:
HydrusData.ShowException( e )
return True
@ -1306,13 +1315,22 @@ class MediaContainerDragClickReportingFilter( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.LeftButton:
try:
self._canvas.BeginDrag()
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.LeftButton:
self._canvas.BeginDrag()
elif event.type() == QC.QEvent.MouseButtonRelease and event.button() == QC.Qt.LeftButton:
self._canvas.EndDrag()
elif event.type() == QC.QEvent.MouseButtonRelease and event.button() == QC.Qt.LeftButton:
except Exception as e:
self._canvas.EndDrag()
HydrusData.ShowException( e )
return True
return False

View File

@ -462,11 +462,20 @@ class CanvasHoverFrame( QW.QFrame ):
def eventFilter( self, object, event ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.Resize:
try:
self._SizeAndPosition()
if event.type() == QC.QEvent.Resize:
self._SizeAndPosition()
except Exception as e:
HydrusData.ShowException( e )
return True
return False
@ -1490,21 +1499,30 @@ class NotePanel( QW.QWidget ):
self._note_text.installEventFilter( self )
def eventFilter( self, object, event ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.MouseButtonPress:
try:
if event.button() == QC.Qt.LeftButton:
if event.type() == QC.QEvent.MouseButtonPress:
self.editNote.emit( self._name )
if event.button() == QC.Qt.LeftButton:
self.editNote.emit( self._name )
else:
self._note_text.setVisible( not self._note_text.isVisible() )
self._note_visible = self._note_text.isVisible()
else:
self._note_text.setVisible( not self._note_text.isVisible() )
self._note_visible = self._note_text.isVisible()
return True
except Exception as e:
HydrusData.ShowException( e )
return True

View File

@ -1526,7 +1526,6 @@ class MediaContainer( QW.QWidget ):
def _MakeMediaWindow( self ):
old_media_window = self._media_window
destroy_old_media_window = True
do_neighbour_prefetch_emit = True

View File

@ -1,3 +1,4 @@
import functools
import locale
import os
import traceback
@ -37,6 +38,8 @@ except Exception as e:
MPV_IS_AVAILABLE = False
damaged_file_hashes = set()
def GetClientAPIVersionString():
try:
@ -72,9 +75,37 @@ def GetClientAPIVersionString():
'''
def EmergencyDumpOutGlobal( probably_crashy, reason ):
# this is Qt thread so we can talk to this guy no prob
MPVHellBasket.instance().emergencyDumpOut.emit( probably_crashy, reason )
def log_handler( loglevel, component, message ):
HydrusData.DebugPrint( '[{}] {}: {}'.format( loglevel, component, message ) )
# ok important bug dude, if you have multiple mpv windows and hence log handlers, then somehow the mpv dll or python-mpv wrapper is delivering at least some log events to the wrong player's event loop
# so my mapping here to preserve the mpv widget for a particular log message and then dump out the player in emergency is only going to work half the time
nah_it_is_fine_bro_tests = [
'rescan-external-files' in message
]
if True in nah_it_is_fine_bro_tests and not HG.mpv_report_mode:
return
if loglevel == 'error' and 'ffmpeg' in component:
probably_crashy_tests = [
'Invalid NAL unit size' in message,
'Error splitting the input' in message
]
HG.client_controller.CallBlockingToQt( HG.client_controller.gui, EmergencyDumpOutGlobal, True in probably_crashy_tests, f'{component}: {message}' )
HydrusData.DebugPrint( '[MPV {}] {}: {}'.format( loglevel, component, message ) )
MPVFileLoadedEventType = QP.registerEventType()
@ -86,7 +117,20 @@ class MPVFileLoadedEvent( QC.QEvent ):
QC.QEvent.__init__( self, MPVFileLoadedEventType )
'''
MPVLogEventType = QP.registerEventType()
class MPVLogEvent( QC.QEvent ):
def __init__( self, player, event ):
QC.QEvent.__init__( self, MPVLogEventType )
self.player = player
self.event = event
'''
MPVFileSeekedEventType = QP.registerEventType()
class MPVFileSeekedEvent( QC.QEvent ):
@ -97,6 +141,30 @@ class MPVFileSeekedEvent( QC.QEvent ):
class MPVHellBasket( QC.QObject ):
emergencyDumpOut = QC.Signal( bool, str )
my_instance = None
def __init__( self ):
QC.QObject.__init__( self )
MPVHellBasket.my_instance = self
@staticmethod
def instance() -> 'MPVHellBasket':
if MPVHellBasket.my_instance is None:
MPVHellBasket.my_instance = MPVHellBasket()
return MPVHellBasket.my_instance
LOCALE_IS_SET = False
#Not sure how well this works with hardware acceleration. This just renders to a QWidget. In my tests it seems fine, even with vdpau video out, but I'm not 100% sure it actually uses hardware acceleration.
@ -129,9 +197,13 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self.setAttribute( QC.Qt.WA_NativeWindow )
# loglevels: fatal, error, debug
loglevel = 'debug' if HG.mpv_report_mode else 'fatal'
loglevel = 'debug' if HG.mpv_report_mode else 'error'
self._player = mpv.MPV( wid = str( int( self.winId() ) ), log_handler = log_handler, loglevel = loglevel )
self._player = mpv.MPV(
wid = str( int( self.winId() ) ),
log_handler = log_handler,
loglevel = loglevel
)
# hydev notes on OSC:
# OSC is by default off, default input bindings are by default off
@ -163,6 +235,7 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._file_is_loaded = False
self._disallow_seek_on_this_file = False
self._have_shown_human_error_on_this_file = False
self._times_to_play_animation = 0
@ -190,6 +263,8 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self.we_are_newer_api = False
MPVHellBasket.instance().emergencyDumpOut.connect( self.EmergencyDumpOut )
def _GetAudioOptionNames( self ):
@ -268,7 +343,13 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
QW.QApplication.instance().postEvent( self, MPVFileLoadedEvent() )
'''
@player.event_callback( mpv.MpvEventID.LOG_MESSAGE )
def log_event( event ):
QW.QApplication.instance().postEvent( self, MPVLogEvent( player, event ) )
'''
def ClearMedia( self ):
@ -277,35 +358,44 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def eventFilter( self, watched, event ):
if event.type() == MPVFileLoadedEventType:
try:
self._file_is_loaded = True
return True
elif event.type() == MPVFileSeekedEventType:
if not self._file_is_loaded:
if event.type() == MPVFileLoadedEventType:
self._file_is_loaded = True
return True
elif event.type() == MPVFileSeekedEventType:
if not self._file_is_loaded:
return True
current_timestamp_s = self._player.time_pos
if self._media is not None and current_timestamp_s is not None and current_timestamp_s <= 1.0:
self._current_seek_to_start_count += 1
if self._stop_for_slideshow:
self.Pause()
if self._times_to_play_animation != 0 and self._current_seek_to_start_count >= self._times_to_play_animation:
self.Pause()
return True
current_timestamp_s = self._player.time_pos
except Exception as e:
if self._media is not None and current_timestamp_s is not None and current_timestamp_s <= 1.0:
self._current_seek_to_start_count += 1
if self._stop_for_slideshow:
self.Pause()
if self._times_to_play_animation != 0 and self._current_seek_to_start_count >= self._times_to_play_animation:
self.Pause()
HydrusData.ShowException( e )
return True
@ -313,6 +403,58 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
return False
def EmergencyDumpOut( self, probably_crashy, reason ):
# we had to rewrite this thing due to some threading/loop/event issues at the lower mpv level
# when we have an emergency, we now broadcast to all mpv players at once, they all crash out, to be safe
original_media = self._media
if original_media is None:
# this MPV window is probably not the one that had a problem
return
media_line = '\n\nIts hash is: {}'.format( original_media.GetHash().hex() )
if probably_crashy:
self.ClearMedia()
global damaged_file_hashes
hash = original_media.GetHash()
if hash in damaged_file_hashes:
return
damaged_file_hashes.add( hash )
if not self._have_shown_human_error_on_this_file:
self._have_shown_human_error_on_this_file = True
if probably_crashy:
message = f'Sorry, this media appears to have a serious problem! To avoid crashes, MPV will not attempt to play it! The file is possibly truncated or otherwise corrupted, but if you think it is good, please send it to hydev for more testing. The specific errors should be written to the log.{media_line}'
HydrusData.DebugPrint( message )
QP.CallAfter( QW.QMessageBox.critical, self, 'Error', f'{message}\n\nThe first error was:\n\n{reason}' )
else:
message = f'A media loaded in MPV appears to have had an error. This may be not a big deal, or it may be a crash. The specific errors should be written after this message. They are not positively known as crashy, but if you are getting crashes, please send the file and these errors to hydev so he can test his end.{media_line}'
HydrusData.DebugPrint( message )
def GetAnimationBarStatus( self ):
buffer_indices = None
@ -544,6 +686,15 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
return
global damaged_file_hashes
if media is not None and media.GetHash() in damaged_file_hashes:
self.ClearMedia()
return
self._file_is_loaded = False
self._disallow_seek_on_this_file = False
@ -579,18 +730,23 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
try:
self._player.command( 'playlist-remove', 'current' )
self._player.command( 'stop' )
# used to have this, it could raise errors if the load failed
# self._player.command( 'playlist-remove', 'current' )
except Exception as e:
HydrusData.PrintException( e )
pass # sometimes happens after an error, let's see if we can figure it out, like wait for buffering to finish and try again?
pass
else:
self._have_shown_human_error_on_this_file = False
hash = self._media.GetHash()
mime = self._media.GetMime()

View File

@ -2032,47 +2032,56 @@ class ListBox( QW.QScrollArea ):
def eventFilter( self, watched, event ):
# we do the event filter since we need to 'scroll' the click, so we capture the event on the widget, not ourselves
if watched == self.widget():
try:
if event.type() == QC.QEvent.MouseButtonPress:
# we do the event filter since we need to 'scroll' the click, so we capture the event on the widget, not ourselves
if watched == self.widget():
self._HandleClick( event )
event.accept()
return True
elif event.type() == QC.QEvent.MouseButtonRelease:
self._in_drag = False
event.ignore()
elif event.type() == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton:
if event.type() == QC.QEvent.MouseButtonPress:
ctrl_down = event.modifiers() & QC.Qt.ControlModifier
shift_down = event.modifiers() & QC.Qt.ShiftModifier
self._HandleClick( event )
action_occurred = self._Activate( ctrl_down, shift_down )
event.accept()
if action_occurred:
return True
elif event.type() == QC.QEvent.MouseButtonRelease:
self._in_drag = False
event.ignore()
elif event.type() == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton:
self.mouseActivationOccurred.emit()
ctrl_down = event.modifiers() & QC.Qt.ControlModifier
shift_down = event.modifiers() & QC.Qt.ShiftModifier
action_occurred = self._Activate( ctrl_down, shift_down )
if action_occurred:
self.mouseActivationOccurred.emit()
else:
QW.QScrollArea.mouseDoubleClickEvent( self, event )
else:
event.accept()
QW.QScrollArea.mouseDoubleClickEvent( self, event )
return True
event.accept()
return True
except Exception as e:
HydrusData.ShowException( e )
return True
return False
@ -2424,6 +2433,26 @@ class ListBoxTags( ListBox ):
return False
def _NewDuplicateFilterPage( self, predicates ):
activate_window = HG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
predicates = ClientGUISearch.FleshOutPredicates( self, predicates )
if len( predicates ) == 0:
return
s = sorted( ( predicate.ToString() for predicate in predicates ) )
page_name = 'duplicates: ' + ', '.join( s )
location_context = self._GetCurrentLocationContext()
HG.client_controller.pub( 'new_page_duplicates', location_context, initial_predicates = predicates, page_name = page_name, activate_window = activate_window )
def _NewSearchPages( self, pages_of_predicates ):
activate_window = HG.client_controller.new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' )
@ -2597,49 +2626,58 @@ class ListBoxTags( ListBox ):
def eventFilter( self, watched, event ):
# we do the event filter since we need to 'scroll' the click, so we capture the event on the widget, not ourselves
if watched == self.widget():
try:
if event.type() == QC.QEvent.MouseButtonPress:
# we do the event filter since we need to 'scroll' the click, so we capture the event on the widget, not ourselves
if watched == self.widget():
if event.button() == QC.Qt.MiddleButton:
if event.type() == QC.QEvent.MouseButtonPress:
self._HandleClick( event )
if self.can_spawn_new_windows:
if event.button() == QC.Qt.MiddleButton:
(predicates, or_predicate, inverse_predicates, namespace_predicate, inverse_namespace_predicate) = self._GetSelectedPredicatesAndInverseCopies()
self._HandleClick( event )
if len( predicates ) > 0:
if self.can_spawn_new_windows:
shift_down = event.modifiers() & QC.Qt.ShiftModifier
(predicates, or_predicate, inverse_predicates, namespace_predicate, inverse_namespace_predicate) = self._GetSelectedPredicatesAndInverseCopies()
if shift_down and or_predicate is not None:
if len( predicates ) > 0:
predicates = (or_predicate,)
shift_down = event.modifiers() & QC.Qt.ShiftModifier
if shift_down and or_predicate is not None:
predicates = (or_predicate,)
self._NewSearchPages( [ predicates ] )
self._NewSearchPages( [ predicates ] )
event.accept()
return True
event.accept()
elif event.type() == QC.QEvent.MouseButtonRelease:
return True
elif event.type() == QC.QEvent.MouseButtonRelease:
if event.button() == QC.Qt.RightButton:
self.ShowMenu()
event.accept()
return True
if event.button() == QC.Qt.RightButton:
self.ShowMenu()
event.accept()
return True
except Exception as e:
HydrusData.ShowException( e )
return True
return ListBox.eventFilter( self, watched, event )
@ -3049,6 +3087,10 @@ class ListBoxTags( ListBox ):
ClientGUIMenus.AppendSeparator( search_menu )
ClientGUIMenus.AppendMenuItem( search_menu, f'open a new duplicate filter page for {selection_string}', 'Open a new duplicate filter page starting with the selected predicates.', self._NewDuplicateFilterPage, predicates )
ClientGUIMenus.AppendSeparator( search_menu )
self._AddEditMenu( search_menu )

View File

@ -166,7 +166,7 @@ class BetterListCtrl( QW.QTreeWidget ):
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown )
self.itemDoubleClicked.connect( self.EventItemActivated )
self.itemDoubleClicked.connect( self.ProcessActivateAction )
self.header().setSectionsMovable( False ) # can only turn this on when we move from data/sort tuples
# self.header().setFirstSectionMovable( True ) # same
@ -598,7 +598,14 @@ class BetterListCtrl( QW.QTreeWidget ):
if self._activation_callback is not None:
self._activation_callback()
try:
self._activation_callback()
except Exception as e:
HydrusData.ShowException( e )
@ -743,7 +750,14 @@ class BetterListCtrl( QW.QTreeWidget ):
if self._activation_callback is not None:
self._activation_callback()
try:
self._activation_callback()
except Exception as e:
HydrusData.ShowException( e )
@ -1664,7 +1678,14 @@ class BetterListCtrlPanel( QW.QWidget ):
return
self._UpdateButtons()
try:
self._UpdateButtons()
except Exception as e:
HydrusData.ShowException( e )
def ImportFromDragDrop( self, paths ):

View File

@ -42,13 +42,30 @@ def CreateManagementController( page_name, management_type ):
return management_controller
def CreateManagementControllerDuplicateFilter():
def CreateManagementControllerDuplicateFilter(
location_context = None,
initial_predicates = None,
page_name = None
):
default_location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext()
if location_context is None:
location_context = HG.client_controller.new_options.GetDefaultLocalLocationContext()
management_controller = CreateManagementController( 'duplicates', MANAGEMENT_TYPE_DUPLICATE_FILTER )
if initial_predicates is None:
initial_predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING ) ]
file_search_context = ClientSearch.FileSearchContext( location_context = default_location_context, predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_EVERYTHING ) ] )
if page_name is None:
page_name = 'duplicates'
management_controller = CreateManagementController( page_name, MANAGEMENT_TYPE_DUPLICATE_FILTER )
file_search_context = ClientSearch.FileSearchContext( location_context = location_context, predicates = initial_predicates )
synchronised = HG.client_controller.new_options.GetBoolean( 'default_search_synchronised' )
@ -775,6 +792,21 @@ class ManagementController( HydrusSerialisable.SerialisableBase ):
return self._management_type in ( MANAGEMENT_TYPE_IMPORT_HDD, MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER, MANAGEMENT_TYPE_IMPORT_MULTIPLE_GALLERY, MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER, MANAGEMENT_TYPE_IMPORT_URLS )
def NotifyLoadingWithHashes( self ):
if self.HasVariable( 'file_search_context' ):
file_search_context = self.GetVariable( 'file_search_context' )
file_search_context.SetComplete()
def SetDirty( self ):
self._SerialisableChangeMade()
def SetPageName( self, name ):
if name != self._page_name:

View File

@ -988,11 +988,6 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
page_name = 'preparation (needs work)'
if not self._have_done_first_maintenance_numbers_show:
self._main_notebook.SelectPage( self._main_left_panel )
else:
self._num_searched.SetValue( 'All potential duplicates found at this distance.', total_num_files, total_num_files )
@ -1002,8 +997,6 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._main_notebook.setTabText( 0, page_name )
self._have_done_first_maintenance_numbers_show = True
def _UpdatePotentialDuplicatesCount( self, potential_duplicates_count ):
@ -5216,6 +5209,8 @@ class ManagementPanelQuery( ManagementPanel ):
self._management_controller.SetVariable( 'synchronised', synchronised )
self._management_controller.SetDirty()
if synchronised:
self._RefreshQuery()
@ -5321,6 +5316,8 @@ class ManagementPanelQuery( ManagementPanel ):
self._management_controller.SetVariable( 'file_search_context', file_search_context.Duplicate() )
self._management_controller.SetDirty()
QP.CallAfter( qt_code )

View File

@ -443,6 +443,11 @@ class Page( QW.QWidget ):
self._management_controller.SetVariable( 'page_key', self._page_key )
if len( initial_hashes ) > 0:
self._management_controller.NotifyLoadingWithHashes()
self._initialised = len( initial_hashes ) == 0
self._pre_initialisation_media_results = []
@ -2237,64 +2242,73 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
def eventFilter( self, watched, event ):
if event.type() in ( QC.QEvent.MouseButtonDblClick, QC.QEvent.MouseButtonRelease ):
try:
screen_position = QG.QCursor.pos()
if watched == self.tabBar():
if event.type() in ( QC.QEvent.MouseButtonDblClick, QC.QEvent.MouseButtonRelease ):
tab_pos = self.tabBar().mapFromGlobal( screen_position )
screen_position = QG.QCursor.pos()
over_a_tab = tab_pos != -1
over_tab_greyspace = tab_pos == -1
else:
over_a_tab = False
widget_under_mouse = QW.QApplication.instance().widgetAt( screen_position )
if widget_under_mouse is None:
if watched == self.tabBar():
over_tab_greyspace = None
tab_pos = self.tabBar().mapFromGlobal( screen_position )
over_a_tab = tab_pos != -1
over_tab_greyspace = tab_pos == -1
else:
if self.count() == 0 and isinstance( widget_under_mouse, QW.QStackedWidget ):
over_a_tab = False
widget_under_mouse = QW.QApplication.instance().widgetAt( screen_position )
if widget_under_mouse is None:
over_tab_greyspace = True
over_tab_greyspace = None
else:
over_tab_greyspace = widget_under_mouse == self
if self.count() == 0 and isinstance( widget_under_mouse, QW.QStackedWidget ):
over_tab_greyspace = True
else:
over_tab_greyspace = widget_under_mouse == self
if event.type() == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton and over_tab_greyspace and not over_a_tab:
self.EventNewPageFromScreenPosition( screen_position )
return True
elif event.type() == QC.QEvent.MouseButtonRelease:
if event.button() == QC.Qt.RightButton and ( over_a_tab or over_tab_greyspace ):
self.ShowMenuFromScreenPosition( screen_position )
return True
elif event.button() == QC.Qt.MiddleButton and over_tab_greyspace and not over_a_tab:
self.EventNewPageFromScreenPosition( screen_position )
return True
if event.type() == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton and over_tab_greyspace and not over_a_tab:
self.EventNewPageFromScreenPosition( screen_position )
return True
elif event.type() == QC.QEvent.MouseButtonRelease:
if event.button() == QC.Qt.RightButton and ( over_a_tab or over_tab_greyspace ):
self.ShowMenuFromScreenPosition( screen_position )
return True
elif event.button() == QC.Qt.MiddleButton and over_tab_greyspace and not over_a_tab:
self.EventNewPageFromScreenPosition( screen_position )
return True
except Exception as e:
HydrusData.ShowException( e )
return True
return False
@ -3223,11 +3237,18 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
return page
def NewPageDuplicateFilter( self, on_deepest_notebook = False ):
def NewPageDuplicateFilter(
self,
location_context = None,
initial_predicates = None,
page_name = None,
select_page = True,
on_deepest_notebook = False
):
management_controller = ClientGUIManagementController.CreateManagementControllerDuplicateFilter()
management_controller = ClientGUIManagementController.CreateManagementControllerDuplicateFilter( location_context = location_context, initial_predicates = initial_predicates, page_name = page_name )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook )
return self.NewPage( management_controller, on_deepest_notebook = on_deepest_notebook, select_page = select_page )
def NewPageImportGallery( self, page_name = None, on_deepest_notebook = False, select_page = True ):
@ -3285,7 +3306,14 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
if initial_predicates is None:
initial_predicates = []
if len( initial_hashes ) > 0:
initial_predicates = [ ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_SYSTEM_HASH, value = ( tuple( initial_hashes ), 'sha256' ) ) ]
else:
initial_predicates = []
if page_name is None:
@ -3293,7 +3321,8 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
page_name = 'files'
search_enabled = len( initial_hashes ) == 0
search_disabled = len( initial_hashes ) > 0 and len( initial_predicates ) == 0
search_enabled = not search_disabled
new_options = self._controller.new_options
@ -3313,6 +3342,12 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
file_search_context = ClientSearch.FileSearchContext( location_context = location_context, tag_context = tag_context, predicates = initial_predicates )
if len( initial_hashes ) > 0:
# this is important, it is consulted deeper to determine query refresh on start!
file_search_context.SetComplete()
management_controller = ClientGUIManagementController.CreateManagementControllerQuery( page_name, file_search_context, search_enabled )
if initial_sort is not None:

View File

@ -396,15 +396,24 @@ class MediaCollectControl( QW.QWidget ):
def eventFilter( self, watched, event ):
if watched == self._collect_comboctrl:
try:
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.MiddleButton:
if watched == self._collect_comboctrl:
self.SetCollect( ClientMedia.MediaCollect( collect_unmatched = self._media_collect.collect_unmatched ) )
return True
if event.type() == QC.QEvent.MouseButtonPress and event.button() == QC.Qt.MiddleButton:
self.SetCollect( ClientMedia.MediaCollect( collect_unmatched = self._media_collect.collect_unmatched ) )
return True
except Exception as e:
HydrusData.ShowException( e )
return True
return False

View File

@ -1243,66 +1243,98 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def eventFilter( self, watched, event ):
if watched == self._text_ctrl:
try:
if event.type() == QC.QEvent.Wheel:
if watched == self._text_ctrl:
current_results_list = self._dropdown_notebook.currentWidget()
if self._text_ctrl.text() == '' and len( current_results_list ) == 0:
if event.type() == QC.QEvent.Wheel:
if event.angleDelta().y() > 0:
current_results_list = self._dropdown_notebook.currentWidget()
if self._text_ctrl.text() == '' and len( current_results_list ) == 0:
self.movePageLeft.emit()
if event.angleDelta().y() > 0:
self.movePageLeft.emit()
else:
self.movePageRight.emit()
else:
event.accept()
self.movePageRight.emit()
return True
elif event.modifiers() & QC.Qt.ControlModifier:
if event.angleDelta().y() > 0:
current_results_list.MoveSelectionUp()
else:
current_results_list.MoveSelectionDown()
event.accept()
return True
elif self._float_mode and not self._dropdown_hidden:
# it is annoying to scroll on this lad when float is around, so swallow it here
event.accept()
return True
event.accept()
elif self._float_mode:
return True
# I could probably wangle this garbagewith setFocusProxy on all the children of the dropdown, assuming that wouldn't break anything, but this seems to work ok nonetheless
elif event.modifiers() & QC.Qt.ControlModifier:
if event.angleDelta().y() > 0:
if event.type() == QC.QEvent.FocusIn:
current_results_list.MoveSelectionUp()
self._DropdownHideShow()
else:
return False
current_results_list.MoveSelectionDown()
elif event.type() == QC.QEvent.FocusOut:
current_focus_widget = QW.QApplication.focusWidget()
if current_focus_widget is not None and ClientGUIFunctions.IsQtAncestor( current_focus_widget, self._dropdown_window ):
self._temporary_focus_widget = current_focus_widget
self._temporary_focus_widget.installEventFilter( self )
else:
self._DropdownHideShow()
return False
event.accept()
return True
elif self._float_mode and not self._dropdown_hidden:
# it is annoying to scroll on this lad when float is around, so swallow it here
event.accept()
return True
elif self._float_mode:
elif self._temporary_focus_widget is not None and watched == self._temporary_focus_widget:
# I could probably wangle this garbagewith setFocusProxy on all the children of the dropdown, assuming that wouldn't break anything, but this seems to work ok nonetheless
if event.type() == QC.QEvent.FocusIn:
if self._float_mode and event.type() == QC.QEvent.FocusOut:
self._DropdownHideShow()
self._temporary_focus_widget.removeEventFilter( self )
return False
elif event.type() == QC.QEvent.FocusOut:
self._temporary_focus_widget = None
current_focus_widget = QW.QApplication.focusWidget()
if current_focus_widget is not None and ClientGUIFunctions.IsQtAncestor( current_focus_widget, self._dropdown_window ):
if current_focus_widget is None:
# happens sometimes when moving tabs in the tags dropdown list
ClientGUIFunctions.SetFocusLater( self._text_ctrl )
elif ClientGUIFunctions.IsQtAncestor( current_focus_widget, self._dropdown_window ):
self._temporary_focus_widget = current_focus_widget
@ -1317,34 +1349,11 @@ class AutoCompleteDropdown( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
elif self._temporary_focus_widget is not None and watched == self._temporary_focus_widget:
except Exception as e:
if self._float_mode and event.type() == QC.QEvent.FocusOut:
self._temporary_focus_widget.removeEventFilter( self )
self._temporary_focus_widget = None
current_focus_widget = QW.QApplication.focusWidget()
if current_focus_widget is None:
# happens sometimes when moving tabs in the tags dropdown list
ClientGUIFunctions.SetFocusLater( self._text_ctrl )
elif ClientGUIFunctions.IsQtAncestor( current_focus_widget, self._dropdown_window ):
self._temporary_focus_widget = current_focus_widget
self._temporary_focus_widget.installEventFilter( self )
else:
self._DropdownHideShow()
return False
HydrusData.ShowException( e )
return True
return False

View File

@ -4147,11 +4147,16 @@ class ReviewServiceTrashSubPanel( ClientGUICommon.StaticBox ):
hashes = HG.client_controller.Read( 'trash_hashes' )
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
for group_of_hashes in HydrusData.SplitIteratorIntoChunks( hashes, 16 ):
content_update = HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, group_of_hashes )
service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update ] }
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
time.sleep( 0.01 )
HG.client_controller.pub( 'service_updated', service )

View File

@ -583,11 +583,20 @@ class ButtonWithMenuArrow( QW.QToolButton ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.Show and watched == self._menu:
try:
pos = QG.QCursor.pos()
if event.type() == QC.QEvent.Show and watched == self._menu:
pos = QG.QCursor.pos()
self._menu.move( pos )
return True
self._menu.move( pos )
except Exception as e:
HydrusData.ShowException( e )
return True
@ -1929,11 +1938,20 @@ class TextCatchEnterEventFilter( QC.QObject ):
def eventFilter( self, watched, event ):
if event.type() == QC.QEvent.KeyPress and event.key() in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ):
try:
self._callable()
if event.type() == QC.QEvent.KeyPress and event.key() in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ):
self._callable()
event.accept()
return True
event.accept()
except Exception as e:
HydrusData.ShowException( e )
return True

View File

@ -64,7 +64,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'service_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'file_service_key', 'deleted_file_service_key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'download', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'file_service_keys', 'deleted_file_service_keys', 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@ -948,6 +948,8 @@ class HydrusResourceBooruFile( HydrusResourceBooru ):
share_key = request.parsed_request_args[ 'share_key' ]
hash = request.parsed_request_args[ 'hash' ]
is_attachment = request.parsed_request_args.GetValue( 'download', bool, default_value = False )
HG.client_controller.local_booru_manager.CheckFileAuthorised( share_key, hash )
media_result = HG.client_controller.local_booru_manager.GetMediaResult( share_key, hash )
@ -963,7 +965,7 @@ class HydrusResourceBooruFile( HydrusResourceBooru ):
raise HydrusExceptions.NotFoundException( 'Could not find that file!' )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, path = path )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, path = path, is_attachment = is_attachment )
return response_context
@ -2597,7 +2599,9 @@ class HydrusResourceClientAPIRestrictedGetFilesGetFile( HydrusResourceClientAPIR
raise HydrusExceptions.NotFoundException( 'Could not find that file!' )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, path = path )
is_attachment = request.parsed_request_args.GetValue( 'download', bool, default_value = False )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, path = path, is_attachment = is_attachment )
return response_context

View File

@ -988,6 +988,16 @@ class NetworkDomainManager( HydrusSerialisable.SerialisableBase ):
self.SetGUGs( gugs )
def DeleteParsers( self, deletee_names ):
with self._lock:
parsers = [ parser for parser in self._parsers if parser.GetName() not in deletee_names ]
self.SetParsers( parsers )
def DeleteURLClasses( self, deletee_names ):
with self._lock:

View File

@ -100,8 +100,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 531
CLIENT_API_VERSION = 46
SOFTWARE_VERSION = 532
CLIENT_API_VERSION = 47
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -596,6 +596,15 @@ class HydrusResource( Resource ):
do_finish = True
if response_context.IsAttachmentDownload():
content_disposition_type = 'attachment'
else:
content_disposition_type = 'inline'
if response_context.HasPath():
path = response_context.GetPath()
@ -610,7 +619,7 @@ class HydrusResource( Resource ):
fileObject = open( path, 'rb' )
content_disposition = 'inline; filename="' + filename + '"'
content_disposition = f'{content_disposition_type}; filename="{filename}"'
request.setHeader( 'Content-Disposition', str( content_disposition ) )
@ -672,7 +681,7 @@ class HydrusResource( Resource ):
content_length = len( body_bytes )
content_disposition = 'inline'
content_disposition = content_disposition_type
request.setHeader( 'Content-Type', content_type )
request.setHeader( 'Content-Length', str( content_length ) )
@ -1213,7 +1222,7 @@ class HydrusResourceWelcome( HydrusResource ):
class ResponseContext( object ):
def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None ):
def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None, is_attachment = False ):
if body is None:
@ -1246,6 +1255,7 @@ class ResponseContext( object ):
self._body_bytes = body_bytes
self._path = path
self._cookies = cookies
self._is_attachment = is_attachment
def GetBodyBytes( self ):
@ -1265,3 +1275,8 @@ class ResponseContext( object ):
def HasPath( self ): return self._path is not None
def IsAttachmentDownload( self ):
return self._is_attachment

View File

@ -4932,6 +4932,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( hashlib.sha256( data ).digest(), hash )
self.assertIn( 'inline', response.headers[ 'Content-Disposition' ] )
# succeed with attachment
path = '/get_files/file?file_id={}&download=true'.format( 1 )
connection.request( 'GET', path, headers = headers )
response = connection.getresponse()
data = response.read()
self.assertEqual( response.status, 200 )
self.assertEqual( hashlib.sha256( data ).digest(), hash )
self.assertIn( 'attachment', response.headers[ 'Content-Disposition' ] )
# range request
path = '/get_files/file?file_id={}'.format( 1 )

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB