Version 444

This commit is contained in:
Hydrus Network Developer 2021-06-23 16:11:38 -05:00
parent 64008c8232
commit d45617c11e
49 changed files with 572 additions and 263 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
patreon: hydrus_dev

View File

@ -0,0 +1,135 @@
### Virtual Memory Under Linux
# Why does hydrus keep crashing under Linux when it has lots of virtual memory?
## Symptoms
- Hydrus crashes without a crash log
- Standard error reads `Killed`
- System logs say OOMKiller
- Programs appear to havevery high virtual memory utilization despite low real memory.
## tl;dr :: The fix
Add the followng line to the end of `/etc/sysctl.conf`. You will need admin, so use
`sudo nano /etc/sysctl.conf` or
`sudo gedit /etc/sysctl.conf`
```ini
vm.min_free_kbytes=1153434
vm.overcommit_memory=1
```
Check that you have (enough) swap space or you might still run out of memory.
```sh
sudo swapon --show
```
If you need swap
```sh
sudo fallocate -l 16G /swapfile #make 16GiB of swap
sudo chmod 600 /swapfile
sudo mkswap /swapfile
```
Add to `/etc/fstab` so your swap is mounted on reboot
```
/swapfile swap swap defaults 0 0
```
You may add as many swapfiles as you like, and should add a new swapfile before you delete an old one if you plan to do so, as unmounting a swapfile will evict its contents back in to real memory. You may also wish to use a swapfile type that uses compression, this saves you some disk space for a little bit of a performance hit, but also significantly saves on mostly empty memory.
Reboot for all changes to take effect, or use `sysctl` to set `vm` variables.
## Details
Linux's memory allocator is lazy and does not perform opportunistic reclaim. This means that the system will continue to give your process memory from the real and virtual memory pool(swap) until there is none left.
Linux will only cleanup if the available total real and virtual memory falls below the **watermark** as defined in the system control configuration file `/etc/sysctl.conf`.
The watermark's name is `vm.min_free_kbytes`, it is the number of kilobytes the system keeps in reserve, and therefore the maximum amount of memory the system can allocate in one go before needing to reclaim memory it gave eariler but which is no longer in use.
The default value is `vm.min_free_kbytes=65536`, which means 66MiB (megabytes).
If for a given request the amount of memory asked to be allocated is under `vm.min_free_kbytes`, but this would result in an ammount of total free memory less than `vm.min_free_kbytes` then the OS will clean up memory to service the request.
If `vm.min_free_kbytes` is less than the ammount requested and there is no virtual memory left, then the system is officially unable to service the request and will lauch the OOMKiller (Out of Memory Killer) to free memory by kiling memory glut processes.
Increase the `vm.min_free_kbytes` value to prevent this scenario.
### The OOM Killer
The OOM kill decides which program to kill to reclaim memory, since hydrus loves memory it is usually picked first, even if another program asking for memory caused the OOM condition. Setting the minimum free kilobytes higher will avoid the running of the OOMkiller which is always preferable, and almost always preventable.
### Memory Overcommmit
We mentioned that Linux will keep giving out memory, but actually it's possible for Linux to launch the OOM killer if it just feel like our program is aking for too much memory too quickly. Since hydrus is a heavyweight scientific processing package we need to turn this feature off. To turn it off change the value of `vm.overcommit_memory` which defaults to `2`.
Set `vm.overcommit_memory=1` this prevents the OS from using a heuristic and it will just always give memory to anyone who asks for it.
### What about swappiness?
Swapiness is a setting you might have seen, but it only determines Linux's desire to spend a little bit of time moving memory you haven't touched in a while out of real memory and into virtual memory, it will not prevent the OOM condition it just determines how much time to use for moving things into swap.
### Virtual Memory Under Linux 2: The rememoryning
# Why does my Linux system studder or become unresponsive when hydrus has been running a while?
You are running out of pages because Linux releases I/O buffer pages only when a file is closed. Thus the OS is waiting for you to hit the watermark(as described in "why is hydrus crashing") to start freeing pages, which causes the chug. When contents is written from memory to disk the page is retained so that if you reread that part of the disk the OS does not need to access disk it just pulls it from the much faster memory. This is usually a good thing, but Hydrus does not close database files so it eats up pages over time. This is really good for hydrus but sucks for the responsiveness of other apps, and will cause hydrus to consume pages after doing a lengthy operation in anticipation of needing them again, even when it is thereafter idle. You need to set `vm.dirtytime_expire_seconds` to a lower value.
> `vm.dirtytime_expire_seconds`
> When a lazytime inode is constantly having its pages dirtied, the inode with
> an updated timestamp will never get chance to be written out. And, if the
> only thing that has happened on the file system is a dirtytime inode caused
> by an atime update, a worker will be scheduled to make sure that inode
> eventually gets pushed out to disk. This tunable is used to define when dirty
> inode is old enough to be eligible for writeback by the kernel flusher threads.
> And, it is also used as the interval to wakeup dirtytime writeback thread.
On many distros this happens only once every 12 hours, try setting it close to every one hour or 2. This will cause the OS to drop pages that were written over 1-2 hours ago. Returning them to the free store for use by other programs.
https://www.kernel.org/doc/Documentation/sysctl/vm.txt
### Virtual Memory Under Linux 3: The return of the memory
# Why does everything become clunky for a bit if I have tuned all of the above settings?
The kernel launches a process called `kswapd` to swap and reclaim memory pages, its behaviour is goverened by the following two values
- `vm.vfs_cache_pressure` The tendancy for the kernel to reclaim I/O cache for files and directories. Default=100, set to 110 to bias the kernel into reclaiming I/O pages over keeping them at a "fair rate" compared to other pages. Hydrus tends to write a lot of files and then ignore them for a long time, so its a good idea to prefer freeing pages for infrequent I/O.
**Note**: Increasing `vfs_cache_pressure` significantly beyond 100 may have negative performance impact. Reclaim code needs to take various locks to find freeable directory and inode objects. With `vfs_cache_pressure=1000`, it will look for ten times more freeable objects than there are.
- `watermark_scale_factor`
This factor controls the aggressiveness of kswapd. It defines the amount of memory left in a node/system before kswapd is woken up and how much memory needs to be free before kswapd goes back to sleep. The unit is in fractions of 10,000. The default value of 10 means the distances between watermarks are 0.1% of the available memory in the node/system. The maximum value is 1000, or 10% of memory. A high rate of threads entering direct reclaim (allocstall) or kswapd going to sleep prematurely (kswapd_low_wmark_hit_quickly) can indicate that the number of free pages kswapd maintains for latency reasons is too small for the allocation bursts occurring in the system. This knob can then be used to tune kswapd aggressiveness accordingly.
I like to keep `watermark_scale_factor` at 70 (70/10,000)=0.7%, so kswapd will run until at least 0.7% of system memory has been reclaimed.
i.e. If 32GiB (real and virt) of memory, it will try to keep at least 0.224 GiB immediately available.
### Virtual Memory Under Linux 4: Unleash the memory
An example /etc/sysctl.conf section for virtual memory settings.
########
# virtual memory
########
#1 always overcommit, prevents the kernel from using a heuristic to decide that a process is bad for asking for a lot of memory at once and killing it.
#https://www.kernel.org/doc/Documentation/vm/overcommit-accounting
vm.overcommit_memory=1
#force linux to reclaim pages if under a gigabyte
#is available so large chunk allocates don't fire off the OOM killer
vm.min_free_kbytes = 1153434
#Start freeing up pages that have been written but which are in open files, after 2 hours.
#Allows pages in long lived files to be reclaimed
vm.dirtytime_expire_seconds = 7200
#Have kswapd try to reclaim .7% = 70/10000 of pages before returning to sleep
#This increases responsiveness by reclaiming a larger portion of pages in low memory condition
#So that the next time you make a large allocation the kernel doesn't have to stall and look for pages to free immediately.
vm.watermark_scale_factor=70
#Have the kernel prefer to reclaim I/O pages at 110% of the rate at which it frees other pages.
#Don't set this value much over 100 or the kernel will spend all its time reclaiming I/O pages
vm.vfs_cache_pressure=110

View File

@ -9,13 +9,21 @@
<p class="warning">The PTR is now run by users with more bandwidth than I had to give, so the bandwidth limits are gone! If you would like to talk with the new management, please check the <a href="https://discord.gg/wPHPCUZ">discord</a>.</p>
<p class="warning">A guide and schema for the new PTR is <a href="https://github.com/Zweibach/text/blob/master/Hydrus/PTR.md">here</a>.</p>
<h3 id="first_off"><a href="#first_off">first off</a></h3>
<p>I have purposely not pre-baked any default repositories into the client. You have to choose to connect yourself. <b>The client will never connect anywhere until you tell it to.</b></p>
<p>For a long time, I ran the Public Tag Repository. It grew to 650 million tags and I no longer had the bandwidth or janitor time it deserved. It is now run by users.</p>
<p>I created a 'frozen' copy of the PTR when I stopped running it. If you are an advanced user, you can run your own new tag repository starting from that frozen point or, if you know python or SQLite and wish to play around with its data, get more easily accessible Hydrus Tag Archives of its tags and siblings and pairs, right <a href="https://mega.nz/#F!w7REiS7a!bTKhQvZP48Fpo-zj5MAlhQ">here</a>.</p>
<p>I don't like it when programs I use connect anywhere without asking me, so I have purposely not pre-baked any default repositories into the client. You have to choose to connect yourself. <b>The client will never connect anywhere until you tell it to.</b></p>
<p>For a long time, I ran the Public Tag Repository myself and was the lone janitor. It grew to 650 million tags, and siblings and parents were just getting complicated, and I no longer had the bandwidth or time it deserved. It is now run by users.</p>
<p>There also used to be just one user account that everyone shared. Everyone was essentially the same Anon, and all uploads were merged to that one ID. As the PTR became more popular, and more sophisticated and automatically generated content was being added, it became increasingly difficult for the janitors to separate good submissions from bad and undo large scale mistakes.</p>
<p>That old shared account is now a 'read-only' account. This account can only download--it cannot upload new tags or siblings/parents. Users who want to upload now generate their own individual accounts, which are still Anon, but separate, which helps janitors approve and deny uploaded petitions more accurately and efficiently.</p>
<p>I recommend using the shared read-only account, below, to start with, but if you decide you would like to upload, making your own account is easy--you just click the 'check for automatic account creation' button in <i>services-&gt;manage services</i>, and you should be good. You can change your access key on an existing service--you don't need to delete and re-add or anything--and your client should quickly resync and recognise your new permissions.</p>
<h3 id="privacy"><A href="#privacy">privacy</a></h3>
<p>I have tried very hard to ensure the PTR respects your privacy. While an account is tied to the different content that is uploaded, all a server knows about that account is a couple of random hexadecimal texts and which rows of content you uploaded. It obviously needs to be aware of your IP address to accept your network request, but it forgets it as soon as the job is done. Normal users are never told which accounts submitted any content, so the only privacy implications are against janitors or (more realistically, since the janitor UI is even more buggy and feature-poor than the hydrus front-end!) the server owner or anyone else with raw access to the server as it operates or its database files.</p>
<p>Most users should have very few worries about privacy. The general rule is that it is always healthy to use a VPN, but please check <a href="privacy.html">here for a full discussion</a>.</p>
<h3 id="ssd"><a href="#ssd">a note on resources</a></h3>
<p><b><span class="warning">If you are on an HDD, or your SSD does not have at least 64GB of free space, do not add the PTR!</span></b></p>
<p>The PTR has been operating since 2011 and is now huge, more than a billion mappings! Your client will be downloading and indexing them all, which is currently (2021-06) about 6GB of bandwidth and 50GB of hard drive space. It will take <i>hours</i> of total processing time to catch up on all the years of submissions. <span class="warning">Furthermore, because of mechanical drive latency, HDDs are too slow to process all the content in reasonable time. Syncing is only recommended if your <a href="database_migration.html">hydrus db is on an SSD</a>.</span> Even then, it is healthier and allows the client to 'grow into' the PTR if the work is done in small pieces in the background, either during idle time or shutdown time, rather than trying to do it all at once. Just leave it to download and process on its own--it usually takes a couple of weeks to quietly catch up. You'll see tags appear on your files as it proceeds, first on older, then all the way up to new files just uploaded a couple days ago. Once you are synced, the daily processing work to stay synced is usually just a few minutes. If you leave your client on all the time in the background, you'll likely never notice it.</b></p>
<h3 id="easy_setup"><a href="#easy_setup">easy setup</a></h3>
<p>Hit <i>help->add the public tag repository</i> and you will all be set up.</p>
<h3 id="manually"><a href="#manually">manually</a></h3>
<p>To add a new repository to your client, hit <i>services->manage services</i> and click the add button:</p>
<p>Hit <i>services->manage services</i> and click <i>add->hydrus tag repository</i>. You'll get a panel, fill it out like this:</p>
<p><img src="edit_repos_public_tag_repo.png" /></p>
<p>Here's the info so you can copy it:</p>
<ul>
@ -23,8 +31,10 @@
<li>45871</li>
<li>4a285629721ca442541ef2c15ea17d1f7f7578b0c3f4f5f2a05f8f0ab297786f</li>
</ul>
<p>It is worth checking the 'test address' and 'test access key' buttons just to double-check your firewall and key are all correct.</p>
<p><b>Tags are rich, cpu-intensive metadata. The Public Tag Repository has hundreds of millions of mappings, and your client will eventually download and index them all. Be aware that the PTR has been growing since 2011 and now has hundreds of millions of mappings. As of 2020-03, it requires about 4GB of bandwidth and file storage, and your database itself will grow by 25GB! It will take <i>hours</i> of total processing time to fully synchronise. <span class="warning">Because of mechanical drive latency, HDDs are often too slow to process hundreds of millions of tags in reasonable time. Syncing with large repositories is only recommended <a href="database_migration.html">if your hydrus db is on an SSD</a>.</span> Even then, it is best left to work on this in small pieces in the background, either during idle time or shutdown time, so unless you are an advanced user, just leave it to download and process on its own--it usually takes a couple of weeks to quietly catch up.</b></p>
<p>Note that because this is the public shared key, you can ignore the '<span class="warning">DO NOT SHARE</span>' red text warning.</p>
<p>It is worth checking the 'test address' and 'test access key' buttons just to double-check your firewall and key are all correct. Notice the 'check for automatic account creation' button, for if and when you decide you want to contribute to the PTR.</p>
<p>Then you can check your PTR at any time under <i>services->review services</i>, under the 'remote' tab:</p>
<p><img src="review_repos_public_tag_repo.png" /></p>
<h3 id="quicksync"><a href="#quicksync">jump-starting an install</a></h3>
<p>A user kindly manages a store of update files and pre-processed empty client databases to get your synced quicker. This is generally recommended for advanced users or those following a guide, but if you are otherwise interested, please check it out:</p>
<p><a href="https://cuddlebear92.github.io/Quicksync/">https://cuddlebear92.github.io/Quicksync/</a></p>

