Version 497
This commit is contained in:
parent
67c3d129e7
commit
dabdbe2861
|
@ -345,28 +345,13 @@ jobs:
|
|||
pyinstaller server-win.spec
|
||||
pyinstaller client-win.spec
|
||||
dir -r
|
||||
-
|
||||
# yo pretty sure we'll need to install this manually once we are on windows server 2022
|
||||
# https://github.com/actions/virtual-environments/issues/4856
|
||||
name: InnoSetup
|
||||
run: |
|
||||
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
|
||||
ISCC.exe InnoSetup.iss /DVersion=${{ github.ref_name }}
|
||||
-
|
||||
name: Compress Client
|
||||
run: |
|
||||
cd .\dist
|
||||
7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract5.zip 'Hydrus Network'
|
||||
cd ..
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Windows-Install
|
||||
path: dist\HydrusInstaller.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
-
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
@ -447,12 +432,27 @@ jobs:
|
|||
pyinstaller server-win.spec
|
||||
pyinstaller client-win.spec
|
||||
dir -r
|
||||
-
|
||||
# yo pretty sure we'll need to install this manually once we are on windows server 2022
|
||||
# https://github.com/actions/virtual-environments/issues/4856
|
||||
name: InnoSetup
|
||||
run: |
|
||||
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
|
||||
ISCC.exe InnoSetup.iss /DVersion=${{ github.ref_name }}
|
||||
-
|
||||
name: Compress Client
|
||||
run: |
|
||||
cd .\dist
|
||||
7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract6.zip 'Hydrus Network'
|
||||
cd ..
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Windows-Install
|
||||
path: dist\HydrusInstaller.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
|
@ -485,7 +485,7 @@ jobs:
|
|||
mkdir ubuntu windows
|
||||
mv MacOS-DMG5/HydrusNetwork5.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt5.-.App.dmg
|
||||
mv MacOS-DMG6/HydrusNetwork6.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt6.-.App.dmg
|
||||
mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe
|
||||
mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Installer.exe
|
||||
mv Windows-Extract5/Windows-Extract5.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip
|
||||
mv Windows-Extract6/Windows-Extract6.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip
|
||||
mv Ubuntu-Extract5/Ubuntu-Extract5.tar.gz Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz
|
||||
|
@ -496,7 +496,7 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe
|
||||
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Installer.exe
|
||||
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip
|
||||
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip
|
||||
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz
|
||||
|
|
|
@ -3,6 +3,42 @@
|
|||
!!! note
|
||||
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
|
||||
|
||||
## [Version 497](https://github.com/hydrusnetwork/hydrus/releases/tag/v497)
|
||||
|
||||
### misc
|
||||
* I bulked out the 'star' rating shape a bit more, since the new pentragram, while it looked better than my old 'by-eye' star, was a bit thin. if you prefer the pentagram, this is now selectable as a new shape type under manage services
|
||||
* the Windows installer is now Qt6 exclusively. there are no special update instructions, it should all just work™
|
||||
* the 'manage tag siblings/parents' dialogs now have explicit delete buttons, which should make mass-deletes a little easier to do. some of the background code is cleaned up too, and the 'add' button is moved up to the main button row
|
||||
* you can now hide all sibling and/or parent text-suffix 'decorators' in the manage tags and autocomplete dropdown taglists, with four new checkboxes under _options->tags_. the right-click menus of these lists let you temporarily show/hide too, just like 'hide/show parent rows'
|
||||
* when you change the namespace sort in the options, the existing collect-by dropdowns now update instantly (previously, existing pages needed a client restart to see any changes)
|
||||
* I updated how the media viewer 'note' hover window lays out and does its 'how tall should I be?' estimate. it fits better, being exactly just tall enough in more cases, but it still seems to have trouble with multiple notes that include wrapping text
|
||||
* added a link to the new flatpak release (easy Linux running-from-source setup) that a user made to the install help
|
||||
* fixed an issue with the new 'default' file import options when you right-click a watcher/gallery download--the 'show files' menu now correctly adapts to you having a default file import options
|
||||
* if you are set to elide page tab names, then all pages will tooltip their names on mouseover
|
||||
* new clients now start with (ctrl+page up/down) as 'move page selection left/right'
|
||||
|
||||
### client api
|
||||
* the Client API routine that fetches file statuses for a given URL no longer double-checks 'already in db' results against your actual file system. this check is more appropriate to an actual working import process, so it now defaults off in the Client API
|
||||
* if you want to do this check because you are searching for missing files, you can turn it back on with the new 'doublecheck_file_system' parameter.
|
||||
* the client api help has been updated to reference this
|
||||
* the client api's Server header is now "client api/32 (497)". NOT "client api/17". it was stating the hydrus network version erroneously. it now states client api version and software version. if you are able to parse this header, it makes '/api_version' request superfluous
|
||||
* the client api version is now 32
|
||||
|
||||
### multiline parsing
|
||||
* the parser now supports limited multiline parsing. the main changes are hardcoded: the formulae beneath note content parsers and those that do subsidiary page parser splitting no longer remove newlines when they parse. all the parsing UI and the test panels and so on are now aware of this and set flags in all the right places, and parsed notes are now washed through the new trimming/cleaning method, and everything _seems_ to basically work. the main remaining problems is the complicated string processing UI has mixed single/multi-line testing support. some looks great, most gets coerced to single-line just for the previewed test results
|
||||
* as an example, the default hentai foundry downloader now grabs the artist description as a multi-line note
|
||||
* the parsing sub-system that extracts cohesive strings from complex html blocks now inserts newlines at 'p' and 'br' tags
|
||||
* trying to parse clean multiline notes still caused several formatting issues this week, so I have updated the automatic note-washing routine to standardise hydrus notes in several new ways that I hope will not be too disruptive to manually written notes:
|
||||
* the note washing routine now coerces all newline characters to 'backslash-n', regardless of platform
|
||||
* the note washing routine now trims each line, so no leading or trailing whitespace anywhere. I am open to changing this in future, maybe for handwritten notes where you really want an indent somewhere, but parsing from complex nested html tags is making a heap of weird extra whitespace, for which this is a clean solution
|
||||
* the note washing routine now trims newline gaps that are greater than two-newlines. you can split paragraphs by one empty line, but no more
|
||||
* there may be other issues figuring out cleanly formatted strings from nested html tags--so give it a go and let me know what you think. maybe p and br blocks should always make two newlines, so we have separated paragraphs, maybe I need to parse more blocks, like h1 and friends. any specific example html blocks would also be helpful
|
||||
|
||||
### cleanup
|
||||
* refactored ClientGUIParsing to its own 'parsing' module and split everything into four less tangled files
|
||||
* cleaned up a bunch of taglist text presentation code, mostly simplicity and clarity in prep for future updates
|
||||
* updated the checker options button to use a Qt signal instead of a callable
|
||||
|
||||
## [Version 496](https://github.com/hydrusnetwork/hydrus/releases/tag/v496)
|
||||
|
||||
### note import options
|
||||
|
@ -307,43 +343,3 @@
|
|||
* last week's update also gives a time estimate in its pre-popup, based on 60k files per minute
|
||||
* removed some old database cache data that wasn't cleared in a previous update
|
||||
* a variety of misc UI text fixes and cleanup
|
||||
|
||||
## [Version 486](https://github.com/hydrusnetwork/hydrus/releases/tag/v486)
|
||||
|
||||
* **This week's release is for advanced users only! I make a big change, and I want to make sure the update is fast and there are no unusual problems before rolling it out to all users.**
|
||||
### all my files
|
||||
* the client adds a new virtual file service this week, 'all my files', which is an umbrella covering all your local file domains. if you do not engage the multiple local file services system, you won't see it much, but if you do, you'll now have a convenient tool for saying 'all my stuff' without including trash and repository updates
|
||||
* **it will take a minute or two to generate this new service on update. if you have a client with millions of files, it may take a while**
|
||||
* 'all my files' now appears in the file domain selector button on your tag entry box if you have more than one local file domain. selecting this searches the union of all your local file domains with fast and precise count (as opposed to 'multiple locations' of the full union, which will have imprecise counts and be slower). it also does duplicate file work laser-fast (again, unlike 'multiple locations', which is often slow due to UNION complexity)
|
||||
* 'all my files' also appears in review and manage services, very similarly to 'all local files'
|
||||
* a heap of hacks I instituted when getting multiple local file services ready are now replaced with this clean 'yeah this file is valued and worth looking at' domain. for instance, downloader pages now view files in this way.
|
||||
* mr bones and the file history chart also use 'all my files', and are significantly faster to calculate. the chart also excludes repo update files and trash now
|
||||
* calls to delete or undelete on 'all my files' (this is mostly Client API and some 'default' situations) will be converted to a blanket 'force send to trash' and 'force undelete all deleted records'
|
||||
* the 'undelete files?' dialog is now a button selection dialog. it also now has an 'all the above' option when more than one local service may apply, which tells the client to undelete to all services the files have been deleted from
|
||||
* updated multiple local file services help to talk a little about the new domain
|
||||
* rearranged the sort in a couple of places where the different local file services appear. they should now be: local file domains, all my files, trash, repo updates, all local files
|
||||
* ADVANCED: the 'presentation import options' under 'file import options' now allows a full-fledged location context using the new multiple local file services system rather than the previous 'in your files(and trash too)' choice. it defaults to the new 'all my files' domain
|
||||
|
||||
### misc
|
||||
* thanks to a user, the 'getting started with downloading' help has had a full pass. if you have had trouble with downloaders, particularly if you are unsure about what file import options are for, or what subscriptions are, please check it out!
|
||||
* the 'media viewers' shortcut set gets three new zoom actions: 'switch between 100% and max', 'switch between canvas and max', and 'zoom to max' (issue #1141)
|
||||
* if a media type is set to do 'exact zooms', it will now not exceed the otherwise specified max zoom
|
||||
* the file sort widget will now preserve ascending/descending status on sort type changes (rather than resetting to default) if the asc/desc strings do not change. so, if you are on 'import time'/'oldest first', and switch to 'archive time', it will now stay on 'oldest' rather than resetting to 'newest'
|
||||
* the manage tag siblings dialog now tries to automatically break loops for you, just like it will automatically break A->B, A->C conflicts. this works on manual entry or mass import
|
||||
* the manage tag siblings dialog now shows the stated 'reason' for any pair change (e.g. "AUTO-PETITION TO BREAK LOOP") in the 'note' column
|
||||
* the 'short' animation scanbar--when your mouse is away--now keeps a short disabled volume button beside it. I found it very annoying how the scan nub would jump a few pixels left/right as this popped up and down, so now it is the same width big and small
|
||||
* right-clicking on files when in pages with 'multiple locations' file domains is now much much faster
|
||||
* the filename tagging dialog now starts with the 'tags for all' focused, and the 'press up/down on empty input' shortcuts are now plugged in, so pressing up/down will change service
|
||||
* I believe I may have completely eliminated the additional superlag that sometimes occurs when adding or deleting a service. it was a database maintenance routine getting carried away with other outstanding work
|
||||
* move/add actions in the new multiple local file system now operate asynchronously and politely, spreading their work time out when the client is busy, and for large jobs they will also make a cancellable progress popup
|
||||
* cleaned up how the autocomplete entry sends some of its signals to other parts of the program
|
||||
* did some misc help and code edits/refactoring, including brushing up the Windows install section with more advanced options
|
||||
* removed the 'hydrus zooms big bad' warning from the 'media' options page. hydrus zooms big good now!
|
||||
|
||||
### some database stuff
|
||||
* tl;dr: database cleans up after itself better now
|
||||
* some users have had trouble with database journal files (the 'wal' files in your db directory) on certain clients getting huge after lots of work, multiple GB, and causing the OS a headache if the journal is doing work through a computer sleep. these journals are 'supposed' to checkpoint and clean themselves up naturally, but I think a busy database chokes them. therefore, I have improved the hydrus maintenance this week: 1) the 'journal size limit' PRAGMA, which applies softly after every 30 seconds or so, is now 128MB down from 1GB. 2) databases in PERSIST (rare) mode will now specifically zero out their journal fifteen minutes. 3) databases in WAL mode (the default), in addition to regular PASSIVE checkpointing now every five minutes, will force an additional TRUNCATE checkpoint every fifteen. this should force a regular full flush and maybe help some other problems like gigantic memory bloat the same users sometimes saw. if you are a very advanced user and do active debug on the database while hydrus is using it, please note this new TRUNCATE command is aggressive and may block itself or you inconveniently. let me know how you get on!
|
||||
* moved the recent 'be careful of usb drives' section in 'installing' help to 'help my db is broke.txt'. it is very likely this problem was related to the above WAL stuff, and it was not just usb drives, I rewrote it as generalised help for anyone who gets 'delayed write failed' errors at the OS level
|
||||
* massively optimised several critical duplicate files filtering methods if the current location context has more than one file domain, and I think I cleared out the basic 'get duplicate info for this file' call of all slow calls in complex location contexts
|
||||
* the repair routine that regenerates mapping caches if any tables are missing on boot is now more reliable and covers the entirety of the mappings cache system using the new modules system. it also now regenerates just for the tag services with missing tables, not the whole cache
|
||||
* if multiple types of mapping cache tables are missing on boot, and multiple waves of regenerations covering different areas are planned, duplicate regenerations will now be skipped
|
||||
|
|
|
@ -120,7 +120,8 @@ Required Headers: n/a
|
|||
Arguments: n/a
|
||||
|
||||
Response:
|
||||
: Some simple JSON describing the current api version (and hydrus client version, if you are interested).
|
||||
: Some simple JSON describing the current api version (and hydrus client version, if you are interested).
|
||||
: Note that this is mostly obselete now, since the 'Server' header of every response (and a duplicated 'Hydrus-Server' one, if you have a complicated proxy situation that overwrites 'Server') are now in the form "client api/{client_api_version} ({software_version})", e.g. "client api/32 (497)".
|
||||
|
||||
```json title="Example response"
|
||||
{
|
||||
|
@ -670,6 +671,7 @@ Required Headers: n/a
|
|||
Arguments:
|
||||
:
|
||||
* `url`: (the url you want to ask about)
|
||||
* `doublecheck_file_system`: true or false (optional, defaults False)
|
||||
|
||||
Example request:
|
||||
: for URL `http://safebooru.org/index.php?page=post&s=view&id=2753608`:
|
||||
|
@ -691,17 +693,18 @@ Response:
|
|||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `url_file_statuses` is a list of zero-to-n JSON Objects, each representing a file match the client found in its database for the URL. Typically, it will be of length 0 (for as-yet-unvisited URLs or Gallery/Watchable URLs that are not attached to files) or 1, but sometimes multiple files are given the same URL (sometimes by mistaken misattribution, sometimes by design, such as pixiv manga pages). Handling n files per URL is a pain but an unavoidable issue you should account for.
|
||||
|
||||
`status` is the same as for `/add_files/add_file`:
|
||||
|
||||
* 0 - File not in database, ready for import (you will only see this very rarely--usually in this case you will just get no matches)
|
||||
* 2 - File already in database
|
||||
* 3 - File previously deleted
|
||||
|
||||
`hash` is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text you may recognise from hydrus's normal import workflow.
|
||||
|
||||
|
||||
The `url_file_statuses` is a list of zero-to-n JSON Objects, each representing a file match the client found in its database for the URL. Typically, it will be of length 0 (for as-yet-unvisited URLs or Gallery/Watchable URLs that are not attached to files) or 1, but sometimes multiple files are given the same URL (sometimes by mistaken misattribution, sometimes by design, such as pixiv manga pages). Handling n files per URL is a pain but an unavoidable issue you should account for.
|
||||
|
||||
`status` is the same as for `/add_files/add_file`:
|
||||
|
||||
* 0 - File not in database, ready for import (you will only see this very rarely--usually in this case you will just get no matches)
|
||||
* 2 - File already in database
|
||||
* 3 - File previously deleted
|
||||
|
||||
`hash` is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text you may recognise from hydrus's normal import workflow.
|
||||
|
||||
If you set `doublecheck_file_system` to `true`, then any result that is 'already in db' (2) will be double-checked against the actual file system. This check happens on any normal file import process, just to check for and fix missing files (if the file is missing, the status becomes 0--new), but the check can take more than a few milliseconds on an HDD or a network drive, so the default behaviour, assuming you mostly just want to spam for 'seen this before' file statuses, is to not do it.
|
||||
|
||||
### **GET `/add_urls/get_url_info`** { id="add_urls_get_url_info" }
|
||||
|
||||
|
|
|
@ -49,7 +49,9 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
|
|||
5. If it still doesn't work, see if you can do the same for libmpv.so and libcdio.so--or consider [running from source](running_from_source.md)
|
||||
* You can also try [running the Windows version in wine](wine.md).
|
||||
* **Third parties (not maintained by Hydrus Developer)**:
|
||||
If you use Arch Linux, you can check out the AUR package a user maintains [here](https://aur.archlinux.org/packages/hydrus/).
|
||||
* (These both run from source, so if you have trouble with the built release, they may work better for you!)
|
||||
* [AUR package](https://aur.archlinux.org/packages/hydrus/)
|
||||
* [flatpak](https://flathub.org/apps/details/io.github.hydrusnetwork.hydrus)
|
||||
|
||||
|
||||
=== "From Source"
|
||||
|
|
|
@ -33,6 +33,42 @@
|
|||
<div class="content">
|
||||
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
|
||||
<ul>
|
||||
<li><h3 id="version_497"><a href="#version_497">version 497</a></h3></li>
|
||||
<ul>
|
||||
<li>misc:</li>
|
||||
<li>I bulked out the 'star' rating shape a bit more, since the new pentragram, while it looked better than my old 'by-eye' star, was a bit thin. if you prefer the pentagram, this is now selectable as a new shape type under manage services</li>
|
||||
<li>the Windows installer is now Qt6 exclusively. there are no special update instructions, it should all just work™</li>
|
||||
<li>the 'manage tag siblings/parents' dialogs now have explicit delete buttons, which should make mass-deletes a little easier to do. some of the background code is cleaned up too, and the 'add' button is moved up to the main button row</li>
|
||||
<li>you can now hide all sibling and/or parent text-suffix 'decorators' in the manage tags and autocomplete dropdown taglists, with four new checkboxes under _options->tags_. the right-click menus of these lists let you temporarily show/hide too, just like 'hide/show parent rows'</li>
|
||||
<li>when you change the namespace sort in the options, the existing collect-by dropdowns now update instantly (previously, existing pages needed a client restart to see any changes)</li>
|
||||
<li>I updated how the media viewer 'note' hover window lays out and does its 'how tall should I be?' estimate. it fits better, being exactly just tall enough in more cases, but it still seems to have trouble with multiple notes that include wrapping text</li>
|
||||
<li>added a link to the new flatpak release (easy Linux running-from-source setup) that a user made to the install help</li>
|
||||
<li>fixed an issue with the new 'default' file import options when you right-click a watcher/gallery download--the 'show files' menu now correctly adapts to you having a default file import options</li>
|
||||
<li>if you are set to elide page tab names, then all pages will tooltip their names on mouseover</li>
|
||||
<li>new clients now start with (ctrl+page up/down) as 'move page selection left/right'</li>
|
||||
<li>.</li>
|
||||
<li>client api:</li>
|
||||
<li>the Client API routine that fetches file statuses for a given URL no longer double-checks 'already in db' results against your actual file system. this check is more appropriate to an actual working import process, so it now defaults off in the Client API</li>
|
||||
<li>if you want to do this check because you are searching for missing files, you can turn it back on with the new 'doublecheck_file_system' parameter.</li>
|
||||
<li>the client api help has been updated to reference this</li>
|
||||
<li>the client api's Server header is now "client api/32 (497)". NOT "client api/17". it was stating the hydrus network version erroneously. it now states client api version and software version. if you are able to parse this header, it makes '/api_version' request superfluous</li>
|
||||
<li>the client api version is now 32</li>
|
||||
<li>.</li>
|
||||
<li>multiline parsing:</li>
|
||||
<li>the parser now supports limited multiline parsing. the main changes are hardcoded: the formulae beneath note content parsers and those that do subsidiary page parser splitting no longer remove newlines when they parse. all the parsing UI and the test panels and so on are now aware of this and set flags in all the right places, and parsed notes are now washed through the new trimming/cleaning method, and everything _seems_ to basically work. the main remaining problems is the complicated string processing UI has mixed single/multi-line testing support. some looks great, most gets coerced to single-line just for the previewed test results</li>
|
||||
<li>as an example, the default hentai foundry downloader now grabs the artist description as a multi-line note</li>
|
||||
<li>the parsing sub-system that extracts cohesive strings from complex html blocks now inserts newlines at 'p' and 'br' tags</li>
|
||||
<li>trying to parse clean multiline notes still caused several formatting issues this week, so I have updated the automatic note-washing routine to standardise hydrus notes in several new ways that I hope will not be too disruptive to manually written notes:</li>
|
||||
<li>the note washing routine now coerces all newline characters to 'backslash-n', regardless of platform</li>
|
||||
<li>the note washing routine now trims each line, so no leading or trailing whitespace anywhere. I am open to changing this in future, maybe for handwritten notes where you really want an indent somewhere, but parsing from complex nested html tags is making a heap of weird extra whitespace, for which this is a clean solution</li>
|
||||
<li>the note washing routine now trims newline gaps that are greater than two-newlines. you can split paragraphs by one empty line, but no more</li>
|
||||
<li>there may be other issues figuring out cleanly formatted strings from nested html tags--so give it a go and let me know what you think. maybe p and br blocks should always make two newlines, so we have separated paragraphs, maybe I need to parse more blocks, like h1 and friends. any specific example html blocks would also be helpful</li>
|
||||
<li>.</li>
|
||||
<li>cleanup:</li>
|
||||
<li>refactored ClientGUIParsing to its own 'parsing' module and split everything into four less tangled files</li>
|
||||
<li>cleaned up a bunch of taglist text presentation code, mostly simplicity and clarity in prep for future updates</li>
|
||||
<li>updated the checker options button to use a Qt signal instead of a callable</li>
|
||||
</ul>
|
||||
<li><h3 id="version_496"><a href="#version_496">version 496</a></h3></li>
|
||||
<ul>
|
||||
<li>note import options:</li>
|
||||
|
|
|
@ -376,6 +376,9 @@ def GetDefaultShortcuts():
|
|||
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_CHARACTER, ord( 'Z' ), ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_UNDO ) )
|
||||
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_CHARACTER, ord( 'P' ), ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_COMMAND_PALETTE ) )
|
||||
|
||||
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_PAGE_UP, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MOVE_PAGES_SELECTION_LEFT ) )
|
||||
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_PAGE_DOWN, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MOVE_PAGES_SELECTION_RIGHT ) )
|
||||
|
||||
shortcuts.append( main_gui )
|
||||
|
||||
media_viewer_browser = ClientGUIShortcuts.ShortcutSet( 'media_viewer_browser' )
|
||||
|
|
|
@ -244,6 +244,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_taglists' ] = True
|
||||
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_autocomplete_taglists' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'show_parent_decorators_on_storage_taglists' ] = True
|
||||
self._dictionary[ 'booleans' ][ 'show_parent_decorators_on_storage_autocomplete_taglists' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'show_sibling_decorators_on_storage_taglists' ] = True
|
||||
self._dictionary[ 'booleans' ][ 'show_sibling_decorators_on_storage_autocomplete_taglists' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'show_session_size_warnings' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'delete_lock_for_archived_files' ] = False
|
||||
|
|
|
@ -84,7 +84,7 @@ def ConvertParseResultToPrettyString( result ):
|
|||
|
||||
note_name = additional_info
|
||||
|
||||
return 'note "{}": {}'.format( note_name, parsed_text )
|
||||
return 'note "{}":{}{}'.format( note_name, os.linesep, parsed_text )
|
||||
|
||||
elif content_type == HC.CONTENT_TYPE_HASH:
|
||||
|
||||
|
@ -192,7 +192,7 @@ def ConvertParsableContentToPrettyString( parsable_content, include_veto = False
|
|||
|
||||
note_names = sorted( additional_infos )
|
||||
|
||||
s = 'notes: {}'.format( ', '.join( note_names ) )
|
||||
s = 'notes:{}'.format( ', '.join( note_names ) )
|
||||
|
||||
pretty_strings.append( s )
|
||||
|
||||
|
@ -348,10 +348,26 @@ def GetHTMLTagString( tag: bs4.Tag ):
|
|||
# on a version update, these suddenly went semi bonkers and wouldn't pull text unless the types of the subtag were explicitly set
|
||||
# so we'll just do it ourselves
|
||||
|
||||
all_strings = []
|
||||
|
||||
try:
|
||||
|
||||
all_strings = [ str( c ) for c in tag.descendants if isinstance( c, ( bs4.NavigableString, bs4.CData ) ) ]
|
||||
all_strings = [ s for s in all_strings if len( s ) > 0 ]
|
||||
for sub_tag in tag.descendants:
|
||||
|
||||
if sub_tag.name in ( 'br', 'p' ):
|
||||
|
||||
all_strings.append( os.linesep )
|
||||
|
||||
continue
|
||||
|
||||
|
||||
if not isinstance( sub_tag, ( bs4.NavigableString, bs4.CData ) ):
|
||||
|
||||
continue
|
||||
|
||||
|
||||
all_strings.append( str( sub_tag ) )
|
||||
|
||||
|
||||
except:
|
||||
|
||||
|
@ -666,7 +682,7 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
|
|||
return os.linesep
|
||||
|
||||
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text ):
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -676,20 +692,23 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
|
|||
return self._string_processor
|
||||
|
||||
|
||||
def Parse( self, parsing_context, parsing_text ):
|
||||
def Parse( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
|
||||
|
||||
raw_texts = self._ParseRawTexts( parsing_context, parsing_text )
|
||||
raw_texts = self._ParseRawTexts( parsing_context, parsing_text, collapse_newlines )
|
||||
|
||||
raw_texts = [ HydrusText.RemoveNewlines( raw_text ) for raw_text in raw_texts ]
|
||||
if collapse_newlines:
|
||||
|
||||
raw_texts = [ HydrusText.RemoveNewlines( raw_text ) for raw_text in raw_texts ]
|
||||
|
||||
|
||||
texts = self._string_processor.ProcessStrings( raw_texts )
|
||||
|
||||
return texts
|
||||
|
||||
|
||||
def ParsePretty( self, parsing_context, parsing_text ):
|
||||
def ParsePretty( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
|
||||
|
||||
texts = self.Parse( parsing_context, parsing_text )
|
||||
texts = self.Parse( parsing_context, parsing_text, collapse_newlines )
|
||||
|
||||
pretty_texts = [ MakeParsedTextPretty( text ) for text in texts ]
|
||||
|
||||
|
@ -766,7 +785,7 @@ class ParseFormulaCompound( ParseFormula ):
|
|||
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
|
||||
|
||||
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text ):
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
|
||||
|
||||
def get_stream_string( index, s ):
|
||||
|
||||
|
@ -788,7 +807,7 @@ class ParseFormulaCompound( ParseFormula ):
|
|||
|
||||
for formula in self._formulae:
|
||||
|
||||
stream = formula.Parse( parsing_context, parsing_text )
|
||||
stream = formula.Parse( parsing_context, parsing_text, collapse_newlines )
|
||||
|
||||
if len( stream ) == 0: # no contents were found for one of the /1 replace components, so no valid strings can be made.
|
||||
|
||||
|
@ -912,7 +931,7 @@ class ParseFormulaContextVariable( ParseFormula ):
|
|||
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
|
||||
|
||||
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text ):
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
|
||||
|
||||
raw_texts = []
|
||||
|
||||
|
@ -1124,7 +1143,7 @@ class ParseFormulaHTML( ParseFormula ):
|
|||
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
|
||||
|
||||
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text ):
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
|
||||
|
||||
try:
|
||||
|
||||
|
@ -1758,7 +1777,7 @@ class ParseFormulaJSON( ParseFormula ):
|
|||
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
|
||||
|
||||
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text ):
|
||||
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
|
||||
|
||||
try:
|
||||
|
||||
|
@ -2158,7 +2177,9 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
try:
|
||||
|
||||
parsed_texts = list( self._formula.Parse( parsing_context, parsing_text ) )
|
||||
collapse_newlines = self._content_type != HC.CONTENT_TYPE_NOTES
|
||||
|
||||
parsed_texts = list( self._formula.Parse( parsing_context, parsing_text, collapse_newlines ) )
|
||||
|
||||
except HydrusExceptions.ParseException as e:
|
||||
|
||||
|
@ -2169,6 +2190,11 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
|
|||
raise e
|
||||
|
||||
|
||||
if self._content_type == HC.CONTENT_TYPE_NOTES:
|
||||
|
||||
parsed_texts = [ HydrusText.CleanNoteText( parsed_text ) for parsed_text in parsed_texts ]
|
||||
|
||||
|
||||
if self._content_type == HC.CONTENT_TYPE_URLS:
|
||||
|
||||
if 'url' in parsing_context:
|
||||
|
@ -2240,7 +2266,7 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def ParsePretty( self, parsing_context, parsing_text ):
|
||||
def ParsePretty( self, parsing_context, parsing_text: str ):
|
||||
|
||||
try:
|
||||
|
||||
|
@ -2553,7 +2579,9 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
|
||||
try:
|
||||
|
||||
posts = formula.Parse( parsing_context, converted_parsing_text )
|
||||
collapse_newlines = False
|
||||
|
||||
posts = formula.Parse( parsing_context, converted_parsing_text, collapse_newlines )
|
||||
|
||||
except HydrusExceptions.ParseException:
|
||||
|
||||
|
|
|
@ -1324,7 +1324,7 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
from hydrus.client.metadata import ClientRatings
|
||||
|
||||
dictionary[ 'shape' ] = ClientRatings.STAR
|
||||
dictionary[ 'shape' ] = ClientRatings.FAT_STAR
|
||||
|
||||
like_colours = {}
|
||||
|
||||
|
@ -10423,7 +10423,7 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
from hydrus.client.metadata import ClientRatings
|
||||
|
||||
dictionary[ 'shape' ] = ClientRatings.STAR
|
||||
dictionary[ 'shape' ] = ClientRatings.FAT_STAR
|
||||
|
||||
like_colours = {}
|
||||
|
||||
|
@ -11447,6 +11447,36 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 496:
|
||||
|
||||
try:
|
||||
|
||||
domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
|
||||
|
||||
domain_manager.Initialise()
|
||||
|
||||
#
|
||||
|
||||
domain_manager.OverwriteDefaultParsers( ( 'hentai foundry file page parser', ) )
|
||||
|
||||
#
|
||||
|
||||
domain_manager.TryToLinkURLClassesAndParsers()
|
||||
|
||||
#
|
||||
|
||||
self.modules_serialisable.SetJSONDump( domain_manager )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
message = 'Trying to update some downloader objects failed! Please let hydrus dev know!'
|
||||
|
||||
self.pub_initial_message( message )
|
||||
|
||||
|
||||
|
||||
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
|
||||
|
||||
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
|
||||
|
|
|
@ -57,7 +57,6 @@ from hydrus.client.gui import ClientGUIFunctions
|
|||
from hydrus.client.gui import ClientGUILogin
|
||||
from hydrus.client.gui import ClientGUIMediaControls
|
||||
from hydrus.client.gui import ClientGUIMenus
|
||||
from hydrus.client.gui import ClientGUIParsing
|
||||
from hydrus.client.gui import ClientGUIPopupMessages
|
||||
from hydrus.client.gui import ClientGUIScrolledPanels
|
||||
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
||||
|
@ -86,6 +85,8 @@ from hydrus.client.gui.networking import ClientGUINetwork
|
|||
from hydrus.client.gui.pages import ClientGUIManagement
|
||||
from hydrus.client.gui.pages import ClientGUIPages
|
||||
from hydrus.client.gui.pages import ClientGUISession
|
||||
from hydrus.client.gui.parsing import ClientGUIParsing
|
||||
from hydrus.client.gui.parsing import ClientGUIParsingLegacy
|
||||
from hydrus.client.gui.services import ClientGUIClientsideServices
|
||||
from hydrus.client.gui.services import ClientGUIServersideServices
|
||||
from hydrus.client.gui.widgets import ClientGUICommon
|
||||
|
@ -4327,7 +4328,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
|
|||
|
||||
with ClientGUITopLevelWindowsPanels.DialogManage( self, title ) as dlg:
|
||||
|
||||
panel = ClientGUIParsing.ManageParsingScriptsPanel( dlg )
|
||||
panel = ClientGUIParsingLegacy.ManageParsingScriptsPanel( dlg )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ from hydrus.client import ClientPaths
|
|||
from hydrus.client.gui import ClientGUIDialogs
|
||||
from hydrus.client.gui import ClientGUIDialogsQuick
|
||||
from hydrus.client.gui import ClientGUIFunctions
|
||||
from hydrus.client.gui import ClientGUIParsing
|
||||
from hydrus.client.gui import ClientGUIScrolledPanels
|
||||
from hydrus.client.gui import ClientGUIStringControls
|
||||
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
||||
|
@ -26,6 +25,7 @@ from hydrus.client.gui.lists import ClientGUIListBoxes
|
|||
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
|
||||
from hydrus.client.gui.lists import ClientGUIListCtrl
|
||||
from hydrus.client.gui.networking import ClientGUINetworkJobControl
|
||||
from hydrus.client.gui.parsing import ClientGUIParsing
|
||||
from hydrus.client.gui.widgets import ClientGUICommon
|
||||
from hydrus.client.gui.widgets import ClientGUIMenuButton
|
||||
from hydrus.client.importing import ClientImporting
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,20 +24,7 @@ default_numerical_colours[ ClientRatings.DISLIKE ] = ( ( 0, 0, 0 ), ( 255, 255,
|
|||
default_numerical_colours[ ClientRatings.NULL ] = ( ( 0, 0, 0 ), ( 191, 191, 191 ) )
|
||||
default_numerical_colours[ ClientRatings.MIXED ] = ( ( 0, 0, 0 ), ( 95, 95, 95 ) )
|
||||
|
||||
STAR_COORDS = []
|
||||
|
||||
STAR_COORDS.append( QC.QPointF( 6, 0 ) ) # top
|
||||
STAR_COORDS.append( QC.QPointF( 9, 4 ) )
|
||||
STAR_COORDS.append( QC.QPointF( 12, 4 ) ) # right
|
||||
STAR_COORDS.append( QC.QPointF( 9, 8 ) )
|
||||
STAR_COORDS.append( QC.QPointF( 10, 12 ) ) # bottom right
|
||||
STAR_COORDS.append( QC.QPointF( 6, 10 ) )
|
||||
STAR_COORDS.append( QC.QPointF( 2, 12 ) ) # bottom left
|
||||
STAR_COORDS.append( QC.QPointF( 3, 8 ) )
|
||||
STAR_COORDS.append( QC.QPointF( 0, 4 ) ) # left
|
||||
STAR_COORDS.append( QC.QPointF( 3, 4 ) )
|
||||
|
||||
STAR_COORDS = [
|
||||
PENTAGRAM_STAR_COORDS = [
|
||||
QC.QPointF( 6, 0 ), # top
|
||||
QC.QPointF( 7.5, 4.5 ),
|
||||
QC.QPointF( 12, 4.5 ), # right
|
||||
|
@ -50,6 +37,19 @@ STAR_COORDS = [
|
|||
QC.QPointF( 4.5, 4.5 )
|
||||
]
|
||||
|
||||
FAT_STAR_COORDS = [
|
||||
QC.QPointF( 6, 0 ), # top
|
||||
QC.QPointF( 7.8, 4.1 ),
|
||||
QC.QPointF( 12, 4.6 ), # right
|
||||
QC.QPointF( 8.9, 7.6 ),
|
||||
QC.QPointF( 9.8, 12 ), # bottom right
|
||||
QC.QPointF( 6, 9.8 ),
|
||||
QC.QPointF( 2.2, 12 ), # bottom left
|
||||
QC.QPointF( 3.1, 7.6 ),
|
||||
QC.QPointF( 0, 4.5 ), # left
|
||||
QC.QPointF( 4.2, 4.1 )
|
||||
]
|
||||
|
||||
def DrawLike( painter, x, y, service_key, rating_state ):
|
||||
|
||||
painter.setRenderHint( QG.QPainter.Antialiasing, True )
|
||||
|
@ -69,13 +69,15 @@ def DrawLike( painter, x, y, service_key, rating_state ):
|
|||
|
||||
painter.drawRect( x+2, y+2, 12, 12 )
|
||||
|
||||
elif shape == ClientRatings.STAR:
|
||||
elif shape in ( ClientRatings.FAT_STAR, ClientRatings.PENTAGRAM_STAR ):
|
||||
|
||||
offset = QC.QPoint( x + 1, y + 1 )
|
||||
|
||||
painter.translate( offset )
|
||||
|
||||
painter.drawPolygon( QG.QPolygonF( STAR_COORDS ) )
|
||||
coords = FAT_STAR_COORDS if shape == ClientRatings.FAT_STAR else PENTAGRAM_STAR_COORDS
|
||||
|
||||
painter.drawPolygon( QG.QPolygonF( coords ) )
|
||||
|
||||
painter.translate( -offset )
|
||||
|
||||
|
@ -104,13 +106,15 @@ def DrawNumerical( painter, x, y, service_key, rating_state, rating ):
|
|||
|
||||
painter.drawRect( x + 2 + x_delta, y + 2, 12, 12 )
|
||||
|
||||
elif shape == ClientRatings.STAR:
|
||||
elif shape in ( ClientRatings.FAT_STAR, ClientRatings.PENTAGRAM_STAR ):
|
||||
|
||||
offset = QC.QPoint( x + 1 + x_delta, y + 1 )
|
||||
|
||||
painter.translate( offset )
|
||||
|
||||
painter.drawPolygon( QG.QPolygonF( STAR_COORDS ) )
|
||||
coords = FAT_STAR_COORDS if shape == ClientRatings.FAT_STAR else PENTAGRAM_STAR_COORDS
|
||||
|
||||
painter.drawPolygon( QG.QPolygonF( coords ) )
|
||||
|
||||
painter.translate( -offset )
|
||||
|
||||
|
@ -162,7 +166,7 @@ def GetStars( service_key, rating_state, rating ):
|
|||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
return ( ClientRatings.STAR, 0 )
|
||||
return ( ClientRatings.FAT_STAR, 0 )
|
||||
|
||||
|
||||
shape = service.GetShape()
|
||||
|
|
|
@ -3406,6 +3406,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._expand_parents_on_storage_taglists = QW.QCheckBox( general_panel )
|
||||
self._expand_parents_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
|
||||
|
||||
self._show_parent_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
|
||||
self._show_parent_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
|
||||
self._show_sibling_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
|
||||
self._show_sibling_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
|
||||
|
||||
self._ac_select_first_with_count = QW.QCheckBox( general_panel )
|
||||
|
||||
#
|
||||
|
@ -3442,13 +3448,23 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._default_tag_service_search_page.SetValue( self._new_options.GetKey( 'default_tag_service_search_page' ) )
|
||||
|
||||
self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
|
||||
|
||||
self._expand_parents_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' )
|
||||
|
||||
self._expand_parents_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
|
||||
|
||||
self._expand_parents_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
|
||||
|
||||
self._show_parent_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' ) )
|
||||
self._show_parent_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents either hang below tags or summarise in a suffix.' )
|
||||
|
||||
self._show_parent_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
|
||||
self._show_parent_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
|
||||
|
||||
self._show_sibling_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' ) )
|
||||
self._show_sibling_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and siblings summarise in a suffix.' )
|
||||
|
||||
self._show_sibling_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
|
||||
self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
|
||||
|
||||
self._ac_select_first_with_count.setChecked( self._new_options.GetBoolean( 'ac_select_first_with_count' ) )
|
||||
|
||||
#
|
||||
|
@ -3464,8 +3480,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
rows.append( ( 'Default tag service in manage tag dialogs: ', self._default_tag_service_tab ) )
|
||||
rows.append( ( 'Remember last used default tag service in manage tag dialogs: ', self._save_default_tag_service_tab_on_change ) )
|
||||
rows.append( ( 'Default tag service in search pages: ', self._default_tag_service_search_page ) )
|
||||
rows.append( ( 'Show parent info by default on edit/write taglists: ', self._show_parent_decorators_on_storage_taglists ) )
|
||||
rows.append( ( 'Show parent info by default on edit/write autocomplete taglists: ', self._show_parent_decorators_on_storage_autocomplete_taglists ) )
|
||||
rows.append( ( 'Show parents expanded by default on edit/write taglists: ', self._expand_parents_on_storage_taglists ) )
|
||||
rows.append( ( 'Show parents expanded by default on edit/write autocomplete taglists: ', self._expand_parents_on_storage_autocomplete_taglists ) )
|
||||
rows.append( ( 'Show sibling info by default on edit/write taglists: ', self._show_sibling_decorators_on_storage_taglists ) )
|
||||
rows.append( ( 'Show sibling info by default on edit/write autocomplete taglists: ', self._show_sibling_decorators_on_storage_autocomplete_taglists ) )
|
||||
rows.append( ( 'By default, select the first tag result with actual count in write-autocomplete: ', self._ac_select_first_with_count ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( general_panel, rows )
|
||||
|
@ -3507,8 +3527,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._new_options.SetKey( 'default_tag_service_search_page', self._default_tag_service_search_page.GetValue() )
|
||||
|
||||
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_taglists', self._show_parent_decorators_on_storage_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists', self._show_parent_decorators_on_storage_autocomplete_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'expand_parents_on_storage_taglists', self._expand_parents_on_storage_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'expand_parents_on_storage_autocomplete_taglists', self._expand_parents_on_storage_autocomplete_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_taglists', self._show_sibling_decorators_on_storage_taglists.isChecked() )
|
||||
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists', self._show_sibling_decorators_on_storage_autocomplete_taglists.isChecked() )
|
||||
|
||||
self._new_options.SetBoolean( 'ac_select_first_with_count', self._ac_select_first_with_count.isChecked() )
|
||||
|
||||
#
|
||||
|
|
|
@ -208,7 +208,7 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
|
||||
self._this_is_a_random_sample_sub.setToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' )
|
||||
|
||||
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options, update_callable = self._CheckerOptionsUpdated )
|
||||
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options )
|
||||
|
||||
self._file_presentation_panel = ClientGUICommon.StaticBox( self, 'file publication' )
|
||||
|
||||
|
@ -336,6 +336,8 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self.widget().setLayout( vbox )
|
||||
|
||||
self._checker_options.valueChanged.connect( self._CheckerOptionsUpdated )
|
||||
|
||||
self._UpdateDelayText()
|
||||
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@ from hydrus.client import ClientParsing
|
|||
from hydrus.client import ClientSearch
|
||||
from hydrus.client import ClientThreading
|
||||
from hydrus.client.gui import ClientGUIDialogs
|
||||
from hydrus.client.gui import ClientGUIParsing
|
||||
from hydrus.client.gui import QtPorting as QP
|
||||
from hydrus.client.gui.lists import ClientGUIListBoxes
|
||||
from hydrus.client.gui.lists import ClientGUIListBoxesData
|
||||
from hydrus.client.gui.parsing import ClientGUIParsingLegacy
|
||||
from hydrus.client.gui.widgets import ClientGUICommon
|
||||
from hydrus.client.metadata import ClientTags
|
||||
from hydrus.client.metadata import ClientTagSorting
|
||||
|
@ -463,7 +463,7 @@ class FileLookupScriptTagsPanel( QW.QWidget ):
|
|||
|
||||
self._fetch_button.setEnabled( False )
|
||||
|
||||
self._script_management = ClientGUIParsing.ScriptManagementControl( self )
|
||||
self._script_management = ClientGUIParsingLegacy.ScriptManagementControl( self )
|
||||
|
||||
self._tags = ListBoxTagsSuggestionsFavourites( self, self._service_key, activate_callable, sort_tags = True )
|
||||
|
||||
|
|
|
@ -2932,14 +2932,21 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._show_all = QW.QCheckBox( self )
|
||||
|
||||
# leave up here since other things have updates based on them
|
||||
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
|
||||
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
||||
|
||||
self._tag_parents = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
|
||||
self._tag_parents = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
|
||||
|
||||
self._listctrl_panel.SetListCtrl( self._tag_parents )
|
||||
|
||||
self._tag_parents.Sort()
|
||||
|
||||
self._listctrl_panel.AddButton( 'add', self._AddButton, enabled_check_func = self._CanAddFromCurrentInput )
|
||||
self._listctrl_panel.AddButton( 'delete', self._DeleteSelectedRows, enabled_only_on_selection = True )
|
||||
|
||||
menu_items = []
|
||||
|
||||
menu_items.append( ( 'normal', 'from clipboard', 'Load parents from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, False ) ) )
|
||||
|
@ -2958,9 +2965,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._listctrl_panel.setEnabled( False )
|
||||
|
||||
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
|
||||
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._children, ( 12, 6 ) )
|
||||
|
||||
self._children.setMinimumHeight( preview_height )
|
||||
|
@ -2974,10 +2978,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._parent_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterParents, default_location_context, service_key, show_paste_button = True )
|
||||
self._parent_input.setEnabled( False )
|
||||
|
||||
self._add = QW.QPushButton( 'add', self )
|
||||
self._add.clicked.connect( self.EventAddButton )
|
||||
self._add.setEnabled( False )
|
||||
|
||||
#
|
||||
|
||||
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' + os.linesep + '.' )
|
||||
|
@ -3014,7 +3014,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._add, CC.FLAGS_ON_RIGHT )
|
||||
QP.AddToLayout( vbox, tags_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, input_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
|
@ -3022,8 +3021,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
#
|
||||
|
||||
self._tag_parents.itemSelectionChanged.connect( self._SetButtonStatus )
|
||||
|
||||
self._children.listBoxChanged.connect( self._UpdateListCtrlData )
|
||||
self._parents.listBoxChanged.connect( self._UpdateListCtrlData )
|
||||
self._show_all.clicked.connect( self._UpdateListCtrlData )
|
||||
|
@ -3031,6 +3028,23 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
|
||||
|
||||
|
||||
def _AddButton( self ):
|
||||
|
||||
children = self._children.GetTags()
|
||||
parents = self._parents.GetTags()
|
||||
|
||||
pairs = list( itertools.product( children, parents ) )
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
self._children.SetTags( [] )
|
||||
self._parents.SetTags( [] )
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
def _AddPairs( self, pairs, add_only = False ):
|
||||
|
||||
pairs = list( pairs )
|
||||
|
@ -3323,6 +3337,16 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
return True
|
||||
|
||||
|
||||
def _CanAddFromCurrentInput( self ):
|
||||
|
||||
if len( self._children.GetTags() ) == 0 or len( self._parents.GetTags() ) == 0:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _ConvertPairToListCtrlTuples( self, pair ):
|
||||
|
||||
( child, parent ) = pair
|
||||
|
@ -3350,6 +3374,18 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _DeleteSelectedRows( self ):
|
||||
|
||||
parents_to_children = collections.defaultdict( set )
|
||||
|
||||
pairs = self._tag_parents.GetData( only_selected = True )
|
||||
|
||||
if len( pairs ) > 0:
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
|
||||
|
||||
def _DeserialiseImportString( self, import_string ):
|
||||
|
||||
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
|
||||
|
@ -3467,30 +3503,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._UpdateListCtrlData()
|
||||
|
||||
|
||||
def _ListCtrlActivated( self ):
|
||||
|
||||
parents_to_children = collections.defaultdict( set )
|
||||
|
||||
pairs = self._tag_parents.GetData( only_selected = True )
|
||||
|
||||
if len( pairs ) > 0:
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
|
||||
|
||||
def _SetButtonStatus( self ):
|
||||
|
||||
if len( self._children.GetTags() ) == 0 or len( self._parents.GetTags() ) == 0:
|
||||
|
||||
self._add.setEnabled( False )
|
||||
|
||||
else:
|
||||
|
||||
self._add.setEnabled( True )
|
||||
|
||||
|
||||
|
||||
def _UpdateListCtrlData( self ):
|
||||
|
||||
children = self._children.GetTags()
|
||||
|
@ -3553,7 +3565,7 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
|
||||
|
@ -3567,27 +3579,10 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
|
||||
def EventAddButton( self ):
|
||||
|
||||
children = self._children.GetTags()
|
||||
parents = self._parents.GetTags()
|
||||
|
||||
pairs = list( itertools.product( children, parents ) )
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
self._children.SetTags( [] )
|
||||
self._parents.SetTags( [] )
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
|
||||
|
||||
def GetContentUpdates( self ):
|
||||
|
||||
# we make it manually here because of the mass pending tags done (but not undone on a rescind) on a pending pair!
|
||||
|
@ -3935,12 +3930,19 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._show_all = QW.QCheckBox( self )
|
||||
|
||||
# leave up here since other things have updates based on them
|
||||
self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
self._new_sibling = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
|
||||
|
||||
self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
|
||||
self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
|
||||
|
||||
self._listctrl_panel.SetListCtrl( self._tag_siblings )
|
||||
|
||||
self._listctrl_panel.AddButton( 'add', self._AddButton, enabled_check_func = self._CanAddFromCurrentInput )
|
||||
self._listctrl_panel.AddButton( 'delete', self._DeleteSelectedRows, enabled_only_on_selection = True )
|
||||
|
||||
self._tag_siblings.Sort()
|
||||
|
||||
menu_items = []
|
||||
|
@ -3961,9 +3963,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._listctrl_panel.setEnabled( False )
|
||||
|
||||
self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
|
||||
self._new_sibling = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._old_siblings, ( 12, 6 ) )
|
||||
|
||||
self._old_siblings.setMinimumHeight( preview_height )
|
||||
|
@ -3976,10 +3975,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._new_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.SetNew, default_location_context, service_key )
|
||||
self._new_input.setEnabled( False )
|
||||
|
||||
self._add = QW.QPushButton( 'add', self )
|
||||
self._add.clicked.connect( self.EventAddButton )
|
||||
self._add.setEnabled( False )
|
||||
|
||||
#
|
||||
|
||||
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' )
|
||||
|
@ -4016,7 +4011,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._add, CC.FLAGS_ON_RIGHT )
|
||||
QP.AddToLayout( vbox, text_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, input_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
|
@ -4024,14 +4018,35 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
#
|
||||
|
||||
self._tag_siblings.itemSelectionChanged.connect( self._SetButtonStatus )
|
||||
|
||||
self._show_all.clicked.connect( self._UpdateListCtrlData )
|
||||
self._old_siblings.listBoxChanged.connect( self._UpdateListCtrlData )
|
||||
|
||||
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
|
||||
|
||||
|
||||
def _AddButton( self ):
|
||||
|
||||
if self._current_new is not None and len( self._old_siblings.GetTags() ) > 0:
|
||||
|
||||
olds = self._old_siblings.GetTags()
|
||||
|
||||
pairs = [ ( old, self._current_new ) for old in olds ]
|
||||
|
||||
self._AutoPetitionConflicts( pairs )
|
||||
|
||||
self._AutoPetitionLoops( pairs )
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
self._old_siblings.SetTags( set() )
|
||||
self.SetNew( set() )
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
|
||||
def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reason = None ):
|
||||
|
||||
pairs = list( pairs )
|
||||
|
@ -4376,6 +4391,16 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
return True
|
||||
|
||||
|
||||
def _CanAddFromCurrentInput( self ):
|
||||
|
||||
if self._current_new is None or len( self._old_siblings.GetTags() ) == 0:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _ConvertPairToListCtrlTuples( self, pair ):
|
||||
|
||||
( old, new ) = pair
|
||||
|
@ -4424,6 +4449,18 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _DeleteSelectedRows( self ):
|
||||
|
||||
pairs = self._tag_siblings.GetData( only_selected = True )
|
||||
|
||||
if len( pairs ) > 0:
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
|
||||
def _DeserialiseImportString( self, import_string ):
|
||||
|
||||
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
|
||||
|
@ -4549,30 +4586,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._UpdateListCtrlData()
|
||||
|
||||
|
||||
def _ListCtrlActivated( self ):
|
||||
|
||||
pairs = self._tag_siblings.GetData( only_selected = True )
|
||||
|
||||
if len( pairs ) > 0:
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
|
||||
def _SetButtonStatus( self ):
|
||||
|
||||
if self._current_new is None or len( self._old_siblings.GetTags() ) == 0:
|
||||
|
||||
self._add.setEnabled( False )
|
||||
|
||||
else:
|
||||
|
||||
self._add.setEnabled( True )
|
||||
|
||||
|
||||
|
||||
def _UpdateListCtrlData( self ):
|
||||
|
||||
olds = self._old_siblings.GetTags()
|
||||
|
@ -4640,30 +4653,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
|
||||
|
||||
def EventAddButton( self ):
|
||||
|
||||
if self._current_new is not None and len( self._old_siblings.GetTags() ) > 0:
|
||||
|
||||
olds = self._old_siblings.GetTags()
|
||||
|
||||
pairs = [ ( old, self._current_new ) for old in olds ]
|
||||
|
||||
self._AutoPetitionConflicts( pairs )
|
||||
|
||||
self._AutoPetitionLoops( pairs )
|
||||
|
||||
self._AddPairs( pairs )
|
||||
|
||||
self._old_siblings.SetTags( set() )
|
||||
self.SetNew( set() )
|
||||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
def GetContentUpdates( self ):
|
||||
|
@ -4741,7 +4731,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._UpdateListCtrlData()
|
||||
|
||||
self._SetButtonStatus()
|
||||
self._listctrl_panel.UpdateButtons()
|
||||
|
||||
|
||||
def SetTagBoxFocus( self ):
|
||||
|
|
|
@ -2376,228 +2376,3 @@ class WidgetEventFilter ( QC.QObject ):
|
|||
def EVT_SIZE( self, callback ):
|
||||
|
||||
self._AddCallback( 'EVT_SIZE', callback )
|
||||
|
||||
# wew lad
|
||||
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
|
||||
class CheckBoxDelegate(QW.QStyledItemDelegate):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
super( CheckBoxDelegate, self ).__init__(parent)
|
||||
|
||||
|
||||
def createEditor( self, parent, op, idx ):
|
||||
|
||||
self.editor = QW.QCheckBox( parent )
|
||||
|
||||
|
||||
class CollectComboCtrl( QW.QComboBox ):
|
||||
|
||||
itemChanged = QC.Signal()
|
||||
|
||||
def __init__( self, parent, media_collect ):
|
||||
|
||||
QW.QComboBox.__init__( self, parent )
|
||||
|
||||
self.view().pressed.connect( self._HandleItemPressed )
|
||||
|
||||
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
|
||||
self.setItemDelegate( CheckBoxDelegate() )
|
||||
|
||||
self.setModel( QG.QStandardItemModel( self ) )
|
||||
|
||||
text_and_data_tuples = set()
|
||||
|
||||
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
|
||||
|
||||
namespaces = media_sort.GetNamespaces()
|
||||
|
||||
try:
|
||||
|
||||
text_and_data_tuples.update( namespaces )
|
||||
|
||||
except:
|
||||
|
||||
HydrusData.DebugPrint( 'Bad namespaces: {}'.format( namespaces ) )
|
||||
|
||||
HydrusData.ShowText( 'Hey, your namespace-based sorts are likely damaged. Details have been written to the log, please let hydev know!' )
|
||||
|
||||
|
||||
|
||||
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
|
||||
|
||||
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
|
||||
|
||||
for ratings_service in ratings_services:
|
||||
|
||||
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
|
||||
|
||||
|
||||
for ( text, data ) in text_and_data_tuples:
|
||||
|
||||
self.Append( text, data )
|
||||
|
||||
# Trick to display custom text
|
||||
|
||||
self._cached_text = ''
|
||||
|
||||
if media_collect.DoesACollect():
|
||||
|
||||
CallAfter( self.SetCollectByValue, media_collect )
|
||||
|
||||
|
||||
|
||||
def paintEvent( self, e ):
|
||||
|
||||
painter = QW.QStylePainter( self )
|
||||
painter.setPen( self.palette().color( QG.QPalette.Text ) )
|
||||
|
||||
opt = QW.QStyleOptionComboBox()
|
||||
self.initStyleOption( opt )
|
||||
|
||||
opt.currentText = self._cached_text
|
||||
|
||||
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
|
||||
|
||||
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
|
||||
|
||||
|
||||
def GetValues( self ):
|
||||
|
||||
namespaces = []
|
||||
rating_service_keys = []
|
||||
|
||||
for index in self.GetCheckedIndices():
|
||||
|
||||
(collect_type, collect_data) = self.itemData( index, QC.Qt.UserRole )
|
||||
|
||||
if collect_type == 'namespace':
|
||||
|
||||
namespaces.append( collect_data )
|
||||
|
||||
elif collect_type == 'rating':
|
||||
|
||||
rating_service_keys.append( collect_data )
|
||||
|
||||
collect_strings = self.GetCheckedStrings()
|
||||
|
||||
if len( collect_strings ) > 0:
|
||||
|
||||
description = 'collect by ' + '-'.join( collect_strings )
|
||||
|
||||
else:
|
||||
|
||||
description = 'no collections'
|
||||
|
||||
|
||||
return ( namespaces, rating_service_keys, description )
|
||||
|
||||
|
||||
def hidePopup(self):
|
||||
|
||||
if not self.view().underMouse():
|
||||
|
||||
QW.QComboBox.hidePopup( self )
|
||||
|
||||
|
||||
def SetValue( self, text ):
|
||||
|
||||
self._cached_text = text
|
||||
|
||||
self.setCurrentText( text )
|
||||
|
||||
|
||||
def SetCollectByValue( self, media_collect ):
|
||||
|
||||
try:
|
||||
|
||||
indices_to_check = []
|
||||
|
||||
for index in range( self.count() ):
|
||||
|
||||
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
|
||||
|
||||
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
|
||||
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
|
||||
|
||||
if p1 or p2:
|
||||
|
||||
indices_to_check.append( index )
|
||||
|
||||
self.SetCheckedIndices( indices_to_check )
|
||||
|
||||
self.itemChanged.emit()
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.ShowText( 'Failed to set a collect-by value!' )
|
||||
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
|
||||
def SetCheckedIndices( self, indices_to_check ):
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if idx in indices_to_check:
|
||||
|
||||
item.setCheckState( QC.Qt.Checked )
|
||||
|
||||
else:
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
|
||||
|
||||
|
||||
def GetCheckedIndices( self ):
|
||||
|
||||
indices = []
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked: indices.append( idx )
|
||||
|
||||
return indices
|
||||
|
||||
|
||||
def GetCheckedStrings( self ):
|
||||
|
||||
strings = [ ]
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked: strings.append( item.text() )
|
||||
|
||||
return strings
|
||||
|
||||
|
||||
def Append( self, str, data ):
|
||||
|
||||
self.addItem( str, userData = data )
|
||||
|
||||
item = self.model().item( self.count() - 1, 0 )
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
|
||||
def _HandleItemPressed( self, index ):
|
||||
|
||||
item = self.model().itemFromIndex( index )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked:
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
else:
|
||||
|
||||
item.setCheckState( QC.Qt.Checked )
|
||||
|
||||
self.SetValue( self._cached_text )
|
||||
self.itemChanged.emit()
|
||||
|
|
|
@ -40,8 +40,6 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
|
|||
self._canvas_key = canvas_key
|
||||
self._current_media = None
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
|
||||
HG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
|
||||
|
||||
|
@ -1346,7 +1344,7 @@ class NotePanel( QW.QWidget ):
|
|||
vbox = QP.VBoxLayout( margin = 0 )
|
||||
|
||||
QP.AddToLayout( vbox, self._note_name, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._note_text, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._note_text, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self._note_text.setVisible( self._note_visible )
|
||||
|
||||
|
@ -1382,16 +1380,38 @@ class NotePanel( QW.QWidget ):
|
|||
spacing = self.layout().spacing()
|
||||
margin = self.layout().contentsMargins().top()
|
||||
|
||||
height = self._note_name.heightForWidth( width ) + margin * 2
|
||||
total_height = 0
|
||||
|
||||
expected_widget_height = self._note_name.heightForWidth( width )
|
||||
|
||||
if self._note_name.width() >= width and self._note_name.height() > expected_widget_height:
|
||||
|
||||
# there's some mysterious padding that I can't explain, probably a layout flag legacy issue, so we override here if the width seems correct
|
||||
|
||||
expected_widget_height = self._note_name.height()
|
||||
|
||||
|
||||
total_height += expected_widget_height
|
||||
|
||||
if self._note_text.isVisibleTo( self ):
|
||||
|
||||
height += spacing
|
||||
total_height += spacing
|
||||
|
||||
height += self._note_text.heightForWidth( width ) + margin * 2
|
||||
expected_widget_height = self._note_text.heightForWidth( width )
|
||||
|
||||
if self._note_text.width() >= width and self._note_text.height() > expected_widget_height:
|
||||
|
||||
# there's some mysterious padding that I can't explain, probably a layout flag legacy issue, so we override here if the width seems correct
|
||||
|
||||
expected_widget_height = self._note_text.height()
|
||||
|
||||
|
||||
total_height += expected_widget_height
|
||||
|
||||
|
||||
return height
|
||||
total_height += margin * 2
|
||||
|
||||
return total_height
|
||||
|
||||
|
||||
def IsNoteVisible( self ) -> bool:
|
||||
|
@ -1469,7 +1489,12 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
|
|||
spacing = self.layout().spacing()
|
||||
margin = self.layout().contentsMargins().top()
|
||||
|
||||
best_guess_at_height_for_width = sum( ( spacing + ( margin * 2 ) + note_panel.heightForWidth( my_ideal_width ) for note_panel in self._names_to_note_panels.values() ) ) - spacing
|
||||
my_axis_frame_width = self.frameWidth() * 2
|
||||
my_axis_margin = margin * 2
|
||||
|
||||
note_panel_width = my_ideal_width - ( my_axis_frame_width + my_axis_margin )
|
||||
|
||||
best_guess_at_height_for_width = sum( ( spacing + ( margin * 2 ) + note_panel.heightForWidth( note_panel_width ) for note_panel in self._names_to_note_panels.values() ) ) - spacing
|
||||
|
||||
best_guess_at_height_for_width += self.frameWidth() * 2
|
||||
|
||||
|
@ -1514,7 +1539,7 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
|
|||
|
||||
note_panel = NotePanel( self, name, note, note_visible )
|
||||
|
||||
QP.AddToLayout( self._vbox, note_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( self._vbox, note_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self._names_to_note_panels[ name ] = note_panel
|
||||
|
||||
|
|
|
@ -39,12 +39,13 @@ from hydrus.client.metadata import ClientTags
|
|||
|
||||
class CheckerOptionsButton( ClientGUICommon.BetterButton ):
|
||||
|
||||
def __init__( self, parent, checker_options, update_callable = None ):
|
||||
valueChanged = QC.Signal( ClientImportOptions.CheckerOptions )
|
||||
|
||||
def __init__( self, parent, checker_options: ClientImportOptions.CheckerOptions ):
|
||||
|
||||
ClientGUICommon.BetterButton.__init__( self, parent, 'checker options', self._EditOptions )
|
||||
|
||||
self._checker_options = checker_options
|
||||
self._update_callable = update_callable
|
||||
|
||||
self._SetToolTip()
|
||||
|
||||
|
@ -77,10 +78,7 @@ class CheckerOptionsButton( ClientGUICommon.BetterButton ):
|
|||
|
||||
self._SetToolTip()
|
||||
|
||||
if self._update_callable is not None:
|
||||
|
||||
self._update_callable( self._checker_options )
|
||||
|
||||
self.valueChanged.emit( self._checker_options )
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
@ -1547,7 +1545,7 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ):
|
|||
|
||||
checker_options = ClientImportOptions.CheckerOptions()
|
||||
|
||||
self._checker_options_button = CheckerOptionsButton( checker_panel, checker_options, update_callable = self._SetCheckerOptions )
|
||||
self._checker_options_button = CheckerOptionsButton( checker_panel, checker_options )
|
||||
|
||||
self._checker_download_control = ClientGUINetworkJobControl.NetworkJobControl( checker_panel )
|
||||
|
||||
|
@ -1615,6 +1613,8 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ):
|
|||
self._import_options_button.noteImportOptionsChanged.connect( self._SetNoteImportOptions )
|
||||
self._import_options_button.tagImportOptionsChanged.connect( self._SetTagImportOptions )
|
||||
|
||||
self._checker_options_button.valueChanged.connect( self._SetCheckerOptions )
|
||||
|
||||
self._UpdateControlsForNewWatcher()
|
||||
|
||||
HG.client_controller.gui.RegisterUIUpdateWindow( self )
|
||||
|
|
|
@ -927,7 +927,7 @@ class ListBox( QW.QScrollArea ):
|
|||
|
||||
TEXT_X_PADDING = 3
|
||||
|
||||
def __init__( self, parent: QW.QWidget, child_rows_allowed: bool, terms_may_have_child_rows: bool, height_num_chars = 10, has_async_text_info = False ):
|
||||
def __init__( self, parent: QW.QWidget, terms_may_have_sibling_or_parent_info: bool, height_num_chars = 10, has_async_text_info = False ):
|
||||
|
||||
QW.QScrollArea.__init__( self, parent )
|
||||
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
|
||||
|
@ -956,8 +956,11 @@ class ListBox( QW.QScrollArea ):
|
|||
|
||||
self._num_rows_per_page = 0
|
||||
|
||||
self._child_rows_allowed = child_rows_allowed
|
||||
self._terms_may_have_child_rows = terms_may_have_child_rows
|
||||
self._show_sibling_decorators = True
|
||||
self._show_parent_decorators = True
|
||||
self._extra_parent_rows_allowed = True
|
||||
|
||||
self._terms_may_have_sibling_or_parent_info = terms_may_have_sibling_or_parent_info
|
||||
|
||||
#
|
||||
|
||||
|
@ -1068,7 +1071,9 @@ class ListBox( QW.QScrollArea ):
|
|||
self._StartAsyncTextInfoLookup( term )
|
||||
|
||||
|
||||
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
|
||||
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
|
||||
|
||||
self._total_positional_rows += term.GetRowCount( show_parent_rows )
|
||||
|
||||
|
||||
if len( previously_selected_terms ) > 0:
|
||||
|
@ -1650,7 +1655,9 @@ class ListBox( QW.QScrollArea ):
|
|||
self._terms_to_positional_indices[ term ] = self._total_positional_rows
|
||||
self._positional_indices_to_terms[ self._total_positional_rows ] = term
|
||||
|
||||
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
|
||||
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
|
||||
|
||||
self._total_positional_rows += term.GetRowCount( show_parent_rows )
|
||||
|
||||
|
||||
|
||||
|
@ -2006,11 +2013,11 @@ class ListBox( QW.QScrollArea ):
|
|||
|
||||
|
||||
|
||||
def SetChildRowsAllowed( self, value: bool ):
|
||||
def SetExtraParentRowsAllowed( self, value: bool ):
|
||||
|
||||
if self._terms_may_have_child_rows and self._child_rows_allowed != value:
|
||||
if self._terms_may_have_sibling_or_parent_info and self._extra_parent_rows_allowed != value:
|
||||
|
||||
self._child_rows_allowed = value
|
||||
self._extra_parent_rows_allowed = value
|
||||
|
||||
self._RegenTermsToIndices()
|
||||
|
||||
|
@ -2025,6 +2032,34 @@ class ListBox( QW.QScrollArea ):
|
|||
self._minimum_height_num_chars = minimum_height_num_chars
|
||||
|
||||
|
||||
def SetParentDecoratorsAllowed( self, value: bool ):
|
||||
|
||||
if self._terms_may_have_sibling_or_parent_info and self._show_parent_decorators != value:
|
||||
|
||||
self._show_parent_decorators = value
|
||||
|
||||
# i.e. if we just hid/showed parent sub-rows
|
||||
if self._extra_parent_rows_allowed:
|
||||
|
||||
self._RegenTermsToIndices()
|
||||
|
||||
self._SetVirtualSize()
|
||||
|
||||
|
||||
self.widget().update()
|
||||
|
||||
|
||||
|
||||
def SetSiblingDecoratorsAllowed( self, value: bool ):
|
||||
|
||||
if self._terms_may_have_sibling_or_parent_info and self._show_sibling_decorators != value:
|
||||
|
||||
self._show_sibling_decorators = value
|
||||
|
||||
self.widget().update()
|
||||
|
||||
|
||||
|
||||
def sizeHint( self ):
|
||||
|
||||
size_hint = QW.QScrollArea.sizeHint( self )
|
||||
|
@ -2055,15 +2090,20 @@ class ListBoxTags( ListBox ):
|
|||
|
||||
self._tag_display_type = tag_display_type
|
||||
|
||||
child_rows_allowed = HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_taglists' )
|
||||
terms_may_have_child_rows = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
||||
terms_may_have_sibling_or_parent_info = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
||||
|
||||
ListBox.__init__( self, parent, child_rows_allowed, terms_may_have_child_rows, *args, **kwargs )
|
||||
ListBox.__init__( self, parent, terms_may_have_sibling_or_parent_info, *args, **kwargs )
|
||||
|
||||
if terms_may_have_sibling_or_parent_info:
|
||||
|
||||
self._show_parent_decorators = HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' )
|
||||
self._show_sibling_decorators = HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' )
|
||||
|
||||
self._extra_parent_rows_allowed = HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_taglists' )
|
||||
|
||||
|
||||
self._render_for_user = not self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
||||
|
||||
self._sibling_decoration_allowed = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
|
||||
|
||||
self._page_key = None # placeholder. if a subclass sets this, it changes menu behaviour to allow 'select this tag' menu pubsubs
|
||||
|
||||
self._UpdateBackgroundColour()
|
||||
|
@ -2135,7 +2175,9 @@ class ListBoxTags( ListBox ):
|
|||
|
||||
namespace_colours = self._GetNamespaceColours()
|
||||
|
||||
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._sibling_decoration_allowed, self._child_rows_allowed )
|
||||
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
|
||||
|
||||
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._show_sibling_decorators, self._show_parent_decorators, show_parent_rows )
|
||||
|
||||
rows_of_texts_and_colours = []
|
||||
|
||||
|
@ -2391,32 +2433,39 @@ class ListBoxTags( ListBox ):
|
|||
|
||||
menu = QW.QMenu()
|
||||
|
||||
if self._terms_may_have_child_rows:
|
||||
if self._terms_may_have_sibling_or_parent_info:
|
||||
|
||||
add_it = True
|
||||
|
||||
if self._child_rows_allowed:
|
||||
if self._show_parent_decorators:
|
||||
|
||||
if len( self._ordered_terms ) == self._total_positional_rows:
|
||||
if self._extra_parent_rows_allowed:
|
||||
|
||||
# no parents to hide!
|
||||
if len( self._ordered_terms ) != self._total_positional_rows:
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'collapse parent rows', 'Show/hide parents.', self.SetExtraParentRowsAllowed, not self._extra_parent_rows_allowed )
|
||||
|
||||
|
||||
add_it = False
|
||||
else:
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'expand parent rows', 'Show/hide parents.', self.SetExtraParentRowsAllowed, not self._extra_parent_rows_allowed )
|
||||
|
||||
|
||||
message = 'hide parent rows'
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'hide parent decorators', 'Show/hide parent info.', self.SetParentDecoratorsAllowed, not self._show_parent_decorators )
|
||||
|
||||
else:
|
||||
|
||||
message = 'show parent rows'
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'show parent decorators', 'Show/hide parent info.', self.SetParentDecoratorsAllowed, not self._show_parent_decorators )
|
||||
|
||||
|
||||
if add_it:
|
||||
if self._show_sibling_decorators:
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, message, 'Show/hide parents.', self.SetChildRowsAllowed, not self._child_rows_allowed )
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'hide sibling decorators', 'Show/hide sibling info.', self.SetSiblingDecoratorsAllowed, not self._show_sibling_decorators )
|
||||
|
||||
ClientGUIMenus.AppendSeparator( menu )
|
||||
else:
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'show sibling decorators', 'Show/hide sibling info.', self.SetSiblingDecoratorsAllowed, not self._show_sibling_decorators )
|
||||
|
||||
|
||||
ClientGUIMenus.AppendSeparator( menu )
|
||||
|
||||
|
||||
copy_menu = QW.QMenu( menu )
|
||||
|
@ -3612,7 +3661,7 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
|
|||
item_to_tag_key_wrapper = lambda term: term.GetTag()
|
||||
item_to_sibling_key_wrapper = item_to_tag_key_wrapper
|
||||
|
||||
if self._sibling_decoration_allowed:
|
||||
if self._show_sibling_decorators:
|
||||
|
||||
item_to_sibling_key_wrapper = lambda term: term.GetBestTag()
|
||||
|
||||
|
|
|
@ -56,12 +56,12 @@ class ListBoxItem( object ):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def GetRowCount( self, child_rows_allowed: bool ):
|
||||
def GetRowCount( self, show_parent_rows: bool ):
|
||||
|
||||
return 1
|
||||
|
||||
|
@ -95,7 +95,7 @@ class ListBoxItemTagSlice( ListBoxItem ):
|
|||
return []
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.Tuple[ str, str ] ]:
|
||||
|
||||
presentation_text = self.GetCopyableText()
|
||||
|
||||
|
@ -167,7 +167,7 @@ class ListBoxItemNamespaceColour( ListBoxItem ):
|
|||
return []
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
|
||||
return [ [ ( self.GetCopyableText(), self._namespace ) ] ]
|
||||
|
||||
|
@ -245,9 +245,9 @@ class ListBoxItemTextTag( ListBoxItem ):
|
|||
return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = self._tag ) ]
|
||||
|
||||
|
||||
def GetRowCount( self, child_rows_allowed: bool ):
|
||||
def GetRowCount( self, show_parent_rows: bool ):
|
||||
|
||||
if self._parent_tags is None or not child_rows_allowed:
|
||||
if self._parent_tags is None or not show_parent_rows:
|
||||
|
||||
return 1
|
||||
|
||||
|
@ -257,7 +257,7 @@ class ListBoxItemTextTag( ListBoxItem ):
|
|||
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
|
||||
# this should be with counts or whatever, but we need to think about this more lad
|
||||
|
||||
|
@ -276,11 +276,11 @@ class ListBoxItemTextTag( ListBoxItem ):
|
|||
|
||||
if self._parent_tags is not None:
|
||||
|
||||
if child_rows_allowed:
|
||||
if show_parent_rows:
|
||||
|
||||
self._AppendParentsTextWithNamespaces( rows_of_texts_with_namespaces, render_for_user )
|
||||
|
||||
elif sibling_decoration_allowed:
|
||||
elif parent_decoration_allowed:
|
||||
|
||||
self._AppendParentSuffixTagTextWithNamespace( first_row_of_texts_with_namespaces )
|
||||
|
||||
|
@ -338,7 +338,7 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
|
|||
|
||||
if with_counts:
|
||||
|
||||
return ''.join( ( text for ( text, namespace ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, False )[0] ) )
|
||||
return ''.join( ( text for ( text, namespace ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, False, False )[0] ) )
|
||||
|
||||
else:
|
||||
|
||||
|
@ -353,7 +353,7 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
|
|||
return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = self._tag ) ]
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
|
||||
# this should be with counts or whatever, but we need to think about this more lad
|
||||
|
||||
|
@ -412,11 +412,11 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
|
|||
|
||||
if self._parent_tags is not None:
|
||||
|
||||
if child_rows_allowed:
|
||||
if show_parent_rows:
|
||||
|
||||
self._AppendParentsTextWithNamespaces( rows_of_texts_with_namespaces, render_for_user )
|
||||
|
||||
elif sibling_decoration_allowed:
|
||||
elif parent_decoration_allowed:
|
||||
|
||||
self._AppendParentSuffixTagTextWithNamespace( first_row_of_texts_with_namespaces )
|
||||
|
||||
|
@ -473,9 +473,9 @@ class ListBoxItemPredicate( ListBoxItem ):
|
|||
|
||||
|
||||
|
||||
def GetRowCount( self, child_rows_allowed: bool ):
|
||||
def GetRowCount( self, show_parent_rows: bool ):
|
||||
|
||||
if child_rows_allowed:
|
||||
if show_parent_rows:
|
||||
|
||||
return 1 + len( self._predicate.GetParentPredicates() )
|
||||
|
||||
|
@ -485,7 +485,7 @@ class ListBoxItemPredicate( ListBoxItem ):
|
|||
|
||||
|
||||
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
|
||||
|
||||
rows_of_texts_and_namespaces = []
|
||||
|
||||
|
@ -508,14 +508,14 @@ class ListBoxItemPredicate( ListBoxItem ):
|
|||
|
||||
if len( parent_preds ) > 0:
|
||||
|
||||
if child_rows_allowed:
|
||||
if show_parent_rows:
|
||||
|
||||
for parent_pred in self._predicate.GetParentPredicates():
|
||||
|
||||
rows_of_texts_and_namespaces.append( parent_pred.GetTextsAndNamespaces( render_for_user ) )
|
||||
|
||||
|
||||
elif sibling_decoration_allowed:
|
||||
elif parent_decoration_allowed:
|
||||
|
||||
parents_text = ' ({} parents)'.format( HydrusData.ToHumanInt( len( parent_preds ) ) )
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ from hydrus.client.gui import ClientGUIDialogs
|
|||
from hydrus.client.gui import ClientGUIDialogsQuick
|
||||
from hydrus.client.gui import ClientGUIFunctions
|
||||
from hydrus.client.gui import ClientGUIMenus
|
||||
from hydrus.client.gui import ClientGUIParsing
|
||||
from hydrus.client.gui import ClientGUIScrolledPanels
|
||||
from hydrus.client.gui import ClientGUIFileSeedCache
|
||||
from hydrus.client.gui import ClientGUIGallerySeedLog
|
||||
|
@ -45,6 +44,7 @@ from hydrus.client.gui.networking import ClientGUIHydrusNetwork
|
|||
from hydrus.client.gui.networking import ClientGUINetworkJobControl
|
||||
from hydrus.client.gui.pages import ClientGUIResults
|
||||
from hydrus.client.gui.pages import ClientGUIResultsSortCollect
|
||||
from hydrus.client.gui.parsing import ClientGUIParsingFormulae
|
||||
from hydrus.client.gui.search import ClientGUIACDropdown
|
||||
from hydrus.client.gui.widgets import ClientGUICommon
|
||||
from hydrus.client.gui.widgets import ClientGUIControls
|
||||
|
@ -975,6 +975,8 @@ class ManagementPanel( QW.QScrollArea ):
|
|||
|
||||
self._media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self, management_controller = self._management_controller, silent = silent_collect )
|
||||
|
||||
self._media_collect.ListenForNewOptions()
|
||||
|
||||
if not self.SHOW_COLLECT:
|
||||
|
||||
self._media_collect.hide()
|
||||
|
@ -2206,7 +2208,9 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
|
|||
|
||||
( importer, ) = selected_importers
|
||||
|
||||
single_selected_presentation_import_options = importer.GetFileImportOptions().GetPresentationImportOptions()
|
||||
fio = importer.GetFileImportOptions()
|
||||
|
||||
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
|
||||
|
||||
|
||||
AddPresentationSubmenu( menu, 'downloader', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
|
||||
|
@ -2757,7 +2761,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
|
|||
|
||||
self._watcher_url_input.setPlaceholderText( 'watcher url' )
|
||||
|
||||
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._watchers_panel, checker_options, self._OptionsUpdated )
|
||||
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._watchers_panel, checker_options )
|
||||
|
||||
show_downloader_options = True
|
||||
allow_default_selection = True
|
||||
|
@ -2811,6 +2815,8 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
|
|||
self._import_options_button.noteImportOptionsChanged.connect( self._OptionsUpdated )
|
||||
self._import_options_button.tagImportOptionsChanged.connect( self._OptionsUpdated )
|
||||
|
||||
self._checker_options.valueChanged.connect( self._OptionsUpdated )
|
||||
|
||||
|
||||
def _AddURLs( self, urls, filterable_tags = None, additional_service_keys_to_tags = None ):
|
||||
|
||||
|
@ -3076,7 +3082,9 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
|
|||
|
||||
( watcher, ) = selected_watchers
|
||||
|
||||
single_selected_presentation_import_options = watcher.GetFileImportOptions().GetPresentationImportOptions()
|
||||
fio = watcher.GetFileImportOptions()
|
||||
|
||||
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
|
||||
|
||||
|
||||
AddPresentationSubmenu( menu, 'watcher', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
|
||||
|
@ -3780,7 +3788,7 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
|
|||
|
||||
formula = simple_downloader_formula.GetFormula()
|
||||
|
||||
control = ClientGUIParsing.EditFormulaPanel( panel, formula, lambda: ClientParsing.ParsingTestData( {}, ( '', ) ) )
|
||||
control = ClientGUIParsingFormulae.EditFormulaPanel( panel, formula, lambda: ClientParsing.ParsingTestData( {}, ( '', ) ) )
|
||||
|
||||
panel.SetControl( control )
|
||||
|
||||
|
|
|
@ -1559,7 +1559,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
|
||||
page_name = HydrusText.ElideText( full_page_name, max_page_name_chars )
|
||||
|
||||
do_tooltip = len( page_name ) != len( full_page_name )
|
||||
do_tooltip = len( page_name ) != len( full_page_name ) or HG.client_controller.new_options.GetBoolean( 'elide_page_tab_names' )
|
||||
|
||||
num_string = ''
|
||||
|
||||
|
|
|
@ -4933,6 +4933,7 @@ class Thumbnail( Selectable ):
|
|||
#
|
||||
# EDIT 2: I think it may only look weird when the thumb banner has opacity. Maybe I need to learn about CompositionModes
|
||||
#
|
||||
# EDIT 3: Appalently Qt 6.4.0 may fix the basic 100% UI scale QImage init bug!
|
||||
|
||||
painter = QG.QPainter( qt_image )
|
||||
|
||||
|
@ -4973,6 +4974,7 @@ class Thumbnail( Selectable ):
|
|||
|
||||
f = QG.QFont( HG.client_controller.gui.font() )
|
||||
|
||||
# this line magically fixes the bad text, as above
|
||||
f.setStyleStrategy( QG.QFont.PreferAntialias )
|
||||
|
||||
painter.setFont( f )
|
||||
|
|
|
@ -5,6 +5,7 @@ from qtpy import QtGui as QG
|
|||
from qtpy import QtWidgets as QW
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusExceptions
|
||||
from hydrus.core import HydrusGlobals as HG
|
||||
|
||||
|
@ -21,6 +22,258 @@ from hydrus.client.gui.widgets import ClientGUIMenuButton
|
|||
from hydrus.client.media import ClientMedia
|
||||
from hydrus.client.metadata import ClientTags
|
||||
|
||||
# wew lad
|
||||
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
|
||||
class CheckBoxDelegate( QW.QStyledItemDelegate ):
|
||||
|
||||
def __init__( self, parent = None ):
|
||||
|
||||
super( CheckBoxDelegate, self ).__init__( parent )
|
||||
|
||||
|
||||
def createEditor( self, parent, op, idx ):
|
||||
|
||||
self.editor = QW.QCheckBox( parent )
|
||||
|
||||
|
||||
|
||||
class CollectComboCtrl( QW.QComboBox ):
|
||||
|
||||
itemChanged = QC.Signal()
|
||||
|
||||
def __init__( self, parent, media_collect ):
|
||||
|
||||
QW.QComboBox.__init__( self, parent )
|
||||
|
||||
self.view().pressed.connect( self._HandleItemPressed )
|
||||
|
||||
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
|
||||
self.setItemDelegate( CheckBoxDelegate() )
|
||||
|
||||
self.setModel( QG.QStandardItemModel( self ) )
|
||||
|
||||
self._InitialiseChoices()
|
||||
|
||||
# Trick to display custom text
|
||||
|
||||
self._cached_text = ''
|
||||
|
||||
if media_collect.DoesACollect():
|
||||
|
||||
QP.CallAfter( self.SetCollectByValue, media_collect )
|
||||
|
||||
|
||||
|
||||
def _InitialiseChoices( self ):
|
||||
|
||||
text_and_data_tuples = set()
|
||||
|
||||
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
|
||||
|
||||
namespaces = media_sort.GetNamespaces()
|
||||
|
||||
try:
|
||||
|
||||
text_and_data_tuples.update( namespaces )
|
||||
|
||||
except:
|
||||
|
||||
HydrusData.DebugPrint( 'Bad namespaces: {}'.format( namespaces ) )
|
||||
|
||||
HydrusData.ShowText( 'Hey, your namespace-based sorts are likely damaged. Details have been written to the log, please let hydev know!' )
|
||||
|
||||
|
||||
|
||||
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
|
||||
|
||||
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
|
||||
|
||||
for ratings_service in ratings_services:
|
||||
|
||||
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
|
||||
|
||||
|
||||
for ( text, data ) in text_and_data_tuples:
|
||||
|
||||
self.Append( text, data )
|
||||
|
||||
|
||||
|
||||
def paintEvent( self, e ):
|
||||
|
||||
painter = QW.QStylePainter( self )
|
||||
painter.setPen( self.palette().color( QG.QPalette.Text ) )
|
||||
|
||||
opt = QW.QStyleOptionComboBox()
|
||||
self.initStyleOption( opt )
|
||||
|
||||
opt.currentText = self._cached_text
|
||||
|
||||
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
|
||||
|
||||
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
|
||||
|
||||
|
||||
def GetValues( self ):
|
||||
|
||||
namespaces = []
|
||||
rating_service_keys = []
|
||||
|
||||
for index in self.GetCheckedIndices():
|
||||
|
||||
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
|
||||
|
||||
if collect_type == 'namespace':
|
||||
|
||||
namespaces.append( collect_data )
|
||||
|
||||
elif collect_type == 'rating':
|
||||
|
||||
rating_service_keys.append( collect_data )
|
||||
|
||||
|
||||
|
||||
collect_strings = self.GetCheckedStrings()
|
||||
|
||||
if len( collect_strings ) > 0:
|
||||
|
||||
description = 'collect by ' + '-'.join( collect_strings )
|
||||
|
||||
else:
|
||||
|
||||
description = 'no collections'
|
||||
|
||||
|
||||
return ( namespaces, rating_service_keys, description )
|
||||
|
||||
|
||||
def hidePopup(self):
|
||||
|
||||
if not self.view().underMouse():
|
||||
|
||||
QW.QComboBox.hidePopup( self )
|
||||
|
||||
|
||||
def SetValue( self, text ):
|
||||
|
||||
self._cached_text = text
|
||||
|
||||
self.setCurrentText( text )
|
||||
|
||||
|
||||
def SetCollectByValue( self, media_collect ):
|
||||
|
||||
try:
|
||||
|
||||
indices_to_check = []
|
||||
|
||||
for index in range( self.count() ):
|
||||
|
||||
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
|
||||
|
||||
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
|
||||
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
|
||||
|
||||
if p1 or p2:
|
||||
|
||||
indices_to_check.append( index )
|
||||
|
||||
|
||||
|
||||
self.SetCheckedIndices( indices_to_check )
|
||||
|
||||
self.itemChanged.emit()
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.ShowText( 'Failed to set a collect-by value!' )
|
||||
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
|
||||
|
||||
def SetCheckedIndices( self, indices_to_check ):
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if idx in indices_to_check:
|
||||
|
||||
item.setCheckState( QC.Qt.Checked )
|
||||
|
||||
else:
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
|
||||
|
||||
|
||||
def GetCheckedIndices( self ):
|
||||
|
||||
indices = []
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked:
|
||||
|
||||
indices.append( idx )
|
||||
|
||||
|
||||
|
||||
return indices
|
||||
|
||||
|
||||
def GetCheckedStrings( self ):
|
||||
|
||||
strings = [ ]
|
||||
|
||||
for idx in range( self.count() ):
|
||||
|
||||
item = self.model().item( idx )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked:
|
||||
|
||||
strings.append( item.text() )
|
||||
|
||||
|
||||
|
||||
return strings
|
||||
|
||||
|
||||
def Append( self, str, data ):
|
||||
|
||||
self.addItem( str, userData = data )
|
||||
|
||||
item = self.model().item( self.count() - 1, 0 )
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
|
||||
def ReinitialiseChoices( self ):
|
||||
|
||||
self.clear()
|
||||
|
||||
self._InitialiseChoices()
|
||||
|
||||
|
||||
def _HandleItemPressed( self, index ):
|
||||
|
||||
item = self.model().itemFromIndex( index )
|
||||
|
||||
if item.checkState() == QC.Qt.Checked:
|
||||
|
||||
item.setCheckState( QC.Qt.Unchecked )
|
||||
|
||||
else:
|
||||
|
||||
item.setCheckState( QC.Qt.Checked )
|
||||
|
||||
self.SetValue( self._cached_text )
|
||||
self.itemChanged.emit()
|
||||
|
||||
class MediaCollectControl( QW.QWidget ):
|
||||
|
||||
def __init__( self, parent, management_controller = None, silent = False ):
|
||||
|
@ -42,7 +295,7 @@ class MediaCollectControl( QW.QWidget ):
|
|||
|
||||
self._silent = silent
|
||||
|
||||
self._collect_comboctrl = QP.CollectComboCtrl( self, self._media_collect )
|
||||
self._collect_comboctrl = CollectComboCtrl( self, self._media_collect )
|
||||
|
||||
choice_tuples = [
|
||||
( 'collect unmatched', True ),
|
||||
|
@ -146,12 +399,26 @@ class MediaCollectControl( QW.QWidget ):
|
|||
return self._media_collect
|
||||
|
||||
|
||||
def ListenForNewOptions( self ):
|
||||
|
||||
HG.client_controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
|
||||
|
||||
|
||||
def NotifyAdvancedMode( self ):
|
||||
|
||||
self._UpdateButtonsVisible()
|
||||
|
||||
|
||||
def SetCollect( self, media_collect: ClientMedia.MediaCollect ):
|
||||
def NotifyNewOptions( self ):
|
||||
|
||||
media_collect = self._media_collect.Duplicate()
|
||||
|
||||
self._collect_comboctrl.ReinitialiseChoices()
|
||||
|
||||
self.SetCollect( media_collect, do_broadcast = False )
|
||||
|
||||
|
||||
def SetCollect( self, media_collect: ClientMedia.MediaCollect, do_broadcast = True ):
|
||||
|
||||
self._media_collect = media_collect
|
||||
|
||||
|
@ -166,7 +433,10 @@ class MediaCollectControl( QW.QWidget ):
|
|||
self._collect_comboctrl.blockSignals( False )
|
||||
self._collect_unmatched.blockSignals( False )
|
||||
|
||||
self._BroadcastCollect()
|
||||
if do_broadcast:
|
||||
|
||||
self._BroadcastCollect()
|
||||
|
||||
|
||||
|
||||
def SetCollectFromPage( self, page_key, media_collect ):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,744 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import typing
|
||||
|
||||
from qtpy import QtCore as QC
|
||||
from qtpy import QtWidgets as QW
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusExceptions
|
||||
from hydrus.core import HydrusFileHandling
|
||||
from hydrus.core import HydrusGlobals as HG
|
||||
from hydrus.core import HydrusTemp
|
||||
from hydrus.core import HydrusText
|
||||
|
||||
from hydrus.client import ClientConstants as CC
|
||||
from hydrus.client import ClientParsing
|
||||
from hydrus.client import ClientStrings
|
||||
from hydrus.client.gui import ClientGUIDialogs
|
||||
from hydrus.client.gui import ClientGUIFunctions
|
||||
from hydrus.client.gui import ClientGUIStringControls
|
||||
from hydrus.client.gui import QtPorting as QP
|
||||
from hydrus.client.gui.widgets import ClientGUICommon
|
||||
from hydrus.client.networking import ClientNetworkingJobs
|
||||
|
||||
class TestPanel( QW.QWidget ):
|
||||
|
||||
def __init__( self, parent, object_callable, test_data: typing.Optional[ ClientParsing.ParsingTestData ] = None ):
|
||||
|
||||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
if test_data is None:
|
||||
|
||||
test_data = ClientParsing.ParsingTestData( {}, ( '', ) )
|
||||
|
||||
|
||||
self._collapse_newlines = True
|
||||
|
||||
self._object_callable = object_callable
|
||||
|
||||
self._example_parsing_context = ClientGUIStringControls.StringToStringDictButton( self, 'edit example parsing context' )
|
||||
|
||||
self._data_preview_notebook = QW.QTabWidget( self )
|
||||
|
||||
raw_data_panel = QW.QWidget( self._data_preview_notebook )
|
||||
|
||||
self._example_data_raw_description = ClientGUICommon.BetterStaticText( raw_data_panel )
|
||||
|
||||
self._copy_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().copy, self._Copy )
|
||||
self._copy_button.setToolTip( 'Copy the current example data to the clipboard.' )
|
||||
|
||||
self._fetch_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().link, self._FetchFromURL )
|
||||
self._fetch_button.setToolTip( 'Fetch data from a URL.' )
|
||||
|
||||
self._paste_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().paste, self._Paste )
|
||||
self._paste_button.setToolTip( 'Paste the current clipboard data into here.' )
|
||||
|
||||
self._example_data_raw_preview = QW.QPlainTextEdit( raw_data_panel )
|
||||
self._example_data_raw_preview.setReadOnly( True )
|
||||
|
||||
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._example_data_raw_preview, ( 60, 9 ) )
|
||||
|
||||
self._example_data_raw_preview.setMinimumWidth( width )
|
||||
self._example_data_raw_preview.setMinimumHeight( height )
|
||||
|
||||
self._test_parse = ClientGUICommon.BetterButton( self, 'test parse', self.TestParse )
|
||||
|
||||
self._results = QW.QPlainTextEdit( self )
|
||||
|
||||
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._results, ( 80, 12 ) )
|
||||
|
||||
self._results.setMinimumWidth( width )
|
||||
self._results.setMinimumHeight( height )
|
||||
|
||||
#
|
||||
|
||||
self._example_parsing_context.SetValue( test_data.parsing_context )
|
||||
|
||||
self._example_data_raw = ''
|
||||
|
||||
self._results.setPlainText( 'Successfully parsed results will be printed here.' )
|
||||
|
||||
#
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._example_data_raw_description, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._copy_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._fetch_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._example_data_raw_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
raw_data_panel.setLayout( vbox )
|
||||
|
||||
self._data_preview_notebook.addTab( raw_data_panel, 'raw data' )
|
||||
self._data_preview_notebook.setCurrentWidget( raw_data_panel )
|
||||
|
||||
#
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, self._example_parsing_context, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._data_preview_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, self._test_parse, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._results, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.setLayout( vbox )
|
||||
|
||||
if len( test_data.texts ) > 0:
|
||||
|
||||
QP.CallAfter( self._SetExampleData, test_data.texts[0] )
|
||||
|
||||
|
||||
|
||||
def _Copy( self ):
|
||||
|
||||
HG.client_controller.pub( 'clipboard', 'text', self._example_data_raw )
|
||||
|
||||
|
||||
def _FetchFromURL( self ):
|
||||
|
||||
def qt_code( example_data, example_bytes ):
|
||||
|
||||
if not self or not QP.isValid( self ):
|
||||
|
||||
return
|
||||
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
example_parsing_context[ 'url' ] = url
|
||||
example_parsing_context[ 'post_index' ] = '0'
|
||||
|
||||
self._example_parsing_context.SetValue( example_parsing_context )
|
||||
|
||||
self._SetExampleData( example_data, example_bytes = example_bytes )
|
||||
|
||||
|
||||
def do_it( url ):
|
||||
|
||||
network_job = ClientNetworkingJobs.NetworkJob( 'GET', url )
|
||||
|
||||
network_job.OverrideBandwidth()
|
||||
|
||||
HG.client_controller.network_engine.AddJob( network_job )
|
||||
|
||||
example_bytes = None
|
||||
|
||||
try:
|
||||
|
||||
network_job.WaitUntilDone()
|
||||
|
||||
example_data = network_job.GetContentText()
|
||||
|
||||
example_bytes = network_job.GetContentBytes()
|
||||
|
||||
except HydrusExceptions.CancelledException:
|
||||
|
||||
example_data = 'fetch cancelled'
|
||||
|
||||
except Exception as e:
|
||||
|
||||
example_data = 'fetch failed:' + os.linesep * 2 + str( e )
|
||||
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
|
||||
QP.CallAfter( qt_code, example_data, example_bytes )
|
||||
|
||||
|
||||
message = 'Enter URL to fetch data for.'
|
||||
|
||||
with ClientGUIDialogs.DialogTextEntry( self, message, placeholder = 'enter url', allow_blank = False) as dlg:
|
||||
|
||||
if dlg.exec() == QW.QDialog.Accepted:
|
||||
|
||||
url = dlg.GetValue()
|
||||
|
||||
HG.client_controller.CallToThread( do_it, url )
|
||||
|
||||
|
||||
|
||||
|
||||
def _Paste( self ):
|
||||
|
||||
try:
|
||||
|
||||
raw_text = HG.client_controller.GetClipboardText()
|
||||
|
||||
try:
|
||||
|
||||
raw_bytes = raw_text.decode( 'utf-8' )
|
||||
|
||||
except:
|
||||
|
||||
raw_bytes = None
|
||||
|
||||
|
||||
except HydrusExceptions.DataMissing as e:
|
||||
|
||||
QW.QMessageBox.critical( self, 'Error', str(e) )
|
||||
|
||||
return
|
||||
|
||||
|
||||
self._SetExampleData( raw_text, example_bytes = raw_bytes )
|
||||
|
||||
|
||||
def _SetExampleData( self, example_data, example_bytes = None ):
|
||||
|
||||
self._example_data_raw = example_data
|
||||
|
||||
test_parse_ok = True
|
||||
looked_like_json = False
|
||||
|
||||
MAX_CHARS_IN_PREVIEW = 1024 * 64
|
||||
|
||||
if len( example_data ) > 0:
|
||||
|
||||
good_type_found = True
|
||||
|
||||
if HydrusText.LooksLikeJSON( example_data ):
|
||||
|
||||
# prioritise this, so if the JSON contains some HTML, it'll overwrite here. decent compromise
|
||||
|
||||
looked_like_json = True
|
||||
|
||||
parse_phrase = 'looks like JSON'
|
||||
|
||||
elif HydrusText.LooksLikeHTML( example_data ):
|
||||
|
||||
# can't just throw this at bs4 to see if it 'works', as it'll just wrap any unparsable string in some bare <html><body><p> tags
|
||||
|
||||
parse_phrase = 'looks like HTML'
|
||||
|
||||
else:
|
||||
|
||||
good_type_found = False
|
||||
|
||||
if example_bytes is not None:
|
||||
|
||||
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath()
|
||||
|
||||
try:
|
||||
|
||||
with open( temp_path, 'wb' ) as f:
|
||||
|
||||
f.write( example_bytes )
|
||||
|
||||
|
||||
mime = HydrusFileHandling.GetMime( temp_path )
|
||||
|
||||
except:
|
||||
|
||||
mime = HC.APPLICATION_UNKNOWN
|
||||
|
||||
finally:
|
||||
|
||||
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
mime = HC.APPLICATION_UNKNOWN
|
||||
|
||||
|
||||
|
||||
if good_type_found:
|
||||
|
||||
description = HydrusData.ToHumanBytes( len( example_data ) ) + ' total, ' + parse_phrase
|
||||
|
||||
example_data_to_show = example_data
|
||||
|
||||
if looked_like_json:
|
||||
|
||||
try:
|
||||
|
||||
j = HG.client_controller.parsing_cache.GetJSON( example_data )
|
||||
|
||||
example_data_to_show = json.dumps( j, indent = 4 )
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if len( example_data_to_show ) > MAX_CHARS_IN_PREVIEW:
|
||||
|
||||
preview = 'PREVIEW:' + os.linesep + str( example_data_to_show[:MAX_CHARS_IN_PREVIEW] )
|
||||
|
||||
else:
|
||||
|
||||
preview = example_data_to_show
|
||||
|
||||
|
||||
else:
|
||||
|
||||
if mime in HC.ALLOWED_MIMES:
|
||||
|
||||
description = 'that looked like a {}!'.format( HC.mime_string_lookup[ mime ] )
|
||||
|
||||
preview = 'no preview'
|
||||
|
||||
test_parse_ok = False
|
||||
|
||||
else:
|
||||
|
||||
description = 'that did not look like HTML or JSON, but will try to show it anyway'
|
||||
|
||||
if len( example_data ) > MAX_CHARS_IN_PREVIEW:
|
||||
|
||||
preview = 'PREVIEW:' + os.linesep + repr( example_data[:MAX_CHARS_IN_PREVIEW] )
|
||||
|
||||
else:
|
||||
|
||||
preview = repr( example_data )
|
||||
|
||||
|
||||
|
||||
|
||||
else:
|
||||
|
||||
description = 'no example data set yet'
|
||||
preview = ''
|
||||
|
||||
test_parse_ok = False
|
||||
|
||||
|
||||
self._test_parse.setEnabled( test_parse_ok )
|
||||
|
||||
self._example_data_raw_description.setText( description )
|
||||
self._example_data_raw_preview.setPlainText( preview )
|
||||
|
||||
|
||||
def GetExampleParsingContext( self ):
|
||||
|
||||
return self._example_parsing_context.GetValue()
|
||||
|
||||
|
||||
def GetTestData( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
return ClientParsing.ParsingTestData( example_parsing_context, ( self._example_data_raw, ) )
|
||||
|
||||
|
||||
def GetTestDataForChild( self ):
|
||||
|
||||
return self.GetTestData()
|
||||
|
||||
|
||||
def SetCollapseNewlines( self, value: bool ):
|
||||
|
||||
self._collapse_newlines = value
|
||||
|
||||
|
||||
def SetExampleData( self, example_data, example_bytes = None ):
|
||||
|
||||
self._SetExampleData( example_data, example_bytes = example_bytes )
|
||||
|
||||
|
||||
def SetExampleParsingContext( self, example_parsing_context ):
|
||||
|
||||
self._example_parsing_context.SetValue( example_parsing_context )
|
||||
|
||||
|
||||
def TestParse( self ):
|
||||
|
||||
obj = self._object_callable()
|
||||
|
||||
test_data = self.GetTestData()
|
||||
|
||||
test_text = ''
|
||||
|
||||
# change this to be for every text, do a diff panel, whatever
|
||||
|
||||
if len( test_data.texts ) > 0:
|
||||
|
||||
test_text = test_data.texts[0]
|
||||
|
||||
|
||||
try:
|
||||
|
||||
if 'post_index' in test_data.parsing_context:
|
||||
|
||||
del test_data.parsing_context[ 'post_index' ]
|
||||
|
||||
|
||||
if isinstance( obj, ClientParsing.ParseFormula ):
|
||||
|
||||
results_text = obj.ParsePretty( test_data.parsing_context, test_data.texts[0], self._collapse_newlines )
|
||||
|
||||
else:
|
||||
|
||||
results_text = obj.ParsePretty( test_data.parsing_context, test_data.texts[0] )
|
||||
|
||||
|
||||
self._results.setPlainText( results_text )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
etype = type( e )
|
||||
|
||||
( etype, value, tb ) = sys.exc_info()
|
||||
|
||||
trace = ''.join( traceback.format_exception( etype, value, tb ) )
|
||||
|
||||
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
|
||||
|
||||
self._results.setPlainText( message )
|
||||
|
||||
|
||||
|
||||
|
||||
class TestPanelFormula( TestPanel ):
|
||||
|
||||
def GetTestDataForStringProcessor( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
formula = self._object_callable()
|
||||
|
||||
try:
|
||||
|
||||
formula.SetStringProcessor( ClientStrings.StringProcessor() )
|
||||
|
||||
texts = formula.Parse( example_parsing_context, self._example_data_raw, self._collapse_newlines )
|
||||
|
||||
except:
|
||||
|
||||
texts = [ '' ]
|
||||
|
||||
|
||||
return ClientParsing.ParsingTestData( example_parsing_context, texts )
|
||||
|
||||
|
||||
class TestPanelPageParser( TestPanel ):
|
||||
|
||||
def __init__( self, parent, object_callable, pre_parsing_converter_callable, test_data = None ):
|
||||
|
||||
self._pre_parsing_converter_callable = pre_parsing_converter_callable
|
||||
|
||||
TestPanel.__init__( self, parent, object_callable, test_data = test_data )
|
||||
|
||||
post_conversion_panel = QW.QWidget( self._data_preview_notebook )
|
||||
|
||||
self._example_data_post_conversion_description = ClientGUICommon.BetterStaticText( post_conversion_panel )
|
||||
|
||||
self._copy_button_post_conversion = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().copy, self._CopyPostConversion )
|
||||
self._copy_button_post_conversion.setToolTip( 'Copy the current post conversion data to the clipboard.' )
|
||||
|
||||
self._refresh_post_conversion_button = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
|
||||
self._example_data_post_conversion_preview = QW.QPlainTextEdit( post_conversion_panel )
|
||||
self._example_data_post_conversion_preview.setReadOnly( True )
|
||||
|
||||
#
|
||||
|
||||
self._example_data_post_conversion = ''
|
||||
|
||||
#
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._example_data_post_conversion_description, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._copy_button_post_conversion, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._refresh_post_conversion_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._example_data_post_conversion_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
post_conversion_panel.setLayout( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._data_preview_notebook.addTab( post_conversion_panel, 'post pre-parsing conversion' )
|
||||
|
||||
|
||||
def _CopyPostConversion( self ):
|
||||
|
||||
HG.client_controller.pub( 'clipboard', 'text', self._example_data_post_conversion )
|
||||
|
||||
|
||||
def _RefreshDataPreviews( self ):
|
||||
|
||||
self._SetExampleData( self._example_data_raw )
|
||||
|
||||
|
||||
def _SetExampleData( self, example_data, example_bytes = None ):
|
||||
|
||||
TestPanel._SetExampleData( self, example_data, example_bytes = example_bytes )
|
||||
|
||||
pre_parsing_converter = self._pre_parsing_converter_callable()
|
||||
|
||||
if pre_parsing_converter.MakesChanges():
|
||||
|
||||
try:
|
||||
|
||||
post_conversion_example_data = ClientParsing.MakeParsedTextPretty( pre_parsing_converter.Convert( self._example_data_raw ) )
|
||||
|
||||
if len( post_conversion_example_data ) > 1024:
|
||||
|
||||
preview = 'PREVIEW:' + os.linesep + str( post_conversion_example_data[:1024] )
|
||||
|
||||
else:
|
||||
|
||||
preview = post_conversion_example_data
|
||||
|
||||
|
||||
parse_phrase = 'uncertain data type'
|
||||
|
||||
# can't just throw this at bs4 to see if it 'works', as it'll just wrap any unparsable string in some bare <html><body><p> tags
|
||||
if HydrusText.LooksLikeHTML( post_conversion_example_data ):
|
||||
|
||||
parse_phrase = 'looks like HTML'
|
||||
|
||||
|
||||
# put this second, so if the JSON contains some HTML, it'll overwrite here. decent compromise
|
||||
if HydrusText.LooksLikeJSON( example_data ):
|
||||
|
||||
parse_phrase = 'looks like JSON'
|
||||
|
||||
|
||||
description = HydrusData.ToHumanBytes( len( post_conversion_example_data ) ) + ' total, ' + parse_phrase
|
||||
|
||||
except Exception as e:
|
||||
|
||||
post_conversion_example_data = self._example_data_raw
|
||||
|
||||
etype = type( e )
|
||||
|
||||
( etype, value, tb ) = sys.exc_info()
|
||||
|
||||
trace = ''.join( traceback.format_exception( etype, value, tb ) )
|
||||
|
||||
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
|
||||
|
||||
preview = message
|
||||
|
||||
description = 'Could not convert.'
|
||||
|
||||
|
||||
else:
|
||||
|
||||
post_conversion_example_data = self._example_data_raw
|
||||
|
||||
preview = 'No changes made.'
|
||||
|
||||
description = self._example_data_raw_description.text()
|
||||
|
||||
|
||||
self._example_data_post_conversion_description.setText( description )
|
||||
|
||||
self._example_data_post_conversion = post_conversion_example_data
|
||||
|
||||
self._example_data_post_conversion_preview.setPlainText( preview )
|
||||
|
||||
|
||||
def GetTestDataForChild( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
return ClientParsing.ParsingTestData( example_parsing_context, ( self._example_data_post_conversion, ) )
|
||||
|
||||
|
||||
class TestPanelPageParserSubsidiary( TestPanelPageParser ):
|
||||
|
||||
def __init__( self, parent, object_callable, pre_parsing_converter_callable, formula_callable, test_data = None ):
|
||||
|
||||
TestPanelPageParser.__init__( self, parent, object_callable, pre_parsing_converter_callable, test_data = test_data )
|
||||
|
||||
self._formula_callable = formula_callable
|
||||
|
||||
post_separation_panel = QW.QWidget( self._data_preview_notebook )
|
||||
|
||||
self._example_data_post_separation_description = ClientGUICommon.BetterStaticText( post_separation_panel )
|
||||
|
||||
self._copy_button_post_separation = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().copy, self._CopyPostSeparation )
|
||||
self._copy_button_post_separation.setToolTip( 'Copy the current post separation data to the clipboard.' )
|
||||
|
||||
self._refresh_post_separation_button = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
|
||||
self._example_data_post_separation_preview = QW.QPlainTextEdit( post_separation_panel )
|
||||
self._example_data_post_separation_preview.setReadOnly( True )
|
||||
|
||||
#
|
||||
|
||||
self._example_data_post_separation = []
|
||||
|
||||
#
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, self._example_data_post_separation_description, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._copy_button_post_separation, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, self._refresh_post_separation_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, self._example_data_post_separation_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
post_separation_panel.setLayout( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._data_preview_notebook.addTab( post_separation_panel, 'post separation' )
|
||||
|
||||
|
||||
def _CopyPostSeparation( self ):
|
||||
|
||||
joiner = os.linesep * 2
|
||||
|
||||
HG.client_controller.pub( 'clipboard', 'text', joiner.join( self._example_data_post_separation ) )
|
||||
|
||||
|
||||
def _SetExampleData( self, example_data, example_bytes = None ):
|
||||
|
||||
TestPanelPageParser._SetExampleData( self, example_data, example_bytes = example_bytes )
|
||||
|
||||
formula = self._formula_callable()
|
||||
|
||||
if formula is None:
|
||||
|
||||
separation_example_data = []
|
||||
description = 'No formula set!'
|
||||
preview = ''
|
||||
|
||||
else:
|
||||
|
||||
try:
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
separation_example_data = formula.Parse( example_parsing_context, self._example_data_post_conversion, self._collapse_newlines )
|
||||
|
||||
joiner = os.linesep * 2
|
||||
|
||||
preview = joiner.join( separation_example_data )
|
||||
|
||||
if len( preview ) > 1024:
|
||||
|
||||
preview = 'PREVIEW:' + os.linesep + str( preview[:1024] )
|
||||
|
||||
|
||||
description = HydrusData.ToHumanInt( len( separation_example_data ) ) + ' subsidiary posts parsed'
|
||||
|
||||
except Exception as e:
|
||||
|
||||
separation_example_data = []
|
||||
|
||||
etype = type( e )
|
||||
|
||||
( etype, value, tb ) = sys.exc_info()
|
||||
|
||||
trace = ''.join( traceback.format_exception( etype, value, tb ) )
|
||||
|
||||
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
|
||||
|
||||
preview = message
|
||||
|
||||
description = 'Could not convert.'
|
||||
|
||||
|
||||
|
||||
self._example_data_post_separation_description.setText( description )
|
||||
|
||||
self._example_data_post_separation = separation_example_data
|
||||
|
||||
self._example_data_post_separation_preview.setPlainText( preview )
|
||||
|
||||
|
||||
def GetTestDataForChild( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
return ClientParsing.ParsingTestData( example_parsing_context, list( self._example_data_post_separation ) )
|
||||
|
||||
|
||||
def TestParse( self ):
|
||||
|
||||
formula = self._formula_callable()
|
||||
|
||||
page_parser = self._object_callable()
|
||||
|
||||
try:
|
||||
|
||||
test_data = self.GetTestData()
|
||||
|
||||
test_data.parsing_context[ 'post_index' ] = 0
|
||||
|
||||
if formula is None:
|
||||
|
||||
posts = test_data.texts
|
||||
|
||||
else:
|
||||
|
||||
posts = []
|
||||
|
||||
collapse_newlines = False
|
||||
|
||||
for test_text in test_data.texts:
|
||||
|
||||
posts.extend( formula.Parse( test_data.parsing_context, test_text, collapse_newlines ) )
|
||||
|
||||
|
||||
|
||||
pretty_texts = []
|
||||
|
||||
for post in posts:
|
||||
|
||||
pretty_text = page_parser.ParsePretty( test_data.parsing_context, post )
|
||||
|
||||
pretty_texts.append( pretty_text )
|
||||
|
||||
|
||||
separator = os.linesep * 2
|
||||
|
||||
end_pretty_text = separator.join( pretty_texts )
|
||||
|
||||
self._results.setPlainText( end_pretty_text )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
etype = type( e )
|
||||
|
||||
( etype, value, tb ) = sys.exc_info()
|
||||
|
||||
trace = ''.join( traceback.format_exception( etype, value, tb ) )
|
||||
|
||||
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
|
||||
|
||||
self._results.setPlainText( message )
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -2618,7 +2618,9 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
|
|||
|
||||
favs_list = ListBoxTagsStringsAC( self._dropdown_notebook, self.BroadcastChoices, self._display_tag_service_key, self._float_mode, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE, height_num_chars = height_num_chars )
|
||||
|
||||
favs_list.SetChildRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
|
||||
favs_list.SetExtraParentRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
|
||||
favs_list.SetParentDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
|
||||
favs_list.SetSiblingDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
|
||||
|
||||
return favs_list
|
||||
|
||||
|
@ -2629,7 +2631,9 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
|
|||
|
||||
preds_list = ListBoxTagsPredicatesAC( self._dropdown_notebook, self.BroadcastChoices, self._display_tag_service_key, self._float_mode, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE, height_num_chars = height_num_chars )
|
||||
|
||||
preds_list.SetChildRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
|
||||
preds_list.SetExtraParentRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
|
||||
preds_list.SetParentDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
|
||||
preds_list.SetSiblingDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
|
||||
|
||||
return preds_list
|
||||
|
||||
|
|
|
@ -1245,7 +1245,8 @@ class EditServiceRatingsSubPanel( ClientGUICommon.StaticBox ):
|
|||
|
||||
self._shape.addItem( 'circle', ClientRatings.CIRCLE )
|
||||
self._shape.addItem( 'square', ClientRatings.SQUARE )
|
||||
self._shape.addItem( 'star', ClientRatings.STAR )
|
||||
self._shape.addItem( 'fat star', ClientRatings.FAT_STAR )
|
||||
self._shape.addItem( 'pentagram star', ClientRatings.PENTAGRAM_STAR )
|
||||
|
||||
self._colour_ctrls = {}
|
||||
|
||||
|
@ -1287,10 +1288,22 @@ class EditServiceRatingsSubPanel( ClientGUICommon.StaticBox ):
|
|||
QP.AddToLayout( hbox, border_ctrl, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( hbox, fill_ctrl, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
if colour_type == ClientRatings.LIKE: colour_text = 'liked'
|
||||
elif colour_type == ClientRatings.DISLIKE: colour_text = 'disliked'
|
||||
elif colour_type == ClientRatings.NULL: colour_text = 'not rated'
|
||||
elif colour_type == ClientRatings.MIXED: colour_text = 'a mixture of ratings'
|
||||
if colour_type == ClientRatings.LIKE:
|
||||
|
||||
colour_text = 'liked'
|
||||
|
||||
elif colour_type == ClientRatings.DISLIKE:
|
||||
|
||||
colour_text = 'disliked'
|
||||
|
||||
elif colour_type == ClientRatings.NULL:
|
||||
|
||||
colour_text = 'not rated'
|
||||
|
||||
else:
|
||||
|
||||
colour_text = 'a mixture of ratings'
|
||||
|
||||
|
||||
rows.append( ( 'border/fill for ' + colour_text + ': ', hbox ) )
|
||||
|
||||
|
|
|
@ -312,10 +312,10 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ):
|
|||
parsing_context[ 'url' ] = url
|
||||
|
||||
parsing_formula = simple_downloader_formula.GetFormula()
|
||||
|
||||
collapse_newlines = True
|
||||
file_seeds = []
|
||||
|
||||
for parsed_text in parsing_formula.Parse( parsing_context, parsing_text ):
|
||||
for parsed_text in parsing_formula.Parse( parsing_context, parsing_text, collapse_newlines ):
|
||||
|
||||
try:
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ MIXED = 4
|
|||
|
||||
CIRCLE = 0
|
||||
SQUARE = 1
|
||||
STAR = 2
|
||||
FAT_STAR = 2
|
||||
PENTAGRAM_STAR = 3
|
||||
|
||||
def GetLikeStateFromMedia( media, service_key ):
|
||||
|
||||
|
@ -118,7 +119,7 @@ def GetShape( service_key ):
|
|||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
shape = STAR
|
||||
shape = FAT_STAR
|
||||
|
||||
|
||||
return shape
|
||||
|
|
|
@ -57,7 +57,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
|
|||
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type' }
|
||||
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'file_service_key' }
|
||||
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name', 'reason' }
|
||||
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names' }
|
||||
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names', 'doublecheck_file_system' }
|
||||
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
|
||||
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
|
||||
|
||||
|
@ -1911,6 +1911,8 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientA
|
|||
|
||||
url = request.parsed_request_args.GetValue( 'url', str )
|
||||
|
||||
do_file_system_check = request.parsed_request_args.GetValue( 'doublecheck_file_system', bool, default_value = False )
|
||||
|
||||
if url == '':
|
||||
|
||||
raise HydrusExceptions.BadRequestException( 'Given URL was empty!' )
|
||||
|
@ -1931,7 +1933,10 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientA
|
|||
|
||||
for file_import_status in url_statuses:
|
||||
|
||||
file_import_status = ClientImportFiles.CheckFileImportStatus( file_import_status )
|
||||
if do_file_system_check:
|
||||
|
||||
file_import_status = ClientImportFiles.CheckFileImportStatus( file_import_status )
|
||||
|
||||
|
||||
d = {}
|
||||
|
||||
|
|
|
@ -1996,7 +1996,14 @@ def CheckHydrusVersion( service_type, response ):
|
|||
raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
|
||||
|
||||
|
||||
( service_string_gumpf, network_version ) = server_header.split( '/' )
|
||||
# might be "hydrus tag repository/17" or "hydrus tag repository/17 (498)" kind of thing
|
||||
|
||||
( service_string_gumpf, network_version ) = server_header.split( '/', 1 )
|
||||
|
||||
if ' ' in network_version:
|
||||
|
||||
( network_version, software_version_gumpf ) = network_version.split( ' ', 1 )
|
||||
|
||||
|
||||
network_version = int( network_version )
|
||||
|
||||
|
|
|
@ -765,7 +765,9 @@ class NetworkLoginManager( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
formula = ClientParsing.ParseFormulaHTML( tag_rules = [ ClientParsing.ParseRuleHTML( rule_type = ClientParsing.HTML_RULE_TYPE_DESCENDING, tag_name = 'meta', tag_attributes = { 'id' : 'tumblr_form_key' } ) ], content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE, attribute_to_fetch = "content" )
|
||||
|
||||
results = formula.Parse( {}, html )
|
||||
collapse_newlines = True
|
||||
|
||||
results = formula.Parse( {}, html, collapse_newlines )
|
||||
|
||||
if len( results ) != 1:
|
||||
|
||||
|
|
|
@ -80,8 +80,8 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 20
|
||||
SOFTWARE_VERSION = 496
|
||||
CLIENT_API_VERSION = 31
|
||||
SOFTWARE_VERSION = 497
|
||||
CLIENT_API_VERSION = 32
|
||||
|
||||
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
||||
|
|
|
@ -21,10 +21,34 @@ re_leading_space_or_garbage = re.compile( r'^(\s|-|system:)+' )
|
|||
re_leading_single_colon = re.compile( '^:(?!:)' )
|
||||
re_leading_byte_order_mark = re.compile( '^\ufeff' ) # unicode .txt files prepend with this, wew
|
||||
|
||||
HYDRUS_NOTE_NEWLINE = '\n'
|
||||
|
||||
def CleanNoteText( t: str ):
|
||||
|
||||
# trim leading and trailing whitespace
|
||||
|
||||
t = t.strip()
|
||||
|
||||
# wash all newlines to be os.linesep
|
||||
|
||||
lines = t.splitlines()
|
||||
|
||||
# now trim each line
|
||||
|
||||
lines = [ line.strip() for line in lines ]
|
||||
|
||||
t = HYDRUS_NOTE_NEWLINE.join( lines )
|
||||
|
||||
# now replace big gaps with reasonable ones
|
||||
|
||||
double_newline = HYDRUS_NOTE_NEWLINE * 2
|
||||
triple_newline = HYDRUS_NOTE_NEWLINE * 3
|
||||
|
||||
while triple_newline in t:
|
||||
|
||||
t = t.replace( triple_newline, double_newline )
|
||||
|
||||
|
||||
return t
|
||||
|
||||
|
||||
|
|
|
@ -23,7 +23,14 @@ class HydrusService( Site ):
|
|||
|
||||
service_type = self._service.GetServiceType()
|
||||
|
||||
self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION )
|
||||
if service_type == HC.CLIENT_API_SERVICE:
|
||||
|
||||
self._server_version_string = '{}/{} ({})'.format( HC.service_string_lookup[ service_type ], str( HC.CLIENT_API_VERSION ), str( HC.SOFTWARE_VERSION ) )
|
||||
|
||||
else:
|
||||
|
||||
self._server_version_string = '{}/{}'.format( HC.service_string_lookup[ service_type ], str( HC.NETWORK_VERSION ) )
|
||||
|
||||
|
||||
root = self._InitRoot()
|
||||
|
||||
|
@ -63,4 +70,3 @@ class HydrusService( Site ):
|
|||
request.setHeader( 'Hydrus-Server', self._server_version_string )
|
||||
|
||||
return Site.getResourceFor( self, request )
|
||||
|
|
@ -432,10 +432,6 @@ class HydrusResource( Resource ):
|
|||
self._service_key = self._service.GetServiceKey()
|
||||
self._domain = domain
|
||||
|
||||
service_type = self._service.GetServiceType()
|
||||
|
||||
self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION )
|
||||
|
||||
|
||||
def _callbackCheckAccountRestrictions( self, request: HydrusServerRequest.HydrusRequest ):
|
||||
|
||||
|
@ -733,6 +729,11 @@ class HydrusResource( Resource ):
|
|||
|
||||
if client == 'hydrus':
|
||||
|
||||
if ' ' in network_version:
|
||||
|
||||
( network_version, software_version_gumpf ) = network_version.split( ' ', 1 )
|
||||
|
||||
|
||||
request.is_hydrus_user_agent = True
|
||||
|
||||
network_version = int( network_version )
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Loading…
Reference in New Issue