|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, ) )
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 ):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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 ):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
|
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.5 KiB |