View File

@ -8,6 +8,19 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_444"><a href="#version_444">version 444</a></h3></li>
<ul>
<li>gave the 'access keys' and 'privacy' help pages a complete pass. the access keys section talks about the read-only shared key, and how to generate you own account, and the privacy section now compiles, as comprehensively as I could, our various discussions about multiple accounts, what you shouldn't upload to the PTR (basically your own name lol), self-signed https certificates, and what information is actually stored on an account</li>
<li>expanded the 'getting started - installing' help page with a 'how to run the client' section, including bundling the excellent Linux virtual memory guide written by a user</li>
<li>fixed the new 'fill in subscription with gap downloader' button, which was initialising with the wrong downloader at times (usually on the first gap downloader opened, when it opened a new page with it)</li>
<li>you can now set 'all known files' for the tag autocomplete in 'write' contexts (e.g. manage tags dialog) when not in advanced mode</li>
<li>cleaned up how a variety of delayed UI calls are registered and present information about themselves. every UI job now has a nice human name for debug purposes. this should improve program stability and clear some odd rare errors when closing some dialogs (this mostly affected certain linux users)</li>
<li>when an asynchronous UI job fails with a dead window, or if fails to publish to its window for a non-dead reason and then the window dies before that failure returns, the error handling code now catches and silences the error. an example of this would be clicking 'refresh account' on review services, then closing the window before the lagging job raises 'connection failure'</li>
<li>when windows are rescued from off screen, their frame key is now stated in the popup note</li>
<li>if your version of OpenCV is unable to load PNG files, your client should now be able to load serialised object PNGs (like those in the downloader system) correctly (the same PIL fallback for regular media files now works for deserialisation too)</li>
<li>the hydrus log path is finally month-zero-padded, ha ha ha</li>
<li>misc cleanup and label fixes</li>
</ul>
<li><h3 id="version_443"><a href="#version_443">version 443</a></h3></li>
<ul>
<li>quality of life:</li>
@ -30,7 +43,7 @@
<li>'database is complicated' menu label is updated to 'database is stored in multiple locations'</li>
<li>_options->gui pages->controls_ now has a little explanatory text about autocomplete dropdowns and some tooltips</li>
<li>migrate database dialog has some red warning text up top and a small layout and label text pass. the 'portable?' is now 'beneath db?'</li>
<li>the repositery hash_id and tag_id normalisation routines have two improvements: the error now shows specific service_ids that failed to lookup, and the mass-service_hash_id lookup now handles the situation where a hash_id is mapped by more than one service_id</li>
<li>the repository hash_id and tag_id normalisation routines have two improvements: the error now shows specific service_ids that failed to lookup, and the mass-service_hash_id lookup now handles the situation where a hash_id is mapped by more than one service_id</li>
<li>repository definition reprocessing now corrects bad service_id rows, which will better heal clients that previously processed bad data</li>
<li>the client api and server in general should be better about giving 404s on certain sorts of missing files (it could dump out with 500 in some cases before)</li>
<li>it isn't perfect by any means, but the autocomplete dropdown should be a _little_ better about hiding itself in float mode if the parent text input box is scrolled off screen</li>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -38,6 +38,22 @@
</ul>
<p>By default, hydrus stores all its data&#x2014;options, files, subscriptions, <i>everything</i>&#x2014;entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine if you manually move it.</p>
<p><span class="warning">However, for macOS users:</span> the Hydrus App is <b>non-portable</b> and puts your database in ~/Library/Hydrus (i.e. /Users/[You]/Library/Hydrus). You can update simply by replacing the old App with the new, but if you wish to backup, you should be looking at ~/Library/Hydrus, not the App itself.</p>
<h3 id="running"><a href="#running">running</a></h3>
<p>To run the client:</p>
<p>for Windows:</p>
<ul>
<li>For the installer, run the Start menu shortcut it added.</li>
<li>For the extract, run 'client.exe' in the base directory, or make a shortcut to it.</li>
</ul>
<p>for macOS:</p>
<ul>
<li>Run the App you installed.</li>
</ul>
<p>for Linux:</p>
<ul>
<li>Run the 'client' executable in the base directory. You may be able to double-click it, otherwise you are talking './client' from the terminal.</li>
<li>If you experience virtual memory crashes, please review <a href="Fixing_Hydrus_Random_Crashes_Under_Linux.md">this thorough guide</a> by a user.</li>
</ul>
<h3 id="updating"><a href="#updating">updating</a></h3>
<p class="warning">Hydrus is imageboard-tier software, wild and fun but unprofessional. It is written by one Anon spinning a lot of plates. Mistakes happen from time to time, usually in the update process. There are also no training wheels to stop you from accidentally overwriting your whole db if you screw around. Be careful when updating. Make backups beforehand!</p>
<p><b>Hydrus does not auto-update. It will stay the same version unless you download and install a new one.</b></p>

View File

@ -42,13 +42,12 @@
<p>This search will return all files that have the tag 'fanfic' and one or more of 'medium:text', a positive value for the like/dislike rating 'read later', or PDF mime.</p>
<h3 id="tag_repositories"><a href="#tag_repositories">tag repositories</a></h3>
<p>It can take a long time to tag even small numbers of files well, so I created <i>tag repositories</i> so people can share the work.</p>
<p>Tag repos store many file->tag relationships. Anyone who has an access key to the repository can sync with it and hence download all these relationships. If any of their own files match up, they will get those tags. Access keys will also usually have permission to upload new tags and ask for incorrect existing ones to be deleted.</p>
<p>Anyone can run a tag repository, but it is a bit complicated for new users. I ran a public tag repository for a long time, and now this large central store is run by users. It has hundreds of millions of tags and is free to access and contribute to.</p>
<p>To connect with it, please check <a href="access_keys.html">here</a>.</h3>
<p>Tag repos store many file->tag relationships. Anyone who has an access key to the repository can sync with it and hence download all these relationships. If any of their own files match up, they will get those tags. Access keys will also usually have permission to upload new tags and ask for incorrect ones to be deleted.</p>
<p>Anyone can run a tag repository, but it is a bit complicated for new users. I ran a public tag repository for a long time, and now this large central store is run by users. It has over a billion tags and is free to access and contribute to.</p>
<p>To connect with it, please check <a href="access_keys.html">here</a>. <b class="warning">Please read that page if you want to try out the PTR. It is only appropriate for someone on an SSD!</b></h3>
<p>If you add it, your client will download updates from the repository over time and, usually when it is idle or shutting down, 'process' them into its database until it is fully synchronised. The processing step is CPU and HDD heavy, and you can customise when it happens in <i>file->options->maintenance and processing</i>. As the repository synchronises, you should see some new tags appear, particularly on famous files that lots of people have.</p>
<p><b>Tags are rich, cpu-intensive metadata. The Public Tag Repository has hundreds of millions of mappings, and your client will eventually download and index them all. Be aware that the PTR has been growing since 2011 and now has hundreds of millions of mappings. As of 2020-03, it requires about 4GB of bandwidth and file storage, and your database itself will grow by 25GB! It will take <i>hours</i> of total processing time to fully synchronise. <span class="warning">Because of mechanical drive latency, HDDs are often too slow to process hundreds of millions of tags in reasonable time. Syncing with large repositories is only recommended <a href="database_migration.html">if your hydrus db is on an SSD</a>.</span> Even then, it is best left to work on this in small pieces in the background, either during idle time or shutdown time, so unless you are an advanced user, just leave it to download and process on its own--it usually takes a couple of weeks to quietly catch up.</b></p>
<p>You can watch more detailed synchronisation progress in the <i>services->review services</i> window.</p>
<p><img src="tag_repo_review.png" /></p>
<p><img src="review_repos_public_tag_repo.png" /></p>
<p>Your new service should now be listed on the left of the manage tags dialog. Adding tags to a repository works very similarly to the 'my tags' service except hitting 'apply' will not immediately confirm your changes--it will put them in a queue to be uploaded. These 'pending' tags will be counted with a plus '+' or minus '-' sign:</p>
<p><a href="rlm_pending.png"><img src="rlm_pending.png" width="960" height="540" /></a></p>
<p>Notice that a 'pending' menu has appeared on the main window. This lets you start the upload when you are ready and happy with everything that you have queued.</p>

View File

@ -7,58 +7,94 @@
<body>
<div class="content">
<h3 id="intro"><a href="#intro">privacy</a></h3>
<p>Repositories are designed to respect your privacy. They never know what you are searching for. The client synchronises (copies) the repository's entire file or mapping list to its internal database, and does its own searches over those internal caches, all on your hard drive. <span class="warning">It <i>never</i> sends search queries outside your own computer, nor does it log what you do look for</span>. Your searches are your business, and no-one else's.</p>
<p class="warning">The PTR has a public shared access key. You do not have to contact anyone to get the key, so no one can infer who you are from it, and all regular user uploads are merged together, making it all a big mess. The PTR is more private than this document's worst case scenarios.</p>
<p>The only privacy risk for hydrus's repositories are in what you upload (ultimately by using the pending menu at the top of the program). Even then, it would typically be very difficult even for an admin to figure anything about you, but it is possible.</p>
<p>Repositories know nothing more about your client than they can infer from what you choose upload, and the software usually commands them to forget as much as possible as soon as possible. Specifically:</p>
<table cellpadding="5" cellspacing="2" border="1">
<tr>
<td />
<th colspan="2">tag repository</th>
<th colspan="2">file repository</th>
</tr>
<tr>
<td />
<th>upload mappings</th>
<th>download mappings</th>
<th>upload file</th>
<th>download file</th>
</tr>
<tr>
<th>Anonymous account is linked to action</th>
<td>Yes</td>
<td>No</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<th>IP address is remembered</th>
<td>No</td>
<td>No</td>
<td class="warning">Maybe</td>
<td>No</td>
</tr>
</table>
<p>i.e:</p>
<p>
<ul>
<li>If you download anything from any repository, your accessing it will not be recorded. A running total of your approximate bandwidth and number of queries made for the current month <i>is</i> kept so the respective administrator can combat leechers.</li>
<li>If you upload a mapping to a tag repository, your anonymous account is linked so the administrator can quickly revoke all of a rule-breaker's contributions. Your IP address is forgotten.</li>
<li>If you upload a file to a file repository, your anonymous account is linked so the administrator can quickly revoke all of a rule-breaker's contributions. Your IP <span class="warning">may</span> be recorded, depending on whether the repository's administrator has decided to enable ip upload-logging or not.</li>
</ul>
</p>
<p>Furthermore:</p>
<p>
<ul>
<li>Administrators for a particular repository can see which accounts uploaded what. If IP addresses are available, they can discover which IP uploaded a particular file, and when.</li>
<li>Repositories do not talk to each other.</li>
<li>All accounts are anonymous. Repositories do not <i>know</i> any of their accounts' access keys and cannot produce them on demand; they can determine whether a particular access key refers to a particular account, but the access keys themselves are all irreversibly hashed inside the repository database.</li>
</ul>
</p>
<p>As always, there are some clever exceptions, mostly in servers between friends that will just have a handful of users, where the admin would be handing out registration tokens and, with effort, could pick through the limited user creation records to figure out which access key you were. In that case, if you were to tag a file three years before it surfaced on the internet, and the admin knew you are attached to the account that made that tag, they could infer you most likely created it. If you set up a file repository for just a friend and yourself, it becomes trivial by elimination to guess who uploaded the NarutoXSonichu shota diaper fanon. If you sign up for a file repository that hosts only certain stuff and rack up a huge bandwidth record for the current month, anyone who knows that and also knows the account is yours alone will know basically what you were up to.</p>
<p>The PTR has a shared access key that is already public, so the risks are far smaller. No one can figure out who you are from the access key.</p>
<p>Note that the code is freely available and entirely mutable. If someone wants to put the time in, they could create a file repository that looks from the outside like any other but nonetheless logs the IP and nature of every request. As with any website, protect yourself, and if you do not trust an admin, do not give them or their server any information about you.</p>
<p><a href="https://en.wikipedia.org/wiki/AOL_search_data_leak">Even anonymised records can reveal personally identifying information.</a> Don't trust anyone on any site who plans to release internal maps of 'anonymised' accounts -> content, even for some benevolent academic purpose.</p>
<p class="warning">tl;dr: Using a trustworthy VPN for all your remotely fun internet traffic is a good idea. It is cheap and easy these days, and it offers multiple levels of general protection.</p>
<p>I have tried very hard to ensure the hydrus network servers respect your privacy. They do not work like normal websites, and the amount of information your client will reveal to them is very limited. For most general purposes, normal users can rest assured that their activity on a repository like the Public Tag Repository (PTR) is effectively completely anonymous.</p>
<p>You need an account to connect, but all that really means serverside is a random number with a random passcode. Your client tells nothing more to the server than the exact content you upload to it (e.g. tag mappings, which are a tag+file_hash pair). The server needs to be aware of your IP address to accept your network request, but in all but one situation--uploading a file to a file repository when the administrator has set to save IPs for DMCA purposes--it forgets your IP as soon as the job is done.</p>
<p>Servers remember which accounts upload which content, but they do not communicate this to any place. The main potential privacy worries are over a malicious janitor or--more realistically, since the janitor UI is even more buggy and feature-poor than the hydrus front-end!--a malicious server owner or anyone else who gains raw access to the server's raw database files or its code as it operates. Even in the case where you cannot trust the server you are talking to, hydrus should be <i>fairly</i> robust, simply because the client does not say much to the server, nor that often. The only realistic worries, as I talk about in detail below, are if you actually upload personal files or tag personal files with real names. I can't do much about being Anon if you (accidentally or not), declare who you are.</p>
<p>So, in general, if you are on a good VPN and tagging anime babes from boorus, I think we are near perfect on privacy. That said, our community is rightly constantly thinking about this topic, so in the following I have tried to go into exhaustive detail. Some of the vulnerabilities are impractical and esoteric, but if nothing else it is fun to think about. If you can think of more problems, or decent mitigations, let me know!</p>
<h3 id="https_certificates"><a href="#https_certificates">https certificates</a></h3>
<p>Hydrus servers only communicate in https, so anyone who is able to casually observe your traffic (say your roommate cracked your router, or the guy running the coffee shop whose wifi you are using likes to snoop) should not ever be able to see what data you are sending or receiving. If you do not use a VPN, they will be able to see that you <i>are</i> talking to the repository (and the repository will technically see who you are, too, though as above, it normally isn't interested). Someone more powerful, like your ISP or Government, may be able to do more:</p>
<ul>
<li>
<p><b>If you just start a new server yourself.</b></p>
<p>When you first make a server, the 'certificate' it creates to enable https is a low quality one. It is called 'self-signed' because it is only endorsed by itself and it is not tied to a particular domain on the internet that everyone agrees on via DNS. Your traffic to this server is still encrypted, but an advanced attacker who stands between you and the server could potentially perform what is called a man-in-the-middle attack and see your traffic.</p>
<p>This problem is fairly mitigated by using a VPN, since even if someone were able to MitM your connection, they know no more than your VPN's location, not your IP.</p>
<p><i>A future version of the network will further mitigate this problem by having you enter unverified certificates into a certificate manager and then compare to that store on future requests, to try to detect if a MitM attack is occurring.</i></p>
</li>
<li>
<p><b>If the server is on a domain and now uses a proper verified certificate.</b></p>
<p>If the admin hosts the server on a website domain (rather than a raw IP address) and gets a proper certificate for that domain from a service like Let's Encrypt, they can swap that into the server and then your traffic should be protected from any eavesdropper. It is still good to use a VPN to further obscure <i>who</i> you are, including from the server admin.</p>
</li>
</ul>
<p>You can check how good a server's certificate is by loading its base address in the form https://host:port into your browser. If it has a nice certificate--like the <a href="https://ptr.hydrus.network:45871/">PTR</a>--the welcome page will load instantly. If it is still on self-signed, you'll get one of those 'can't show this page unless you make an exception' browser error pages before it will show.</p>
<h3 id="accounts"><a href="#accounts">accounts</a></h3>
<p>An account has two hex strings, like this:</p>
<ul>
<li>
<b>Access key: 4a285629721ca442541ef2c15ea17d1f7f7578b0c3f4f5f2a05f8f0ab297786f</b>
<p>This is in your <i>services->manage services</i> panel, and acts like a password. Keep this absolutely secret--only you know it, and no one else ever needs to. If the server has not had its code changed, it does not actually know this string, but it is stores special data that lets it <i>verify</i> it when you 'log in'.</p>
</li>
<li>
<b>Account ID: 207d592682a7962564d52d2480f05e72a272443017553cedbd8af0fecc7b6e0a</b>
<p>This can be copied from a button in your <i>services->review services</i> panel, and acts a bit like a semi-private username. Only janitors should ever have access to this. If you ever want to contact the server admin about an account upgrade or similar, they will need to know this so they can load up your account and alter it.</p>
</li>
</ul>
<p>When you generate a new account, the client first asks for a list of available account types, then asks for a registration token for one of them, then uses the token to generate an access key. The server is never told anything about you, and it forgets your IP address as soon as it finishes talking to you.</p>
<p>Your account also stores a bandwidth use record and some miscellaneous data such as when the account was created, if and when it expires, what permissions and bandwidth rules it has, an aggregate score of how often it has petitions approved rather than denied, and whether it is currently banned. I do not think someone inspecting the bandwidth record could figure out what you were doing based on byte counts (especially as with every new month the old month's bandwidth records are compressed to just one number) beyond the rough time you synced and whether you have done much uploading. Since only a janitor can see your account and could feasibly attempt to inspect bandwidth data, they would already know this information.</p>
<h3 id="downloading"><a href="#downloading">downloading</a></h3>
<p>When you sync with a repository, your client will download and then keep up to date with all the metadata the server knows. This metadata is downloaded the same way by all users, and it comes in a completely anonymous format. The server does not know what you are interested in, and no one who downloads knows who uploaded what. Since the client regularly updates, a detailed analysis of the raw update files will reveal roughly when a tag or other row was added or deleted, although that timestamp is no more precise than the duration of the update period (by default, 100,000 seconds, or a little over a day).</p>
<p>Your client will never ask the server for information about a particular file or tag. You download everything in generic chunks, form a local index of that information, and then all queries are performed on your own hard drive with your own CPU.</p>
<p>By just downloading, even if the server owner were to identify you by your IP address, all they know is that you sync. They cannot tell anything about your files.</p>
<p>In the case of a file repository, you client downloads all the thumbnails automatically, but then you download actual files separately as you like. The server does not log which files you download.</p>
<h3 id="uploading"><a href="#uploading">uploading</a></h3>
<p>When you upload, your account is linked to the rows of content you add. This is so janitors can group petitions by who makes them, undo large mistakes easily, and even leave you a brief message (like "please stop adding those clothing siblings") for your client to pick up the next time it syncs your account. So, what are the privacy concerns with that? Isn't the account 'Anon'?</p>
<p><a href="https://en.wikipedia.org/wiki/AOL_search_data_leak">Privacy can be tricky</a>. Hydrus tech is obviously far, far better than anything normal consumers use, but here I believe are the remaining barriers to pure Anonymity, assuming someone with resources was willing to put a lot of work in to attack you:</p>
<p class="warning">I am using the PTR as the example since that is what most people are using. If you are uploading to a server run between friends, privacy is obviously more difficult to preserve--if there are only three users, it may not be too hard to figure out who is uploading the NarutoXSonichu diaperfur content! If you are talking to a server with a small group of users, don't upload anything crazy or personally identifying unless that's the point of the server.</p>
<ul>
<li>
<h4>IP Address Across Network</h4>
<p><i>Attacker:</i> ISP/Government.</p>
<p><i>Exposure:</i> That you use the PTR.</p>
<p><i>Problem:</i> Your IP address may be recorded by servers in between you and the PTR (e.g. your ISP/Government). Anyone who could convert that IP address and timestamp into your identity would know you were a PTR user.</p>
<p><i>Mitigation:</i> Use a trustworthy VPN.</p>
</li>
<li>
<h4>IP Address At PTR</h4>
<p><i>Attacker:</i> PTR administrator or someone else who has access to the server as it runs.</p>
<p><i>Exposure:</i> Which PTR account you are.</p>
<p><i>Problem:</i> I may be lying to you about the server forgetting IPs, or the admin running the PTR may have secretly altered its code. If the malicious admin were able to convert IP address and timestamp into your identity, they obviously be able to link that to your account and thus its various submissions.</p>
<p><i>Mitigation:</i> Use a trustworthy VPN.</p>
</li>
<li>
<h4>Time Identifiable Uploads</h4>
<p><i>Attacker:</i> Anyone with an account on the PTR.</p>
<p><i>Exposure:</i> That you use the PTR.</p>
<p><i>Problem:</i> If a tag was added way before the file was public, then it is likely the original owner tagged it. An example would be if you were an artist and you tagged your own work on the PTR two weeks before publishing the work. Anyone who looked through the server updates carefully and compared to file publish dates, particularly if they were targeting you already, could notice the date discrepancy and know you were a PTR user.</p>
<p><i>Mitigation:</i> Don't tag any file you plan to share if you are currently the only person who has any copies. Upload it, then tag it.</p>
</li>
<li>
<h4>Content Identifiable Uploads</h4>
<p><i>Attacker:</i> Anyone with an account on the PTR.</p>
<p><i>Exposure:</i> That you use the PTR.</p>
<p><i>Problem:</i> All uploads are shared anonymously with other users, but if the content itself is identifying, you may be exposed. An example would be if there was some popular lewd file floating around of you and your girlfriend, but no one knew who was in it. If you decided to tag it with accurate 'person:' tags, anyone synced with the PTR, when they next looked at that file, would see those person tags. The same would apply if the file was originally private but then leaked.</p>
<p><i>Mitigation:</i> Just like an imageboard, do not upload any personally identifying information.</p>
</li>
<li>
<h4>Individual Account Cross-referencing</h4>
<p><i>Attacker:</i> Someone with access to the server database files after you accidentally identify yoursely with a Time/Content Identifiable Upload as above.</p>
<p><i>Exposure:</i> Which PTR account you are.</p>
<p><i>Problem:</i> If you accidentally tie your identity to an individual content row, then anyone who can see which account uploaded what will be able to identify all your other uploads.</p>
<p><i>Mitigation:</i> Best practise is to not upload any personally identifying information. Another solution is to share your access key with several users. Then your history (and potential exposure) is mixed up with theirs.</p>
</li>
<li>
<h4>Big Brain Individual Account Mapping Cross-referencing</h4>
<p><i>Attacker:</i> Someone who has access to tag/file favourite lists on another site and has access to the server database files.</p>
<p><i>Exposure:</i> Which PTR account another website's account uses.</p>
<p><i>Problem:</i> Someone who had raw access to the PTR database and also some booru users' 'favourite tag/artist' lists and was very clever could try to cross reference those two lists and connect a particular PTR account to a particular booru account based on similar tag distributions. There would be lost of holes in the PTR record, since only the first account to upload a tag mapping is linked to it, but maybe it would be possible to get high confidence on a match if you have really distinct tastes. Favourites list are probably decent fingerprints, and there may be a shadow of that in your PTR uploads, although I also think there are enough users uploading and 'competing' for saved records on different tags that each users' shadow would be too indistinct to really pull this off.</p>
<p><i>Mitigation:</i> Privacy is tricky, and who knows what the scrapers of the future are going to do with all the cloud data they are sucking up. Although the PTR is potentialy vulnerable to this attack if it ever falls into bad hands in the future, accounts on regular websites are already being aggregated by the big marketing engines, and this will only happen in more clever ways in future. Don't save your spicy favourites on a website, even if that list is private, since if that site gets hacked or just bought out one day, someone really clever could start connecting dots ten years from now.</p>
</li>
</ul>
<p><b><i>My Next Planned Mitigation:</i> On my end, I am going to create a 'null account', which will take possession of all content submissions after a delay, say, 90 days. This will allow janitors to process petitions and recent mistakes on a per-account level but still let the server forget account source on all 'archived' uploads, completely anonymising all that history and I <i>think</i> pretty much obviating most account cross-referencing worries.</b></p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -18,6 +18,8 @@
<p>Installing some packages on windows with pip may need Visual Studio's C++ Build Tools for your version of python. Although these tools are free, it can be a pain to get them through the official (and often huge) downloader installer from Microsoft. Instead, install Chocolatey and use this one simple line:</p>
<blockquote>choco install -y vcbuildtools visualstudio2017buildtools</blockquote>
<p>Trust me, just do this, it will save a ton of headaches!</a>
<p>This can also be helpful for Windows 10 python work generally:</p>
<blockquote>choco install -y windows-sdk-10.0</blockquote>
<h3 id="what_you_need"><a href="#what_you_need">what you will need</a></h3>
<p>You will need basic python experience, python 3.x and a number of python modules, all through pip.</p>
<p>First of all, get the actual program. The github repo is <a href="https://github.com/hydrusnetwork/hydrus">https://github.com/hydrusnetwork/hydrus</a>. If you are familiar with git, you can just clone the repo to the location you want, but if not, then just go to the <a href="https://github.com/hydrusnetwork/hydrus/releases/latest">latest release</a> and download and extract the source code .zip or .tar.gz somewhere. The same database location rules apply for the source release as the builds, so if you are not planning to redirect the database with the -d launch parameter, make sure the directory has write permissions (e.g. in Windows, don't put it in "Program Files")</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -454,17 +454,21 @@ class Controller( HydrusController.HydrusController ):
raise HydrusExceptions.ShutdownException()
def CallAfterQtSafe( self, window, func, *args, **kwargs ) -> ClientThreading.QtAwareJob:
def CallAfterQtSafe( self, window, label, func, *args, **kwargs ) -> ClientThreading.QtAwareJob:
return self.CallLaterQtSafe( window, 0, func, *args, **kwargs )
return self.CallLaterQtSafe( window, 0, label, func, *args, **kwargs )
def CallLaterQtSafe( self, window, initial_delay, func, *args, **kwargs ) -> ClientThreading.QtAwareJob:
def CallLaterQtSafe( self, window, initial_delay, label, func, *args, **kwargs ) -> ClientThreading.QtAwareJob:
job_scheduler = self._GetAppropriateJobScheduler( initial_delay )
# we set a label so the call won't have to look at Qt objects for a label in the wrong place
call = HydrusData.Call( func, *args, **kwargs )
call.SetLabel( label )
job = ClientThreading.QtAwareJob( self, job_scheduler, window, initial_delay, call )
if job_scheduler is not None:
@ -475,13 +479,17 @@ class Controller( HydrusController.HydrusController ):
return job
def CallRepeatingQtSafe(self, window, initial_delay, period, func, *args, **kwargs):
def CallRepeatingQtSafe( self, window, initial_delay, period, label, func, *args, **kwargs ) -> ClientThreading.QtAwareRepeatingJob:
job_scheduler = self._GetAppropriateJobScheduler( period )
# we set a label so the call won't have to look at Qt objects for a label in the wrong place
call = HydrusData.Call( func, *args, **kwargs )
job = ClientThreading.QtAwareRepeatingJob(self, job_scheduler, window, initial_delay, period, call)
call.SetLabel( label )
job = ClientThreading.QtAwareRepeatingJob( self, job_scheduler, window, initial_delay, period, call )
if job_scheduler is not None:
@ -1205,7 +1213,7 @@ class Controller( HydrusController.HydrusController ):
job.WakeOnPubSub( 'wake_idle_workers' )
self._daemon_jobs[ 'synchronise_repositories' ] = job
job = self.CallRepeatingQtSafe( self, 10.0, 10.0, self.CheckMouseIdle )
job = self.CallRepeatingQtSafe( self, 10.0, 10.0, 'repeating mouse idle check', self.CheckMouseIdle )
self._daemon_jobs[ 'check_mouse_idle' ] = job
if self.db.IsFirstStart():

View File

@ -12,6 +12,7 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusImageHandling
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
@ -249,13 +250,25 @@ def LoadFromPNG( path ):
HydrusPaths.MirrorFile( path, temp_path )
numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED )
except Exception as e:
HydrusData.ShowException( e )
raise Exception( 'That did not appear to be a valid image!' )
try:
numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED )
except Exception as e:
try:
pil_image = HydrusImageHandling.GeneratePILImage( temp_path )
numpy_image = HydrusImageHandling.GenerateNumPyImageFromPILImage( pil_image, dequantize = False )
except Exception as e:
HydrusData.ShowException( e )
raise Exception( 'That did not appear to be a valid image!' )
finally:

View File

@ -508,9 +508,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._RefreshStatusBar()
self._bandwidth_repeating_job = self._controller.CallRepeatingQtSafe(self, 1.0, 1.0, self.REPEATINGBandwidth)
self._bandwidth_repeating_job = self._controller.CallRepeatingQtSafe( self, 1.0, 1.0, 'repeating bandwidth status update', self.REPEATINGBandwidth )
self._page_update_repeating_job = self._controller.CallRepeatingQtSafe(self, 0.25, 0.25, self.REPEATINGPageUpdate)
self._page_update_repeating_job = self._controller.CallRepeatingQtSafe( self, 0.25, 0.25, 'repeating page update', self.REPEATINGPageUpdate )
self._clipboard_watcher_repeating_job = None
@ -549,7 +549,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._UpdateSystemTrayIcon( currently_booting = True )
self._controller.CallLaterQtSafe( self, 0.5, self._InitialiseSession ) # do this in callafter as some pages want to talk to controller.gui, which doesn't exist yet!
self._controller.CallLaterQtSafe( self, 0.5, 'initialise session', self._InitialiseSession ) # do this in callafter as some pages want to talk to controller.gui, which doesn't exist yet!
def _AboutWindow( self ):
@ -762,11 +762,13 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
text = 'This will automatically set up your client with the Public Tag Repository, just as if you had added it manually under services->manage services.'
text = 'This will automatically set up your client with public shared \'read-only\' account for the Public Tag Repository, just as if you had added it manually under services->manage services.'
text += os.linesep * 2
text += 'Be aware that the PTR has been growing since 2011 and now has hundreds of millions of mappings. As of 2020-03, it requires about 4GB of bandwidth and file storage, and your database itself will grow by 25GB! Processing also takes a lot of CPU and HDD work, and, due to the unavoidable mechanical latency of HDDs, will only work in reasonable time if your hydrus database is on an SSD.'
text += 'Over the coming weeks, your client will download updates and then process them into your database in idle time, and the PTR\'s tags will increasingly appear across your files. If you decide to upload tags, it is just a couple of clicks (under services->manage services again) to generate your own account that has permission to do so.'
text += os.linesep * 2
text += 'If you are on a mechanical HDD or do not have the space on your SSD, cancel out now.'
text += 'Be aware that the PTR has been growing since 2011 and now has more than a billion mappings. As of 2021-06, it requires about 6GB of bandwidth and file storage, and your database itself will grow by 50GB! Processing also takes a lot of CPU and HDD work, and, due to the unavoidable mechanical latency of HDDs, will only work in reasonable time if your hydrus database is on an SSD.'
text += os.linesep * 2
text += '++++If you are on a mechanical HDD or will not be able to free up enough space on your SSD, cancel out now.++++'
if have_it_already:
@ -874,6 +876,29 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
def _BootOrStopClipboardWatcherIfNeeded( self ):
allow_watchers = self._controller.new_options.GetBoolean( 'watch_clipboard_for_watcher_urls' )
allow_other_recognised_urls = self._controller.new_options.GetBoolean( 'watch_clipboard_for_other_recognised_urls' )
if allow_watchers or allow_other_recognised_urls:
if self._clipboard_watcher_repeating_job is None:
self._clipboard_watcher_repeating_job = self._controller.CallRepeatingQtSafe( self, 1.0, 1.0, 'repeating clipboard watcher', self.REPEATINGClipboardWatcher )
else:
if self._clipboard_watcher_destination_page_watcher is not None:
self._clipboard_watcher_repeating_job.Cancel()
self._clipboard_watcher_repeating_job = None
def _CheckDBIntegrity( self ):
message = 'This will check the database for missing and invalid entries. It may take several minutes to complete.'
@ -1691,7 +1716,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if self._clipboard_watcher_repeating_job is None:
self._clipboard_watcher_repeating_job = self._controller.CallRepeatingQtSafe(self, 1.0, 1.0, self.REPEATINGClipboardWatcher)
self._BootOrStopClipboardWatcherIfNeeded()
@ -2210,17 +2235,17 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
last_session_save_period_minutes = self._controller.new_options.GetInteger( 'last_session_save_period_minutes' )
#self._controller.CallLaterQtSafe(self, 1.0, self.adjustSize ) # some i3 thing--doesn't layout main gui on init for some reason
#self._controller.CallLaterQtSafe(self, 1.0, 'adjust size', self.adjustSize ) # some i3 thing--doesn't layout main gui on init for some reason
self._controller.CallLaterQtSafe(self, last_session_save_period_minutes * 60, self.AutoSaveLastSession)
self._controller.CallLaterQtSafe(self, last_session_save_period_minutes * 60, 'auto save session', self.AutoSaveLastSession )
self._clipboard_watcher_repeating_job = self._controller.CallRepeatingQtSafe(self, 1.0, 1.0, self.REPEATINGClipboardWatcher)
self._BootOrStopClipboardWatcherIfNeeded()
self._controller.ReportFirstSessionLoaded()
self._controller.CallLaterQtSafe( self, 0.25, do_it, default_gui_session, load_a_blank_page )
self._controller.CallLaterQtSafe( self, 0.25, 'load a blank page', do_it, default_gui_session, load_a_blank_page )
def _LockServer( self, service_key, lock ):
@ -4097,39 +4122,39 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
t = 0.25
HG.client_controller.CallLaterQtSafe(self, t, self._notebook.NewPageQuery, default_local_file_service_key, page_name ='test', on_deepest_notebook = True)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self._notebook.NewPageQuery, default_local_file_service_key, page_name = 'test', on_deepest_notebook = True )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_PAGE_OF_PAGES))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_PAGE_OF_PAGES ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, page_of_pages.NewPageQuery, default_local_file_service_key, page_name ='test', on_deepest_notebook = False)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', page_of_pages.NewPageQuery, default_local_file_service_key, page_name ='test', on_deepest_notebook = False )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_DUPLICATE_FILTER_PAGE))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_DUPLICATE_FILTER_PAGE ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_GALLERY_DOWNLOADER_PAGE))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_GALLERY_DOWNLOADER_PAGE ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_SIMPLE_DOWNLOADER_PAGE))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_SIMPLE_DOWNLOADER_PAGE ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_URL_DOWNLOADER_PAGE))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_URL_DOWNLOADER_PAGE ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_WATCHER_DOWNLOADER_PAGE))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_NEW_WATCHER_DOWNLOADER_PAGE ) )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.ProposeSaveGUISession, 'last session' )
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProposeSaveGUISession, 'last session' )
return page_of_pages
@ -4138,7 +4163,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._notebook.CloseCurrentPage()
HG.client_controller.CallLaterQtSafe(self, 0.5, self._UnclosePage)
HG.client_controller.CallLaterQtSafe( self, 0.5, 'test job', self._UnclosePage )
def qt_close_pages( page_of_pages ):
@ -4151,18 +4176,18 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for i in indices:
HG.client_controller.CallLaterQtSafe(self, t, page_of_pages._ClosePage, i)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', page_of_pages._ClosePage, i )
t += 0.25
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self._notebook.CloseCurrentPage)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self._notebook.CloseCurrentPage )
t += 0.25
HG.client_controller.CallLaterQtSafe(self, t, self.DeleteAllClosedPages)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.DeleteAllClosedPages )
def qt_test_ac():
@ -4175,17 +4200,17 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
t = 0.5
HG.client_controller.CallLaterQtSafe(self, t, page.SetSearchFocus)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', page.SetSearchFocus )
ac_widget = page.GetManagementPanel()._tag_autocomplete._text_ctrl
t += 0.5
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_SET_MEDIA_FOCUS))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_SET_MEDIA_FOCUS ) )
t += 0.5
HG.client_controller.CallLaterQtSafe(self, t, self.ProcessApplicationCommand, CAC.ApplicationCommand(CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_SET_SEARCH_FOCUS))
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self.ProcessApplicationCommand, CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_SIMPLE, CAC.SIMPLE_SET_SEARCH_FOCUS ) )
t += 0.5
@ -4193,36 +4218,36 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for c in 'the colour of her hair':
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, ord( c ), text = c )
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, ord( c ), text = c )
t += 0.01
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += SYS_PRED_REFRESH
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += SYS_PRED_REFRESH
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Down)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Down )
t += 0.05
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += SYS_PRED_REFRESH
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Down)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Down )
t += 0.05
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += SYS_PRED_REFRESH
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
for i in range( 16 ):
@ -4230,44 +4255,44 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for j in range( i + 1 ):
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Down)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Down )
t += 0.1
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += SYS_PRED_REFRESH
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, None, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, None, QC.Qt.Key_Return )
t += 1.0
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Down)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Down )
t += 0.05
HG.client_controller.CallLaterQtSafe(self, t, uias.Char, ac_widget, QC.Qt.Key_Return)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', uias.Char, ac_widget, QC.Qt.Key_Return )
t += 1.0
HG.client_controller.CallLaterQtSafe(self, t, self._notebook.CloseCurrentPage)
HG.client_controller.CallLaterQtSafe( self, t, 'test job', self._notebook.CloseCurrentPage )
def do_it():
# pages
page_of_pages = HG.client_controller.CallBlockingToQt(self, qt_open_pages)
page_of_pages = HG.client_controller.CallBlockingToQt( self, qt_open_pages )
time.sleep( 4 )
HG.client_controller.CallBlockingToQt(self, qt_close_unclose_one_page)
HG.client_controller.CallBlockingToQt( self, qt_close_unclose_one_page )
time.sleep( 1.5 )
HG.client_controller.CallBlockingToQt(self, qt_close_pages, page_of_pages)
HG.client_controller.CallBlockingToQt( self, qt_close_pages, page_of_pages )
time.sleep( 5 )
@ -4275,7 +4300,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
# a/c
HG.client_controller.CallBlockingToQt(self, qt_test_ac)
HG.client_controller.CallBlockingToQt( self, qt_test_ac )
HG.client_controller.CallToThread( do_it )
@ -4975,7 +5000,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if self._CurrentlyMinimisedOrHidden() or dialog_is_open or not ClientGUIFunctions.TLWOrChildIsActive( self ):
self._controller.CallLaterQtSafe( self, 0.5, self.AddModalMessage, job_key )
self._controller.CallLaterQtSafe( self, 0.5, 'modal message wait loop', self.AddModalMessage, job_key )
else:
@ -5019,7 +5044,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if only_save_last_session_during_idle and not self._controller.CurrentlyIdle():
self._controller.CallLaterQtSafe( self, 60, self.AutoSaveLastSession )
self._controller.CallLaterQtSafe( self, 60, 'auto session save wait loop', self.AutoSaveLastSession )
else:
@ -5037,7 +5062,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
controller.SaveGUISession( session )
controller.CallLaterQtSafe( win, next_call_delay, callable )
controller.CallLaterQtSafe( win, next_call_delay, 'auto save session', callable )
self._controller.CallToThread( do_it, self._controller, session, self, next_call_delay, callable )
@ -6898,7 +6923,7 @@ Try to keep this below 10 million!'''
if db_going_to_hang_if_we_hit_it or menu_open:
self._controller.CallLaterQtSafe( self, 0.5, self.RefreshMenu )
self._controller.CallLaterQtSafe( self, 0.5, 'menu refresh wait loop', self.RefreshMenu )
return
@ -6917,7 +6942,7 @@ Try to keep this below 10 million!'''
if len( self._dirty_menus ) > 0:
self._controller.CallLaterQtSafe( self, 0.5, self.RefreshMenu )
self._controller.CallLaterQtSafe( self, 0.5, 'refresh menu', self.RefreshMenu )
@ -6951,7 +6976,7 @@ Try to keep this below 10 million!'''
if self._ui_update_repeating_job is None:
self._ui_update_repeating_job = self._controller.CallRepeatingQtSafe(self, 0.0, 0.1, self.REPEATINGUIUpdate)
self._ui_update_repeating_job = self._controller.CallRepeatingQtSafe( self, 0.0, 0.1, 'repeating ui update', self.REPEATINGUIUpdate )
@ -7008,9 +7033,7 @@ Try to keep this below 10 million!'''
if not ( allow_watchers or allow_other_recognised_urls ):
self._clipboard_watcher_repeating_job.Cancel()
self._clipboard_watcher_repeating_job = None
self._BootOrStopClipboardWatcherIfNeeded()
return

View File

@ -31,7 +31,7 @@ class CaptureAPIAccessPermissionsRequestPanel( ClientGUIScrolledPanels.ReviewPan
self.widget().setLayout( vbox )
self._repeating_job = HG.client_controller.CallRepeatingQtSafe(self, 0.0, 0.5, self.REPEATINGUpdate)
self._repeating_job = HG.client_controller.CallRepeatingQtSafe( self, 0.0, 0.5, 'repeating client api permissions check', self.REPEATINGUpdate )
def GetAPIAccessPermissions( self ):

View File

@ -72,7 +72,14 @@ class AsyncQtJob( object ):
c = self._errback_callable
HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb )
try:
HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb )
except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ):
return
return
@ -98,7 +105,14 @@ class AsyncQtJob( object ):
c = self._errback_callable
HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb )
try:
HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb )
except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ):
return

View File

@ -112,7 +112,7 @@ class DialogChooseNewServiceMethod( Dialog ):
self._should_register = False
HG.client_controller.CallAfterQtSafe( self._register, self._register.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._register )
def EventRegister( self ):
@ -198,7 +198,7 @@ class DialogGenerateNewAccounts( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._ok )
def EventOK( self ):
@ -356,7 +356,7 @@ class DialogInputLocalBooruShare( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( self._ok )
def EventCopyExternalShareURL( self ):
@ -463,7 +463,7 @@ class DialogInputNamespaceRegex( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( self._ok )
def EventOK( self ):
@ -557,7 +557,7 @@ class DialogInputTags( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( self._tag_autocomplete, self._tag_autocomplete.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( self._tag_autocomplete )
def EnterTags( self, tags ):
@ -639,7 +639,7 @@ class DialogInputUPnPMapping( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( self._ok )
def GetInfo( self ):
@ -1034,7 +1034,7 @@ class DialogYesYesNo( Dialog ):
QP.SetInitialSize( self, size_hint )
HG.client_controller.CallAfterQtSafe( yes_buttons[0], yes_buttons[0].setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( yes_buttons[0] )
def _DoYes( self, value ):

View File

@ -79,11 +79,11 @@ def GetYesNo( win, message, title = 'Are you sure?', yes_label = 'yes', no_label
if auto_yes_time is not None:
job = HG.client_controller.CallLaterQtSafe( dlg, auto_yes_time, dlg.done, QW.QDialog.Accepted )
job = HG.client_controller.CallLaterQtSafe( dlg, auto_yes_time, 'dialog auto-yes', dlg.done, QW.QDialog.Accepted )
elif auto_no_time is not None:
job = HG.client_controller.CallLaterQtSafe( dlg, auto_no_time, dlg.done, QW.QDialog.Rejected )
job = HG.client_controller.CallLaterQtSafe( dlg, auto_no_time, 'dialog auto-no', dlg.done, QW.QDialog.Rejected )
try:

View File

@ -549,9 +549,9 @@ class EditGUGsPanel( ClientGUIScrolledPanels.EditPanel ):
menu_items = []
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_gugs.html' ) )
call = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_gugs.html' ) )
menu_items.append( ( 'normal', 'open the gugs help', 'Open the help page for gugs in your web browser.', page_func ) )
menu_items.append( ( 'normal', 'open the gugs help', 'Open the help page for gugs in your web browser.', call ) )
help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )
@ -1780,9 +1780,9 @@ class EditURLClassesPanel( ClientGUIScrolledPanels.EditPanel ):
menu_items = []
page_func = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_url_classes.html' ) )
call = HydrusData.Call( ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'downloader_url_classes.html' ) )
menu_items.append( ( 'normal', 'open the url classes help', 'Open the help page for url classes in your web browser.', page_func ) )
menu_items.append( ( 'normal', 'open the url classes help', 'Open the help page for url classes in your web browser.', call ) )
help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items )

View File

@ -16,6 +16,7 @@ from hydrus.client import ClientConstants as CC
from hydrus.client import ClientExporting
from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUITags
from hydrus.client.gui import ClientGUITime
@ -651,13 +652,13 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._UpdateTxtButton()
HG.client_controller.CallAfterQtSafe( self._export, self._export.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( self._export )
self._paths.itemSelectionChanged.connect( self._RefreshTags )
if do_export_and_then_quit:
QP.CallAfter( self._DoExport, True )
HG.client_controller.CallAfterQtSafe( self, 'doing export before dialog quit', self._DoExport, True )

View File

@ -184,19 +184,22 @@ class EditFileSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
ClientGUIMenus.AppendMenuItem( menu, 'open selected import files in a new page', 'Show all the known selected files in a new thumbnail page. This is complicated, so cannot always be guaranteed, even if the import says \'success\'.', self._ShowSelectionInNewPage )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'copy sources', 'Copy all the selected sources to clipboard.', self._CopySelectedFileSeedData )
ClientGUIMenus.AppendMenuItem( menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'open sources', 'Open all the selected sources in your file explorer or web browser.', self._OpenSelectedFileSeedData )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'try again', 'Reset the progress of all the selected imports.', HydrusData.Call( self._SetSelected, CC.STATUS_UNKNOWN ) )
ClientGUIMenus.AppendMenuItem( menu, 'skip', 'Skip all the selected imports.', HydrusData.Call( self._SetSelected, CC.STATUS_SKIPPED ) )
ClientGUIMenus.AppendMenuItem( menu, 'delete from list', 'Remove all the selected imports.', self._DeleteSelected )
ClientGUIMenus.AppendMenuItem( menu, 'delete from list', 'Remove all the selected imports.', HydrusData.Call( self._DeleteSelected ) )
return menu

View File

@ -4,6 +4,7 @@ from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusText
from hydrus.client.gui import QtPorting as QP
@ -266,6 +267,10 @@ def SetBitmapButtonBitmap( button, bitmap ):
button.last_bitmap = bitmap
def SetFocusLater( win: QW.QWidget ):
HG.client_controller.CallAfterQtSafe( win, 'set focus to a window', win.setFocus, QC.Qt.OtherFocusReason )
def TLWIsActive( window ):
return window.window() == QW.QApplication.activeWindow()

View File

@ -1567,7 +1567,7 @@ class EditLocalImportFilenameTaggingPanel( ClientGUIScrolledPanels.EditPanel ):
self._schedule_refresh_file_list_job = None
self._schedule_refresh_file_list_job = HG.client_controller.CallLaterQtSafe(self, 0.5, self.RefreshFileList)
self._schedule_refresh_file_list_job = HG.client_controller.CallLaterQtSafe( self, 0.5, 'refresh path list', self.RefreshFileList )
@ -1658,7 +1658,7 @@ class EditFilenameTaggingOptionPanel( ClientGUIScrolledPanels.EditPanel ):
self._schedule_refresh_tags_job = None
self._schedule_refresh_tags_job = HG.client_controller.CallLaterQtSafe(self, 0.5, self.RefreshTags)
self._schedule_refresh_tags_job = HG.client_controller.CallLaterQtSafe( self, 0.5, 'refresh tags', self.RefreshTags )
class GalleryImportPanel( ClientGUICommon.StaticBox ):

View File

@ -627,13 +627,13 @@ class PopupMessageManager( QW.QWidget ):
job_key.SetVariable( 'popup_text_1', 'initialising popup message manager\u2026' )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.25, 0.5, self.REPEATINGUpdate )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.25, 0.5, 'repeating popup message update', self.REPEATINGUpdate )
self._summary_bar.expandCollapse.connect( self.ExpandCollapse )
HG.client_controller.CallLaterQtSafe(self, 0.5, self.AddMessage, job_key)
HG.client_controller.CallLaterQtSafe( self, 0.5, 'initialise message', self.AddMessage, job_key )
HG.client_controller.CallLaterQtSafe(self, 1.0, job_key.Delete)
HG.client_controller.CallLaterQtSafe( self, 1.0, 'delete initial message', job_key.Delete )
def _CheckPending( self ):
@ -1140,7 +1140,7 @@ class PopupMessageDialogPanel( QW.QWidget ):
self._message_pubbed = False
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.25, 0.5, self.REPEATINGUpdate )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.25, 0.5, 'repeating popup dialog update', self.REPEATINGUpdate )
def CleanBeforeDestroy( self ):

View File

@ -4,6 +4,7 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
@ -28,7 +29,7 @@ class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.Resizing
self.widget().setLayout( vbox )
HG.client_controller.CallAfterQtSafe( self._commit, self._commit.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._commit )
class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
@ -65,7 +66,7 @@ class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPane
self.widget().setLayout( vbox )
HG.client_controller.CallAfterQtSafe( self._commit, self._commit.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._commit )
class QuestionYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
@ -99,7 +100,7 @@ class QuestionYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
self.widget().setLayout( vbox )
HG.client_controller.CallAfterQtSafe( self._yes, self._yes.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._yes )

View File

@ -1370,8 +1370,8 @@ class EditFileNotesPanel( ClientGUIScrolledPanels.EditPanel ):
self._notebook.setCurrentIndex( 0 )
HG.client_controller.CallAfterQtSafe( first_panel, first_panel.setFocus, QC.Qt.OtherFocusReason )
HG.client_controller.CallAfterQtSafe( first_panel, first_panel.moveCursor, QG.QTextCursor.End )
ClientGUIFunctions.SetFocusLater( first_panel )
HG.client_controller.CallAfterQtSafe( first_panel, 'moving cursor to end', first_panel.moveCursor, QG.QTextCursor.End )
#
@ -1420,8 +1420,8 @@ class EditFileNotesPanel( ClientGUIScrolledPanels.EditPanel ):
self._notebook.setCurrentWidget( control )
HG.client_controller.CallAfterQtSafe( control, control.setFocus, QC.Qt.OtherFocusReason )
HG.client_controller.CallAfterQtSafe( control, control.moveCursor, QG.QTextCursor.End )
ClientGUIFunctions.SetFocusLater( control )
HG.client_controller.CallAfterQtSafe( control, 'moving cursor to end', control.moveCursor, QG.QTextCursor.End )
self._UpdateButtons()
@ -2499,7 +2499,7 @@ class EditSelectFromListButtonsPanel( ClientGUIScrolledPanels.EditPanel ):
if not first_focused:
HG.client_controller.CallAfterQtSafe( button, button.setFocus, QC.Qt.OtherFocusReason)
ClientGUIFunctions.SetFocusLater( button )
first_focused = True

View File

@ -1549,7 +1549,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'If \'last session\' above, autosave it how often (minutes)?', self._last_session_save_period_minutes ) )
rows.append( ( 'If \'last session\' above, only autosave during idle time?', self._only_save_last_session_during_idle ) )
rows.append( ( 'Number of session backups to keep: ', self._number_of_gui_session_backups ) )
rows.append( ( 'Show warning popup if session size exceeds 500k: ', self._show_session_size_warnings ) )
rows.append( ( 'Show warning popup if session size exceeds 10,000,000: ', self._show_session_size_warnings ) )
sessions_gridbox = ClientGUICommon.WrapInGrid( self._sessions_panel, rows )
@ -3749,7 +3749,7 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'global', 'media', 'main_gui' ] )
HG.client_controller.CallAfterQtSafe( self, self._SetSearchFocus )
ClientGUIFunctions.SetFocusLater( self._url_input )
def _Copy( self ):

View File

@ -883,7 +883,7 @@ def THREADMigrateDatabase( controller, source, portable_locations, dest ):
def qt_code( job_key ):
HG.client_controller.CallLaterQtSafe( controller.gui, 3.0, controller.Exit )
HG.client_controller.CallLaterQtSafe( controller.gui, 3.0, 'close program', controller.Exit )
# no parent because this has to outlive the gui, obvs

View File

@ -173,7 +173,7 @@ class PNGExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._export.setText( 'done!' )
HG.client_controller.CallLaterQtSafe(self._export, 2.0, self._export.setText, 'export')
HG.client_controller.CallLaterQtSafe( self._export, 2.0, 'png export set text', self._export.setText, 'export' )
class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
@ -278,6 +278,6 @@ class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._export.setText( 'done!' )
HG.client_controller.CallLaterQtSafe(self._export, 2.0, self._export.setText, 'export')
HG.client_controller.CallLaterQtSafe( self._export, 2.0, 'png export set text', self._export.setText, 'export' )

View File

@ -1170,7 +1170,7 @@ class EditSubscriptionQueryPanel( ClientGUIScrolledPanels.EditPanel ):
self._query_text.selectAll()
HG.client_controller.CallAfterQtSafe( self._query_text, self._query_text.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._query_text )
def _GetValue( self ) -> typing.Tuple[ ClientImportSubscriptionQuery.SubscriptionQueryHeader, ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ]:

View File

@ -1348,7 +1348,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
self._redundant_st.setText( text )
HG.client_controller.CallLaterQtSafe( self._redundant_st, 2, self._redundant_st.setText, '' )
HG.client_controller.CallLaterQtSafe( self._redundant_st, 2, 'clear redundant error', self._redundant_st.setText, '' )
def _SimpleAddBlacklistMultiple( self, tag_slices ):
@ -1982,7 +1982,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
if page is not None:
HG.client_controller.CallAfterQtSafe( page, page.SetTagBoxFocus )
HG.client_controller.CallAfterQtSafe( page, 'setting page focus', page.SetTagBoxFocus )
@ -3833,7 +3833,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
if page is not None:
HG.client_controller.CallAfterQtSafe( page, page.SetTagBoxFocus )
HG.client_controller.CallAfterQtSafe( page, 'setting page focus', page.SetTagBoxFocus )

View File

@ -14,7 +14,7 @@ from hydrus.client.gui import QtPorting as QP
CHILD_POSITION_PADDING = 24
FUZZY_PADDING = 10
def GetSafePosition( position: QC.QPoint ):
def GetSafePosition( position: QC.QPoint, frame_key ):
# some window managers size the windows just off screen to cut off borders
# so choose a test position that's a little more lenient
@ -37,7 +37,7 @@ def GetSafePosition( position: QC.QPoint ):
if rescue_screen == first_display:
message = 'A window that wanted to display at "{}" was rescued from apparent off-screen to the new location at "{}".'.format( position, rescue_position )
message = 'A window with frame key "{}" that wanted to display at "{}" was rescued from apparent off-screen to the new location at "{}".'.format( frame_key, position, rescue_position )
return ( rescue_position, message )
@ -49,7 +49,7 @@ def GetSafePosition( position: QC.QPoint ):
HydrusData.PrintException( e )
message = 'A window that wanted to display at "{}" could not be rescued from off-screen! Please let hydrus dev know!'
message = 'A window with frame key "{}" that wanted to display at "{}" could not be rescued from off-screen! Please let hydrus dev know!'.format( frame_key, position )
return ( None, message )
@ -196,7 +196,7 @@ def SaveTLWSizeAndPosition( tlw: QW.QWidget, frame_key ):
if not ( maximised or fullscreen ):
( safe_position, position_message ) = GetSafePosition( tlw.pos() )
( safe_position, position_message ) = GetSafePosition( tlw.pos(), frame_key )
if safe_position is not None:
@ -298,7 +298,7 @@ def SetInitialTLWSizeAndPosition( tlw: QW.QWidget, frame_key ):
( safe_position, position_message ) = GetSafePosition( desired_position )
( safe_position, position_message ) = GetSafePosition( desired_position, frame_key )
if we_care_about_off_screen_messages and position_message is not None:
@ -682,7 +682,7 @@ class FrameThatResizes( Frame ):
def EventSizeAndPositionChanged( self, event ):
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
HG.client_controller.CallLaterQtSafe( self, 0.1, SaveTLWSizeAndPosition, self, self._frame_key )
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
return True # was: event.ignore()
@ -705,7 +705,7 @@ class MainFrameThatResizes( MainFrame ):
def EventSizeAndPositionChanged( self, event ):
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
HG.client_controller.CallLaterQtSafe( self, 0.1, SaveTLWSizeAndPosition, self, self._frame_key )
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
return True # was: event.ignore()

View File

@ -1,4 +1,3 @@
import collections
import typing
from qtpy import QtCore as QC
@ -1897,7 +1896,7 @@ class CanvasPanel( Canvas ):
# brush this up to handle different service keys
# undelete do an optional service key too
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = HG.client_controller.services_manager.GetName )
for file_service_key in local_file_service_keys_we_are_in:
@ -2415,7 +2414,7 @@ class CanvasWithHovers( CanvasWithDetails ):
self._timer_cursor_hide_job = HG.client_controller.CallLaterQtSafe( self, 0.1, self._HideCursorCheck )
self._timer_cursor_hide_job = HG.client_controller.CallLaterQtSafe( self, 0.1, 'hide cursor check', self._HideCursorCheck )
def _TryToCloseWindow( self ):
@ -3346,7 +3345,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
HG.client_controller.CallLaterQtSafe( self, 0.1, catch_up )
HG.client_controller.CallLaterQtSafe( self, 0.1, 'duplicates filter post-processing wait', catch_up )
def SetMedia( self, media ):
@ -3557,9 +3556,9 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
if not image_cache.HasImageRenderer( hash ):
# we do qt safe to make sure the job is cancelled if we are destroyed, but then that launches an immediate off-qt thread to actually poke around in image cache land, which _should_ be fast, but let's not mess around
# we do qt safe to make sure the job is cancelled if we are destroyed
HG.client_controller.CallLaterQtSafe( self, delay, HG.client_controller.CallToThread, image_cache.PrefetchImageRenderer, media )
HG.client_controller.CallLaterQtSafe( self, delay, 'image pre-fetch', image_cache.PrefetchImageRenderer, media )
@ -4157,7 +4156,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
self._timer_slideshow_interval = interval
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, self.DoSlideshow )
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, 'slideshow', self.DoSlideshow )
@ -4190,11 +4189,11 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
self._ShowNext()
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, self.DoSlideshow )
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, self._timer_slideshow_interval, 'slideshow', self.DoSlideshow )
else:
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, 0.1, self.DoSlideshow )
self._timer_slideshow_job = HG.client_controller.CallLaterQtSafe( self, 0.1, 'slideshow', self.DoSlideshow )
@ -4367,7 +4366,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
# brush this up to handle different service keys
# undelete do an optional service key too
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = HG.client_controller.services_manager.GetName )
for file_service_key in local_file_service_keys_we_are_in:

View File

@ -591,7 +591,7 @@ class ReviewAllBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._bandwidths.Sort()
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, self._Update )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, 'repeating all bandwidth status update', self._Update )
#
@ -950,9 +950,9 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
#
self._rules_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, self._UpdateRules )
self._rules_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, 'repeating bandwidth rules update', self._UpdateRules )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 1.0, self._Update )
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 1.0, 'repeating bandwidth status update', self._Update )
def _EditRules( self ):

View File

@ -2403,7 +2403,7 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
def SetSearchFocus( self ):
HG.client_controller.CallAfterQtSafe( self._query_input, self._query_input.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._query_input )
def Start( self ):
@ -3222,7 +3222,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
def SetSearchFocus( self ):
HG.client_controller.CallAfterQtSafe( self._watcher_url_input, self._watcher_url_input.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._watcher_url_input )
def Start( self ):
@ -3677,7 +3677,7 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
def SetSearchFocus( self ):
HG.client_controller.CallAfterQtSafe( self._page_url_input, self._page_url_input.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._page_url_input )
def Start( self ):
@ -3828,7 +3828,7 @@ class ManagementPanelImporterURLs( ManagementPanelImporter ):
def SetSearchFocus( self ):
HG.client_controller.CallAfterQtSafe( self._url_input, self._url_input.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._url_input )
def Start( self ):
@ -4904,7 +4904,7 @@ class ManagementPanelQuery( ManagementPanel ):
if self._search_enabled:
HG.client_controller.CallAfterQtSafe( self._tag_autocomplete, self._tag_autocomplete.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._tag_autocomplete )

View File

@ -530,7 +530,7 @@ class Page( QW.QSplitter ):
if CGC.core().MenuIsOpen():
self._controller.CallLaterQtSafe( self, 0.5, clean_up_old_panel )
self._controller.CallLaterQtSafe( self, 0.5, 'menu closed panel swap loop', clean_up_old_panel )
return
@ -898,7 +898,7 @@ class Page( QW.QSplitter ):
status = 'Loading initial files\u2026 ' + HydrusData.ConvertValueRangeToPrettyString( len( initial_media_results ), len( initial_hashes ) )
controller.CallAfterQtSafe( self, qt_code_status, status )
controller.CallAfterQtSafe( self, 'setting status bar loading string', qt_code_status, status )
QP.CallAfter( qt_code_status, status )
@ -2573,7 +2573,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
self._CloseAllPages( polite = False, delete_pages = True )
self._controller.CallLaterQtSafe(self, 1.0, self.AppendGUISessionFreshest, name, load_in_a_page_of_pages = False)
self._controller.CallLaterQtSafe( self, 1.0, 'append session', self.AppendGUISessionFreshest, name, load_in_a_page_of_pages = False )
else:
@ -2824,7 +2824,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
# this is here for now due to the pagechooser having a double-layer dialog on a booru choice, which messes up some focus inheritance
self._controller.CallLaterQtSafe( self, 0.5, page.SetSearchFocus )
self._controller.CallLaterQtSafe( self, 0.5, 'set page focus', page.SetSearchFocus )
return page

View File

@ -3268,7 +3268,7 @@ class MediaPanelThumbnails( MediaPanel ):
all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } )
all_local_file_domains = services_manager.Filter( all_specific_file_domains, ( HC.LOCAL_FILE_DOMAIN, ) )
all_local_file_domains_sorted = sorted( all_local_file_domains, key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
all_local_file_domains_sorted = sorted( all_local_file_domains, key = HG.client_controller.services_manager.GetName )
all_file_repos = services_manager.Filter( all_specific_file_domains, ( HC.FILE_REPOSITORY, ) )
@ -4192,7 +4192,7 @@ class MediaPanelThumbnails( MediaPanel ):
hashes_to_media_results = { media_result.GetHash() : media_result for media_result in media_results }
HG.client_controller.CallLaterQtSafe( win, 0, qt_do_update, hashes_to_media_results )
HG.client_controller.CallAfterQtSafe( win, 'new file info notification', qt_do_update, hashes_to_media_results )
affected_hashes = self._hashes.intersection( hashes )

View File

@ -362,7 +362,7 @@ def ReadFetch(
return
HG.client_controller.CallLaterQtSafe( win, 0.0, results_callable, job_key, parsed_autocomplete_text, results_cache, matches )
HG.client_controller.CallAfterQtSafe( win, 'read a/c fetch', results_callable, job_key, parsed_autocomplete_text, results_cache, matches )
def PutAtTopOfMatches( matches: list, predicate: ClientSearch.Predicate, insert_if_does_not_exist: bool = True ):
@ -503,7 +503,7 @@ def WriteFetch( win, job_key, results_callable, parsed_autocomplete_text: Client
InsertTagPredicates( matches, display_tag_service_key, parsed_autocomplete_text )
HG.client_controller.CallLaterQtSafe( win, 0.0, results_callable, job_key, parsed_autocomplete_text, results_cache, matches )
HG.client_controller.CallAfterQtSafe( win, 'write a/c fetch', results_callable, job_key, parsed_autocomplete_text, results_cache, matches )
class ListBoxTagsPredicatesAC( ClientGUIListBoxes.ListBoxTagsPredicates ):
@ -819,7 +819,7 @@ class AutoCompleteDropdown( QW.QWidget ):
self._ScheduleResultsRefresh( 0.0 )
HG.client_controller.CallLaterQtSafe( self, 0.05, self._DropdownHideShow )
HG.client_controller.CallLaterQtSafe( self, 0.05, 'hide/show dropdown', self._DropdownHideShow )
def _BroadcastChoices( self, predicates, shift_down ):
@ -923,7 +923,7 @@ class AutoCompleteDropdown( QW.QWidget ):
self._schedule_results_refresh_job.Cancel()
self._schedule_results_refresh_job = HG.client_controller.CallLaterQtSafe( self, delay, self._UpdateSearchResults )
self._schedule_results_refresh_job = HG.client_controller.CallLaterQtSafe( self, delay, 'a/c results refresh', self._UpdateSearchResults )
def _SetupTopListBox( self ):
@ -1167,7 +1167,7 @@ class AutoCompleteDropdown( QW.QWidget ):
if self._float_mode and event.type() in ( QC.QEvent.WindowActivate, QC.QEvent.WindowDeactivate ):
# we delay this slightly because when you click from dropdown to text, the deactivate event fires before the focusin, leading to a frame of hide
HG.client_controller.CallLaterQtSafe( self, 0.05, self._DropdownHideShow )
HG.client_controller.CallLaterQtSafe( self, 0.05, 'hide/show dropdown', self._DropdownHideShow )
return False
@ -1385,6 +1385,11 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
HG.client_controller.sub( self, 'NotifyNewServices', 'notify_new_services' )
def _AddAllKnownFilesServiceTypeIfAllowed( self, service_types_in_order ):
raise NotImplementedError()
def _ChangeFileService( self, file_service_key ):
if not HG.client_controller.services_manager.ServiceExists( file_service_key ):
@ -1497,10 +1502,7 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
service_types_in_order.append( HC.FILE_REPOSITORY )
if advanced_mode and self._allow_all_known_files:
service_types_in_order.append( HC.COMBINED_FILE )
self._AddAllKnownFilesServiceTypeIfAllowed( service_types_in_order )
services = services_manager.GetServices( service_types_in_order )
@ -1672,6 +1674,16 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
self._search_pause_play.valueChanged.connect( self.SetSynchronised )
def _AddAllKnownFilesServiceTypeIfAllowed( self, service_types_in_order ):
advanced_mode = HG.client_controller.new_options.GetBoolean( 'advanced_mode' )
if advanced_mode and self._allow_all_known_files:
service_types_in_order.append( HC.COMBINED_FILE )
def _AdvancedORInput( self ):
title = 'enter advanced OR predicates'
@ -2008,7 +2020,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
AppendLoadingPredicate( stub_predicates )
HG.client_controller.CallLaterQtSafe( self, 0.2, self.SetStubPredicates, job_key, stub_predicates, parsed_autocomplete_text )
HG.client_controller.CallLaterQtSafe( self, 0.2, 'set stub predicates', self.SetStubPredicates, job_key, stub_predicates, parsed_autocomplete_text )
if self._under_construction_or_predicate is None:
@ -2485,6 +2497,14 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
self._dropdown_window.setLayout( vbox )
def _AddAllKnownFilesServiceTypeIfAllowed( self, service_types_in_order ):
if self._allow_all_known_files:
service_types_in_order.append( HC.COMBINED_FILE )
def _BroadcastChoices( self, predicates, shift_down ):
tags = { predicate.GetValue() for predicate in predicates }
@ -2620,7 +2640,7 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
AppendLoadingPredicate( stub_predicates )
HG.client_controller.CallLaterQtSafe( self, 0.2, self.SetStubPredicates, job_key, stub_predicates, parsed_autocomplete_text )
HG.client_controller.CallLaterQtSafe( self, 0.2, 'set stub predicates', self.SetStubPredicates, job_key, stub_predicates, parsed_autocomplete_text )
tag_search_context = ClientSearch.TagSearchContext( service_key = self._tag_service_key, display_service_key = self._display_tag_service_key )

View File

@ -11,6 +11,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientSearch
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIShortcuts
@ -612,7 +613,7 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
if len( static_pred_buttons ) > 0 and len( editable_pred_panels ) == 0:
HG.client_controller.CallAfterQtSafe( static_pred_buttons[0], static_pred_buttons[0].setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( static_pred_buttons[0] )
self.widget().setLayout( vbox )
@ -654,7 +655,7 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ):
self.setLayout( hbox )
HG.client_controller.CallAfterQtSafe( self._ok, self._ok.setFocus, QC.Qt.OtherFocusReason )
ClientGUIFunctions.SetFocusLater( self._ok )
def _DefaultsMenu( self ):

View File

@ -204,7 +204,7 @@ class EditFavouriteSearchesPanel( ClientGUIScrolledPanels.EditPanel ):
if initial_search_row_to_edit is not None:
HG.client_controller.CallLaterQtSafe( self, 0.5, self._AddNewFavouriteSearch, initial_search_row_to_edit )
HG.client_controller.CallLaterQtSafe( self, 0.5, 'add new favourite search', self._AddNewFavouriteSearch, initial_search_row_to_edit )

View File

@ -1,4 +1,3 @@
import collections
import os
import time
import typing
@ -56,7 +55,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ):
service_string = HC.service_string_lookup[ service_type ]
menu_items.append( ( 'normal', service_string, 'Add a new ' + service_string + '.', HydrusData.Call( self._Add, service_type ) ) )
menu_items.append( ( 'normal', service_string, 'Add a new {}.'.format( service_string ), HydrusData.Call( self._Add, service_type ) ) )
self._add_button = ClientGUIMenuButton.MenuButton( self, 'add', menu_items = menu_items )
@ -88,7 +87,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ):
if auto_account_creation_service_key is not None:
HG.client_controller.CallLaterQtSafe( self, 1.2, self._Edit, auto_account_creation_service_key = auto_account_creation_service_key )
HG.client_controller.CallLaterQtSafe( self, 1.2, 'auto-account creation spawn', self._Edit, auto_account_creation_service_key = auto_account_creation_service_key )
@ -715,7 +714,7 @@ class EditServiceRestrictedSubPanel( ClientGUICommon.StaticBox ):
if auto_account_creation_service_key is not None:
HG.client_controller.CallLaterQtSafe( self, 1.2, self._STARTFetchAutoAccountCreationAccountTypes )
HG.client_controller.CallLaterQtSafe( self, 1.2, 'auto-account service spawn', self._STARTFetchAutoAccountCreationAccountTypes )
@ -2446,7 +2445,7 @@ class ReviewServiceRestrictedSubPanel( ClientGUICommon.StaticBox ):
if len( p_s ) == 0:
menu_items.append( ( 'label', 'no special permissions', 'no special permissions', None ) )
menu_items.append( ( 'label', 'can only download', 'can only download', None ) )
else:
@ -3476,7 +3475,7 @@ class ReviewServiceRatingSubPanel( ClientGUICommon.StaticBox ):
menu_items = []
menu_items.append( ( 'normal', 'for deleted files', 'delete all set ratings for files that have since been deleted', HydrusData.Call( self._ClearRatings, 'delete_for_deleted_files', 'deleted files' ) ) )
menu_items.append( ( 'normal', 'for deleted files', 'delete all set ratings for files that have since been deleted', HydrusData.Call( self._ClearRatings, 'delete_for_deleted_files', 'deleted files' ) ) )
menu_items.append( ( 'normal', 'for all non-local files', 'delete all set ratings for files that are not in this client right now', HydrusData.Call( self._ClearRatings, 'delete_for_non_local_files', 'non-local files' ) ) )
menu_items.append( ( 'separator', None, None, None ) )
menu_items.append( ( 'normal', 'for all files', 'delete all set ratings for all files', HydrusData.Call( self._ClearRatings, 'delete_for_all_files', 'ALL FILES' ) ) )

View File

@ -1347,7 +1347,7 @@ class MultipleGalleryImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
gug = HG.client_controller.network_engine.domain_manager.GetGUG( self._gug_key_and_name )
gug = HG.client_controller.network_engine.domain_manager.GetGUG( gug_key_and_name )
if gug is None:
@ -1356,8 +1356,6 @@ class MultipleGalleryImport( HydrusSerialisable.SerialisableBase ):
return
self._gug_key_and_name = gug.GetGUGKeyAndName() # just a refresher, to keep up with any changes
initial_search_urls = gug.GenerateGalleryURLs( query_text )
if len( initial_search_urls ) == 0:
@ -1367,7 +1365,7 @@ class MultipleGalleryImport( HydrusSerialisable.SerialisableBase ):
return
gallery_import = GalleryImport( query = query_text, source_name = self._gug_key_and_name[1], initial_search_urls = initial_search_urls, start_file_queue_paused = self._start_file_queues_paused, start_gallery_queue_paused = self._start_gallery_queues_paused )
gallery_import = GalleryImport( query = query_text, source_name = gug_key_and_name[1], initial_search_urls = initial_search_urls, start_file_queue_paused = self._start_file_queues_paused, start_gallery_queue_paused = self._start_gallery_queues_paused )
gallery_import.SetFileLimit( file_limit )

View File

@ -81,7 +81,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 443
SOFTWARE_VERSION = 444
CLIENT_API_VERSION = 17
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -1584,13 +1584,12 @@ class Call( object ):
def __init__( self, func, *args, **kwargs ):
self._label = None
self._func = func
self._args = args
self._kwargs = kwargs
self._default_label = 'Call: {}( {}, {} )'.format( self._func, self._args, self._kwargs )
self._human_label = None
def __call__( self ):
@ -1599,22 +1598,34 @@ class Call( object ):
def __repr__( self ):
return self._default_label
label = self._GetLabel()
return 'Call: {}'.format( label )
def GetLabel( self ):
def _GetLabel( self ) -> str:
if self._human_label is None:
if self._label is None:
return self._default_label
# this can actually cause an error with Qt objects that are dead or from the wrong thread, wew!
label = '{}( {}, {} )'.format( self._func, self._args, self._kwargs )
else:
label = self._label
return self._human_label
return label
def GetLabel( self ) -> str:
return self._GetLabel()
def SetLabel( self, label: str ):
self._human_label = label
self._label = label
class ContentUpdate( object ):

View File

@ -239,9 +239,12 @@ def GenerateNumPyImage( path, mime, force_pil = False ):
return numpy_image
def GenerateNumPyImageFromPILImage( pil_image ):
def GenerateNumPyImageFromPILImage( pil_image, dequantize = True ):
pil_image = Dequantize( pil_image )
if dequantize:
pil_image = Dequantize( pil_image )
( w, h ) = pil_image.size

View File

@ -13,7 +13,6 @@ class HydrusLogger( object ):
self._db_dir = db_dir
self._prefix = prefix
self._log_path_base = self._GetLogPathBase()
self._lock = threading.Lock()
self._log_closed = False
@ -57,16 +56,13 @@ class HydrusLogger( object ):
( current_year, current_month ) = ( current_time_struct.tm_year, current_time_struct.tm_mon )
log_path = self._log_path_base + ' - ' + str( current_year ) + '-' + str( current_month ) + '.log'
log_filename = '{} - {}-{:02}.log'.format( self._prefix, current_year, current_month )
log_path = os.path.join( self._db_dir, log_filename )
return log_path
def _GetLogPathBase( self ):
return os.path.join( self._db_dir, self._prefix )
def _OpenLog( self ):
self._log_path = self._GetLogPath()

View File

@ -18,7 +18,7 @@ def DoClick( click, panel, do_delayed_ok_afterwards = False ):
if do_delayed_ok_afterwards:
HG.test_controller.CallLaterQtSafe( panel, 1, PressKeyOnFocusedWindow, QC.Qt.Key_Return )
HG.test_controller.CallLaterQtSafe( panel, 1, 'test click', PressKeyOnFocusedWindow, QC.Qt.Key_Return )
QW.QApplication.processEvents()

View File

@ -431,9 +431,9 @@ class Controller( object ):
CallToThreadLongRunning = CallToThread
def CallAfterQtSafe( self, window, func, *args, **kwargs ):
def CallAfterQtSafe( self, window, label, func, *args, **kwargs ):
self.CallLaterQtSafe( window, 0, func, *args, **kwargs )
self.CallLaterQtSafe( window, 0, label, func, *args, **kwargs )
def CallLater( self, initial_delay, func, *args, **kwargs ):
@ -447,18 +447,20 @@ class Controller( object ):
return job
def CallLaterQtSafe( self, window, initial_delay, func, *args, **kwargs ):
def CallLaterQtSafe( self, window, initial_delay, label, func, *args, **kwargs ):
call = HydrusData.Call( func, *args, **kwargs )
job = ClientThreading.QtAwareJob(self, self._job_scheduler, window, initial_delay, call)
call.SetLabel( label )
job = ClientThreading.QtAwareJob( self, self._job_scheduler, window, initial_delay, call )
self._job_scheduler.AddJob( job )
return job
def CallRepeating( self, initial_delay, period, func, *args, **kwargs ):
def CallRepeating( self, initial_delay, period, label, func, *args, **kwargs ):
call = HydrusData.Call( func, *args, **kwargs )
@ -469,7 +471,7 @@ class Controller( object ):
return job
def CallRepeatingQtSafe( self, window, initial_delay, period, func, *args, **kwargs ):
def CallRepeatingQtSafe( self, window, initial_delay, period, label, func, *args, **kwargs ):
call = HydrusData.Call( func, *args, **kwargs )

View File

@ -69,11 +69,11 @@ class TestDBDialogs( unittest.TestCase ):
dlg.SetPanel( panel )
HG.test_controller.CallLaterQtSafe(dlg, 2, panel.Add)
HG.test_controller.CallLaterQtSafe( dlg, 2, 'test job', panel.Add )
HG.test_controller.CallLaterQtSafe(dlg, 4, OKChildDialog, panel)
HG.test_controller.CallLaterQtSafe( dlg, 4, 'test job', OKChildDialog, panel )
HG.test_controller.CallLaterQtSafe(dlg, 6, HitCancelButton, dlg)
HG.test_controller.CallLaterQtSafe( dlg, 6, 'test job', HitCancelButton, dlg )
result = dlg.exec()
@ -92,7 +92,7 @@ class TestNonDBDialogs( unittest.TestCase ):
with ClientGUIDialogs.DialogChooseNewServiceMethod( None ) as dlg:
HG.test_controller.CallLaterQtSafe( dlg, 1, HitButton, dlg._register )
HG.test_controller.CallLaterQtSafe( dlg, 1, 'test job', HitButton, dlg._register )
result = dlg.exec()
@ -105,7 +105,7 @@ class TestNonDBDialogs( unittest.TestCase ):
with ClientGUIDialogs.DialogChooseNewServiceMethod( None ) as dlg:
HG.test_controller.CallLaterQtSafe( dlg, 1, HitButton, dlg._setup )
HG.test_controller.CallLaterQtSafe( dlg, 1, 'test job', HitButton, dlg._setup )
result = dlg.exec()
@ -118,7 +118,7 @@ class TestNonDBDialogs( unittest.TestCase ):
with ClientGUIDialogs.DialogChooseNewServiceMethod( None ) as dlg:
HG.test_controller.CallLaterQtSafe( dlg, 1, HitCancelButton, dlg )
HG.test_controller.CallLaterQtSafe( dlg, 1, 'test job', HitCancelButton, dlg )
result = dlg.exec()