From f7b4d42fb542cb93ace997eda271d15544f45a78 Mon Sep 17 00:00:00 2001 From: Hydrus Network Developer Date: Wed, 2 Oct 2024 14:44:01 -0500 Subject: [PATCH] Version 592 --- docs/changelog.md | 85 +- docs/developer_api.md | 81 + docs/downloader_parsers_formulae.md | 55 +- docs/images/edit_nested_formula_panel.png | Bin 0 -> 97227 bytes ...anel.png => edit_zipper_formula_panel.png} | Bin docs/old_changelog.html | 39 + hydrus/client/ClientController.py | 3 + hydrus/client/ClientDefaults.py | 19 - hydrus/client/ClientFiles.py | 8 + hydrus/client/ClientMigration.py | 14 +- hydrus/client/ClientParsing.py | 171 +- hydrus/client/ClientServices.py | 4 +- hydrus/client/ClientThreading.py | 4 +- hydrus/client/db/ClientDB.py | 60 +- hydrus/client/db/ClientDBFilesStorage.py | 2 +- hydrus/client/db/ClientDBTagParents.py | 5 + hydrus/client/db/ClientDBTagSiblings.py | 5 + .../client/duplicates/ClientAutoDuplicates.py | 6 +- .../client/exporting/ClientExportingFiles.py | 2 +- hydrus/client/gui/ClientGUIAPI.py | 4 +- hydrus/client/gui/ClientGUICharts.py | 4 +- hydrus/client/gui/ClientGUICore.py | 2 +- hydrus/client/gui/ClientGUIDialogsManage.py | 2 +- hydrus/client/gui/ClientGUIDownloaders.py | 2 +- hydrus/client/gui/ClientGUIDragDrop.py | 6 +- hydrus/client/gui/ClientGUIFrames.py | 2 +- hydrus/client/gui/ClientGUIOptionsPanels.py | 2 +- hydrus/client/gui/ClientGUIPanels.py | 2 +- hydrus/client/gui/ClientGUIPopupMessages.py | 2 +- hydrus/client/gui/ClientGUIShortcuts.py | 4 +- hydrus/client/gui/ClientGUISplash.py | 2 +- hydrus/client/gui/ClientGUIStringControls.py | 100 +- hydrus/client/gui/ClientGUISubscriptions.py | 6 +- hydrus/client/gui/ClientGUISystemTray.py | 2 +- hydrus/client/gui/ClientGUITagSuggestions.py | 4 +- hydrus/client/gui/ClientGUITags.py | 11 +- hydrus/client/gui/ClientGUITopLevelWindows.py | 10 +- .../gui/ClientGUITopLevelWindowsPanels.py | 8 +- hydrus/client/gui/QtPorting.py | 30 +- hydrus/client/gui/canvas/ClientGUICanvas.py | 48 +- .../gui/canvas/ClientGUICanvasHoverFrames.py | 8 +- .../client/gui/canvas/ClientGUICanvasMedia.py | 2 +- hydrus/client/gui/canvas/ClientGUIMPV.py | 2 +- .../client/gui/exporting/ClientGUIExport.py | 6 +- .../gui/importing/ClientGUIFileSeedCache.py | 6 +- .../gui/importing/ClientGUIGallerySeedLog.py | 6 +- .../client/gui/importing/ClientGUIImport.py | 12 +- .../gui/importing/ClientGUIImportFolders.py | 4 +- .../gui/importing/ClientGUIImportOptions.py | 14 +- hydrus/client/gui/lists/ClientGUIListBook.py | 2 +- hydrus/client/gui/lists/ClientGUIListBoxes.py | 28 +- .../gui/media/ClientGUIMediaControls.py | 6 +- .../metadata/ClientGUIMetadataMigration.py | 8 +- .../ClientGUIMetadataMigrationCommon.py | 4 +- .../ClientGUIMetadataMigrationExporters.py | 4 +- .../ClientGUIMetadataMigrationImporters.py | 6 +- .../ClientGUIMetadataMigrationTest.py | 4 +- .../gui/metadata/ClientGUIMigrateTags.py | 135 +- hydrus/client/gui/metadata/ClientGUITime.py | 6 +- .../gui/networking/ClientGUIHydrusNetwork.py | 8 +- .../client/gui/networking/ClientGUILogin.py | 14 +- .../client/gui/networking/ClientGUINetwork.py | 22 +- .../networking/ClientGUINetworkJobControl.py | 2 +- .../gui/pages/ClientGUIManagementPanels.py | 102 +- .../gui/pages/ClientGUIMediaResultsPanel.py | 2585 ++++++++ .../ClientGUIMediaResultsPanelLoading.py | 53 + .../pages/ClientGUIMediaResultsPanelMenus.py | 254 + ... ClientGUIMediaResultsPanelSortCollect.py} | 2 +- .../ClientGUIMediaResultsPanelThumbnails.py | 2520 ++++++++ .../gui/pages/ClientGUINewPageChooser.py | 2 +- hydrus/client/gui/pages/ClientGUIPages.py | 31 +- hydrus/client/gui/pages/ClientGUIResults.py | 5367 ----------------- hydrus/client/gui/pages/ClientGUISession.py | 6 +- .../gui/pages/ClientGUISessionLegacy.py | 2 +- .../gui/panels/ClientGUIManageOptionsPanel.py | 10 +- .../panels/ClientGUIRepairFileSystemPanel.py | 2 +- .../gui/panels/ClientGUIScrolledPanels.py | 2 +- .../ClientGUIScrolledPanelsButtonQuestions.py | 4 +- .../ClientGUIScrolledPanelsCommitFiltering.py | 6 +- .../gui/panels/ClientGUIScrolledPanelsEdit.py | 14 +- .../panels/ClientGUIScrolledPanelsReview.py | 22 +- .../ClientGUIScrolledPanelsSelectFromList.py | 6 +- hydrus/client/gui/parsing/ClientGUIParsing.py | 10 +- .../gui/parsing/ClientGUIParsingFormulae.py | 236 +- .../gui/parsing/ClientGUIParsingLegacy.py | 6 +- .../gui/parsing/ClientGUIParsingTest.py | 5 +- .../client/gui/search/ClientGUIACDropdown.py | 71 +- hydrus/client/gui/search/ClientGUILocation.py | 4 +- .../gui/search/ClientGUIPredicatesMultiple.py | 2 +- .../gui/search/ClientGUIPredicatesSingle.py | 70 +- hydrus/client/gui/search/ClientGUISearch.py | 6 +- .../gui/search/ClientGUISearchPanels.py | 10 +- .../services/ClientGUIClientsideServices.py | 46 +- .../ClientGUIModalClientsideServiceActions.py | 2 +- .../services/ClientGUIServersideServices.py | 10 +- .../widgets/ClientGUIApplicationCommand.py | 2 +- .../client/gui/widgets/ClientGUIBandwidth.py | 4 +- .../gui/widgets/ClientGUIColourPicker.py | 2 +- hydrus/client/gui/widgets/ClientGUICommon.py | 34 +- hydrus/client/gui/widgets/ClientGUIRegex.py | 2 +- hydrus/client/importing/ClientImportLocal.py | 2 +- .../ClientImportSubscriptionLegacy.py | 2 +- .../ClientImportSubscriptionQuery.py | 2 +- .../importing/ClientImportSubscriptions.py | 2 +- hydrus/client/media/ClientMedia.py | 7 +- .../ClientMetadataMigrationImporters.py | 2 + .../networking/ClientNetworkingBandwidth.py | 2 +- .../client/networking/ClientNetworkingGUG.py | 4 +- .../client/networking/ClientNetworkingJobs.py | 10 +- .../networking/ClientNetworkingLogin.py | 8 +- .../networking/ClientNetworkingSessions.py | 2 +- .../networking/ClientNetworkingURLClass.py | 2 +- .../networking/api/ClientLocalServer.py | 3 +- .../api/ClientLocalServerResourcesGetFiles.py | 40 +- .../client/search/ClientSearchAutocomplete.py | 4 +- hydrus/core/HydrusConstants.py | 7 +- hydrus/core/HydrusDB.py | 17 +- hydrus/core/HydrusDBBase.py | 2 +- hydrus/core/HydrusDBModule.py | 2 +- hydrus/core/HydrusSerialisable.py | 5 +- hydrus/core/HydrusText.py | 28 + hydrus/core/HydrusThreading.py | 14 +- hydrus/core/networking/HydrusServer.py | 3 +- hydrus/core/networking/HydrusServerAMP.py | 2 +- .../core/networking/HydrusServerResources.py | 2 +- hydrus/server/ServerController.py | 2 +- hydrus/server/ServerDB.py | 2 +- hydrus/test/TestClientAPI.py | 25 + hydrus/test/TestClientMetadataMigration.py | 2 - hydrus/test/TestClientMigration.py | 72 +- hydrus/test/TestClientParsing.py | 76 +- hydrus/test/TestController.py | 2 +- hydrus/test/TestHydrusTags.py | 6 +- requirements.txt | 2 +- setup_venv.bat | 64 +- setup_venv.command | 45 +- setup_venv.sh | 45 +- static/build_files/linux/requirements.txt | 2 +- static/build_files/macos/requirements.txt | 2 +- static/build_files/windows/requirements.txt | 2 +- .../derpibooru.org file page parser.png | Bin 3007 -> 3117 bytes .../advanced/requirements_core.txt | 5 +- .../advanced/requirements_opencv_old.txt | 2 +- .../advanced/requirements_other_future.txt | 2 + .../advanced/requirements_other_normal.txt | 2 + .../advanced/requirements_pillow_new.txt | 2 - .../advanced/requirements_pillow_old.txt | 2 - .../advanced/requirements_server.txt | 2 +- 148 files changed, 7113 insertions(+), 6064 deletions(-) create mode 100644 docs/images/edit_nested_formula_panel.png rename docs/images/{edit_compound_formula_panel.png => edit_zipper_formula_panel.png} (100%) create mode 100644 hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py create mode 100644 hydrus/client/gui/pages/ClientGUIMediaResultsPanelLoading.py create mode 100644 hydrus/client/gui/pages/ClientGUIMediaResultsPanelMenus.py rename hydrus/client/gui/pages/{ClientGUIResultsSortCollect.py => ClientGUIMediaResultsPanelSortCollect.py} (99%) create mode 100644 hydrus/client/gui/pages/ClientGUIMediaResultsPanelThumbnails.py delete mode 100644 hydrus/client/gui/pages/ClientGUIResults.py create mode 100644 static/requirements/advanced/requirements_other_future.txt create mode 100644 static/requirements/advanced/requirements_other_normal.txt delete mode 100644 static/requirements/advanced/requirements_pillow_new.txt delete mode 100644 static/requirements/advanced/requirements_pillow_old.txt diff --git a/docs/changelog.md b/docs/changelog.md index a89f834e9..d0a3514ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,54 @@ title: Changelog !!! note This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html). +## [Version 592](https://github.com/hydrusnetwork/hydrus/releases/tag/v592) + +### misc + +* the 'read' autocomplete dropdown has a new one-click 'clear search' button, just beside the favourites 'star' menu button. the 'empty page' favourite is removed from new users' defaults +* in an alteration to the recent Autocomplete key processing, Ctrl+c/Ctrl+Insert _will_ now propagate to the results list if you currently have none of the text input selected (i.e. if it would have been a no-op on the text input, we assume you wanted whatever is selected in the list) +* in the normal thumbnail/viewer menu and _review services_, the 'files' entry is renamed to 'locations'. this continues work in the left hand button of the autocomplete dropdown where you set the 'location', which can be all sorts of complicated things these days, rather than just 'file service key selector'. I don't think I'll rename 'my files' or anything, but I will try to emphasise this 'locations' idea more when I am talking about local file domains etc.. in other places going forward; what I often think of as 'oh yeah the files bit' isn't actually referring to the files themselves, but where they are located, so let's be precise +* last week's tag pair filtering in _tags->migrate tags_ now has 'if either the left or right of the pair have count', and when you hit 'Go' with any of the new count filter checkboxes hit, the preview summary on the yes/no confirmation dialog talks about it +* any time a watcher subject is parsed, if the text contains non-decoded html entities (like `>`), they are now auto-converted to normal chars. these strings are often ripped from odd places and are only used for user display, so this just makes that simpler +* if you are set to remove trashed files from view, this now works when the files are in multpile local file domains, and you choose 'delete from all local file services', and you are looking at 'all my files' or a subset of your local file domains +* we now log any time (when the client is non-idle) that a database job's work inside the transaction wrapper takes more than 15 seconds to complete +* fixed an issue caused by the sibling or parents system doing some regen work at an unlucky time + +### default downloaders + +* thanks to user help, the derpibooru post parser now additionally grabs the raw markdown of a description as a second note. this catches links and images better than the html string parse. if you strictly only want one of these notes, please feel free to dive into _network->downloaders->defailt import options_ for your derpi downloader and try to navigate the 'note import options' hell I designed and let me know how it could be more user friendly + +### parsing system + +* added a new NESTED formula type. this guy holds two formulae of any type internally, parsing the document with the first and passing those results on to the second. it is designed to solve the problem of 'how do I parse this JSON tucked inside HTML' and _vice versa_. various encoding stuff all seems to be handled, no extra work needed +* added Nested formula stuff to the 'how to make a downloader' help +* made all the screenshot in the parsing formula help clickable +* renamed the COMPOUND formula to ZIPPER formula +* all the 'String Processor' buttons across the program now have copy and paste buttons, so it is now easy to duplicate some rules you set up +* in the parsing system, sidecar importer, and clipboard watcher, all strings are now cleansed of errant 'surrogate' characters caused by the source incorrectly providing utf-16 garbage in a utf-8 stream. fingers crossed, the cleansing here will actually _fix_ problem characters by converting them to utf-8, but we'll see +* thanks to a user, the JSON parsing system has a new 'de-minify json' parsing rule, which decompresses a particular sort of minified JSON that expresses multiply-referenced values using list positions. as it happened that I added NESTED formulae this week, I wonder if we will migrate this capability to the string processing system, but let's give it time to breathe + +### client api + +* fixed the permission check on the new 'get file/thumbnail local path' commands--due to me copy/pasting stupidly, they were still just checking 'search files' perm +* added `/get_files/local_file_storage_locations`, which spits out the stuff in _database->move media files_ and lets you do local file access _en masse_ +* added help and a unit test for this new command +* the client api version is now 72 + +### some security/library updates + +* the 'old' OpenCV version in the `(a)dvanced` setup, which pointed to version 4.5.3.56, which had the webp vulnerability, is no longer an option. I believe this means that the program will no longer run on python 3.7. I understad Win 7 can run python 3.8 at the latest, so we are nearing the end of the line on that front +* the old/new Pillow choice in `(a)dvanced` setup, which offered support for python 3.7, is removed +* I have added a new question to the `(a)dvanced` venv setup to handle misc 'future' tests better, and I added a new future test for two security patches for `setuptools` and `requests`: +* A) `setuptools` is updated to 70.3.0 (from 69.1.1) to resolve a security issue related to downloading packages from bad places (don't think this would ever affect us, but we'll be good) +* B) `requests` is updated to 2.32.3 (from 2.31.0) to resolve a security issue with verify=False (the specific problem doesn't matter for us, but we'll be good) +* if you run from source and want to help me test, you might like to rebuild your venv this week and choose the new future choice. these version increments do not appear to be a big deal, so assuming no problems I will roll these new libraries into a 'future' test build next week, and then into the normal builds a week after + +### boring code cleanup + +* did a bunch more `super()` refactoring. I think all `__init__` is now converted across the program, and I cleared all the normal calls in the canvas and media results panel code too +* refactored `ClientGUIResults` into four files for the core class, the loading, the thumbnails, and some menu gubbins. also unified the mish-mash of `Results` and `MediaPanel` nomenclature to `MediaResultsPanel` + ## [Version 591](https://github.com/hydrusnetwork/hydrus/releases/tag/v591) ### misc @@ -337,40 +385,3 @@ title: Changelog * the `psutil` library is now technically optional. it is still needed for a bunch of normal operations like 'is the client currently running?' and 'how much free space is there on this drive?', but if it is missing the client will now boot and try to muddle through anyway * did more multi-column list 'select, sort, and scroll' tech for: the url class parameters list; the url class links list; the external launch paths list; the tag suggestion related tags namespace lists; import folder filename tagging options; manage custom network context headers; the string to string match widget list; the string match to string match widget list; the edit subscription queries list; and more of the edit subscriptions subscription list, including the merge and separate actions * updated the QuickSync help a little, clarifying it is only useful for _new, empty_ clients - -## [Version 582](https://github.com/hydrusnetwork/hydrus/releases/tag/v582) - -### fixes - -* fixed an issue where setting a file 'collect' was not automatically sorting the collected objects internally properly. normally when you collect, each collected object is supposed to be sorted internally by filesize or namespace or whatever--this is working again -* fixed a weird internal error state in the import folders manager where it could get confused about and throw an error regarding the import folders' next work times if an internal update notification occured during an import folder working -* fixed a typo error with the shortcut set 'special duplicate' button -* fixed pasting new query texts into the manage subscriptions dialog when one of the pasted texts resurrects a DEAD query. my new summary generation text was handling the DEAD report wrong! - -### misc - -* the advanced 'all deleted files' service, which is mostly just used for behind the scenes caching calculations, is renamed to 'deleted from anywhere'. the related 'regen->all deleted files' database command is also moved to 'check and repair->sync combined deleted files' -* the edit tag filter panel's 'load' button now shows all the current tag repositories' tag filters -* when you hit ctrl-enter on some tags (or otherwise trigger a linked remove+add action) in an active search list (e.g. top-left on a search page), which causes those tags to invert and thus sometimes sorted to a different position, the current selection now propagates through the inversion, with the keyboard focus moved to the post-topmost item. so, you can now basically hit ctrl+enter twice for a no-op -* fixed the paste button in the new 'purge tags' dialog -* thanks to a user, we have a new 'Purple' stylesheet -* I tweaked the some default stylesheet colours and think I fixed the display of the 'valid/invalid' controls you sometimes see (for instance in the new regex input, which goes green/red) for dark mode stylesheets that don't define colours for these. previously, the dark mode text, usually a light grey, was being washed out by the default green - -### custom colours in QSS - -* you can now set the _options->colours_ colours in a QSS stylesheet! if you are a stylesheet maker, check the default_hydrus.qss file to see how it works--it is the same deal as the animation scanbar previously -* the options in _options->colours_ remain, but they are now wrapped in a 'overwrite your stylesheet with these colours' checkbox _for now_. existing users are going to be set to 'yes overwrite', so nothing will suddenly change, but new users are going to default to using whatever the current QSS says. in future, I may collapse the light/darkmode distinction into one option set; I may morph it into a "colour highly rated files' thumbnail borders gold" dynamic options system; I may simply delete the whole thing and replace it with in-client QSS editing or something. not sure, so let's see how it goes and how Qt 7's darkmode stuff turns out. -* I have pasted the hydrus default darkmode colours into all the other stylesheets that come with the program, so new users selecting a darkmode style are going to get something reasonable out of the box rather than the previous ugly clash. the users who made the original stylesheets are welcome to figure out better colours and send them in -* if you are not set to override with the custom colours in _options->colours_, then hitting _help->darkmode_ now gives you a popup telling you what is going on - -### sidecar UI - -* the four 'edit sidecars' panels (under manual imports, import folders, manual exports, and export folders), which use paths and media as sources respectively, now have test panels to review the current sidecar route you have set up. they use up to 25 rows of example file paths/media from the actual thing you are working on. it provides a live update of what the sources you set up will load, so you know you have the json parse or .txt separator set up correct -* for the export folders case, there is a button in the panel 'edit export folders' panel to populate the text context, under your control, since this involves a potentially slow file search -* there is more to do here. I would like better test panels in the sub-dialogs and I'd like to collapse the related 'eight-nested-dialogs-deep' problem, and in the string processing and parsing UI more generally, but I'm happy with this step forward. let me know where it goes wrong! - -### advanced autocomplete logic fixes - -* when you enter a wildcard into a Read tag autocomplete, it no longer always delivers the 'always autocompleting' version. so, if you enter `sa*s`, it will suggest `sa*s (wildcard search)` and perhaps `sa*s (any namespace)`, but it will no longer suggest the `sa*s*` variants until you, obviously, actually type that trailing asterisk yourself. I intermittently had no idea what the hell I was doing when I originally developed this stuff -* the 'unnamespaced input gives `(any namespace)` wildcard results' tag display option is now correctly negatively enforced when entering unnamespaced wildcards. previously it was always adding them, and sometimes inserting them at the top of the list. the `(any namespace)` variant is now always below the unnamespaced when both are present -* fixed up a bunch of jank unit tests that were testing this badly diff --git a/docs/developer_api.md b/docs/developer_api.md index 9cb80f655..e80e6d094 100644 --- a/docs/developer_api.md +++ b/docs/developer_api.md @@ -2247,6 +2247,87 @@ All thumbnails in hydrus have the .thumbnail file extension and in content are e This will 400 if the given file type does not have a thumbnail in hydrus, and it will 404 if there should be a thumbnail but one does not exist and cannot be generated from the source file (which probably would mean that the source file was itself Not Found). +### **GET `/get_files/local_file_storage_locations`** { id="get_local_file_storage_locations" } + +_Get the local file storage locations, as you see under **database->migrate files**._ + +Restricted access: +: YES. Search for Files permission and See Local Paths permission needed. + +Required Headers: n/a + +Arguments: n/a + +Response: +: A list of the different file storage locations and what they store. + +``` json title="Example response" +{ + "locations" : [ + { + "path" : "C:\my_thumbs", + "ideal_weight" : 1, + "max_num_bytes": None, + "prefixes" : [ + "t00", "t01", "t02", "t03", "t04", "t05", "t06", "t07", "t08", "t09", "t0a", "t0b", "t0c", "t0d", "t0e", "t0f", + "t10", "t11", "t12", "t13", "t14", "t15", "t16", "t17", "t18", "t19", "t1a", "t1b", "t1c", "t1d", "t1e", "t1f", + "t20", "t21", "t22", "t23", "t24", "t25", "t26", "t27", "t28", "t29", "t2a", "t2b", "t2c", "t2d", "t2e", "t2f", + "t30", "t31", "t32", "t33", "t34", "t35", "t36", "t37", "t38", "t39", "t3a", "t3b", "t3c", "t3d", "t3e", "t3f", + "t40", "t41", "t42", "t43", "t44", "t45", "t46", "t47", "t48", "t49", "t4a", "t4b", "t4c", "t4d", "t4e", "t4f", + "t50", "t51", "t52", "t53", "t54", "t55", "t56", "t57", "t58", "t59", "t5a", "t5b", "t5c", "t5d", "t5e", "t5f", + "t60", "t61", "t62", "t63", "t64", "t65", "t66", "t67", "t68", "t69", "t6a", "t6b", "t6c", "t6d", "t6e", "t6f", + "t70", "t71", "t72", "t73", "t74", "t75", "t76", "t77", "t78", "t79", "t7a", "t7b", "t7c", "t7d", "t7e", "t7f", + "t80", "t81", "t82", "t83", "t84", "t85", "t86", "t87", "t88", "t89", "t8a", "t8b", "t8c", "t8d", "t8e", "t8f", + "t90", "t91", "t92", "t93", "t94", "t95", "t96", "t97", "t98", "t99", "t9a", "t9b", "t9c", "t9d", "t9e", "t9f", + "ta0", "ta1", "ta2", "ta3", "ta4", "ta5", "ta6", "ta7", "ta8", "ta9", "taa", "tab", "tac", "tad", "tae", "taf", + "tb0", "tb1", "tb2", "tb3", "tb4", "tb5", "tb6", "tb7", "tb8", "tb9", "tba", "tbb", "tbc", "tbd", "tbe", "tbf", + "tc0", "tc1", "tc2", "tc3", "tc4", "tc5", "tc6", "tc7", "tc8", "tc9", "tca", "tcb", "tcc", "tcd", "tce", "tcf", + "td0", "td1", "td2", "td3", "td4", "td5", "td6", "td7", "td8", "td9", "tda", "tdb", "tdc", "tdd", "tde", "tdf", + "te0", "te1", "te2", "te3", "te4", "te5", "te6", "te7", "te8", "te9", "tea", "teb", "tec", "ted", "tee", "tef", + "tf0", "tf1", "tf2", "tf3", "tf4", "tf5", "tf6", "tf7", "tf8", "tf9", "tfa", "tfb", "tfc", "tfd", "tfe", "tff" + ] + }, + { + "path" : "D:\hydrus_files_1", + "ideal_weight" : 5, + "max_num_bytes": None, + "prefixes" : [ + "f00", "f02", "f04", "f05", "f08", "f0c", "f11", "f12", "f13", "f15", "f17", "f18", "f1a", "f1b", "f20", "f23", + "f25", "f26", "f27", "f2b", "f2e", "f2f", "f31", "f35", "f36", "f37", "f38", "f3a", "f40", "f42", "f43", "f44", + "f49", "f4b", "f4d", "f4e", "f50", "f51", "f55", "f59", "f60", "f63", "f64", "f65", "f66", "f68", "f69", "f6e", + "f71", "f73", "f78", "f79", "f7a", "f7d", "f7f", "f82", "f83", "f84", "f86", "f87", "f88", "f89", "f8f", "f90", + "f91", "f96", "f9e", "fa1", "fa4", "fa5", "fa7", "faa", "fad", "faf", "fb1", "fb9", "fba", "fbb", "fbf", "fc1", + "fc4", "fc7", "fc8", "fcf", "fd2", "fd6", "fd7", "fd8", "fd9", "fdf", "fe2", "fe8", "fe9", "fea", "feb", "fec", + "ff4", "ff7", "ffd", "ffe" + ] + }, + { + "path" : "E:\hydrus\hydrus_files_2", + "ideal_weight" : 2, + "max_num_bytes": 805306368000, + "prefixes" : [ + "f01", "f03", "f06", "f07", "f09", "f0a", "f0b", "f0d", "f0e", "f0f", "f10", "f14", "f16", "f19", "f1c", "f1d", + "f1e", "f1f", "f21", "f22", "f24", "f28", "f29", "f2a", "f2c", "f2d", "f30", "f32", "f33", "f34", "f39", "f3b", + "f3c", "f3d", "f3e", "f3f", "f41", "f45", "f46", "f47", "f48", "f4a", "f4c", "f4f", "f52", "f53", "f54", "f56", + "f57", "f58", "f5a", "f5b", "f5c", "f5d", "f5e", "f5f", "f61", "f62", "f67", "f6a", "f6b", "f6c", "f6d", "f6f", + "f70", "f72", "f74", "f75", "f76", "f77", "f7b", "f7c", "f7e", "f80", "f81", "f85", "f8a", "f8b", "f8c", "f8d", + "f8e", "f92", "f93", "f94", "f95", "f97", "f98", "f99", "f9a", "f9b", "f9c", "f9d", "f9f", "fa0", "fa2", "fa3", + "fa6", "fa8", "fa9", "fab", "fac", "fae", "fb0", "fb2", "fb3", "fb4", "fb5", "fb6", "fb7", "fb8", "fbc", "fbd", + "fbe", "fc0", "fc2", "fc3", "fc5", "fc6", "fc9", "fca", "fcb", "fcc", "fcd", "fce", "fd0", "fd1", "fd3", "fd4", + "fd5", "fda", "fdb", "fdc", "fdd", "fde", "fe0", "fe1", "fe3", "fe4", "fe5", "fe6", "fe7", "fed", "fee", "fef", + "ff0", "ff1", "ff2", "ff3", "ff5", "ff6", "ff8", "ff9", "ffa", "ffb", "ffc", "fff" + ] + } + ] +} +``` + +Note that `ideal_weight` and `max_num_bytes` are provided for courtesy and mean nothing fixed. Each storage location might store anything, thumbnails or files or nothing, regardless of the ideal situation. Whenever a folder is non-ideal, the 'move media files' dialog shows "files need to be moved now", but it will still keep doing its thing. + +For now, a prefix only occurs in one location, so there will always be 512 total prefixes in this response, all unique. **However, please note that this will not always be true!** In a future expansion, the client will be, on user command, slowly migrating files from one place to another in the background, and during that time there will be multiple valid locations for a file to actually be. When this happens, you will have to hit all the possible locations and test. + +Also, it won't be long before the client supports moving to _some_ form of three- and four-character prefix. I am still thinking how this will happen other than it will be an atomic change--no slow migration where we try to support both at once--but it will certainly complicate something in here (e.g. while the prefix may be 'f012', maybe the subfolder will be '\f01\2'), so we'll see. + ### **GET `/get_files/render`** { id="get_files_render" } _Get an image file as rendered by Hydrus._ diff --git a/docs/downloader_parsers_formulae.md b/docs/downloader_parsers_formulae.md index eb8cf9eb6..3200283c4 100644 --- a/docs/downloader_parsers_formulae.md +++ b/docs/downloader_parsers_formulae.md @@ -6,9 +6,9 @@ title: Parser Formulae Formulae are tools used by higher-level components of the parsing system. They take some data (typically some HTML or JSON) and return 0 to n strings. For our purposes, these strings will usually be tags, URLs, and timestamps. You will usually see them summarised with this panel: -![](images/edit_formula_panel.png) +[![](images/edit_formula_panel.png)](images/edit_formula_panel.png) -The different types are currently [html](#html_formula), [json](#json_formula), [compound](#compound_formula), and [context variable](#context_variable_formula). +The different types are currently [html](#html_formula), [json](#json_formula), [nested](#nested_formula), [zipper](#zipper_formula), and [context variable](#context_variable_formula). ## html { id="html_formula" } @@ -50,7 +50,7 @@ You might be tempted to just go straight for any `#!html ` with `class="ar Clicking 'edit formula' on an HTML formula gives you this: -![](images/edit_html_formula_panel.png) +[![](images/edit_html_formula_panel.png)](images/edit_html_formula_panel.png) You edit on the left and test on the right. @@ -58,7 +58,7 @@ You edit on the left and test on the right. When you add or edit one of the specific tag search rules, you get this: -![](images/edit_html_tag_rule_panel.png) +[![](images/edit_html_tag_rule_panel.png)](images/edit_html_tag_rule_panel.png) You can set multiple key/value attribute search conditions, but you'll typically be searching for 'class' or 'id' here, if anything. @@ -86,7 +86,7 @@ Most of the time, you'll be searching descendants (i.e. walking down the tree), There isn't a great way to find the `#!html ` or the `#!html ` when looking from above here, as they are lacking a class or id, but you can find the `#!html ` ok, so if you find those and then add a rule where instead of searching descendants, you are 'walking back up ancestors' like this: -![](images/edit_html_formula_panel_descendants_ancestors.png) +[![](images/edit_html_formula_panel_descendants_ancestors.png)](images/edit_html_formula_panel_descendants_ancestors.png) You can solve some tricky problems this way! @@ -116,14 +116,14 @@ The testing panel on the right is important and worth using. Copy the html from This takes some JSON and does a similar style of search: -![](images/edit_json_formula_panel.png) +[![](images/edit_json_formula_panel.png)](images/edit_json_formula_panel.png) It is a bit simpler than HTML--if the current node is a list (called an 'Array' in JSON), you can fetch every item or the xth item, and if it is a dictionary (called an 'Object' in JSON), you can fetch a particular entry by name. Since you can't jump down several layers with attribute lookups or tag names like with HTML, you have to go down every layer one at a time. In any case, if you have something like this: [![](images/json_thread_example.png)](images/json_thread_example.png) !!! note - It is a great idea to check the html or json you are trying to parse with your browser. Some web browsers have excellent developer tools that let you walk through the nodes of the document you are trying to parse in a prettier way than I would ever have time to put together. This image is one of the views Firefox provides if you simply enter a JSON URL. + It is a great idea to check the html or json you are trying to parse with your browser. Most web browsers have excellent developer tools that let you walk through the nodes of the document you are trying to parse in a prettier way than I would ever have time to put together. This image is one of the views Firefox provides if you simply enter a JSON URL. Searching for "posts"->1st list item->"sub" on this data will give you "Nobody like kino here.". @@ -135,15 +135,42 @@ The default is to fetch the final nodes' 'data content', which means coercing si But if you like, you can return the json beneath the current node (which, like HTML, includes the current node). This again will come in useful later. -## compound { id="compound_formula" } +## nested { id="nested_formula" } -If you want to create a string from multiple parsed strings--for instance by appending the 'tim' and the 'ext' in our json example together--you can use a Compound formula. This fetches multiple lists of strings and tries to place them into a single string using `\1` regex substitution syntax: +If you want to parse some JSON that is tucked inside an HTML attribute, or _vice versa_, use a nested formula. This parses the text using one formula type and then passes the result(s) to another. -![](images/edit_compound_formula_panel.png) +[![](images/edit_nested_formula_panel.png)](images/edit_nested_formula_panel.png) + +The especially neat thing about this is the encoded characters like `>` or escaped JSON characters are all handled natively for you. Before we had this, we had to hack our way around with crazy regex. + +## zipper { id="zipper_formula" } + +If you want to combine strings from the results of different parsers--for instance by joining the 'tim' and the 'ext' in our json example--you can use a Zipper formula. This fetches multiple lists of strings and zips their result rows together using `\1` regex substitution syntax: + +[![](images/edit_zipper_formula_panel.png)](images/edit_zipper_formula_panel.png) This is a complicated example taken from one of my thread parsers. I have to take a modified version of the original thread URL (the first rule, so `\1`) and then append the filename (`\2`) and its extension (`\3`) on the end to get the final file URL of a post. You can mix in more characters in the substitution phrase, like `\1.jpg` or even have multiple instances (`https://\2.muhsite.com/\2/\1`), if that is appropriate. -This is where the magic happens, sometimes, so keep it in mind if you need to do something cleverer than the data you have seems to provide. +If your sub-formulae produce multiple results, the Zipper will produce that many also, iterating the sub-lists together. + +```title="Example" +If parser 1 gives: + a + b + c + +And parser 2 gives: + 1 + 2 + 3 + +Using a substitution phrase of "\1-\2" will give: + a-1 + b-2 + c-3 +``` + +If one of the sub-formulae produces fewer results than the others, its final value will be used to fill in the gaps. In this way, you might somewhere parse one prefix and seven suffixes, where joining them will use the same prefix seven times. ## context variable { id="context_variable_formula" } @@ -151,10 +178,10 @@ This is a basic hacky answer to a particular problem. It is a simple key:value d If a different URL Class links to this parser via an API URL, this 'url' variable will always be the API URL (i.e. it literally is the URL used to fetch the data), not any thread/whatever URL the user entered. -![](images/edit_context_variable_formula_panel.png) +[![](images/edit_context_variable_formula_panel.png)](images/edit_context_variable_formula_panel.png) Hit the 'edit example parsing context' to change the URL used for testing. -I have used this several times to stitch together file URLs when I am pulling data from APIs, like in the compound formula example above. In this case, the starting URL is `https://a.4cdn.org/tg/thread/57806016.json`, from which I extract the board name, "tg", using the string converter, and then add in 4chan's CDN domain to make the appropriate base file URL (`https:/i.4cdn.org/tg/`) for the given thread. I only have to jump through this hoop in 4chan's case because they explicitly store file URLs by board name. 8chan on the other hand, for instance, has a static `https://media.8ch.net/file_store/` for all files, so it is a little easier (I think I just do a single 'prepend' string transformation somewhere). +I have used this several times to stitch together file URLs when I am pulling data from APIs, like in the zipper formula example above. In this case, the starting URL is `https://a.4cdn.org/tg/thread/57806016.json`, from which I extract the board name, "tg", using the string converter, and then add in 4chan's CDN domain to make the appropriate base file URL (`https:/i.4cdn.org/tg/`) for the given thread. I only have to jump through this hoop in 4chan's case because they explicitly store file URLs by board name. 8chan on the other hand, for instance, has a static `https://media.8ch.net/file_store/` for all files, so it is a little easier (I think I just do a single 'prepend' string transformation somewhere). -If you want to make some parsers, you will have to get familiar with how different sites store and present their data! \ No newline at end of file +If you want to make some parsers, you will have to get familiar with how different sites store and present their data! diff --git a/docs/images/edit_nested_formula_panel.png b/docs/images/edit_nested_formula_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..3d3aa6c068672b46f4b0774600ef199a18dd16b2 GIT binary patch literal 97227 zcmb@u2UJr{+cp|eK}Cp)3QDtrNK@%GC@Lrd3er1>^d>z4f}o%vpwgu(y>~(j5kU}; z4haydlmH?0BtQuHckp@M^`7&8=lpA(b@o~$>@u@w?wPsEbzhVBPc>9nPFy?z0)bc_ zsovKDfsVmIpu>lcF#=blQ^J{nuS4!yD)&Ie-CT>n$q}2o>UTk)vMA;~i=)8#aaUCX zcMyoRnf`aE%_a8*2t>ORgds#c}zAYGC`(!?p=; zSVkogj1sqRGK_|$E1B&R^RzwdG*a^2@dLf_7%_poUELuvrO0JSCPxE<^Fv+#Y?yr9 z6sPf=ig+xD6X2eJD4+wa4}2q zp76aXDCmx;pulK6sE;se@ekT^72L_**%{p)%|F!Jn+~BaYK|gY(kL<}dU`n+Gq_18 zD|f#NuYYE1>qBn{AWh&%7rQ*15iW}_tQagYgG6xLz9%*A5nWt6f6>Hm{iiHtxdJJ? z(X6ZnGBNqO>Yk&K(l58#kU_vA@Hr4_UchG30>b>k} z`&1}}fFL$8@z!96tN%7o$tLFfa$kjwrin-2184B5eO}hS+F@RALc@sY{xSyM9Ktfd zueg4koIEN@lVxUj0`g!aef$L_`)qbe5jQ$T9ePp~pyYAy<0KI1>|3~t%j7^IPLX-7 z^pbOu(6JUMt#sAt{PK8)N>q$_)XJHyX}C)%oUn<3Zc2MCCrrRr6{Ln|iVzd8XjMFl zk-U%<=Yhe&*_FefpXN{p|Is=HE(s_>uToP6r*TN{7S6D7CURM<-GMRD7Yh+}`BN-tXe?^@bREt(DYiDon% zdjCK76DE*G=)aHqxSABog8Du=QzW+yponV6*fQ~7#KTx_iM$*o)G7y1w` zlfM>$m9aY&48DRHd2ajhpZE0|j7(y?W${8T$LS3S{Bx7IFFags-C60esP*^S--ZE# z%Ij+4`Fo0}rGWVx8_Tx z&qP5Y8zLwC$tEVALpl?m#a*YAxMe+lrJz>o@MEBl?mY?^lLPUBQk12qf|bDZ?kTAZ z&h)E^Qh7;FWuvXyBD3U|_vtIapHz|NnFFUF;M!_x;qPgw3KcDf& zMk(&9vKza;mgD^iy{BLARkXiKqoP)}YJ2>mRk0+1RhvU9}_j*cGWDju$Cm&uiEl-QM3R z6&OYqCf`~Y7dbn}w_ONfMzvD!86%}1%lmnOp zZzDM`xu^hzyN0s7!!#^0W77En;=`^zjvo=pGt|wWR^kw*P58xXKq(_ z8Q7%E#T`DFWU`^A?8fPh)ZN;V)*cOK}qANIwJvwin^^~93(-4i>dAO@6iXHO95#2jXz zB>p(*riCM?94oV5DToQRd{rgV$LQMdZG-W~jo1<&fxg0&y-f@4!H)vxjjeFKXrt6P zL=Sq0Vu*wT=BS3qoT|rMT{?4Yv*!~Eo_*QZ(e@nk*ct+`StUvH;r4QJUiBJ6T_ML# zY{C{x+lzVYBcG%x`fGJt8FdRBbo}o}?adp*8Rg~u1gB=`*4oJ41QK=?X8mrnmD8mL zJWtSLm2|>ZcM-~(gOolTwdG(Ix{=BL;TE&v+F_aBHj0}U5b|cOs;pM8nRKhw&!05M zCTRw2M6dMn?37lF;7?Cs2&FlDHrFgy$U5cqB`;H|`N;x!sou%7l&dI0 zMVX&4FG>ia+QvTQwNt9Z27%No7NtJ%Ai_Y3|C*k4m41$g~K2MciM#9LX`4cv;S#BG=K zkeHBJ0bH?-i+}j=d-X&pgPmZfoR54!Al>@|ffka3Uh_zLgsUI%J2;p95(FjHY;*`# zS$Ce-(bK~ZmsvTla;4GsaKz0tntVwp5v?4eumEOa)#pLOf+nxNDI&v)j#Zem2twFN zmnK3a){$HNYAd&Roh)9TZA!f=)O$sv#1uIOcdGeJGJnmc+emyfO)j-K#Nm~!h*Hm( zeI+FM`~A@({Su+O)h=tshB5)By75yxC(Aw2YGFmpjJZb)PN}YO+$nQ6{^fepiyKMX zr%Y(cb87MB$6qKwRn<>&{?+3Pem`FGC^9G>bc9>|ZWB+oO;75|#~YGc?SY35i`x(0 zNbx6)OpK1{KMf`RFmHx#D_d+gam1yu;4STvq+BBXzB}&)eyW+^3le(TG>PV8<3eYh z@;a%DFi(Bv$Vu~g#iG$3e^f*N``yx_Lfmn+O?EvT++9oCqD{J3IouG)e<0kk?Oi&RL-eR zj3&aLYHD}Tb|qY;XJ=fc5;%=_%>sLH$u&Fnxt<W?`a9*(vTXvsgK31Y?7l{B?R9$GZ$~B!Fu~2 zU*e!dU3PMa+u!$rKJxzaZ)LnBc;>&K3LFSEKQKuDyd_ZM&HvBmnVEHUb=I5nT@zRS zz9Z0*1JIUUD`Sho2hDpbzqR~NI}aY{eV@6tT;{WQf^7CrquwSexPPIDDgbd3aEz)M zmWI--&;Grs6c7YAyE=TJ&U;NcTImtezehUT<-a%nxsc9g!4dDAUsm=k6wG@NB8-hK zXZ;n&C3d<&-`!dS^xYlN>#r@04+`ZRZsHmLcof>OGoQ3Hi9+oX@j3lkvOa$s{418~ zIzR3`In)%)0*@$3 z>yO)Ig9fa&jq1|MSR356+BC&sMnKCvxJcx4oc#y&2M$CW3}~`jD8vyR2}yo#rCqU` zf+_42vF+y<@z_T`m7U9UnjpU3-x5K6`B!}35Bd=(w$$6lhumt5BDQaw@%LFys9u%% zHiX)VP7Ro5q2`mEkhCT@mWE|Vr((Hp7_k2=;ol0B$gx0FWhya8%gkbG_?djRJj!YS z8XGUFwNpdr@W%LDSbAk%-+E4~$4kc?OK-Ggo2Ddw4ib=ghzeUM@{U@}kz-w2XlL6k zz|)?X(f*|6X_llyy>LcQDk0-yfVC*~&zv%pHZ0)c@>jGVP+bmdZ-oldK6|CFQb)pm zSo+@KhQYnL(?Yk+-$+XOnTRoqo&=tKuol|P?_uBzmf%wOnnvAEkA(Z&Gpk=2SaNR~ zS%TfwjG!%Fm#xoykc4MlGOC45hHBrQghALz*Iql!Px~9VQz$J zQp0t?f$lb+G-~tIwF~!Z98Hf>RLs-vQiqZk9#J>u5LNz@mt&=|#*o6ai;D8u+$B7^ zJ4dZY8*xu9ZT{1NyyYeyiqo&?2P?M(XW|<1*Ja1PK0UKIH}~u=qp2omk|oJ|M6$>J zEvv)++V|a<2KGF@MBghtU%tRk#3p{x^i@a3ue>XSm$IK?{`~G{XZ((|*t-=d8!`K1tk=bpX(atOGWuD5dP|RpgcL|`tVHYyl70E#9*$)khxPDjZ^g2 zS5t{*vV8|D|F{Xi@Dt|QTd%3!A3o1R*e|}!x)*-YxnG<(z`$P;Ax4q5iz7?wjFyT_ z1bAu(H&<}!j!$u>c(RG3)sEu^N^^gxMHRrmXHVJ&%&EUGPqusJkbP*q?Y7r5OBACZ z{AaiTF&SlJZfSDYN$;2g6MxTp%9zsUt?%c6!HAsOZsQ;9zak+!(B8^s?`*H-?jBxT z)bLydBAd0k-|12{mj%gNf1YtsGgV9NQFtWWqaVxp&rk%e2&k2+K!o)r?;UpZT2@jY zI6Y)^tNFFJj!n}^ba)8y+HKFM>ecrr^p7n=%-_tb%M~h6#?P&fH844RD8O}i432MY(AEm^#+~p z56k2nm|Y%|vQOhohA_Sr-0gv*z>hYQD}@?ZftJi%*m}>rHUHIWcR7~5 z-BS9ChD3f=o&g!|p4liyV77O3er}~!@!0Uo7A{z{Zvrzpz#d|}{9jY#xqfL|pkn@D zDpv@vOyLk`gA?nZ1CgW@#qHM6fZ4gr&Q$F%9mYj+$T%d1D&}V1%_~-1O0bNyqMdf) z?bFk{KH$U`o{SYSJHIf^D$IiG^$dk2;aiVxqhvMt$nA*YNp?8$D{Os!kxrdK7X?>iJTN3S; zke+v`&ujVntRC*HIprJ2s%f&lZ@_0mcUUJ21ms>}UH*u>_{99GYf)uwE>~^Fk%t-* zpDDMraD}qOe{;I%yeJl#dnYGV;gwUpAF=Ou#1UV*R>)d?i?i)m5ZoHBsFGbxzBB%c zs$}Bn>w@%~O7~jvSJN|ver=Q`i7n{`6!UE3;23TxoyGD-91Qk1OaXy<6%ITN$BBj! z6a5B{w=a8==erRmNc#@6!71((1<=<5E|mg|DNJ3@%)bf&Od!{JWDP!@$4svP#j!8I zvhTr5l&=p>+V5+N$RCHRLdNUNBoKZYW`+BiNx&%gK5GC%+`2zuf(==NsFH{aVh`oK zS3GbBhVDi-0}OzUK%g+2*MiqvyM!~E=QJ~|YUZdKE;y0g1$cCwF_3NeLHCeAt;Z(mm+PVz4qvE^^I z?;{(EfWoq@`;7-?Sz3Ga_HYKEG3tKw@|(;_8`51xS-Z#z0V8V27?O=4Ojoff_T9-cyR-a|#lNnRY=+c?k8(J>C_03Nb8D%fBtI+b zr&09v;pWC%pxUY56=v2U!3s5cz+9Ey{6V%Ok*iR(o180qi>#zl9))ZGFkXezD5iAC z8Q3u6k4ja5 z*$FPn_i>c0ZF=IQAy*|LZ zdWpl{NEg2u&YPm5WUc><$>{PEMrZ;6(C7s-vTVfBn+Gz9@W)lG1}Fw89GrpzC8Y;m z^UODFb`MN71tYU;>%CXefaICKI3BpC-xIAScb4nZqW*Q8)0Ma{_cq7G#^%w-MoXM? zDa&QD@zX$Gh6)|kyPCA%@$ugvr)xwJIjky0b=tTcB28A$-k#cLpI*+`+b9`X| zxZOL)tuL!FH#e6*GArbm?}lp=R`{q7tSCTWhw8M_52D(1f4}VA3g|utMaT5&mU;rZ zjXJ=PKsIE5J<9a4ARTy9FB|tKk3)AC zbH;ZBw6kkEYxOH_KLJpFECBLt{a_ZY@f6PP@tYTe0^v(T)?SrL${&3hT~k=urhVbb zBp0>%7nYcmxmC?3hjvt>WBJZ4IkRVUb*m%^*_%G&k2jhOJGosdZiINzZYg>l2*^MS zrmZ4c%F4l#-CR>5&W4B|RCeI`pE@5XLMua{bm|r!qgnB?t6wx=omn(%NEE)K)|IM& zd^bZ0E z-wuVVW@D@7nGoA|FX@?h#@5q)!dd(-zSwj@*=gxK}H0%_sT_JJS*OGiSdoc5z^W|kmlHm3;fXlpxnD#IS_(y)o7h(!9I$%O52Rf zqyw!Omw-)9@5b!x?5;yM=pvM!%HVj-69P=LltI3slW8S{0o_^!Pw{2_^wU_JLyf|b z;pfrE*4EHPU*-vC?sz@J*Vz>$%b$->7Xpf>tAAUq__I1GIrL=cL_}{8w?T1C^@8I*u5U7)%?f|W{6L&+p+Br&O-aSC`a6U2^ewLH3fwl5|zFdhEk82x_yLvyV`8+VwoS+ zDVSL56ol(_;Z;N6TZ-ddk^cJ6Kf`}jiT`}a%?dMrHC`+$-N6uZ$(HTo0aR6h^a-1^Q4l2)L@=lJ|E#jN;-7+bUk9W%{uJHZ@R>TZh{*6hUE;P?ECCgReDM14##g$6S`Dhcq&J*pT> zyHyj{dDm$dxtG5aV!nULt*)>Fq}lKIF8vlNo;Ar)njLcK#nwWf0Ip0JM-Qu|Q|Ky3 zfIGLuoyJ{euJQ+yw%;PE2!G(GkgPk`8U{W4FCz%Q{_ik z)^4G%w$V0edk{KKs6X{qRN7_#kLdmnmiz6k44jQST`t*MJZE0~&tM^nTP5KyRv5^| zd}+ONBiO_5nNvI%J6Te1l7{XMFbGJZdtX1#a@GChOF~puUm8BFlp}7wceWNqOGfFp z_hF@kW2vVGSKBj+CwNYC^@a@XyF47|!_(s`%CSis>pHl5gh^&dh|Y<~?`_D7fsr>k zxK5vqpEw;G6H^18AROAyh*Hc)Oa@?60=AoZm%LGwl>K>^{nl|lJyPl9szehXD)fCp zaa{)$flMiWCDc3PU`F`@`+yFn7x-{EpkHV0pmv|c)%StPVNwXXk^ zt_Gh9H7S8P>#j>@su?jT(x-t8OF~|RWOu!;ZNBGWAn$jdJ%!Zylwy%fDd^pp{=&

>;jGfeLGg3AR0+%Z~now?yWn)Almu992YwroIC> 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 literal 0 HcmV?d00001 diff --git a/docs/images/edit_compound_formula_panel.png b/docs/images/edit_zipper_formula_panel.png similarity index 100% rename from docs/images/edit_compound_formula_panel.png rename to docs/images/edit_zipper_formula_panel.png diff --git a/docs/old_changelog.html b/docs/old_changelog.html index 7a2f4edd1..3a49777f8 100644 --- a/docs/old_changelog.html +++ b/docs/old_changelog.html @@ -34,6 +34,45 @@

changelog

    +
  • +

    version 592

    +
      +
    • misc

    • +
    • the 'read' autocomplete dropdown has a new one-click 'clear search' button, just beside the favourites 'star' menu button. the 'empty page' favourite is removed from new users' defaults
    • +
    • in an alteration to the recent Autocomplete key processing, Ctrl+c/Ctrl+Insert _will_ now propagate to the results list if you currently have none of the text input selected (i.e. if it would have been a no-op on the text input, we assume you wanted whatever is selected in the list)
    • +
    • in the normal thumbnail/viewer menu and _review services_, the 'files' entry is renamed to 'locations'. this continues work in the left hand button of the autocomplete dropdown where you set the 'location', which can be all sorts of complicated things these days, rather than just 'file service key selector'. I don't think I'll rename 'my files' or anything, but I will try to emphasise this 'locations' idea more when I am talking about local file domains etc.. in other places going forward; what I often think of as 'oh yeah the files bit' isn't actually referring to the files themselves, but where they are located, so let's be precise
    • +
    • last week's tag pair filtering in _tags->migrate tags_ now has 'if either the left or right of the pair have count', and when you hit 'Go' with any of the new count filter checkboxes hit, the preview summary on the yes/no confirmation dialog talks about it
    • +
    • any time a watcher subject is parsed, if the text contains non-decoded html entities (like `>`), they are now auto-converted to normal chars. these strings are often ripped from odd places and are only used for user display, so this just makes that simpler
    • +
    • if you are set to remove trashed files from view, this now works when the files are in multpile local file domains, and you choose 'delete from all local file services', and you are looking at 'all my files' or a subset of your local file domains
    • +
    • we now log any time (when the client is non-idle) that a database job's work inside the transaction wrapper takes more than 15 seconds to complete
    • +
    • fixed an issue caused by the sibling or parents system doing some regen work at an unlucky time
    • +
    • default downloaders

    • +
    • thanks to user help, the derpibooru post parser now additionally grabs the raw markdown of a description as a second note. this catches links and images better than the html string parse. if you strictly only want one of these notes, please feel free to dive into _network->downloaders->defailt import options_ for your derpi downloader and try to navigate the 'note import options' hell I designed and let me know how it could be more user friendly
    • +
    • parsing system

    • +
    • added a new NESTED formula type. this guy holds two formulae of any type internally, parsing the document with the first and passing those results on to the second. it is designed to solve the problem of 'how do I parse this JSON tucked inside HTML' and _vice versa_. various encoding stuff all seems to be handled, no extra work needed
    • +
    • added Nested formula stuff to the 'how to make a downloader' help
    • +
    • made all the screenshot in the parsing formula help clickable
    • +
    • renamed the COMPOUND formula to ZIPPER formula
    • +
    • all the 'String Processor' buttons across the program now have copy and paste buttons, so it is now easy to duplicate some rules you set up
    • +
    • in the parsing system, sidecar importer, and clipboard watcher, all strings are now cleansed of errant 'surrogate' characters caused by the source incorrectly providing utf-16 garbage in a utf-8 stream. fingers crossed, the cleansing here will actually _fix_ problem characters by converting them to utf-8, but we'll see
    • +
    • thanks to a user, the JSON parsing system has a new 'de-minify json' parsing rule, which decompresses a particular sort of minified JSON that expresses multiply-referenced values using list positions. as it happened that I added NESTED formulae this week, I wonder if we will migrate this capability to the string processing system, but let's give it time to breathe
    • +
    • client api

    • +
    • fixed the permission check on the new 'get file/thumbnail local path' commands--due to me copy/pasting stupidly, they were still just checking 'search files' perm
    • +
    • added `/get_files/local_file_storage_locations`, which spits out the stuff in _database->move media files_ and lets you do local file access _en masse_
    • +
    • added help and a unit test for this new command
    • +
    • the client api version is now 72
    • +
    • some security/library updates

    • +
    • the 'old' OpenCV version in the `(a)dvanced` setup, which pointed to version 4.5.3.56, which had the webp vulnerability, is no longer an option. I believe this means that the program will no longer run on python 3.7. I understad Win 7 can run python 3.8 at the latest, so we are nearing the end of the line on that front
    • +
    • the old/new Pillow choice in `(a)dvanced` setup, which offered support for python 3.7, is removed
    • +
    • I have added a new question to the `(a)dvanced` venv setup to handle misc 'future' tests better, and I added a new future test for two security patches for `setuptools` and `requests`:
    • +
    • A) `setuptools` is updated to 70.3.0 (from 69.1.1) to resolve a security issue related to downloading packages from bad places (don't think this would ever affect us, but we'll be good)
    • +
    • B) `requests` is updated to 2.32.3 (from 2.31.0) to resolve a security issue with verify=False (the specific problem doesn't matter for us, but we'll be good)
    • +
    • if you run from source and want to help me test, you might like to rebuild your venv this week and choose the new future choice. these version increments do not appear to be a big deal, so assuming no problems I will roll these new libraries into a 'future' test build next week, and then into the normal builds a week after
    • +
    • boring code cleanup

    • +
    • did a bunch more `super()` refactoring. I think all `__init__` is now converted across the program, and I cleared all the normal calls in the canvas and media results panel code too
    • +
    • refactored `ClientGUIResults` into four files for the core class, the loading, the thumbnails, and some menu gubbins. also unified the mish-mash of `Results` and `MediaPanel` nomenclature to `MediaResultsPanel`
    • +
    +
  • version 591

      diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py index 9a128f710..41722b744 100644 --- a/hydrus/client/ClientController.py +++ b/hydrus/client/ClientController.py @@ -20,6 +20,7 @@ from hydrus.core import HydrusProcess from hydrus.core import HydrusPSUtil from hydrus.core import HydrusSerialisable +from hydrus.core import HydrusText from hydrus.core import HydrusThreading from hydrus.core import HydrusTime from hydrus.core.networking import HydrusNetworking @@ -916,6 +917,8 @@ def GetClipboardText( self ): raise HydrusExceptions.DataMissing( 'No text on the clipboard!' ) + clipboard_text = HydrusText.CleanseImportText( clipboard_text ) + return clipboard_text diff --git a/hydrus/client/ClientDefaults.py b/hydrus/client/ClientDefaults.py index 4c9b788c4..c70f61390 100644 --- a/hydrus/client/ClientDefaults.py +++ b/hydrus/client/ClientDefaults.py @@ -952,25 +952,6 @@ def SetDefaultFavouriteSearchManagerData( favourite_search_manager ): # - foldername = None - name = 'empty page' - - location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.LOCAL_FILE_SERVICE_KEY ) - - tag_context = ClientSearchTagContext.TagContext() - - predicates = [] - - file_search_context = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, tag_context = tag_context, predicates = predicates ) - - synchronised = True - media_sort = None - media_collect = None - - rows.append( ( foldername, name, file_search_context, synchronised, media_sort, media_collect ) ) - - # - favourite_search_manager.SetFavouriteSearchRows( rows ) diff --git a/hydrus/client/ClientFiles.py b/hydrus/client/ClientFiles.py index fc495b831..cf9dc8c49 100644 --- a/hydrus/client/ClientFiles.py +++ b/hydrus/client/ClientFiles.py @@ -1689,6 +1689,14 @@ def GetMissingSubfolders( self ): return self._missing_subfolders + def GetAllSubfolders( self ) -> typing.Collection[ ClientFilesPhysical.FilesStorageSubfolder ]: + + with self._master_locations_rwlock.read: + + return self._GetAllSubfolders() + + + def GetThumbnailPath( self, media ): hash = media.GetHash() diff --git a/hydrus/client/ClientMigration.py b/hydrus/client/ClientMigration.py index e3e84bbd9..f74082f38 100644 --- a/hydrus/client/ClientMigration.py +++ b/hydrus/client/ClientMigration.py @@ -614,7 +614,7 @@ def Prepare( self ): class MigrationSourceHTPA( MigrationSource ): - def __init__( self, controller, path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, needs_count_service_key ): + def __init__( self, controller, path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ): name = os.path.basename( path ) @@ -624,6 +624,7 @@ def __init__( self, controller, path, content_type, left_tag_filter, right_tag_f self._content_type = content_type self._left_tag_filter = left_tag_filter self._right_tag_filter = right_tag_filter + self._either_side_needs_count = either_side_needs_count self._left_side_needs_count = left_side_needs_count self._right_side_needs_count = right_side_needs_count self._needs_count_service_key = needs_count_service_key @@ -658,9 +659,9 @@ def GetSomeData( self ): data = [ ( left_tag, right_tag ) for ( left_tag, right_tag ) in data if self._left_tag_filter.TagOK( left_tag ) and self._right_tag_filter.TagOK( right_tag ) ] - if self._left_side_needs_count or self._right_side_needs_count: + if self._left_side_needs_count or self._right_side_needs_count or self._either_side_needs_count: - data = self._controller.Read( 'migration_filter_pairs_by_count', data, self._content_type, self._left_side_needs_count, self._right_side_needs_count, self._needs_count_service_key ) + data = self._controller.Read( 'migration_filter_pairs_by_count', data, self._content_type, self._left_side_needs_count, self._right_side_needs_count, self._either_side_needs_count, self._needs_count_service_key ) return data @@ -748,7 +749,7 @@ def Prepare( self ): class MigrationSourceTagServicePairs( MigrationSource ): - def __init__( self, controller, tag_service_key, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, needs_count_service_key ): + def __init__( self, controller, tag_service_key, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ): name = controller.services_manager.GetName( tag_service_key ) @@ -761,6 +762,7 @@ def __init__( self, controller, tag_service_key, content_type, left_tag_filter, self._content_statuses = content_statuses self._left_side_needs_count = left_side_needs_count self._right_side_needs_count = right_side_needs_count + self._either_side_needs_count = either_side_needs_count self._needs_count_service_key = needs_count_service_key self._database_temp_job_name = 'migrate_{}'.format( os.urandom( 16 ).hex() ) @@ -780,9 +782,9 @@ def GetSomeData( self ): self._work_to_do = False - if self._left_side_needs_count or self._right_side_needs_count: + if self._left_side_needs_count or self._right_side_needs_count or self._either_side_needs_count: - data = self._controller.Read( 'migration_filter_pairs_by_count', data, self._content_type, self._left_side_needs_count, self._right_side_needs_count, self._needs_count_service_key ) + data = self._controller.Read( 'migration_filter_pairs_by_count', data, self._content_type, self._left_side_needs_count, self._right_side_needs_count, self._either_side_needs_count, self._needs_count_service_key ) return data diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py index 7f3ebcf10..d9d272f63 100644 --- a/hydrus/client/ClientParsing.py +++ b/hydrus/client/ClientParsing.py @@ -1,8 +1,8 @@ import base64 import bs4 import collections +import html import json -import os import re import time import urllib.parse @@ -759,6 +759,12 @@ def RenderJSONParseRule( rule ): s = 'get the entries that match "' + parse_rule.ToString() + '"' + elif parse_rule_type == JSON_PARSE_RULE_TYPE_DEMINIFY_JSON: + + index = parse_rule + + s = 'de-minify json at the ' + HydrusNumbers.IndexToPrettyOrdinalString( index ) + ' item' + return s @@ -823,6 +829,8 @@ def Parse( self, parsing_context, parsing_text: str, collapse_newlines: bool ): raw_texts = self._ParseRawTexts( parsing_context, parsing_text, collapse_newlines ) + raw_texts = HydrusText.CleanseImportTexts( raw_texts ) + if collapse_newlines: # maybe should use HydrusText.DeserialiseNewlinedTexts, but that might change/break some existing parsers with the strip() trim @@ -875,10 +883,10 @@ def ToPrettyMultilineString( self ): -class ParseFormulaCompound( ParseFormula ): +class ParseFormulaZipper( ParseFormula ): - SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_COMPOUND - SERIALISABLE_NAME = 'Compound Parsing Formula' + SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_ZIPPER + SERIALISABLE_NAME = 'Zipper Parsing Formula' SERIALISABLE_VERSION = 2 def __init__( self, formulae = None, sub_phrase = None, string_processor = None ): @@ -1009,7 +1017,7 @@ def GetSubstitutionPhrase( self ): def ToPrettyString( self ): - return 'COMPOUND with ' + HydrusNumbers.ToHumanInt( len( self._formulae ) ) + ' formulae.' + return 'ZIPPER with ' + HydrusNumbers.ToHumanInt( len( self._formulae ) ) + ' formulae.' def ToPrettyMultilineString( self ): @@ -1025,12 +1033,13 @@ def ToPrettyMultilineString( self ): separator = '\n' * 2 - text = '--COMPOUND--' + '\n' * 2 + separator.join( s ) + text = '--ZIPPER--' + '\n' * 2 + separator.join( s ) return text -HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_COMPOUND ] = ParseFormulaCompound + +HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_ZIPPER ] = ParseFormulaZipper class ParseFormulaContextVariable( ParseFormula ): @@ -1748,6 +1757,7 @@ def ToTuple( self ): JSON_PARSE_RULE_TYPE_DICT_KEY = 0 JSON_PARSE_RULE_TYPE_ALL_ITEMS = 1 JSON_PARSE_RULE_TYPE_INDEXED_ITEM = 2 +JSON_PARSE_RULE_TYPE_DEMINIFY_JSON = 3 class ParseFormulaJSON( ParseFormula ): @@ -1875,6 +1885,56 @@ def _GetRawTextsFromJSON( self, j ): + elif parse_rule_type == JSON_PARSE_RULE_TYPE_DEMINIFY_JSON: + + if not isinstance( root, list ): + + continue + + + index = parse_rule + + def _deminify( item ): + """ + Example: + >>> root = [["test", 1], {"key": 2, "value": 3}, 123, "asd"] + >>> _deminify(root[0]) + ["test", {"key": 123, "value": "asd"}] + """ + + if isinstance( item, list ): + + return [ _deminify( i ) for i in item ] + + elif isinstance( item, dict ): + + return { k: _deminify( v ) for k, v in item.items() } + + elif isinstance( item, int ): + + # Don't convert topmost integer + if isinstance( root[ item ], int ): + + return root[ item ] + + + return _deminify( root[ item ] ) + + else: + + return item + + + + try: + + next_roots.append( _deminify( root[ index ] ) ) + + except IndexError: + + continue + + roots = next_roots @@ -2066,8 +2126,92 @@ def ToPrettyMultilineString( self ): return pretty_multiline_string + HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_JSON ] = ParseFormulaJSON +class ParseFormulaNested( ParseFormula ): + + SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_NESTED + SERIALISABLE_NAME = 'Nested Parsing Formula' + SERIALISABLE_VERSION = 1 + + def __init__( self, main_formula = None, sub_formula = None, string_processor = None ): + + super().__init__( string_processor ) + + if main_formula is None: + + main_formula = ParseFormulaHTML() + + + if sub_formula is None: + + sub_formula = ParseFormulaJSON() + + + self._main_formula = main_formula + self._sub_formula = sub_formula + + + def _GetSerialisableInfo( self ): + + serialisable_main_formula = self._main_formula.GetSerialisableTuple() + serialisable_sub_formula = self._sub_formula.GetSerialisableTuple() + serialisable_string_processor = self._string_processor.GetSerialisableTuple() + + return ( serialisable_main_formula, serialisable_sub_formula, serialisable_string_processor ) + + + def _InitialiseFromSerialisableInfo( self, serialisable_info ): + + ( serialisable_main_formula, serialisable_sub_formula, serialisable_string_processor ) = serialisable_info + + self._main_formula = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_main_formula ) + self._sub_formula = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_sub_formula ) + self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor ) + + + def _ParseRawTexts( self, parsing_context, parsing_text: str, collapse_newlines: bool ): + + all_sub_parsed_texts = [] + + main_parsed_texts = self._main_formula.Parse( parsing_context, parsing_text, collapse_newlines ) + + for main_parsed_text in main_parsed_texts: + + sub_parsed_texts = self._sub_formula.Parse( parsing_context, main_parsed_text, collapse_newlines ) + + all_sub_parsed_texts.extend( sub_parsed_texts ) + + + return all_sub_parsed_texts + + + def GetMainFormula( self ): + + return self._main_formula + + + def GetSubFormula( self ): + + return self._sub_formula + + + def ToPrettyString( self ): + + return f'NESTED formulae, taking from "{self._main_formula.ToPrettyString()}" and sending to "{self._sub_formula.ToPrettyString()}".' + + + def ToPrettyMultilineString( self ): + + text = '--NESTED--' + '\n' * 2 + self._main_formula.ToPrettyMultilineString() + '\n->\n' + self._sub_formula.ToPrettyMultilineString() + + return text + + + +HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_PARSE_FORMULA_NESTED ] = ParseFormulaNested + class SimpleDownloaderParsingFormula( HydrusSerialisable.SerialisableBaseNamed ): SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SIMPLE_DOWNLOADER_PARSE_FORMULA @@ -2439,6 +2583,19 @@ def remove_pre_url_gubbins( u ): + if self._content_type == HC.CONTENT_TYPE_TITLE: + + try: + + # handling & gubbins that come through JSON, although the better answer is to convert to an html parser + parsed_texts = [ html.unescape( parsed_text ) for parsed_text in parsed_texts ] + + except: + + HydrusData.Print( f'Could not unescape parsed title text: {parsing_context}' ) + + + if self._content_type == HC.CONTENT_TYPE_VETO: ( veto_if_matches_found, string_match ) = self._additional_info diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py index ff27f7e38..bb9069be0 100644 --- a/hydrus/client/ClientServices.py +++ b/hydrus/client/ClientServices.py @@ -793,7 +793,7 @@ class ServiceRemote( Service ): def __init__( self, service_key, service_type, name, dictionary = None ): - Service.__init__( self, service_key, service_type, name, dictionary = dictionary ) + super().__init__( service_key, service_type, name, dictionary = dictionary ) self.network_context = ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_HYDRUS, self._service_key ) @@ -1537,7 +1537,7 @@ class ServiceRepository( ServiceRestricted ): def __init__( self, service_key, service_type, name, dictionary = None ): - ServiceRestricted.__init__( self, service_key, service_type, name, dictionary = dictionary ) + super().__init__( service_key, service_type, name, dictionary = dictionary ) self._sync_remote_lock = threading.Lock() self._sync_processing_lock = threading.Lock() diff --git a/hydrus/client/ClientThreading.py b/hydrus/client/ClientThreading.py index 3143ac888..b2d6f1b60 100644 --- a/hydrus/client/ClientThreading.py +++ b/hydrus/client/ClientThreading.py @@ -614,7 +614,7 @@ class QtAwareJob( HydrusThreading.SingleJob ): def __init__( self, controller, scheduler, window, initial_delay, work_callable ): - HydrusThreading.SingleJob.__init__( self, controller, scheduler, initial_delay, work_callable ) + super().__init__( controller, scheduler, initial_delay, work_callable ) self._window = window @@ -662,7 +662,7 @@ class QtAwareRepeatingJob( HydrusThreading.RepeatingJob ): def __init__( self, controller, scheduler, window, initial_delay, period, work_callable ): - HydrusThreading.RepeatingJob.__init__( self, controller, scheduler, initial_delay, period, work_callable ) + super().__init__( controller, scheduler, initial_delay, period, work_callable ) self._window = window diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py index f191dcad1..8d6c841f7 100644 --- a/hydrus/client/db/ClientDB.py +++ b/hydrus/client/db/ClientDB.py @@ -247,7 +247,7 @@ def __init__( self, controller: ClientControllerInterface.ClientControllerInterf self._regen_tags_managers_hash_ids = set() self._regen_tags_managers_tag_ids = set() - HydrusDB.HydrusDB.__init__( self, controller, db_dir, db_name ) + super().__init__( controller, db_dir, db_name ) def _AddFiles( self, service_id, rows ): @@ -5343,7 +5343,7 @@ def _MigrationClearJob( self, database_temp_job_name ): self._Execute( 'DROP TABLE {};'.format( database_temp_job_name ) ) - def _MigrationFilterPairsByCount( self, pairs, content_type, left_side_needs_count, right_side_needs_count, needs_count_service_key ): + def _MigrationFilterPairsByCount( self, pairs, content_type, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ): def tag_has_count( tag_id ): @@ -5365,17 +5365,29 @@ def tag_has_count( tag_id ): for ( a, b ) in pairs: - if left_side_needs_count: + left_side_needs_count_for_this_pair = left_side_needs_count + right_side_needs_count_for_this_pair = right_side_needs_count + + if left_side_needs_count_for_this_pair or either_side_needs_count: a_id = self.modules_tags_local_cache.GetTagId( a ) - if not tag_has_count( a_id ): + has_count = tag_has_count( a_id ) + + if not has_count: - continue + if left_side_needs_count_for_this_pair: + + continue + + elif either_side_needs_count: + + right_side_needs_count_for_this_pair = True + - if right_side_needs_count: + if right_side_needs_count_for_this_pair: b_id = self.modules_tags_local_cache.GetTagId( b ) @@ -5385,7 +5397,9 @@ def tag_has_count( tag_id ): b_id = self.modules_tag_siblings.GetIdealTagId( ClientTags.TAG_DISPLAY_DISPLAY_IDEAL, tag_service_id, b_id ) - if not tag_has_count( b_id ): + has_count = tag_has_count( b_id ) + + if right_side_needs_count_for_this_pair and not has_count: continue @@ -10892,6 +10906,38 @@ def ask_what_to_do_zip_docx_scan(): + if version == 591: + + try: + + domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER ) + + domain_manager.Initialise() + + # + + domain_manager.OverwriteDefaultParsers( [ + 'derpibooru.org 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( HydrusNumbers.ToHumanInt( version + 1 ) ) ) self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) ) diff --git a/hydrus/client/db/ClientDBFilesStorage.py b/hydrus/client/db/ClientDBFilesStorage.py index 7e8a53317..9fa163dac 100644 --- a/hydrus/client/db/ClientDBFilesStorage.py +++ b/hydrus/client/db/ClientDBFilesStorage.py @@ -116,7 +116,7 @@ class DBLocationContextLeaf( DBLocationContext ): def __init__( self, location_context: ClientLocation.LocationContext, files_table_name: str ): - DBLocationContext.__init__( self, location_context ) + super().__init__( location_context ) self._files_table_name = files_table_name diff --git a/hydrus/client/db/ClientDBTagParents.py b/hydrus/client/db/ClientDBTagParents.py index 35280d1b9..4e99745f9 100644 --- a/hydrus/client/db/ClientDBTagParents.py +++ b/hydrus/client/db/ClientDBTagParents.py @@ -888,6 +888,11 @@ def Regen( self, tag_service_ids ): def RegenChains( self, tag_service_ids, tag_ids ): + if self._service_ids_to_applicable_service_ids is None: + + self.GenerateApplicationDicts() + + if len( tag_ids ) == 0: return diff --git a/hydrus/client/db/ClientDBTagSiblings.py b/hydrus/client/db/ClientDBTagSiblings.py index 010d06010..532cb9935 100644 --- a/hydrus/client/db/ClientDBTagSiblings.py +++ b/hydrus/client/db/ClientDBTagSiblings.py @@ -1020,6 +1020,11 @@ def Regen( self, tag_service_ids ): def RegenChains( self, tag_service_ids, tag_ids ): + if self._service_ids_to_applicable_service_ids is None: + + self._GenerateApplicationDicts() + + # as this guy can change ideals, the related parent chains need to be regenned afterwards too if len( tag_ids ) == 0: diff --git a/hydrus/client/duplicates/ClientAutoDuplicates.py b/hydrus/client/duplicates/ClientAutoDuplicates.py index 3ade103c1..b9207be62 100644 --- a/hydrus/client/duplicates/ClientAutoDuplicates.py +++ b/hydrus/client/duplicates/ClientAutoDuplicates.py @@ -36,7 +36,7 @@ def __init__( self ): This guy holds one test and is told to test either the better or worse candidate. Multiple of these stacked up make for 'the better file is a jpeg over one megabyte, the worse file is a jpeg under 100KB'. """ - PairComparator.__init__( self ) + super().__init__() # this guy tests the better or the worse for a single property # user could set up multiple on either side of the equation @@ -79,7 +79,7 @@ def __init__( self ): This guy compares the pair directly. It can say 'yes the better candidate is 4x bigger than the worse'. """ - PairComparator.__init__( self ) + super().__init__() # this work does not need to be done yet! @@ -166,7 +166,7 @@ def __init__( self, name ): This guy holds everything to make a single auto-resolution job work. It knows the search it wants to do, and, when given pairs from that search, will confirm whether one file passes its auto-resolution threshold and should be auto-considered better. """ - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) # the id here will be for the database to match up rules to cached pair statuses. slightly wewmode, but we'll see self._id = -1 diff --git a/hydrus/client/exporting/ClientExportingFiles.py b/hydrus/client/exporting/ClientExportingFiles.py index 0674a33ca..c084e450b 100644 --- a/hydrus/client/exporting/ClientExportingFiles.py +++ b/hydrus/client/exporting/ClientExportingFiles.py @@ -312,7 +312,7 @@ def __init__( show_working_popup = True ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) if export_type == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE: diff --git a/hydrus/client/gui/ClientGUIAPI.py b/hydrus/client/gui/ClientGUIAPI.py index 3ace19cba..ad0dec2a2 100644 --- a/hydrus/client/gui/ClientGUIAPI.py +++ b/hydrus/client/gui/ClientGUIAPI.py @@ -20,7 +20,7 @@ class CaptureAPIAccessPermissionsRequestPanel( ClientGUIScrolledPanels.ReviewPan def __init__( self, parent ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._time_started = HydrusTime.GetNow() @@ -68,7 +68,7 @@ class EditAPIPermissionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, api_permissions: ClientAPI.APIPermissions ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_api_permissions = api_permissions diff --git a/hydrus/client/gui/ClientGUICharts.py b/hydrus/client/gui/ClientGUICharts.py index d0bca57fc..7b6a4c136 100644 --- a/hydrus/client/gui/ClientGUICharts.py +++ b/hydrus/client/gui/ClientGUICharts.py @@ -12,7 +12,7 @@ class BarChartBandwidthHistory( QCh.QtCharts.QChartView ): def __init__( self, parent, monthly_usage ): - QCh.QtCharts.QChartView.__init__( self, parent ) + super().__init__( parent ) divisor = 1.0 unit = 'B' @@ -74,7 +74,7 @@ class FileHistory( QCh.QtCharts.QChartView ): def __init__( self, parent, file_history: dict, show_deleted: bool ): - QCh.QtCharts.QChartView.__init__( self, parent ) + super().__init__( parent ) self._file_history = file_history self._show_deleted = show_deleted diff --git a/hydrus/client/gui/ClientGUICore.py b/hydrus/client/gui/ClientGUICore.py index 9dec16da8..0aa3fded0 100644 --- a/hydrus/client/gui/ClientGUICore.py +++ b/hydrus/client/gui/ClientGUICore.py @@ -13,7 +13,7 @@ class GUICore( QC.QObject ): def __init__( self ): - QC.QObject.__init__( self ) + super().__init__() self._menu_open = False diff --git a/hydrus/client/gui/ClientGUIDialogsManage.py b/hydrus/client/gui/ClientGUIDialogsManage.py index fb2983ad2..ad074d360 100644 --- a/hydrus/client/gui/ClientGUIDialogsManage.py +++ b/hydrus/client/gui/ClientGUIDialogsManage.py @@ -600,7 +600,7 @@ def __init__( self, parent ): title = 'manage local upnp' - ClientGUIDialogs.Dialog.__init__( self, parent, title ) + super().__init__( parent, title ) self._status_st = ClientGUICommon.BetterStaticText( self ) diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py index b797d2fd3..89df54340 100644 --- a/hydrus/client/gui/ClientGUIDownloaders.py +++ b/hydrus/client/gui/ClientGUIDownloaders.py @@ -1102,7 +1102,7 @@ def __init__( self, parent: QW.QWidget, parameter: ClientNetworkingURLClass.URLC self._default_value = ClientGUICommon.NoneableTextCtrl( self, '' ) self._default_value.setToolTip( ClientGUIFunctions.WrapToolTip( 'What actual value will be embedded into the URL sent to the server.' ) ) - self._default_value_string_processor = ClientGUIStringControls.StringProcessorButton( self, parameter.GetDefaultValueStringProcessor(), self._GetTestData ) + self._default_value_string_processor = ClientGUIStringControls.StringProcessorWidget( self, parameter.GetDefaultValueStringProcessor(), self._GetTestData ) tt = 'WARNING WARNING: Extremely Big Brain' tt += '/n' * 2 tt += 'You can apply the parsing system\'s normal String Processor steps to your fixed default value here. For instance, you could append/replace the default value with random hex or today\'s date. This is obviously super advanced, so be careful.' diff --git a/hydrus/client/gui/ClientGUIDragDrop.py b/hydrus/client/gui/ClientGUIDragDrop.py index 3a7fb4fb1..8df1be354 100644 --- a/hydrus/client/gui/ClientGUIDragDrop.py +++ b/hydrus/client/gui/ClientGUIDragDrop.py @@ -21,7 +21,7 @@ class QMimeDataHydrusFiles( QC.QMimeData ): def __init__( self ): - QC.QMimeData.__init__( self ) + super().__init__() self._hydrus_files = None @@ -189,7 +189,7 @@ class FileDropTarget( QC.QObject ): def __init__( self, parent, filenames_callable = None, url_callable = None, media_callable = None ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._parent = parent @@ -301,6 +301,8 @@ def OnData( self, mime_data, result ): text = mime_data.text() + text = HydrusText.CleanseImportText( text ) + text_lines = HydrusText.DeserialiseNewlinedTexts( text ) for text_line in text_lines: diff --git a/hydrus/client/gui/ClientGUIFrames.py b/hydrus/client/gui/ClientGUIFrames.py index f26dab462..bc3224500 100644 --- a/hydrus/client/gui/ClientGUIFrames.py +++ b/hydrus/client/gui/ClientGUIFrames.py @@ -18,7 +18,7 @@ def __init__( self, key_type, keys ): tlw = CG.client_controller.GetMainTLW() - ClientGUITopLevelWindows.Frame.__init__( self, tlw, CG.client_controller.PrepStringForDisplay( title ) ) + super().__init__( tlw, CG.client_controller.PrepStringForDisplay( title ) ) self._key_type = key_type self._keys = keys diff --git a/hydrus/client/gui/ClientGUIOptionsPanels.py b/hydrus/client/gui/ClientGUIOptionsPanels.py index 8242a3f06..87e2d6eee 100644 --- a/hydrus/client/gui/ClientGUIOptionsPanels.py +++ b/hydrus/client/gui/ClientGUIOptionsPanels.py @@ -26,7 +26,7 @@ class OptionsPanelMimesTree( OptionsPanel ): def __init__( self, parent, selectable_mimes ): - OptionsPanel.__init__( self, parent ) + super().__init__( parent ) self._selectable_mimes = set( selectable_mimes ) diff --git a/hydrus/client/gui/ClientGUIPanels.py b/hydrus/client/gui/ClientGUIPanels.py index db7f4dd7e..ae364be58 100644 --- a/hydrus/client/gui/ClientGUIPanels.py +++ b/hydrus/client/gui/ClientGUIPanels.py @@ -11,7 +11,7 @@ class IPFSDaemonStatusAndInteractionPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service_callable ): - ClientGUICommon.StaticBox.__init__( self, parent, 'ipfs daemon' ) + super().__init__( parent, 'ipfs daemon' ) self._is_running = False self._nocopy_enabled = False diff --git a/hydrus/client/gui/ClientGUIPopupMessages.py b/hydrus/client/gui/ClientGUIPopupMessages.py index f6f4aec9d..40880702a 100644 --- a/hydrus/client/gui/ClientGUIPopupMessages.py +++ b/hydrus/client/gui/ClientGUIPopupMessages.py @@ -60,7 +60,7 @@ class PopupMessage( PopupWindow ): def __init__( self, parent, job_status: ClientThreading.JobStatus ): - PopupWindow.__init__( self, parent ) + super().__init__( parent ) self._job_status = job_status diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py index 8002286b6..cb2f66007 100644 --- a/hydrus/client/gui/ClientGUIShortcuts.py +++ b/hydrus/client/gui/ClientGUIShortcuts.py @@ -1206,7 +1206,7 @@ class ShortcutSet( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._shortcuts_to_commands = {} @@ -1692,7 +1692,7 @@ class ShortcutsDeactivationCatcher( QC.QObject ): def __init__( self, shortcuts_handler: ShortcutsHandler, widget: QW.QWidget ): - QC.QObject.__init__( self, shortcuts_handler ) + super().__init__( shortcuts_handler ) self._shortcuts_handler = shortcuts_handler diff --git a/hydrus/client/gui/ClientGUISplash.py b/hydrus/client/gui/ClientGUISplash.py index 471a2f216..18ba23d88 100644 --- a/hydrus/client/gui/ClientGUISplash.py +++ b/hydrus/client/gui/ClientGUISplash.py @@ -234,7 +234,7 @@ def __init__( self, controller, title, frame_splash_status: FrameSplashStatus ): self._controller = controller - QW.QWidget.__init__( self, None ) + super().__init__( None ) self.setWindowFlag( QC.Qt.CustomizeWindowHint ) self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False ) diff --git a/hydrus/client/gui/ClientGUIStringControls.py b/hydrus/client/gui/ClientGUIStringControls.py index 1d70df921..1cbd44644 100644 --- a/hydrus/client/gui/ClientGUIStringControls.py +++ b/hydrus/client/gui/ClientGUIStringControls.py @@ -1,16 +1,20 @@ -import os import typing from qtpy import QtCore as QC from qtpy import QtWidgets as QW +from hydrus.core import HydrusData +from hydrus.core import HydrusExceptions +from hydrus.core import HydrusSerialisable from hydrus.core import HydrusText from hydrus.client import ClientConstants as CC +from hydrus.client import ClientGlobals as CG from hydrus.client import ClientParsing from hydrus.client import ClientStrings from hydrus.client.gui import ClientGUIDialogs from hydrus.client.gui import ClientGUIDialogsMessage +from hydrus.client.gui import ClientGUIDialogsQuick from hydrus.client.gui import ClientGUIFunctions from hydrus.client.gui import ClientGUIStringPanels from hydrus.client.gui import ClientGUITopLevelWindowsPanels @@ -135,11 +139,12 @@ def SetValue( self, string_match: ClientStrings.StringMatch ): self._UpdateLabel() + class StringProcessorButton( ClientGUICommon.BetterButton ): valueChanged = QC.Signal() - def __init__( self, parent, string_processor: ClientStrings.StringProcessor, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ] ): + def __init__( self, parent: QW.QWidget, string_processor: ClientStrings.StringProcessor, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ] ): super().__init__( parent, 'edit string processor', self._Edit ) @@ -199,7 +204,98 @@ def SetValue( self, string_processor: ClientStrings.StringProcessor ): self._UpdateLabel() + self.valueChanged.emit() + + + +class StringProcessorWidget( QW.QWidget ): + + valueChanged = QC.Signal() + + def __init__( self, parent: QW.QWidget, string_processor: ClientStrings.StringProcessor, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ] ): + + super().__init__( parent ) + + self._edit_button = StringProcessorButton( self, string_processor, test_data_callable ) + + self._copy_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().copy, self._Copy ) + self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy String Processor to the clipboard.' ) ) + + self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste ) + self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste a String Processor from the clipboard.' ) ) + + # + + hbox = QP.HBoxLayout() + + QP.AddToLayout( hbox, self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( hbox, self._copy_button, CC.FLAGS_CENTER ) + QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER ) + + self.setLayout( hbox ) + + self._edit_button.valueChanged.connect( self.valueChanged ) + + + def _Copy( self ): + + string_processor = self.GetValue() + + text = string_processor.DumpToString() + + CG.client_controller.pub( 'clipboard', 'text', text ) + + + def _ImportObject( self, obj ): + + if isinstance( obj, ClientStrings.StringProcessor ): + + self.SetValue( obj ) + + else: + + raise Exception( f'The imported object was wrong for this control! It appeared to be a {HydrusData.GetTypeName( obj )}.' ) + + + def _Paste( self ): + + try: + + raw_text = CG.client_controller.GetClipboardText() + + except HydrusExceptions.DataMissing as e: + + HydrusData.PrintException( e ) + + ClientGUIDialogsMessage.ShowCritical( self, 'Problem pasting!', str(e) ) + + return + + + try: + + obj = HydrusSerialisable.CreateFromString( raw_text ) + + self._ImportObject( obj ) + + except Exception as e: + + ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'JSON-serialised Hydrus Object(s)', e ) + + + + def GetValue( self ) -> ClientStrings.StringProcessor: + + return self._edit_button.GetValue() + + + def SetValue( self, string_processor: ClientStrings.StringProcessor ): + + self._edit_button.SetValue( string_processor ) + + + class StringMatchToStringMatchDictControl( QW.QWidget ): def __init__( self, parent, initial_dict: typing.Dict[ ClientStrings.StringMatch, ClientStrings.StringMatch ], min_height = 10, key_name = 'key' ): diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py index 0cd74e1f3..d312f508b 100644 --- a/hydrus/client/gui/ClientGUISubscriptions.py +++ b/hydrus/client/gui/ClientGUISubscriptions.py @@ -254,7 +254,7 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions. subscription = subscription.Duplicate() - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_subscription = subscription self._names_to_edited_query_log_containers = dict( names_to_edited_query_log_containers ) @@ -1290,7 +1290,7 @@ class EditSubscriptionQueryPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader, query_log_container: ClientImportSubscriptionQuery.SubscriptionQueryLogContainer ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_query_header = query_header self._original_query_log_container = query_log_container @@ -1427,7 +1427,7 @@ def __init__( self, parent: QW.QWidget, subscriptions: typing.Collection[ Client subscriptions = [ subscription.Duplicate() for subscription in subscriptions ] - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._existing_query_log_container_names = set() diff --git a/hydrus/client/gui/ClientGUISystemTray.py b/hydrus/client/gui/ClientGUISystemTray.py index 40626f67d..007e5b273 100644 --- a/hydrus/client/gui/ClientGUISystemTray.py +++ b/hydrus/client/gui/ClientGUISystemTray.py @@ -24,7 +24,7 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ): def __init__( self, parent: QW.QWidget ): - QW.QSystemTrayIcon.__init__( self, parent ) + super().__init__( parent ) self._ui_is_currently_shown = True self._ui_is_currently_minimised = False diff --git a/hydrus/client/gui/ClientGUITagSuggestions.py b/hydrus/client/gui/ClientGUITagSuggestions.py index f12c71039..dc83dae8e 100644 --- a/hydrus/client/gui/ClientGUITagSuggestions.py +++ b/hydrus/client/gui/ClientGUITagSuggestions.py @@ -72,7 +72,7 @@ class ListBoxTagsSuggestionsFavourites( ClientGUIListBoxes.ListBoxTagsStrings ): def __init__( self, parent, service_key, activate_callable, sort_tags = True ): - ClientGUIListBoxes.ListBoxTagsStrings.__init__( self, parent, service_key = service_key, sort_tags = sort_tags, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) + super().__init__( parent, service_key = service_key, sort_tags = sort_tags, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) self._activate_callable = activate_callable @@ -123,7 +123,7 @@ class ListBoxTagsSuggestionsRelated( ClientGUIListBoxes.ListBoxTagsPredicates ): def __init__( self, parent, service_key, activate_callable ): - ClientGUIListBoxes.ListBoxTagsPredicates.__init__( self, parent, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) + super().__init__( parent, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE ) self._activate_callable = activate_callable diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py index 80d7ad79a..e33599b1f 100644 --- a/hydrus/client/gui/ClientGUITags.py +++ b/hydrus/client/gui/ClientGUITags.py @@ -2611,7 +2611,6 @@ class _Panel( CAC.ApplicationCommandProcessorMixin, QW.QWidget ): def __init__( self, parent, location_context: ClientLocation.LocationContext, tag_service_key, tag_presentation_location: int, media: typing.List[ ClientMedia.MediaSingleton ], immediate_commit, canvas_key = None ): super().__init__( parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) self._location_context = location_context self._tag_service_key = tag_service_key @@ -3462,7 +3461,7 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent, tags = None ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) self._tag_services = ClientGUICommon.BetterNotebook( self ) @@ -4231,7 +4230,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent, tags = None ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) self._tag_services = ClientGUICommon.BetterNotebook( self ) @@ -5034,7 +5033,7 @@ class ReviewTagDisplayMaintenancePanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._tag_services_notebook = ClientGUICommon.BetterNotebook( self ) @@ -5332,7 +5331,7 @@ class TagFilterButton( ClientGUICommon.BetterButton ): def __init__( self, parent, message, tag_filter, only_show_blacklist = False, label_prefix = None ): - ClientGUICommon.BetterButton.__init__( self, parent, 'tag filter', self._EditTagFilter ) + super().__init__( parent, 'tag filter', self._EditTagFilter ) self._message = message self._tag_filter = tag_filter @@ -5777,7 +5776,7 @@ def __init__( self, parent: QW.QWidget, tag_summary_generator: TagSummaryGenerat label = tag_summary_generator.GenerateExampleSummary() - ClientGUICommon.BetterButton.__init__( self, parent, label, self._Edit ) + super().__init__( parent, label, self._Edit ) self._tag_summary_generator = tag_summary_generator diff --git a/hydrus/client/gui/ClientGUITopLevelWindows.py b/hydrus/client/gui/ClientGUITopLevelWindows.py index 1fe8f3d9d..f74428194 100644 --- a/hydrus/client/gui/ClientGUITopLevelWindows.py +++ b/hydrus/client/gui/ClientGUITopLevelWindows.py @@ -423,7 +423,7 @@ class NewDialog( QP.Dialog ): def __init__( self, parent, title, do_not_activate = False ): - QP.Dialog.__init__( self, parent ) + super().__init__( parent ) if do_not_activate: @@ -627,7 +627,7 @@ def __init__( self, parent, title, frame_key, do_not_activate = False ): self._frame_key = frame_key - NewDialog.__init__( self, parent, title, do_not_activate = do_not_activate ) + super().__init__( parent, title, do_not_activate = do_not_activate ) def _SaveOKPosition( self ): @@ -671,7 +671,7 @@ class MainFrame( QW.QMainWindow ): def __init__( self, parent, title ): - QW.QMainWindow.__init__( self, parent ) + super().__init__( parent ) self.setWindowTitle( title ) @@ -700,7 +700,7 @@ def __init__( self, parent, title, frame_key ): self._frame_key = frame_key - Frame.__init__( self, parent, title ) + super().__init__( parent, title ) self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged ) self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged ) @@ -731,7 +731,7 @@ def __init__( self, parent, title, frame_key ): self._frame_key = frame_key - MainFrame.__init__( self, parent, title ) + super().__init__( parent, title ) self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged ) self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged ) diff --git a/hydrus/client/gui/ClientGUITopLevelWindowsPanels.py b/hydrus/client/gui/ClientGUITopLevelWindowsPanels.py index fb217281e..9f5827da3 100644 --- a/hydrus/client/gui/ClientGUITopLevelWindowsPanels.py +++ b/hydrus/client/gui/ClientGUITopLevelWindowsPanels.py @@ -17,7 +17,7 @@ def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = self._panel = None self._hide_buttons = hide_buttons - ClientGUITopLevelWindows.DialogThatResizes.__init__( self, parent, title, frame_key, do_not_activate = do_not_activate ) + super().__init__( parent, title, frame_key, do_not_activate = do_not_activate ) self._InitialiseButtons() @@ -159,7 +159,7 @@ class DialogEdit( DialogApplyCancel ): def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = False ): - DialogApplyCancel.__init__( self, parent, title, frame_key = frame_key, hide_buttons = hide_buttons ) + super().__init__( parent, title, frame_key = frame_key, hide_buttons = hide_buttons ) @@ -190,7 +190,7 @@ class DialogCustomButtonQuestion( DialogThatTakesScrollablePanel ): def __init__( self, parent, title, frame_key = 'regular_center_dialog' ): - DialogThatTakesScrollablePanel.__init__( self, parent, title, frame_key = frame_key ) + super().__init__( parent, title, frame_key = frame_key ) def _GetButtonBox( self ): @@ -210,7 +210,7 @@ def __init__( self, parent, title, frame_key = 'regular_dialog' ): self._panel = None - ClientGUITopLevelWindows.FrameThatResizes.__init__( self, parent, title, frame_key ) + super().__init__( parent, title, frame_key ) self._ok = QW.QPushButton( 'close', self ) self._ok.clicked.connect( self.close ) diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py index 8b13273f3..0b96bf693 100644 --- a/hydrus/client/gui/QtPorting.py +++ b/hydrus/client/gui/QtPorting.py @@ -44,7 +44,7 @@ class HBoxLayout( QW.QHBoxLayout ): def __init__( self, margin = 2, spacing = 2 ): - QW.QHBoxLayout.__init__( self ) + super().__init__() self.setMargin( margin ) self.setSpacing( spacing ) @@ -60,7 +60,7 @@ class VBoxLayout( QW.QVBoxLayout ): def __init__( self, margin = 2, spacing = 2 ): - QW.QVBoxLayout.__init__( self ) + super().__init__() self.setMargin( margin ) self.setSpacing( spacing ) @@ -326,7 +326,7 @@ class TabBar( QW.QTabBar ): def __init__( self, parent = None ): - QW.QTabBar.__init__( self, parent ) + super().__init__( parent ) if HC.PLATFORM_MACOS: @@ -542,7 +542,7 @@ class TabWidgetWithDnD( QW.QTabWidget ): def __init__( self, parent = None ): - QW.QTabWidget.__init__( self, parent ) + super().__init__( parent ) self.setTabBar( TabBar( self ) ) @@ -992,7 +992,7 @@ class GridLayout( QW.QGridLayout ): def __init__( self, cols = 1, spacing = 2 ): - QW.QGridLayout.__init__( self ) + super().__init__() self._col_count = cols self.setMargin( 2 ) @@ -1282,7 +1282,7 @@ class CallAfterEvent( QC.QEvent ): def __init__( self, fn, *args, **kwargs ): - QC.QEvent.__init__( self, CallAfterEventType ) + super().__init__( CallAfterEventType ) self._fn = fn self._args = args @@ -1301,7 +1301,7 @@ class CallAfterEventCatcher( QC.QObject ): def __init__( self, parent ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self.installEventFilter( self ) @@ -1504,7 +1504,7 @@ class StatusBar( QW.QStatusBar ): def __init__( self, status_widths ): - QW.QStatusBar.__init__( self ) + super().__init__() self._labels = [] @@ -1575,7 +1575,7 @@ class EllipsizedLabel( QW.QLabel ): def __init__( self, parent = None, ellipsize_end = False ): - QW.QLabel.__init__( self, parent ) + super().__init__( parent ) self._ellipsize_end = ellipsize_end @@ -1720,7 +1720,7 @@ def __init__( self, parent = None, **kwargs ): del kwargs['title'] - QW.QDialog.__init__( self, parent, **kwargs ) + super().__init__( parent, **kwargs ) self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False ) @@ -1770,7 +1770,7 @@ class PasswordEntryDialog( Dialog ): def __init__( self, parent, message, caption ): - Dialog.__init__( self, parent ) + super().__init__( parent ) self.setWindowTitle( caption ) @@ -1809,7 +1809,7 @@ class DirDialog( QW.QFileDialog ): def __init__( self, parent = None, message = None ): - QW.QFileDialog.__init__( self, parent ) + super().__init__( parent ) if message is not None: self.setWindowTitle( message ) @@ -1856,7 +1856,7 @@ class FileDialog( QW.QFileDialog ): def __init__( self, parent = None, message = None, acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFile, default_filename = None, default_directory = None, wildcard = None, defaultSuffix = None ): - QW.QFileDialog.__init__( self, parent ) + super().__init__( parent ) if message is not None: @@ -1930,7 +1930,7 @@ class TreeWidgetWithInheritedCheckState( QW.QTreeWidget ): def __init__( self, *args, **kwargs ): - QW.QTreeWidget.__init__( self, *args, **kwargs ) + super().__init__( *args, **kwargs ) self.itemChanged.connect( self._HandleItemCheckStateUpdate ) @@ -2013,7 +2013,7 @@ def __init__( self, parent_widget ): self._parent_widget = parent_widget - QC.QObject.__init__( self, parent_widget ) + super().__init__( parent_widget ) parent_widget.installEventFilter( self ) diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py index 3ac450806..18abac4cc 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvas.py +++ b/hydrus/client/gui/canvas/ClientGUICanvas.py @@ -201,7 +201,7 @@ class CanvasLayout( QW.QLayout ): def __init__( self ): - QW.QLayout.__init__( self ) + super().__init__() self._current_drag_delta = QC.QPoint( 0, 0 ) @@ -1365,7 +1365,7 @@ class MediaContainerDragClickReportingFilter( QC.QObject ): def __init__( self, parent: Canvas ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._canvas = parent @@ -1399,7 +1399,7 @@ class CanvasPanel( Canvas ): def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext ): - Canvas.__init__( self, parent, location_context ) + super().__init__( parent, location_context ) self._page_key = page_key @@ -1647,7 +1647,7 @@ class CanvasWithDetails( Canvas ): def __init__( self, parent, location_context ): - Canvas.__init__( self, parent, location_context ) + super().__init__( parent, location_context ) CG.client_controller.sub( self, 'RedrawDetails', 'refresh_all_tag_presentation_gui' ) @@ -2107,7 +2107,7 @@ class CanvasWithHovers( CanvasWithDetails ): def __init__( self, parent, location_context ): - CanvasWithDetails.__init__( self, parent, location_context ) + super().__init__( parent, location_context ) self._hovers = [] @@ -2244,7 +2244,7 @@ def CleanBeforeDestroy( self ): self.setCursor( QG.QCursor( QC.Qt.ArrowCursor ) ) - CanvasWithDetails.CleanBeforeDestroy( self ) + super().CleanBeforeDestroy() def CloseFromHover( self, canvas_key ): @@ -2356,7 +2356,7 @@ def mouseMoveEvent( self, event ): - CanvasWithDetails.mouseMoveEvent( self, event ) + super().mouseMoveEvent( event ) def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): @@ -2387,7 +2387,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): if not command_processed: - command_processed = CanvasWithDetails.ProcessApplicationCommand( self, command ) + command_processed = super().ProcessApplicationCommand( command ) return command_processed @@ -2412,7 +2412,7 @@ def __init__( self, parent, file_search_context_1: ClientSearchFileSearchContext location_context = file_search_context_1.GetLocationContext() - CanvasWithHovers.__init__( self, parent, location_context ) + super().__init__( parent, location_context ) self._duplicates_right_hover = ClientGUICanvasHoverFrames.CanvasHoverFrameRightDuplicates( self, self, self._canvas_key ) @@ -2563,7 +2563,7 @@ def _Delete( self, media = None, reason = None, file_service_key = None ): default_reason = 'Deleted manually in Duplicate Filter, along with its potential duplicate.' - content_update_packages = CanvasWithHovers._Delete( self, media = media, default_reason = default_reason, file_service_key = file_service_key, just_get_content_update_packages = True ) + content_update_packages = super()._Delete( media = media, default_reason = default_reason, file_service_key = file_service_key, just_get_content_update_packages = True ) deleted = isinstance( content_update_packages, list ) and len( content_update_packages ) > 0 @@ -2691,7 +2691,7 @@ def _DrawBackgroundDetails( self, painter ): else: - CanvasWithHovers._DrawBackgroundDetails( self, painter ) + super()._DrawBackgroundDetails( painter ) @@ -3235,7 +3235,7 @@ def CleanBeforeDestroy( self ): ClientDuplicates.hashes_to_jpeg_quality = {} # clear the cache - CanvasWithHovers.CleanBeforeDestroy( self ) + super().CleanBeforeDestroy() def Delete( self, canvas_key ): @@ -3325,7 +3325,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): if not command_processed: - command_processed = CanvasWithHovers.ProcessApplicationCommand( self, command ) + command_processed = super().ProcessApplicationCommand( command ) return command_processed @@ -3361,7 +3361,7 @@ def catch_up(): def SetMedia( self, media ): - CanvasWithHovers.SetMedia( self, media ) + super().SetMedia( media ) if media is not None: @@ -3423,7 +3423,7 @@ def TryToDoPreClose( self ): - return CanvasWithHovers.TryToDoPreClose( self ) + return super().TryToDoPreClose() def Undelete( self, canvas_key ): @@ -3495,7 +3495,7 @@ def TryToDoPreClose( self ): self.exitFocusMedia.emit( self._current_media ) - return CanvasWithHovers.TryToDoPreClose( self ) + return super().TryToDoPreClose() def _GenerateHoverTopFrame( self ): @@ -3796,7 +3796,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ): def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ): - CanvasMediaList.__init__( self, parent, page_key, location_context, media_results ) + super().__init__( parent, page_key, location_context, media_results ) self._my_shortcuts_handler.AddShortcuts( 'archive_delete_filter' ) @@ -4103,7 +4103,7 @@ class CanvasMediaListNavigable( CanvasMediaList ): def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ): - CanvasMediaList.__init__( self, parent, page_key, location_context, media_results ) + super().__init__( parent, page_key, location_context, media_results ) self._my_shortcuts_handler.AddShortcuts( 'media_viewer_browser' ) @@ -4248,7 +4248,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ): def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results, first_hash ): - CanvasMediaListNavigable.__init__( self, parent, page_key, location_context, media_results ) + super().__init__( parent, page_key, location_context, media_results ) self._slideshow_is_running = False self._last_slideshow_switch_time = 0 @@ -4531,7 +4531,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): if not command_processed: - command_processed = CanvasMediaListNavigable.ProcessApplicationCommand( self, command ) + command_processed = super().ProcessApplicationCommand( command ) return command_processed @@ -4721,11 +4721,11 @@ def ShowMenu( self ): if len( local_duplicable_to_file_service_keys ) > 0 or len( local_moveable_from_and_to_file_service_keys ) > 0: - files_menu = ClientGUIMenus.GenerateMenu( menu ) + locations_menu = ClientGUIMenus.GenerateMenu( menu ) - ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, files_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand ) + ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, locations_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand ) - ClientGUIMenus.AppendMenu( menu, files_menu, 'files' ) + ClientGUIMenus.AppendMenu( menu, locations_menu, 'locations' ) ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._current_media, 1 ) @@ -4740,7 +4740,7 @@ def ShowMenu( self ): def TIMERUIUpdate( self ): - CanvasMediaListNavigable.TIMERUIUpdate( self ) + super().TIMERUIUpdate() if self._slideshow_is_running: diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py index f2025fb0b..b480a21a8 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py +++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py @@ -40,7 +40,7 @@ class RatingIncDecCanvas( ClientGUIRatings.RatingIncDec ): def __init__( self, parent, service_key, canvas_key ): - ClientGUIRatings.RatingIncDec.__init__( self, parent, service_key ) + super().__init__( parent, service_key ) self._canvas_key = canvas_key self._current_media = None @@ -138,7 +138,7 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ): def __init__( self, parent, service_key, canvas_key ): - ClientGUIRatings.RatingLike.__init__( self, parent, service_key ) + super().__init__( parent, service_key ) self._canvas_key = canvas_key self._current_media = None @@ -253,7 +253,7 @@ class RatingNumericalCanvas( ClientGUIRatings.RatingNumerical ): def __init__( self, parent, service_key, canvas_key ): - ClientGUIRatings.RatingNumerical.__init__( self, parent, service_key ) + super().__init__( parent, service_key ) self._canvas_key = canvas_key self._current_media = None @@ -377,7 +377,7 @@ def __init__( self, parent: QW.QWidget, my_canvas, canvas_key ): # this took some hacks, and there is still a bunch of focus and TLW checking code going on here that needs to be cleaned up # note I tried to have them just lower rather than hide and it looked really stupid, so that thought is dead for the current moment. atm I just want to do the same thing as before with no graphics errors - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised ) self.setLineWidth( 2 ) diff --git a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py index d095072e8..f06baabd0 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py +++ b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py @@ -1324,7 +1324,7 @@ class MediaContainerLayout( QW.QLayout ): def __init__( self, static_image ): - QW.QLayout.__init__( self ) + super().__init__() self._static_image = static_image diff --git a/hydrus/client/gui/canvas/ClientGUIMPV.py b/hydrus/client/gui/canvas/ClientGUIMPV.py index a251a82f1..ccee745b0 100644 --- a/hydrus/client/gui/canvas/ClientGUIMPV.py +++ b/hydrus/client/gui/canvas/ClientGUIMPV.py @@ -160,7 +160,7 @@ class MPVHellBasket( QC.QObject ): def __init__( self ): - QC.QObject.__init__( self ) + super().__init__() MPVHellBasket.my_instance = self diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py index 62c80036d..cf81575e6 100644 --- a/hydrus/client/gui/exporting/ClientGUIExport.py +++ b/hydrus/client/gui/exporting/ClientGUIExport.py @@ -47,7 +47,7 @@ class EditExportFoldersPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, export_folders ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._export_folders_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -235,7 +235,7 @@ class EditExportFolderPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, export_folder: ClientExportingFiles.ExportFolder ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._export_folder = export_folder @@ -565,7 +565,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, flat_media, do_export_and_then_quit = False ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) tag_presentation_location = CC.TAG_PRESENTATION_SEARCH_PAGE diff --git a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py index e70d7d8fc..8a0a4795b 100644 --- a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py +++ b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py @@ -327,7 +327,7 @@ class EditFileSeedCachePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, controller, file_seed_cache: ClientImportFileSeeds.FileSeedCache ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._controller = controller self._file_seed_cache = file_seed_cache @@ -896,7 +896,7 @@ def __init__( self, parent, controller, file_seed_cache_get_callable, file_seed_ action.triggered.connect( self._ShowFileSeedCacheFrame ) - ClientGUICommon.ButtonWithMenuArrow.__init__( self, parent, action ) + super().__init__( parent, action ) def _PopulateMenu( self, menu ): @@ -960,7 +960,7 @@ class FileSeedCacheStatusControl( QW.QFrame ): def __init__( self, parent, controller, page_key = None ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) diff --git a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py index a0c7cf9b1..8c3ccec5d 100644 --- a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py +++ b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py @@ -246,7 +246,7 @@ class EditGallerySeedLogPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, controller, read_only: bool, can_generate_more_pages: bool, gallery_type_string: str, gallery_seed_log: ClientImportGallerySeeds.GallerySeedLog ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._controller = controller self._read_only = read_only @@ -592,7 +592,7 @@ def __init__( self, parent, controller, read_only: bool, can_generate_more_pages action.triggered.connect( self._ShowGallerySeedLogFrame ) - ClientGUICommon.ButtonWithMenuArrow.__init__( self, parent, action ) + super().__init__( parent, action ) def _PopulateMenu( self, menu ): @@ -656,7 +656,7 @@ class GallerySeedLogStatusControl( QW.QFrame ): def __init__( self, parent, controller, read_only: bool, can_generate_more_pages: bool, gallery_type_string: str, page_key = None ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) self._controller = controller diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py index 243fcfc6f..9440e88a2 100644 --- a/hydrus/client/gui/importing/ClientGUIImport.py +++ b/hydrus/client/gui/importing/ClientGUIImport.py @@ -51,7 +51,7 @@ class CheckerOptionsButton( ClientGUICommon.BetterButton ): def __init__( self, parent, checker_options: ClientImportOptions.CheckerOptions ): - ClientGUICommon.BetterButton.__init__( self, parent, 'checker options', self._EditOptions ) + super().__init__( parent, 'checker options', self._EditOptions ) self._checker_options = checker_options @@ -836,7 +836,7 @@ def __init__( self, parent, paths ): # but only if the UI can stay helpful. maybe we shouldn't replace the easy UI, but we can replace the guts behind the scenes with metadata routers # however, changing service while maintaining focus and list selection would be great - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._paths = paths @@ -1215,7 +1215,7 @@ class EditFilenameTaggingOptionPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, service_key, filename_tagging_options ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -1307,7 +1307,7 @@ class GalleryImportPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, page_key, name = 'gallery query' ): - ClientGUICommon.StaticBox.__init__( self, parent, name ) + super().__init__( parent, name ) self._page_key = page_key @@ -1579,7 +1579,7 @@ class GUGKeyAndNameSelector( ClientGUICommon.BetterButton ): def __init__( self, parent, gug_key_and_name, update_callable = None ): - ClientGUICommon.BetterButton.__init__( self, parent, 'gallery selector', self._Edit ) + super().__init__( parent, 'gallery selector', self._Edit ) gug = CG.client_controller.network_engine.domain_manager.GetGUG( gug_key_and_name ) @@ -1733,7 +1733,7 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, page_key, name = 'watcher' ): - ClientGUICommon.StaticBox.__init__( self, parent, name ) + super().__init__( parent, name ) self._page_key = page_key self._watcher = None diff --git a/hydrus/client/gui/importing/ClientGUIImportFolders.py b/hydrus/client/gui/importing/ClientGUIImportFolders.py index 784fdc6c8..c9b8f7925 100644 --- a/hydrus/client/gui/importing/ClientGUIImportFolders.py +++ b/hydrus/client/gui/importing/ClientGUIImportFolders.py @@ -34,7 +34,7 @@ class EditImportFoldersPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, import_folders ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) import_folders_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -175,7 +175,7 @@ class EditImportFolderPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, import_folder: ClientImportLocal.ImportFolder ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._import_folder = import_folder diff --git a/hydrus/client/gui/importing/ClientGUIImportOptions.py b/hydrus/client/gui/importing/ClientGUIImportOptions.py index 2b8bb3074..f49cb87a4 100644 --- a/hydrus/client/gui/importing/ClientGUIImportOptions.py +++ b/hydrus/client/gui/importing/ClientGUIImportOptions.py @@ -41,7 +41,7 @@ class EditFileImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, file_import_options: FileImportOptions.FileImportOptions, show_downloader_options: bool, allow_default_selection: bool ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp ) @@ -564,7 +564,7 @@ class EditNoteImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, note_import_options: NoteImportOptions.NoteImportOptions, allow_default_selection: bool, simple_mode = False ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._allow_default_selection = allow_default_selection self._simple_mode = simple_mode @@ -887,7 +887,7 @@ class EditPresentationImportOptions( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, presentation_import_options: PresentationImportOptions.PresentationImportOptions ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1039,7 +1039,7 @@ class EditServiceTagImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, service_key: bytes, service_tag_import_options: TagImportOptions.ServiceTagImportOptions, show_downloader_options: bool = True ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._show_downloader_options = show_downloader_options @@ -1253,7 +1253,7 @@ class EditTagImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, tag_import_options: TagImportOptions.TagImportOptions, show_downloader_options: bool, allow_default_selection: bool ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._show_downloader_options = show_downloader_options @@ -1608,7 +1608,7 @@ class EditImportOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, show_downloader_options: bool, allow_default_selection: bool ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._show_downloader_options = show_downloader_options self._allow_default_selection = allow_default_selection @@ -1803,7 +1803,7 @@ def __init__( self, parent, show_downloader_options: bool, allow_default_selecti action.triggered.connect( self._EditOptions ) - ClientGUICommon.ButtonWithMenuArrow.__init__( self, parent, action ) + super().__init__( parent, action ) self._show_downloader_options = show_downloader_options self._allow_default_selection = allow_default_selection diff --git a/hydrus/client/gui/lists/ClientGUIListBook.py b/hydrus/client/gui/lists/ClientGUIListBook.py index f6311b381..afcd82bf1 100644 --- a/hydrus/client/gui/lists/ClientGUIListBook.py +++ b/hydrus/client/gui/lists/ClientGUIListBook.py @@ -11,7 +11,7 @@ class ListBook( QW.QWidget ): def __init__( self, *args, **kwargs ): - QW.QWidget.__init__( self, *args, **kwargs ) + super().__init__( *args, **kwargs ) self._keys_to_active_pages = {} diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py index 7063d8b83..f8f9e93ae 100644 --- a/hydrus/client/gui/lists/ClientGUIListBoxes.py +++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py @@ -1078,7 +1078,7 @@ class ListBox( QW.QScrollArea ): 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 ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken ) self.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff ) self.setVerticalScrollBarPolicy( QC.Qt.ScrollBarAsNeeded ) @@ -2495,7 +2495,7 @@ def __init__( self, parent, *args, tag_display_type: int = ClientTags.TAG_DISPLA terms_may_have_sibling_or_parent_info = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE - ListBox.__init__( self, parent, terms_may_have_sibling_or_parent_info, *args, **kwargs ) + super().__init__( parent, terms_may_have_sibling_or_parent_info, *args, **kwargs ) self.setObjectName( 'HydrusTagList' ) @@ -3674,7 +3674,7 @@ class ListBoxTagsPredicates( ListBoxTags ): def __init__( self, *args, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, **kwargs ): - ListBoxTags.__init__( self, *args, tag_display_type = tag_display_type, **kwargs ) + super().__init__( *args, tag_display_type = tag_display_type, **kwargs ) def _GenerateTermFromPredicate( self, predicate: ClientSearchPredicate.Predicate ) -> ClientGUIListBoxesData.ListBoxItemPredicate: @@ -3731,7 +3731,7 @@ class ListBoxTagsColourOptions( ListBoxTags ): def __init__( self, parent, initial_namespace_colours ): - ListBoxTags.__init__( self, parent ) + super().__init__( parent ) terms = [] @@ -3838,7 +3838,7 @@ class ListBoxTagsFilter( ListBoxTags ): def __init__( self, parent, read_only = False ): - ListBoxTags.__init__( self, parent ) + super().__init__( parent ) self._read_only = read_only @@ -3941,7 +3941,7 @@ def __init__( self, parent, service_key = None, tag_display_type = ClientTags.TA has_async_text_info = tag_display_type == ClientTags.TAG_DISPLAY_STORAGE - ListBoxTags.__init__( self, parent, has_async_text_info = has_async_text_info, tag_display_type = tag_display_type, **kwargs ) + super().__init__( parent, has_async_text_info = has_async_text_info, tag_display_type = tag_display_type, **kwargs ) self.listBoxChanged.connect( self._NotifyListBoxChanged ) @@ -4065,7 +4065,7 @@ def __init__( self, parent, service_key = None, sort_tags = True, **kwargs ): self._sort_tags = sort_tags - ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, **kwargs ) + super().__init__( parent, service_key = service_key, **kwargs ) self.listBoxChanged.connect( self._NotifyListBoxChanged ) @@ -4244,7 +4244,7 @@ def __init__( self, parent: QW.QWidget, tag_display_type: int, tag_presentation_ service_key = CC.COMBINED_TAG_SERVICE_KEY - ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, tag_display_type = tag_display_type, height_num_chars = 24 ) + super().__init__( parent, service_key = service_key, tag_display_type = tag_display_type, height_num_chars = 24 ) self._tag_presentation_location = tag_presentation_location @@ -4552,16 +4552,16 @@ def SetTagsByMediaResults( self, media_results ): self._DataHasChanged() - def SetTagsByMediaFromMediaPanel( self, media, tags_changed ): + def SetTagsByMediaFromMediaResultsPanel( self, media, tags_changed ): flat_media = ClientMedia.FlattenMedia( media ) media_results = [ m.GetMediaResult() for m in flat_media ] - self.SetTagsByMediaResultsFromMediaPanel( media_results, tags_changed ) + self.SetTagsByMediaResultsFromMediaResultsPanel( media_results, tags_changed ) - def SetTagsByMediaResultsFromMediaPanel( self, media_results, tags_changed ): + def SetTagsByMediaResultsFromMediaResultsPanel( self, media_results, tags_changed ): if not isinstance( media_results, set ): @@ -4667,7 +4667,7 @@ class StaticBoxSorterForListBoxTags( ClientGUICommon.StaticBox ): def __init__( self, parent, title, tag_presentation_location: int, show_siblings_sort = False ): - ClientGUICommon.StaticBox.__init__( self, parent, title ) + super().__init__( parent, title ) self._original_title = title @@ -4735,7 +4735,7 @@ class ListBoxTagsMediaHoverFrame( ListBoxTagsMedia ): def __init__( self, parent, canvas_key, location_context: ClientLocation.LocationContext ): - ListBoxTagsMedia.__init__( self, parent, ClientTags.TAG_DISPLAY_SINGLE_MEDIA, CC.TAG_PRESENTATION_MEDIA_VIEWER, include_counts = False ) + super().__init__( parent, ClientTags.TAG_DISPLAY_SINGLE_MEDIA, CC.TAG_PRESENTATION_MEDIA_VIEWER, include_counts = False ) self._canvas_key = canvas_key self._location_context = location_context @@ -4758,7 +4758,7 @@ class ListBoxTagsMediaTagsDialog( ListBoxTagsMedia ): def __init__( self, parent, tag_presentation_location, enter_func, delete_func ): - ListBoxTagsMedia.__init__( self, parent, ClientTags.TAG_DISPLAY_STORAGE, tag_presentation_location, include_counts = True ) + super().__init__( parent, ClientTags.TAG_DISPLAY_STORAGE, tag_presentation_location, include_counts = True ) self._enter_func = enter_func self._delete_func = delete_func diff --git a/hydrus/client/gui/media/ClientGUIMediaControls.py b/hydrus/client/gui/media/ClientGUIMediaControls.py index 622edb50e..3418b0ffe 100644 --- a/hydrus/client/gui/media/ClientGUIMediaControls.py +++ b/hydrus/client/gui/media/ClientGUIMediaControls.py @@ -55,7 +55,7 @@ def __init__( self, parent, volume_type ): pixmap = self._GetCorrectPixmap() - ClientGUICommon.BetterBitmapButton.__init__( self, parent, pixmap, FlipMute, self._volume_type ) + super().__init__( parent, pixmap, FlipMute, self._volume_type ) CG.client_controller.sub( self, 'UpdateMute', 'new_audio_mute' ) @@ -138,7 +138,7 @@ class _PopupWindow( QW.QFrame ): def __init__( self, parent, canvas_type, direction = 'down' ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self._canvas_type = canvas_type @@ -278,7 +278,7 @@ class VolumeSlider( QW.QSlider ): def __init__( self, parent, volume_type ): - QW.QSlider.__init__( self, parent ) + super().__init__( parent ) self._volume_type = volume_type diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py index 0e47b4cc2..e59657d98 100644 --- a/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py +++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigration.py @@ -33,7 +33,7 @@ class EditSingleFileMetadataRouterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, router: ClientMetadataMigration.SingleFileMetadataRouter, allowed_importer_classes: list, allowed_exporter_classes: list, test_context_factory: ClientGUIMetadataMigrationTest.MigrationTestContextFactory ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_router = router self._allowed_importer_classes = allowed_importer_classes @@ -69,7 +69,7 @@ def __init__( self, parent: QW.QWidget, router: ClientMetadataMigration.SingleFi self._processing_panel = ClientGUICommon.StaticBox( self, 'processing' ) - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( self._processing_panel, string_processor, self._GetExampleStringProcessorTestData ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( self._processing_panel, string_processor, self._GetExampleStringProcessorTestData ) st = ClientGUICommon.BetterStaticText( self._processing_panel, 'You can alter all the texts before export here.' ) @@ -240,7 +240,7 @@ class SingleFileMetadataRoutersControl( ClientGUIListBoxes.AddEditDeleteListBox def __init__( self, parent: QW.QWidget, routers: typing.Collection[ ClientMetadataMigration.SingleFileMetadataRouter ], allowed_importer_classes: list, allowed_exporter_classes: list, test_context_factory: ClientGUIMetadataMigrationTest.MigrationTestContextFactory ): - ClientGUIListBoxes.AddEditDeleteListBox.__init__( self, parent, 5, convert_router_to_pretty_string, self._AddRouter, self._EditRouter ) + super().__init__( parent, 5, convert_router_to_pretty_string, self._AddRouter, self._EditRouter ) self._allowed_importer_classes = allowed_importer_classes self._allowed_exporter_classes = allowed_exporter_classes @@ -333,7 +333,7 @@ class SingleFileMetadataRoutersButton( QW.QPushButton ): def __init__( self, parent: QW.QWidget, routers: typing.Collection[ ClientMetadataMigration.SingleFileMetadataRouter ], allowed_importer_classes: list, allowed_exporter_classes: list, test_context_factory: ClientGUIMetadataMigrationTest.MigrationTestContextFactory ): - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) self._routers = routers self._allowed_importer_classes = allowed_importer_classes diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py index 2b715ac53..e949a578a 100644 --- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py +++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationCommon.py @@ -11,7 +11,7 @@ class EditSidecarDetailsPanel( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget ): - ClientGUICommon.StaticBox.__init__( self, parent, 'sidecar filename' ) + super().__init__( parent, 'sidecar filename' ) self._sidecar_ext = 'txt' @@ -137,7 +137,7 @@ class EditSidecarTXTSeparator( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget ): - ClientGUICommon.StaticBox.__init__( self, parent, 'sidecar txt separator' ) + super().__init__( parent, 'sidecar txt separator' ) self._choice = ClientGUICommon.BetterChoice( self ) diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py index c1e953acd..f0815c381 100644 --- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py +++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationExporters.py @@ -56,7 +56,7 @@ class EditSingleFileMetadataExporterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter, allowed_exporter_classes: list ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_exporter = exporter self._allowed_exporter_classes = allowed_exporter_classes @@ -450,7 +450,7 @@ class SingleFileMetadataExporterButton( QW.QPushButton ): def __init__( self, parent: QW.QWidget, exporter: ClientMetadataMigrationExporters.SingleFileMetadataExporter, allowed_exporter_classes: list ): - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) self._exporter = exporter self._allowed_exporter_classes = allowed_exporter_classes diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py index f2bace5ae..c9fdea062 100644 --- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py +++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationImporters.py @@ -59,7 +59,7 @@ class EditSingleFileMetadataImporterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, importer: ClientMetadataMigrationImporters.SingleFileMetadataImporter, allowed_importer_classes: list ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_importer = importer self._allowed_importer_classes = allowed_importer_classes @@ -134,7 +134,7 @@ def __init__( self, parent: QW.QWidget, importer: ClientMetadataMigrationImporte self._string_processor_panel = QW.QWidget( self ) - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( self, string_processor, self._GetExampleTestData ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( self, string_processor, self._GetExampleTestData ) tt = 'You can alter the texts that come in through this source here.' self._string_processor_button.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) ) @@ -430,7 +430,7 @@ class SingleFileMetadataImportersControl( ClientGUIListBoxes.AddEditDeleteListBo def __init__( self, parent: QW.QWidget, importers: typing.Collection[ ClientMetadataMigrationImporters.SingleFileMetadataImporter ], allowed_importer_classes: list ): - ClientGUIListBoxes.AddEditDeleteListBox.__init__( self, parent, 5, convert_importer_to_pretty_string, self._AddImporter, self._EditImporter ) + super().__init__( parent, 5, convert_importer_to_pretty_string, self._AddImporter, self._EditImporter ) self._allowed_importer_classes = allowed_importer_classes diff --git a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationTest.py b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationTest.py index 939b42b7c..c9849053c 100644 --- a/hydrus/client/gui/metadata/ClientGUIMetadataMigrationTest.py +++ b/hydrus/client/gui/metadata/ClientGUIMetadataMigrationTest.py @@ -32,7 +32,7 @@ class MigrationTestContextFactorySidecar( MigrationTestContextFactory ): def __init__( self, example_file_paths: typing.Collection[ str ] ): - MigrationTestContextFactory.__init__( self ) + super().__init__() self._example_file_paths = example_file_paths @@ -62,7 +62,7 @@ class MigrationTestContextFactoryMedia( MigrationTestContextFactory ): def __init__( self, example_media_results: typing.Collection[ ClientMediaResult.MediaResult ] ): - MigrationTestContextFactory.__init__( self ) + super().__init__() self._example_media_results = example_media_results diff --git a/hydrus/client/gui/metadata/ClientGUIMigrateTags.py b/hydrus/client/gui/metadata/ClientGUIMigrateTags.py index cebb51acd..98678ebe0 100644 --- a/hydrus/client/gui/metadata/ClientGUIMigrateTags.py +++ b/hydrus/client/gui/metadata/ClientGUIMigrateTags.py @@ -44,7 +44,7 @@ class MigrateTagsPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, service_key, hashes = None ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._hashes = hashes @@ -142,16 +142,22 @@ def __init__( self, parent, service_key, hashes = None ): self._migration_source_worse_must_have_count = QW.QCheckBox( self._pair_have_count_panel ) self._migration_source_parent_must_have_count = QW.QCheckBox( self._pair_have_count_panel ) self._migration_source_ideal_must_have_count = QW.QCheckBox( self._pair_have_count_panel ) + self._migration_source_child_or_parent_must_have_count = QW.QCheckBox( self._pair_have_count_panel ) + self._migration_source_worse_or_ideal_must_have_count = QW.QCheckBox( self._pair_have_count_panel ) self._migration_source_child_must_have_count.setText( 'only if child (left) side has count' ) self._migration_source_worse_must_have_count.setText( 'only if worse (left) side has count' ) self._migration_source_parent_must_have_count.setText( 'only if parent (right) side has count' ) - self._migration_source_ideal_must_have_count.setText( 'only if ideal (where right side terminates) has count' ) + self._migration_source_ideal_must_have_count.setText( 'only if ideal (where right side\'s chain terminates) has count' ) + self._migration_source_child_or_parent_must_have_count.setText( 'only if the child or parent has count' ) + self._migration_source_worse_or_ideal_must_have_count.setText( 'only if the worse or ideal has count' ) self._migration_source_child_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the child (left) side has an actual real mappings count in the service.' ) ) self._migration_source_worse_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the worse (left) side has an actual real mappings count in the service.' ) ) self._migration_source_parent_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the parent (right) side has an actual real mappings count in the service.' ) ) self._migration_source_ideal_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the ideal (where the chain of the right side terminates) has an actual real mappings count in the service.' ) ) + self._migration_source_child_or_parent_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the child (left) or parent (right) side has an actual real mappings count in the service.' ) ) + self._migration_source_worse_or_ideal_must_have_count.setToolTip( ClientGUIFunctions.WrapToolTip( 'Only include this pair if the worse (left) or ideal (where the chain of the right side terminates) side has an actual real mappings count in the service.' ) ) self._migration_source_have_count_service = ClientGUICommon.BetterChoice( self._pair_have_count_panel ) @@ -195,6 +201,8 @@ def __init__( self, parent, service_key, hashes = None ): QP.AddToLayout( have_count_vbox, self._migration_source_worse_must_have_count, CC.FLAGS_EXPAND_BOTH_WAYS ) QP.AddToLayout( have_count_vbox, self._migration_source_parent_must_have_count, CC.FLAGS_EXPAND_BOTH_WAYS ) QP.AddToLayout( have_count_vbox, self._migration_source_ideal_must_have_count, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( have_count_vbox, self._migration_source_child_or_parent_must_have_count, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( have_count_vbox, self._migration_source_worse_or_ideal_must_have_count, CC.FLAGS_EXPAND_BOTH_WAYS ) QP.AddToLayout( have_count_vbox, ClientGUICommon.WrapInText( self._migration_source_have_count_service, self._pair_have_count_panel, 'in service: ' ), CC.FLAGS_EXPAND_BOTH_WAYS ) self._pair_have_count_panel.setLayout( have_count_vbox ) @@ -287,11 +295,13 @@ def __init__( self, parent, service_key, hashes = None ): self._migration_source_child_must_have_count.clicked.connect( self._UpdateMigrationControlsPairCount ) self._migration_source_ideal_must_have_count.clicked.connect( self._UpdateMigrationControlsPairCount ) self._migration_source_parent_must_have_count.clicked.connect( self._UpdateMigrationControlsPairCount ) + self._migration_source_child_or_parent_must_have_count.clicked.connect( self._UpdateMigrationControlsPairCount ) + self._migration_source_worse_or_ideal_must_have_count.clicked.connect( self._UpdateMigrationControlsPairCount ) def _MigrationGo( self ): - extra_info = '' + extra_filter_info_strings = [] source_content_statuses_strings = { ( HC.CONTENT_STATUS_CURRENT, ) : 'current', @@ -347,7 +357,7 @@ def _MigrationGo( self ): location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_FILE_SERVICE_KEY ) hashes = self._hashes - extra_info = ' for {} files'.format( HydrusNumbers.ToHumanInt( len( hashes ) ) ) + extra_filter_info_strings.append( 'for {} files'.format( HydrusNumbers.ToHumanInt( len( hashes ) ) ) ) else: @@ -356,17 +366,17 @@ def _MigrationGo( self ): if location_context.IsAllKnownFiles(): - extra_info = ' for all known files' + extra_filter_info_strings.append( 'for all known files' ) else: - extra_info = ' for files in "{}"'.format( location_context.ToString( CG.client_controller.services_manager.GetName ) ) + extra_filter_info_strings.append( 'for files in "{}"'.format( location_context.ToString( CG.client_controller.services_manager.GetName ) ) ) tag_filter = self._migration_source_tag_filter.GetValue() - extra_info += ' and for tags "{}"'.format( HydrusText.ElideText( tag_filter.ToPermittedString(), 96 ) ) + extra_filter_info_strings.append( 'for tags "{}"'.format( HydrusText.ElideText( tag_filter.ToPermittedString(), 96 ) ) ) if source_service_key == self.HTA_SERVICE_KEY: @@ -414,25 +424,72 @@ def _MigrationGo( self ): if left_s == right_s: - extra_info = ' for "{}" on both sides'.format( left_s ) + extra_filter_info_strings.append( f'for "{left_s}" on both sides' ) else: - extra_info = ' for "{}" on the left and "{}" on the right'.format( left_s, right_s ) + extra_filter_info_strings.append( f'for "{left_s}" on the left' ) + extra_filter_info_strings.append( f'for "{right_s}" on the right' ) + needs_count_service_key = self._migration_source_have_count_service.GetValue() + + needs_count_service_name = CG.client_controller.services_manager.GetName( needs_count_service_key ) + if content_type == HC.CONTENT_TYPE_TAG_SIBLINGS: - left_side_needs_count = self._migration_source_worse_must_have_count.isChecked() - right_side_needs_count = self._migration_source_ideal_must_have_count.isChecked() + either_side_needs_count = self._migration_source_worse_or_ideal_must_have_count.isChecked() + + if either_side_needs_count: + + left_side_needs_count = False + right_side_needs_count = False + + extra_filter_info_strings.append( f'where the worse or ideal tag of each pair has count on "{needs_count_service_name}"' ) + + else: + + left_side_needs_count = self._migration_source_worse_must_have_count.isChecked() + right_side_needs_count = self._migration_source_ideal_must_have_count.isChecked() + + if left_side_needs_count: + + extra_filter_info_strings.append( f'where the worse tag of each pair has count on "{needs_count_service_name}"' ) + + + if right_side_needs_count: + + extra_filter_info_strings.append( f'where the ideal tag of each pair\'s chain has count on "{needs_count_service_name}"' ) + + else: - left_side_needs_count = self._migration_source_child_must_have_count.isChecked() - right_side_needs_count = self._migration_source_parent_must_have_count.isChecked() + either_side_needs_count = self._migration_source_child_or_parent_must_have_count.isChecked() + + if either_side_needs_count: + + left_side_needs_count = False + right_side_needs_count = False + + extra_filter_info_strings.append( f'where the child or parent tag of each pair has count on "{needs_count_service_name}"' ) + + else: + + left_side_needs_count = self._migration_source_child_must_have_count.isChecked() + right_side_needs_count = self._migration_source_parent_must_have_count.isChecked() + + if left_side_needs_count: + + extra_filter_info_strings.append( f'where the child tag of each pair has count on "{needs_count_service_name}"' ) + + + if right_side_needs_count: + + extra_filter_info_strings.append( f'where the parent tag of each pair has count on "{needs_count_service_name}"' ) + + - - needs_count_service_key = self._migration_source_have_count_service.GetValue() if source_service_key == self.HTPA_SERVICE_KEY: @@ -443,15 +500,24 @@ def _MigrationGo( self ): return - source = ClientMigration.MigrationSourceHTPA( CG.client_controller, self._source_archive_path, content_type, left_tag_pair_filter, right_tag_pair_filter, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceHTPA( CG.client_controller, self._source_archive_path, content_type, left_tag_pair_filter, right_tag_pair_filter, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) else: - source = ClientMigration.MigrationSourceTagServicePairs( CG.client_controller, source_service_key, content_type, left_tag_pair_filter, right_tag_pair_filter, content_statuses, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceTagServicePairs( CG.client_controller, source_service_key, content_type, left_tag_pair_filter, right_tag_pair_filter, content_statuses, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) - title = 'taking {} {}{} from "{}" and {} "{}"'.format( source_content_statuses_strings[ content_statuses ], HC.content_type_string_lookup[ content_type ], extra_info, source.GetName(), destination_action_strings[ content_action ], destination.GetName() ) + if len( extra_filter_info_strings ) > 0: + + extra_info = ' ' + ' and '.join( extra_filter_info_strings ) + + else: + + extra_info = '' + + + title = f'taking {source_content_statuses_strings[ content_statuses ]} {HC.content_type_string_lookup[ content_type ]}{extra_info} from "{source.GetName()}" and {destination_action_strings[ content_action ]} "{destination.GetName()}"' message = 'Migrations can make huge changes. They can be cancelled early, but any work they do cannot always be undone. Please check that this summary looks correct:' message += '\n' * 2 @@ -862,10 +928,11 @@ def _UpdateMigrationControlsNewType( self ): self._migration_source_child_must_have_count.setVisible( not we_siblings ) self._migration_source_parent_must_have_count.setVisible( not we_siblings ) + self._migration_source_child_or_parent_must_have_count.setVisible( not we_siblings ) self._migration_source_worse_must_have_count.setVisible( we_siblings ) self._migration_source_ideal_must_have_count.setVisible( we_siblings ) - + self._migration_source_worse_or_ideal_must_have_count.setVisible( we_siblings ) self._migration_source.SetValue( self._service_key ) @@ -882,13 +949,37 @@ def _UpdateMigrationControlsPairCount( self ): if content_type == HC.CONTENT_TYPE_TAG_SIBLINGS: - enable_it = self._migration_source_worse_must_have_count.isChecked() or self._migration_source_ideal_must_have_count.isChecked() + enable_individual = not self._migration_source_worse_or_ideal_must_have_count.isChecked() + + self._migration_source_worse_must_have_count.setEnabled( enable_individual ) + self._migration_source_ideal_must_have_count.setEnabled( enable_individual ) + + if not enable_individual: + + enable_service = True + + else: + + enable_service = self._migration_source_worse_must_have_count.isChecked() or self._migration_source_ideal_must_have_count.isChecked() + else: - enable_it = self._migration_source_child_must_have_count.isChecked() or self._migration_source_parent_must_have_count.isChecked() + enable_individual = not self._migration_source_child_or_parent_must_have_count.isChecked() + + self._migration_source_child_must_have_count.setEnabled( enable_individual ) + self._migration_source_parent_must_have_count.setEnabled( enable_individual ) + + if not enable_individual: + + enable_service = True + + else: + + enable_service = self._migration_source_child_must_have_count.isChecked() or self._migration_source_parent_must_have_count.isChecked() + - self._migration_source_have_count_service.setEnabled( enable_it ) + self._migration_source_have_count_service.setEnabled( enable_service ) diff --git a/hydrus/client/gui/metadata/ClientGUITime.py b/hydrus/client/gui/metadata/ClientGUITime.py index 6c8f5dd62..773819bc3 100644 --- a/hydrus/client/gui/metadata/ClientGUITime.py +++ b/hydrus/client/gui/metadata/ClientGUITime.py @@ -52,7 +52,7 @@ class EditCheckerOptions( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, checker_options ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp ) help_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Show help regarding these checker options.' ) ) @@ -591,7 +591,7 @@ class DateTimesButton( ClientGUICommon.BetterButton ): def __init__( self, parent, time_allowed = True, milliseconds_allowed = False, none_allowed = False, only_past_dates = False ): - ClientGUICommon.BetterButton.__init__( self, parent, 'initialising', self._EditDateTime ) + super().__init__( parent, 'initialising', self._EditDateTime ) self._time_allowed = time_allowed self._milliseconds_allowed = milliseconds_allowed @@ -1013,7 +1013,7 @@ class TimeDeltaButton( QW.QPushButton ): def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, monthly_allowed = False ): - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) self._min = min self._show_days = days diff --git a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py index 5b9b0bfb7..2581a00d2 100644 --- a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py +++ b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py @@ -32,7 +32,7 @@ class EditAccountTypePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, service_type: int, account_type: HydrusNetwork.AccountType ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._account_type_key = account_type.GetAccountTypeKey() title = account_type.GetTitle() @@ -215,7 +215,7 @@ def __init__( self, parent, service_type, account_types ): self._service_type = service_type self._original_account_types = account_types - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._deletee_account_type_keys_to_new_account_type_keys = {} @@ -412,7 +412,7 @@ class ListAccountsPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent: QW.QWidget, service_key: bytes, accounts: typing.Collection[ HydrusNetwork.Account ] ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._service = CG.client_controller.services_manager.GetService( self._service_key ) @@ -874,7 +874,7 @@ class ModifyAccountsPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent: QW.QWidget, service_key: bytes, subject_identifiers: typing.Collection[ HydrusNetwork.AccountIdentifier ] ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._service = CG.client_controller.services_manager.GetService( service_key ) diff --git a/hydrus/client/gui/networking/ClientGUILogin.py b/hydrus/client/gui/networking/ClientGUILogin.py index 552750f4b..1363d8bec 100644 --- a/hydrus/client/gui/networking/ClientGUILogin.py +++ b/hydrus/client/gui/networking/ClientGUILogin.py @@ -41,7 +41,7 @@ class EditLoginCredentialsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, credential_definitions, credentials ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -202,7 +202,7 @@ class EditLoginCredentialDefinitionPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, credential_definition ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -252,7 +252,7 @@ class EditLoginsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, engine, login_scripts, domains_to_login_info ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._engine = engine self._login_scripts = login_scripts @@ -1187,7 +1187,7 @@ class ReviewTestResultPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, test_result ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) ( name, url, body, self._downloaded_data, new_temp_strings, new_cookie_strings, result ) = test_result @@ -1277,7 +1277,7 @@ class EditLoginScriptPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, login_script ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_login_script = login_script @@ -1901,7 +1901,7 @@ class EditLoginScriptsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, login_scripts ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) login_scripts_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -2025,7 +2025,7 @@ class EditLoginStepPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, login_step ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # diff --git a/hydrus/client/gui/networking/ClientGUINetwork.py b/hydrus/client/gui/networking/ClientGUINetwork.py index 8aeac3b1f..58fd0a1d7 100644 --- a/hydrus/client/gui/networking/ClientGUINetwork.py +++ b/hydrus/client/gui/networking/ClientGUINetwork.py @@ -39,7 +39,7 @@ class EditBandwidthRulesPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, bandwidth_rules: HydrusNetworking.BandwidthRules, summary ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._bandwidth_rules_ctrl = ClientGUIBandwidth.BandwidthRulesCtrl( self, bandwidth_rules ) @@ -70,7 +70,7 @@ class EditCookiePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, name: str, value: str, domain: str, path: str, expires: HC.noneable_int ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._name = QW.QLineEdit( self ) self._value = QW.QLineEdit( self ) @@ -161,7 +161,7 @@ class EditNetworkContextPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, network_context: ClientNetworkingContexts.NetworkContext, limited_types = None, allow_default = True ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) if limited_types is None: @@ -312,7 +312,7 @@ class EditNetworkContextCustomHeadersPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, network_contexts_to_custom_header_dicts ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -458,7 +458,7 @@ class _EditPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, network_context: ClientNetworkingContexts.NetworkContext, key: str, value: str, approved: int, reason: str ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._network_context = NetworkContextButton( self, network_context, limited_types = ( CC.NETWORK_CONTEXT_GLOBAL, CC.NETWORK_CONTEXT_DOMAIN ), allow_default = False ) @@ -516,7 +516,7 @@ class NetworkContextButton( ClientGUICommon.BetterButton ): def __init__( self, parent, network_context, limited_types = None, allow_default = True ): - ClientGUICommon.BetterButton.__init__( self, parent, network_context.ToString(), self._Edit ) + super().__init__( parent, network_context.ToString(), self._Edit ) self._network_context = network_context self._limited_types = limited_types @@ -563,7 +563,7 @@ def __init__( self, parent, controller ): self._controller = controller - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._history_time_delta_threshold = ClientGUITime.TimeDeltaButton( self, days = True, hours = True, minutes = True, seconds = True ) self._history_time_delta_threshold.timeDeltaChanged.connect( self.EventTimeDeltaChanged ) @@ -844,7 +844,7 @@ def __init__( self, parent, controller, network_context ): self._controller = controller - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._network_context = network_context @@ -1102,7 +1102,7 @@ def __init__( self, parent, controller ): self._controller = controller - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -1218,7 +1218,7 @@ class ReviewNetworkSessionsPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, session_manager ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._session_manager = session_manager @@ -1443,7 +1443,7 @@ class ReviewNetworkSessionPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, session_manager, network_context ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._session_manager = session_manager self._network_context = network_context diff --git a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py index 66d265e62..f53dbaa07 100644 --- a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py +++ b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py @@ -26,7 +26,7 @@ class NetworkJobControl( QW.QFrame ): def __init__( self, parent ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py index d9abf9185..b86dbdfb4 100644 --- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py +++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py @@ -47,8 +47,10 @@ from hydrus.client.gui.networking import ClientGUIHydrusNetwork from hydrus.client.gui.networking import ClientGUINetworkJobControl from hydrus.client.gui.pages import ClientGUIManagementController -from hydrus.client.gui.pages import ClientGUIResults -from hydrus.client.gui.pages import ClientGUIResultsSortCollect +from hydrus.client.gui.pages import ClientGUIMediaResultsPanel +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelLoading +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelThumbnails +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelSortCollect from hydrus.client.gui.panels import ClientGUIScrolledPanels from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit from hydrus.client.gui.parsing import ClientGUIParsingFormulae @@ -130,7 +132,7 @@ class ListBoxTagsMediaManagementPanel( ClientGUIListBoxes.ListBoxTagsMedia ): def __init__( self, parent, management_controller: ClientGUIManagementController.ManagementController, page_key, tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST, tag_autocomplete: typing.Optional[ ClientGUIACDropdown.AutoCompleteDropdownTagsRead ] = None ): - ClientGUIListBoxes.ListBoxTagsMedia.__init__( self, parent, tag_display_type, CC.TAG_PRESENTATION_SEARCH_PAGE, include_counts = True ) + super().__init__( parent, tag_display_type, CC.TAG_PRESENTATION_SEARCH_PAGE, include_counts = True ) self._management_controller = management_controller self._minimum_height_num_chars = 15 @@ -239,7 +241,7 @@ class ManagementPanel( QW.QScrollArea ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - QW.QScrollArea.__init__( self, parent ) + super().__init__( parent ) self.setFrameShape( QW.QFrame.NoFrame ) self.setWidget( QW.QWidget( self ) ) @@ -261,7 +263,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa self._current_selection_tags_list = None - self._media_sort_widget = ClientGUIResultsSortCollect.MediaSortControl( self, media_sort = self._management_controller.GetVariable( 'media_sort' ) ) + self._media_sort_widget = ClientGUIMediaResultsPanelSortCollect.MediaSortControl( self, media_sort = self._management_controller.GetVariable( 'media_sort' ) ) if self._management_controller.HasVariable( 'media_collect' ): @@ -272,7 +274,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa media_collect = ClientMedia.MediaCollect() - self._media_collect_widget = ClientGUIResultsSortCollect.MediaCollectControl( self, media_collect = media_collect ) + self._media_collect_widget = ClientGUIMediaResultsPanelSortCollect.MediaCollectControl( self, media_collect = media_collect ) self._media_collect_widget.ListenForNewOptions() @@ -324,11 +326,11 @@ def _SortChanged( self, media_sort ): self._management_controller.SetVariable( 'media_sort', media_sort ) - def ConnectMediaPanelSignals( self, media_panel: ClientGUIResults.MediaPanel ): + def ConnectMediaResultsPanelSignals( self, media_panel: ClientGUIMediaResultsPanel.MediaResultsPanel ): if self._current_selection_tags_list is not None: - media_panel.selectedMediaTagPresentationChanged.connect( self._current_selection_tags_list.SetTagsByMediaFromMediaPanel ) + media_panel.selectedMediaTagPresentationChanged.connect( self._current_selection_tags_list.SetTagsByMediaFromMediaResultsPanel ) media_panel.selectedMediaTagPresentationIncremented.connect( self._current_selection_tags_list.IncrementTagsByMedia ) self._media_collect_widget.collectChanged.connect( media_panel.Collect ) self._media_sort_widget.sortChanged.connect( media_panel.Sort ) @@ -352,9 +354,9 @@ def CleanBeforeDestroy( self ): pass - def GetDefaultEmptyMediaPanel( self, win: QW.QWidget ) -> ClientGUIResults.MediaPanel: + def GetDefaultEmptyMediaResultsPanel( self, win: QW.QWidget ) -> ClientGUIMediaResultsPanel.MediaResultsPanel: - panel = ClientGUIResults.MediaPanelThumbnails( win, self._page_key, self._management_controller, [] ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( win, self._page_key, self._management_controller, [] ) status = self._GetDefaultEmptyPageStatusOverride() @@ -441,7 +443,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanel.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._duplicates_manager = ClientDuplicates.DuplicatesManager.instance() @@ -859,7 +861,7 @@ def _ResetUnknown( self ): def _SetCurrentMediaAs( self, duplicate_type ): - media_panel = self._page.GetMediaPanel() + media_panel = self._page.GetMediaResultsPanel() change_made = media_panel.SetDuplicateStatusForAll( duplicate_type ) @@ -889,7 +891,7 @@ def _ShowPairInPage( self, media: typing.Collection[ ClientMedia.MediaSingleton media_results = [ m.GetMediaResult() for m in media ] - self._page.GetMediaPanel().AddMediaResults( self._page_key, media_results ) + self._page.GetMediaResultsPanel().AddMediaResults( self._page_key, media_results ) def _ShowPotentialDupes( self, hashes ): @@ -909,11 +911,11 @@ def _ShowPotentialDupes( self, hashes ): media_results = [] - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no dupes found' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._page_state = CC.PAGE_STATE_NORMAL @@ -1140,7 +1142,7 @@ class ManagementPanelImporter( ManagementPanel ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanel.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) def _UpdateImportStatus( self ): @@ -1169,7 +1171,7 @@ class ManagementPanelImporterHDD( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._import_queue_panel = ClientGUICommon.StaticBox( self, 'imports' ) @@ -1266,7 +1268,7 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._last_time_imports_changed = 0 self._next_update_time = 0 @@ -1478,11 +1480,11 @@ def _ClearExistingHighlightAndPanel( self ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no highlighted query' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._gallery_importers_listctrl.UpdateDatas() @@ -1733,9 +1735,9 @@ def _HighlightGalleryImport( self, new_highlight ): if num_to_do > 0: - panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller ) + panel = ClientGUIMediaResultsPanelLoading.MediaResultsPanelLoading( self._page, self._page_key, self._management_controller ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) def work_callable(): @@ -1796,11 +1798,11 @@ def publish_callable( media_results ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no files for this query and its publishing settings' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._highlighted_gallery_import_panel.SetGalleryImport( self._highlighted_gallery_import ) @@ -2074,9 +2076,9 @@ def _ShowSelectedImportersFiles( self, presentation_import_options = None ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) else: @@ -2354,7 +2356,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._last_time_watchers_changed = 0 self._next_update_time = 0 @@ -2593,11 +2595,11 @@ def _ClearExistingHighlightAndPanel( self ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no highlighted watcher' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._watchers_listctrl.UpdateDatas() @@ -2871,9 +2873,9 @@ def _HighlightWatcher( self, new_highlight ): if num_to_do > 0: - panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller ) + panel = ClientGUIMediaResultsPanelLoading.MediaResultsPanelLoading( self._page, self._page_key, self._management_controller ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) def work_callable(): @@ -2934,11 +2936,11 @@ def publish_callable( media_results ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no files for this watcher and its publishing settings' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._highlighted_watcher_panel.SetWatcher( self._highlighted_watcher ) @@ -3205,9 +3207,9 @@ def _ShowSelectedImportersFiles( self, presentation_import_options = None ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) else: @@ -3512,7 +3514,7 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._simple_downloader_import: ClientImportSimpleURLs.SimpleDownloaderImport = self._management_controller.GetVariable( 'simple_downloader_import' ) @@ -3976,7 +3978,7 @@ class ManagementPanelImporterURLs( ManagementPanelImporter ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanelImporter.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) # @@ -4207,7 +4209,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa self._petition_service_key = management_controller.GetVariable( 'petition_service_key' ) - ManagementPanel.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) self._service = self._controller.services_manager.GetService( self._petition_service_key ) self._can_ban = self._service.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_MODERATE ) @@ -5104,13 +5106,13 @@ def _ShowHashes( self, hashes ): media_results = self._controller.Read( 'media_results', hashes ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.Collect( self._media_collect_widget.GetValue() ) panel.Sort( self._media_sort_widget.GetSort() ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) def _SortBy( self, sort_type ): @@ -5721,7 +5723,7 @@ class ManagementPanelQuery( ManagementPanel ): def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ): - ManagementPanel.__init__( self, parent, page, controller, management_controller ) + super().__init__( parent, page, controller, management_controller ) file_search_context = self._management_controller.GetVariable( 'file_search_context' ) @@ -5780,11 +5782,11 @@ def _CancelSearch( self ): self._SetLocationContext( location_context ) - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, [] ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, [] ) panel.SetEmptyPageStatusOverride( 'search cancelled!' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._page_state = CC.PAGE_STATE_SEARCHING_CANCELLED @@ -5859,18 +5861,18 @@ def _RefreshQuery( self ): self._controller.CallToThread( self.THREADDoQuery, self._controller, self._page_key, self._query_job_status, file_search_context, sort_by ) - panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller ) + panel = ClientGUIMediaResultsPanelLoading.MediaResultsPanelLoading( self._page, self._page_key, self._management_controller ) self._page_state = CC.PAGE_STATE_SEARCHING else: - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, [] ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, [] ) panel.SetEmptyPageStatusOverride( 'no search' ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) else: @@ -5899,9 +5901,9 @@ def _UpdateCancelButton( self ): - def ConnectMediaPanelSignals( self, media_panel: ClientGUIResults.MediaPanel ): + def ConnectMediaResultsPanelSignals( self, media_panel: ClientGUIMediaResultsPanel.MediaResultsPanel ): - ManagementPanel.ConnectMediaPanelSignals( self, media_panel ) + ManagementPanel.ConnectMediaResultsPanelSignals( self, media_panel ) media_panel.newMediaAdded.connect( self.PauseSearching ) @@ -6028,7 +6030,7 @@ def ShowFinishedQuery( self, query_job_status, media_results ): location_context = self._management_controller.GetLocationContext() - panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) + panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self._page, self._page_key, self._management_controller, media_results ) panel.SetEmptyPageStatusOverride( 'no files found for this search' ) @@ -6036,7 +6038,7 @@ def ShowFinishedQuery( self, query_job_status, media_results ): panel.Sort( self._media_sort_widget.GetSort() ) - self._page.SwapMediaPanel( panel ) + self._page.SwapMediaResultsPanel( panel ) self._page_state = CC.PAGE_STATE_NORMAL diff --git a/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py new file mode 100644 index 000000000..bdc926ed0 --- /dev/null +++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanel.py @@ -0,0 +1,2585 @@ +import collections +import itertools +import time +import typing + +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW +from qtpy import QtGui as QG + +from hydrus.core import HydrusConstants as HC +from hydrus.core import HydrusData +from hydrus.core import HydrusExceptions +from hydrus.core import HydrusNumbers +from hydrus.core import HydrusPaths +from hydrus.core import HydrusTime +from hydrus.core.networking import HydrusNetwork + +from hydrus.client import ClientApplicationCommand as CAC +from hydrus.client import ClientConstants as CC +from hydrus.client import ClientFiles +from hydrus.client import ClientGlobals as CG +from hydrus.client import ClientPaths +from hydrus.client import ClientServices +from hydrus.client.gui import ClientGUIDialogs +from hydrus.client.gui import ClientGUIDialogsManage +from hydrus.client.gui import ClientGUIDialogsMessage +from hydrus.client.gui import ClientGUIDialogsQuick +from hydrus.client.gui import ClientGUIDuplicates +from hydrus.client.gui import ClientGUIShortcuts +from hydrus.client.gui import ClientGUITags +from hydrus.client.gui import ClientGUITopLevelWindowsPanels +from hydrus.client.gui import QtPorting as QP +from hydrus.client.gui.canvas import ClientGUICanvas +from hydrus.client.gui.canvas import ClientGUICanvasFrame +from hydrus.client.gui.media import ClientGUIMediaSimpleActions +from hydrus.client.gui.media import ClientGUIMediaModalActions +from hydrus.client.gui.networking import ClientGUIHydrusNetwork +from hydrus.client.gui.pages import ClientGUIManagementController +from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit +from hydrus.client.media import ClientMedia +from hydrus.client.media import ClientMediaFileFilter +from hydrus.client.metadata import ClientContentUpdates + +MAC_QUARTZ_OK = True + +if HC.PLATFORM_MACOS: + + try: + + from hydrus.client import ClientMacIntegration + + except: + + MAC_QUARTZ_OK = False + + + +class MediaResultsPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMediaList, QW.QScrollArea ): + + selectedMediaTagPresentationChanged = QC.Signal( list, bool ) + selectedMediaTagPresentationIncremented = QC.Signal( list ) + statusTextChanged = QC.Signal( str ) + + focusMediaChanged = QC.Signal( ClientMedia.Media ) + focusMediaCleared = QC.Signal() + focusMediaPaused = QC.Signal() + refreshQuery = QC.Signal() + + newMediaAdded = QC.Signal() + + def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController, media_results ): + + self._qss_colours = { + CC.COLOUR_THUMBGRID_BACKGROUND : QG.QColor( 255, 255, 255 ), + CC.COLOUR_THUMB_BACKGROUND : QG.QColor( 255, 255, 255 ), + CC.COLOUR_THUMB_BACKGROUND_SELECTED : QG.QColor( 217, 242, 255 ), + CC.COLOUR_THUMB_BACKGROUND_REMOTE : QG.QColor( 32, 32, 36 ), + CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED : QG.QColor( 64, 64, 72 ), + CC.COLOUR_THUMB_BORDER : QG.QColor( 223, 227, 230 ), + CC.COLOUR_THUMB_BORDER_SELECTED : QG.QColor( 1, 17, 26 ), + CC.COLOUR_THUMB_BORDER_REMOTE : QG.QColor( 248, 208, 204 ), + CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED : QG.QColor( 227, 66, 52 ) + } + + self._page_key = page_key + self._management_controller = management_controller + + # TODO: BRUH REWRITE THIS GARBAGE + # we don't really want to be messing around with *args, **kwargs in __init__/super() gubbins, and this is highlighted as we move to super() and see this is all a mess!! + # obviously decouple the list from the panel here so we aren't trying to do everything in one class + super().__init__( self._management_controller.GetLocationContext(), media_results, parent ) + + self.setObjectName( 'HydrusMediaList' ) + + self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken ) + self.setLineWidth( 2 ) + + self.resize( QC.QSize( 20, 20 ) ) + self.setWidget( QW.QWidget( self ) ) + self.setWidgetResizable( True ) + + self._UpdateBackgroundColour() + + self.verticalScrollBar().setSingleStep( 50 ) + + self._focused_media = None + self._last_hit_media = None + self._next_best_media_if_focuses_removed = None + self._shift_select_started_with_this_media = None + self._media_added_in_current_shift_select = set() + + self._empty_page_status_override = None + + CG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' ) + CG.client_controller.sub( self, 'RemoveMedia', 'remove_media' ) + CG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' ) + CG.client_controller.sub( self, 'SelectByTags', 'select_files_with_tags' ) + CG.client_controller.sub( self, 'LaunchMediaViewerOnFocus', 'launch_media_viewer' ) + + self._had_changes_to_tag_presentation_while_hidden = False + + self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'media', 'thumbnails' ] ) + + self.setWidget( self._InnerWidget( self ) ) + self.setWidgetResizable( True ) + + + def __bool__( self ): + + return QP.isValid( self ) + + + def _Archive( self ): + + hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_INBOX ) + + if len( hashes ) > 0: + + if HC.options[ 'confirm_archive' ]: + + if len( hashes ) > 1: + + message = 'Archive ' + HydrusNumbers.ToHumanInt( len( hashes ) ) + ' files?' + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result != QW.QDialog.Accepted: + + return + + + + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, hashes ) ) ) + + + + def _ArchiveDeleteFilter( self ): + + if len( self._selected_media ) == 0: + + media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH, selected_media = set( self._sorted_media ), for_media_viewer = True ) + + else: + + media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH, selected_media = set( self._selected_media ), for_media_viewer = True ) + + + if len( media_results ) > 0: + + self.SetFocusedMedia( None ) + + canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() ) + + canvas_window = ClientGUICanvas.CanvasMediaListFilterArchiveDelete( canvas_frame, self._page_key, self._location_context, media_results ) + + canvas_frame.SetCanvas( canvas_window ) + + canvas_window.exitFocusMedia.connect( self.SetFocusedMedia ) + + + + def _ClearDeleteRecord( self ): + + media = self._GetSelectedFlatMedia() + + ClientGUIMediaModalActions.ClearDeleteRecord( self, media ) + + + def _Delete( self, file_service_key = None, only_those_in_file_service_key = None ): + + if file_service_key is None: + + if len( self._location_context.current_service_keys ) == 1: + + ( possible_suggested_file_service_key, ) = self._location_context.current_service_keys + + if CG.client_controller.services_manager.GetServiceType( possible_suggested_file_service_key ) in HC.SPECIFIC_LOCAL_FILE_SERVICES + ( HC.FILE_REPOSITORY, ): + + file_service_key = possible_suggested_file_service_key + + + + + media_to_delete = ClientMedia.FlattenMedia( self._selected_media ) + + if only_those_in_file_service_key is not None: + + media_to_delete = ClientMedia.FlattenMedia( media_to_delete ) + + media_to_delete = [ m for m in media_to_delete if only_those_in_file_service_key in m.GetLocationsManager().GetCurrent() ] + + + if file_service_key is None or CG.client_controller.services_manager.GetServiceType( file_service_key ) in HC.LOCAL_FILE_SERVICES: + + default_reason = 'Deleted from Media Page.' + + else: + + default_reason = 'admin' + + + try: + + ( hashes_physically_deleted, content_update_packages ) = ClientGUIDialogsQuick.GetDeleteFilesJobs( self, media_to_delete, default_reason, suggested_file_service_key = file_service_key ) + + except HydrusExceptions.CancelledException: + + return + + + if len( hashes_physically_deleted ) > 0: + + self._RemoveMediaByHashes( hashes_physically_deleted ) + + + def do_it( content_update_packages ): + + for content_update_package in content_update_packages: + + CG.client_controller.WriteSynchronous( 'content_updates', content_update_package ) + + + + CG.client_controller.CallToThread( do_it, content_update_packages ) + + + def _DeselectSelect( self, media_to_deselect, media_to_select ): + + if len( media_to_deselect ) > 0: + + for m in media_to_deselect: m.Deselect() + + self._RedrawMedia( media_to_deselect ) + + self._selected_media.difference_update( media_to_deselect ) + + + if len( media_to_select ) > 0: + + for m in media_to_select: m.Select() + + self._RedrawMedia( media_to_select ) + + self._selected_media.update( media_to_select ) + + + self._PublishSelectionChange() + + + def _DownloadSelected( self ): + + hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL ) + + self._DownloadHashes( hashes ) + + + def _DownloadHashes( self, hashes ): + + CG.client_controller.quick_download_manager.DownloadFiles( hashes ) + + + def _EndShiftSelect( self ): + + self._shift_select_started_with_this_media = None + self._media_added_in_current_shift_select = set() + + + def _GetFocusSingleton( self ) -> ClientMedia.MediaSingleton: + + if self._focused_media is not None: + + media_singleton = self._focused_media.GetDisplayMedia() + + if media_singleton is not None: + + return media_singleton + + + + raise HydrusExceptions.DataMissing( 'No media singleton!' ) + + + def _GetMediasForFileCommandTarget( self, file_command_target: int ) -> typing.Collection[ ClientMedia.MediaSingleton ]: + + if file_command_target == CAC.FILE_COMMAND_TARGET_FOCUSED_FILE: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + return [ media.GetDisplayMedia() ] + + + elif file_command_target == CAC.FILE_COMMAND_TARGET_SELECTED_FILES: + + if len( self._selected_media ) > 0: + + medias = self._GetSelectedMediaOrdered() + + return ClientMedia.FlattenMedia( medias ) + + + + return [] + + + def _GetNumSelected( self ): + + return sum( [ media.GetNumFiles() for media in self._selected_media ] ) + + + def _GetPrettyStatusForStatusBar( self ) -> str: + + num_files = len( self._hashes ) + + if self._empty_page_status_override is not None: + + if num_files == 0: + + return self._empty_page_status_override + + else: + + # user has dragged files onto this page or similar + + self._empty_page_status_override = None + + + + num_selected = self._GetNumSelected() + + num_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._sorted_media ) + selected_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._selected_media ) + + s = num_files_string # 23 files + + if num_selected == 0: + + if num_files > 0: + + pretty_total_size = self._GetPrettyTotalSize() + + s += ' - totalling ' + pretty_total_size + + pretty_total_duration = self._GetPrettyTotalDuration() + + if pretty_total_duration != '': + + s += ', {}'.format( pretty_total_duration ) + + + + else: + + s += ' - ' + + # if 1 selected, we show the whole mime string, so no need to specify + if num_selected == 1 or selected_files_string == num_files_string: + + selected_files_string = HydrusNumbers.ToHumanInt( num_selected ) + + + if num_selected == 1: # 23 files - 1 video selected, file_info + + ( selected_media, ) = self._selected_media + + pretty_info_lines = [ line for line in selected_media.GetPrettyInfoLines( only_interesting_lines = True ) if isinstance( line, str ) ] + + s += '{} selected, {}'.format( selected_files_string, ', '.join( pretty_info_lines ) ) + + else: # 23 files - 5 selected, selection_info + + num_inbox = sum( ( media.GetNumInbox() for media in self._selected_media ) ) + + if num_inbox == num_selected: + + inbox_phrase = 'all in inbox' + + elif num_inbox == 0: + + inbox_phrase = 'all archived' + + else: + + inbox_phrase = '{} in inbox and {} archived'.format( HydrusNumbers.ToHumanInt( num_inbox ), HydrusNumbers.ToHumanInt( num_selected - num_inbox ) ) + + + pretty_total_size = self._GetPrettyTotalSize( only_selected = True ) + + s += '{} selected, {}, totalling {}'.format( selected_files_string, inbox_phrase, pretty_total_size ) + + pretty_total_duration = self._GetPrettyTotalDuration( only_selected = True ) + + if pretty_total_duration != '': + + s += ', {}'.format( pretty_total_duration ) + + + + + return s + + + def _GetPrettyTotalDuration( self, only_selected = False ): + + if only_selected: + + media_source = self._selected_media + + else: + + media_source = self._sorted_media + + + if len( media_source ) == 0 or False in ( media.HasDuration() for media in media_source ): + + return '' + + + total_duration = sum( ( media.GetDurationMS() for media in media_source ) ) + + return HydrusTime.MillisecondsDurationToPrettyTime( total_duration ) + + + def _GetPrettyTotalSize( self, only_selected = False ): + + if only_selected: + + media_source = self._selected_media + + else: + + media_source = self._sorted_media + + + total_size = sum( [ media.GetSize() for media in media_source ] ) + + unknown_size = False in ( media.IsSizeDefinite() for media in media_source ) + + if total_size == 0: + + if unknown_size: + + return 'unknown size' + + else: + + return HydrusData.ToHumanBytes( 0 ) + + + else: + + if unknown_size: + + return HydrusData.ToHumanBytes( total_size ) + ' + some unknown size' + + else: + + return HydrusData.ToHumanBytes( total_size ) + + + + + def _GetSelectedHashes( self, is_in_file_service_key = None, discriminant = None, is_not_in_file_service_key = None, ordered = False ): + + if ordered: + + result = [] + + for media in self._GetSelectedMediaOrdered(): + + result.extend( media.GetHashes( is_in_file_service_key, discriminant, is_not_in_file_service_key, ordered ) ) + + + else: + + result = set() + + for media in self._selected_media: + + result.update( media.GetHashes( is_in_file_service_key, discriminant, is_not_in_file_service_key, ordered ) ) + + + + return result + + + def _GetSelectedCollections( self ): + + sorted_selected_collections = [ media for media in self._sorted_media if media.IsCollection() and media in self._selected_media ] + + return sorted_selected_collections + + + def _GetSelectedFlatMedia( self, is_in_file_service_key = None, discriminant = None, is_not_in_file_service_key = None ): + + # this now always delivers sorted results + + sorted_selected_media = [ media for media in self._sorted_media if media in self._selected_media ] + + flat_media = ClientMedia.FlattenMedia( sorted_selected_media ) + + flat_media = [ media for media in flat_media if media.MatchesDiscriminant( is_in_file_service_key = is_in_file_service_key, discriminant = discriminant, is_not_in_file_service_key = is_not_in_file_service_key ) ] + + return flat_media + + + def _GetSelectedMediaOrdered( self ): + + # note that this is fast because sorted_media is custom + return sorted( self._selected_media, key = lambda m: self._sorted_media.index( m ) ) + + + def _GetSortedSelectedMimeDescriptors( self ): + + def GetDescriptor( plural, classes, num_collections ): + + suffix = 's' if plural else '' + + if len( classes ) == 0: + + return 'file' + suffix + + + if len( classes ) == 1: + + ( mime, ) = classes + + if mime == HC.APPLICATION_HYDRUS_CLIENT_COLLECTION: + + collections_suffix = 's' if num_collections > 1 else '' + + return 'file{} in {} collection{}'.format( suffix, HydrusNumbers.ToHumanInt( num_collections ), collections_suffix ) + + else: + + return HC.mime_string_lookup[ mime ] + suffix + + + + if len( classes.difference( HC.IMAGES ) ) == 0: + + return 'image' + suffix + + elif len( classes.difference( HC.ANIMATIONS ) ) == 0: + + return 'animation' + suffix + + elif len( classes.difference( HC.VIDEO ) ) == 0: + + return 'video' + suffix + + elif len( classes.difference( HC.AUDIO ) ) == 0: + + return 'audio file' + suffix + + else: + + return 'file' + suffix + + + + if len( self._sorted_media ) > 1000: + + sorted_mime_descriptor = 'files' + + else: + + sorted_mimes = { media.GetMime() for media in self._sorted_media } + + if HC.APPLICATION_HYDRUS_CLIENT_COLLECTION in sorted_mimes: + + num_collections = len( [ media for media in self._sorted_media if isinstance( media, ClientMedia.MediaCollection ) ] ) + + else: + + num_collections = 0 + + + plural = len( self._sorted_media ) > 1 or sum( ( m.GetNumFiles() for m in self._sorted_media ) ) > 1 + + sorted_mime_descriptor = GetDescriptor( plural, sorted_mimes, num_collections ) + + + if len( self._selected_media ) > 1000: + + selected_mime_descriptor = 'files' + + else: + + selected_mimes = { media.GetMime() for media in self._selected_media } + + if HC.APPLICATION_HYDRUS_CLIENT_COLLECTION in selected_mimes: + + num_collections = len( [ media for media in self._selected_media if isinstance( media, ClientMedia.MediaCollection ) ] ) + + else: + + num_collections = 0 + + + plural = len( self._selected_media ) > 1 or sum( ( m.GetNumFiles() for m in self._selected_media ) ) > 1 + + selected_mime_descriptor = GetDescriptor( plural, selected_mimes, num_collections ) + + + return ( sorted_mime_descriptor, selected_mime_descriptor ) + + + def _HasFocusSingleton( self ) -> bool: + + try: + + media = self._GetFocusSingleton() + + return True + + except HydrusExceptions.DataMissing: + + return False + + + + def _HitMedia( self, media, ctrl, shift ): + + if media is None: + + if not ctrl and not shift: + + self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) ) + self._SetFocusedMedia( None ) + self._EndShiftSelect() + + + else: + + if ctrl and not shift: + + if media.IsSelected(): + + self._DeselectSelect( ( media, ), () ) + + if self._focused_media == media: + + self._SetFocusedMedia( None ) + + + self._EndShiftSelect() + + else: + + self._DeselectSelect( (), ( media, ) ) + + focus_it = False + + if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_ctrl_click' ): + + if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_ctrl_click_only_static' ): + + focus_it = media.GetDurationMS() is None + + else: + + focus_it = True + + + + if focus_it: + + self._SetFocusedMedia( media ) + + else: + + self._last_hit_media = media + + + self._StartShiftSelect( media ) + + + elif shift and self._shift_select_started_with_this_media is not None: + + start_index = self._sorted_media.index( self._shift_select_started_with_this_media ) + + end_index = self._sorted_media.index( media ) + + if start_index < end_index: + + media_from_start_of_shift_to_end = set( self._sorted_media[ start_index : end_index + 1 ] ) + + else: + + media_from_start_of_shift_to_end = set( self._sorted_media[ end_index : start_index + 1 ] ) + + + media_to_deselect = [ m for m in self._media_added_in_current_shift_select if m not in media_from_start_of_shift_to_end ] + media_to_select = [ m for m in media_from_start_of_shift_to_end if not m.IsSelected() ] + + self._media_added_in_current_shift_select.difference_update( media_to_deselect ) + self._media_added_in_current_shift_select.update( media_to_select ) + + self._DeselectSelect( media_to_deselect, media_to_select ) + + focus_it = False + + if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_shift_click' ): + + if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_shift_click_only_static' ): + + focus_it = media.GetDurationMS() is None + + else: + + focus_it = True + + + + if focus_it: + + self._SetFocusedMedia( media ) + + else: + + self._last_hit_media = media + + + else: + + if not media.IsSelected(): + + self._DeselectSelect( self._selected_media, ( media, ) ) + + else: + + self._PublishSelectionChange() + + + self._SetFocusedMedia( media ) + self._StartShiftSelect( media ) + + + + + def _Inbox( self ): + + hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_ARCHIVE, is_in_file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) + + if len( hashes ) > 0: + + if HC.options[ 'confirm_archive' ]: + + if len( hashes ) > 1: + + message = 'Send {} files to inbox?'.format( HydrusNumbers.ToHumanInt( len( hashes ) ) ) + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result != QW.QDialog.Accepted: + + return + + + + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_INBOX, hashes ) ) ) + + + + def _LaunchMediaViewer( self, first_media = None ): + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + if not media.GetLocationsManager().IsLocal(): + + return + + + new_options = CG.client_controller.new_options + + ( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( media.GetMime() ) + + if media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY: + + hash = media.GetHash() + mime = media.GetMime() + + client_files_manager = CG.client_controller.client_files_manager + + path = client_files_manager.GetFilePath( hash, mime ) + + new_options = CG.client_controller.new_options + + launch_path = new_options.GetMimeLaunch( mime ) + + HydrusPaths.LaunchFile( path, launch_path ) + + return + + elif media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW: + + return + + + + media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, for_media_viewer = True ) + + if len( media_results ) > 0: + + if first_media is None and self._focused_media is not None: + + first_media = self._focused_media + + + if first_media is not None: + + first_media = first_media.GetDisplayMedia() + + + if first_media is not None and first_media.GetLocationsManager().IsLocal(): + + first_hash = first_media.GetHash() + + else: + + first_hash = None + + + self.SetFocusedMedia( None ) + + canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() ) + + canvas_window = ClientGUICanvas.CanvasMediaListBrowser( canvas_frame, self._page_key, self._location_context, media_results, first_hash ) + + canvas_frame.SetCanvas( canvas_window ) + + canvas_window.exitFocusMedia.connect( self.SetFocusedMedia ) + + + + def _ManageNotes( self ): + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + ClientGUIMediaModalActions.EditFileNotes( self, media ) + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + def _ManageRatings( self ): + + flat_media = ClientMedia.FlattenMedia( self._selected_media ) + + if len( flat_media ) > 0: + + if len( CG.client_controller.services_manager.GetServices( HC.RATINGS_SERVICES ) ) > 0: + + with ClientGUIDialogsManage.DialogManageRatings( self, flat_media ) as dlg: + + dlg.exec() + + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + + def _ManageTags( self ): + + flat_media = ClientMedia.FlattenMedia( self._GetSelectedMediaOrdered() ) + + if len( flat_media ) > 0: + + num_files = self._GetNumSelected() + + title = 'manage tags for ' + HydrusNumbers.ToHumanInt( num_files ) + ' files' + frame_key = 'manage_tags_dialog' + + with ClientGUITopLevelWindowsPanels.DialogManage( self, title, frame_key ) as dlg: + + panel = ClientGUITags.ManageTagsPanel( dlg, self._location_context, CC.TAG_PRESENTATION_SEARCH_PAGE_MANAGE_TAGS, flat_media ) + + dlg.SetPanel( panel ) + + dlg.exec() + + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + def _ManageTimestamps( self ): + + ordered_selected_media = self._GetSelectedMediaOrdered() + + ordered_selected_flat_media = ClientMedia.FlattenMedia( ordered_selected_media ) + + if len( ordered_selected_flat_media ) > 0: + + ClientGUIMediaModalActions.EditFileTimestamps( self, ordered_selected_flat_media ) + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + def _ManageURLs( self ): + + flat_media = ClientMedia.FlattenMedia( self._selected_media ) + + if len( flat_media ) > 0: + + num_files = self._GetNumSelected() + + title = 'manage urls for {} files'.format( num_files ) + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg: + + panel = ClientGUIScrolledPanelsEdit.EditURLsPanel( dlg, flat_media ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + pending_content_updates = panel.GetValue() + + if len( pending_content_updates ) > 0: + + content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, pending_content_updates ) + + CG.client_controller.Write( 'content_updates', content_update_package ) + + + + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + def _MediaIsVisible( self, media ): + + return True + + + def _ModifyUploaders( self, file_service_key ): + + hashes = self._GetSelectedHashes() + + contents = [ HydrusNetwork.Content( HC.CONTENT_TYPE_FILES, ( hash, ) ) for hash in hashes ] + + if len( contents ) > 0: + + subject_account_identifiers = [ HydrusNetwork.AccountIdentifier( content = content ) for content in contents ] + + frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'manage accounts' ) + + panel = ClientGUIHydrusNetwork.ModifyAccountsPanel( frame, file_service_key, subject_account_identifiers ) + + frame.SetPanel( panel ) + + + + def _OpenFileInWebBrowser( self ): + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + if focused_singleton.GetLocationsManager().IsLocal(): + + hash = focused_singleton.GetHash() + mime = focused_singleton.GetMime() + + client_files_manager = CG.client_controller.client_files_manager + + path = client_files_manager.GetFilePath( hash, mime ) + + self.focusMediaPaused.emit() + + ClientPaths.LaunchPathInWebBrowser( path ) + + + + + def _MacQuicklook( self ): + + if HC.PLATFORM_MACOS and self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + if focused_singleton.GetLocationsManager().IsLocal(): + + hash = focused_singleton.GetHash() + mime = focused_singleton.GetMime() + + client_files_manager = CG.client_controller.client_files_manager + + path = client_files_manager.GetFilePath( hash, mime ) + + self.focusMediaPaused.emit() + + if not MAC_QUARTZ_OK: + + HydrusData.ShowText( 'Sorry, could not do the Quick Look integration--it looks like your venv does not support it. If you are running from source, try rebuilding it!' ) + + + ClientMacIntegration.show_quicklook_for_path( path ) + + + + + def _OpenKnownURL( self ): + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + ClientGUIMediaModalActions.DoOpenKnownURLFromShortcut( self, focused_singleton ) + + + + def _PetitionFiles( self, remote_service_key ): + + hashes = self._GetSelectedHashes() + + if hashes is not None and len( hashes ) > 0: + + remote_service = CG.client_controller.services_manager.GetService( remote_service_key ) + + service_type = remote_service.GetServiceType() + + if service_type == HC.FILE_REPOSITORY: + + if len( hashes ) == 1: + + message = 'Enter a reason for this file to be removed from {}.'.format( remote_service.GetName() ) + + else: + + message = 'Enter a reason for these {} files to be removed from {}.'.format( HydrusNumbers.ToHumanInt( len( hashes ) ), remote_service.GetName() ) + + + with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + reason = dlg.GetValue() + + content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PETITION, hashes, reason = reason ) + + content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( remote_service_key, content_update ) + + CG.client_controller.Write( 'content_updates', content_update_package ) + + + + self.setFocus( QC.Qt.OtherFocusReason ) + + elif service_type == HC.IPFS: + + content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PETITION, hashes, reason = 'ipfs' ) + + content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( remote_service_key, content_update ) + + CG.client_controller.Write( 'content_updates', content_update_package ) + + + + + def _PublishSelectionChange( self, tags_changed = False ): + + if CG.client_controller.gui.IsCurrentPage( self._page_key ): + + if len( self._selected_media ) == 0: + + tags_media = self._sorted_media + + else: + + tags_media = self._selected_media + + + tags_media = list( tags_media ) + + tags_changed = tags_changed or self._had_changes_to_tag_presentation_while_hidden + + self.selectedMediaTagPresentationChanged.emit( tags_media, tags_changed ) + + self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) + + if tags_changed: + + self._had_changes_to_tag_presentation_while_hidden = False + + + elif tags_changed: + + self._had_changes_to_tag_presentation_while_hidden = True + + + + def _PublishSelectionIncrement( self, medias ): + + if CG.client_controller.gui.IsCurrentPage( self._page_key ): + + medias = list( medias ) + + self.selectedMediaTagPresentationIncremented.emit( medias ) + + self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) + + else: + + self._had_changes_to_tag_presentation_while_hidden = True + + + + def _RecalculateVirtualSize( self, called_from_resize_event = False ): + + pass + + + def _RedrawMedia( self, media ): + + pass + + + def _Remove( self, file_filter: ClientMediaFileFilter.FileFilter ): + + hashes = file_filter.GetMediaListHashes( self ) + + if len( hashes ) > 0: + + self._RemoveMediaByHashes( hashes ) + + + + def _RegenerateFileData( self, job_type ): + + flat_media = self._GetSelectedFlatMedia() + + num_files = len( flat_media ) + + if num_files > 0: + + if job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_METADATA: + + message = 'This will reparse the {} selected files\' metadata.'.format( HydrusNumbers.ToHumanInt( num_files ) ) + message += '\n' * 2 + message += 'If the files were imported before some more recent improvement in the parsing code (such as EXIF rotation or bad video resolution or duration or frame count calculation), this will update them.' + + elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL: + + message = 'This will force-regenerate the {} selected files\' thumbnails.'.format( HydrusNumbers.ToHumanInt( num_files ) ) + + elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL: + + message = 'This will regenerate the {} selected files\' thumbnails, but only if they are the wrong size.'.format( HydrusNumbers.ToHumanInt( num_files ) ) + + else: + + message = ClientFiles.regen_file_enum_to_description_lookup[ job_type ] + + + do_it_now = True + + if num_files > 50: + + message += '\n' * 2 + message += 'You have selected {} files, so this job may take some time. You can run it all now or schedule it to the overall file maintenance queue for later spread-out processing.'.format( HydrusNumbers.ToHumanInt( num_files ) ) + + yes_tuples = [] + + yes_tuples.append( ( 'do it now', 'now' ) ) + yes_tuples.append( ( 'do it later', 'later' ) ) + + try: + + result = ClientGUIDialogsQuick.GetYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' ) + + except HydrusExceptions.CancelledException: + + return + + + do_it_now = result == 'now' + + else: + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result != QW.QDialog.Accepted: + + return + + + + if do_it_now: + + self._SetFocusedMedia( None ) + + time.sleep( 0.1 ) + + CG.client_controller.CallToThread( CG.client_controller.files_maintenance_manager.RunJobImmediately, flat_media, job_type ) + + else: + + hashes = { media.GetHash() for media in flat_media } + + CG.client_controller.CallToThread( CG.client_controller.files_maintenance_manager.ScheduleJob, hashes, job_type ) + + + + + def _RescindDownloadSelected( self ): + + hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL ) + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, hashes ) ) ) + + + def _RescindPetitionFiles( self, file_service_key ): + + hashes = self._GetSelectedHashes() + + if hashes is not None and len( hashes ) > 0: + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PETITION, hashes ) ) ) + + + + def _RescindUploadFiles( self, file_service_key ): + + hashes = self._GetSelectedHashes() + + if hashes is not None and len( hashes ) > 0: + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, hashes ) ) ) + + + + def _Select( self, file_filter: ClientMediaFileFilter.FileFilter ): + + matching_media = file_filter.GetMediaListMedia( self ) + + media_to_deselect = self._selected_media.difference( matching_media ) + media_to_select = matching_media.difference( self._selected_media ) + + move_focus = self._focused_media in media_to_deselect or self._focused_media is None + + if move_focus or self._shift_select_started_with_this_media in media_to_deselect: + + self._EndShiftSelect() + + + self._DeselectSelect( media_to_deselect, media_to_select ) + + if move_focus: + + if len( self._selected_media ) == 0: + + self._SetFocusedMedia( None ) + + else: + + # let's not focus if one of the selectees is already visible + + media_visible = True in ( self._MediaIsVisible( media ) for media in self._selected_media ) + + if not media_visible: + + for m in self._sorted_media: + + if m in self._selected_media: + + ctrl = False + shift = False + + self._HitMedia( m, ctrl, shift ) + + self._ScrollToMedia( m ) + + break + + + + + + + + def _SetCollectionsAsAlternate( self ): + + collections = self._GetSelectedCollections() + + if len( collections ) > 0: + + message = 'Are you sure you want to set files in the selected collections as alternates? Each collection will be considered a separate group of alternates.' + message += '\n' * 2 + message += 'Be careful applying this to large groups--any more than a few dozen files, and the client could hang a long time.' + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result == QW.QDialog.Accepted: + + for collection in collections: + + media_group = collection.GetFlatMedia() + + self._SetDuplicates( HC.DUPLICATE_ALTERNATE, media_group = media_group, silent = True ) + + + + + + def _SetDuplicates( self, duplicate_type, media_pairs = None, media_group = None, duplicate_content_merge_options = None, silent = False ): + + if duplicate_type == HC.DUPLICATE_POTENTIAL: + + yes_no_text = 'queue all possible and valid pair combinations into the duplicate filter' + + elif duplicate_content_merge_options is None: + + yes_no_text = 'apply "{}"'.format( HC.duplicate_type_string_lookup[ duplicate_type ] ) + + if duplicate_type in [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] or ( CG.client_controller.new_options.GetBoolean( 'advanced_mode' ) and duplicate_type == HC.DUPLICATE_ALTERNATE ): + + yes_no_text += ' (with default duplicate metadata merge options)' + + new_options = CG.client_controller.new_options + + duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type ) + + + else: + + yes_no_text = 'apply "{}" (with custom duplicate metadata merge options)'.format( HC.duplicate_type_string_lookup[ duplicate_type ] ) + + + file_deletion_reason = 'Deleted from duplicate action on Media Page ({}).'.format( yes_no_text ) + + if media_pairs is None: + + if media_group is None: + + flat_media = self._GetSelectedFlatMedia() + + else: + + flat_media = ClientMedia.FlattenMedia( media_group ) + + + num_files_str = HydrusNumbers.ToHumanInt( len( flat_media ) ) + + if len( flat_media ) < 2: + + return False + + + if duplicate_type in ( HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_POTENTIAL ): + + media_pairs = list( itertools.combinations( flat_media, 2 ) ) + + else: + + first_media = flat_media[0] + + media_pairs = [ ( first_media, other_media ) for other_media in flat_media if other_media != first_media ] + + + else: + + num_files_str = HydrusNumbers.ToHumanInt( len( self._GetSelectedFlatMedia() ) ) + + + if len( media_pairs ) == 0: + + return False + + + if not silent: + + yes_label = 'yes' + no_label = 'no' + + if len( media_pairs ) > 1 and duplicate_type in ( HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_ALTERNATE ): + + media_pairs_str = HydrusNumbers.ToHumanInt( len( media_pairs ) ) + + message = 'Are you sure you want to {} for the {} selected files? The relationship will be applied between every pair combination in the file selection ({} pairs).'.format( yes_no_text, num_files_str, media_pairs_str ) + + if len( media_pairs ) > 100: + + if duplicate_type == HC.DUPLICATE_FALSE_POSITIVE: + + message = 'False positive records are complicated, and setting that relationship for {} files ({} pairs) at once is likely a mistake.'.format( num_files_str, media_pairs_str ) + message += '\n' * 2 + message += 'Are you sure all of these files are all potential duplicates and that they are all false positive matches with each other? If not, I recommend you step back for now.' + + yes_label = 'I know what I am doing' + no_label = 'step back for now' + + elif duplicate_type == HC.DUPLICATE_ALTERNATE: + + message = 'Are you certain all these {} files are alternates with every other member of the selection, and that none are duplicates?'.format( num_files_str ) + message += '\n' * 2 + message += 'If some of them may be duplicates, I recommend you either deselect the possible duplicates and try again, or just leave this group to be processed in the normal duplicate filter.' + + yes_label = 'they are all alternates' + no_label = 'some may be duplicates' + + + + else: + + message = 'Are you sure you want to ' + yes_no_text + ' for the {} selected files?'.format( num_files_str ) + + + result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = yes_label, no_label = no_label ) + + if result != QW.QDialog.Accepted: + + return False + + + + pair_info = [] + + # there's an issue here in that one decision will affect the next. if we say 'copy tags both sides' and say A > B & C, then B's tags, merged with A, should soon merge with C + # therefore, we need to update the media objects as we go here, which means we need duplicates to force content updates on + # this is a little hacky, so maybe a big rewrite here would be nice + + # There's a second issue, wew, in that in order to propagate C back to B, we need to do the whole thing twice! wow! + # some service_key_to_content_updates preservation gubbins is needed as a result + + hashes_to_duplicated_media = {} + hash_pairs_to_content_update_packages = collections.defaultdict( list ) + + for is_first_run in ( True, False ): + + for ( first_media, second_media ) in media_pairs: + + first_hash = first_media.GetHash() + second_hash = second_media.GetHash() + + if first_hash not in hashes_to_duplicated_media: + + hashes_to_duplicated_media[ first_hash ] = first_media.Duplicate() + + + first_duplicated_media = hashes_to_duplicated_media[ first_hash ] + + if second_hash not in hashes_to_duplicated_media: + + hashes_to_duplicated_media[ second_hash ] = second_media.Duplicate() + + + second_duplicated_media = hashes_to_duplicated_media[ second_hash ] + + content_update_packages = hash_pairs_to_content_update_packages[ ( first_hash, second_hash ) ] + + if duplicate_content_merge_options is not None: + + do_not_do_deletes = is_first_run + + # so the important part of this mess is here. we send the duplicated media, which is keeping up with content updates, to the method here + # original 'first_media' is not changed, and won't be until the database Write clears and publishes everything + content_update_packages.append( duplicate_content_merge_options.ProcessPairIntoContentUpdatePackage( first_duplicated_media, second_duplicated_media, file_deletion_reason = file_deletion_reason, do_not_do_deletes = do_not_do_deletes ) ) + + + for content_update_package in content_update_packages: + + for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): + + for content_update in content_updates: + + hashes = content_update.GetHashes() + + if first_hash in hashes: + + first_duplicated_media.GetMediaResult().ProcessContentUpdate( service_key, content_update ) + + + if second_hash in hashes: + + second_duplicated_media.GetMediaResult().ProcessContentUpdate( service_key, content_update ) + + + + + + if is_first_run: + + continue + + + pair_info.append( ( duplicate_type, first_hash, second_hash, content_update_packages ) ) + + + + if len( pair_info ) > 0: + + CG.client_controller.WriteSynchronous( 'duplicate_pair_status', pair_info ) + + return True + + + return False + + + def _SetDuplicatesCustom( self ): + + duplicate_types = [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] + + if CG.client_controller.new_options.GetBoolean( 'advanced_mode' ): + + duplicate_types.append( HC.DUPLICATE_ALTERNATE ) + + + choice_tuples = [ ( HC.duplicate_type_string_lookup[ duplicate_type ], duplicate_type ) for duplicate_type in duplicate_types ] + + try: + + duplicate_type = ClientGUIDialogsQuick.SelectFromList( self, 'select duplicate type', choice_tuples ) + + except HydrusExceptions.CancelledException: + + return + + + new_options = CG.client_controller.new_options + + duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type ) + + with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit duplicate merge options' ) as dlg: + + panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options, for_custom_action = True ) + + dlg.SetPanel( panel ) + + if dlg.exec() == QW.QDialog.Accepted: + + duplicate_content_merge_options = panel.GetValue() + + if duplicate_type == HC.DUPLICATE_BETTER: + + self._SetDuplicatesFocusedBetter( duplicate_content_merge_options = duplicate_content_merge_options ) + + else: + + self._SetDuplicates( duplicate_type, duplicate_content_merge_options = duplicate_content_merge_options ) + + + + + + def _SetDuplicatesFocusedBetter( self, duplicate_content_merge_options = None ): + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + focused_hash = focused_singleton.GetHash() + + flat_media = self._GetSelectedFlatMedia() + + ( better_media, ) = [ media for media in flat_media if media.GetHash() == focused_hash ] + + worse_flat_media = [ media for media in flat_media if media.GetHash() != focused_hash ] + + if len( worse_flat_media ) == 0: + + message = 'Since you only selected one file, would you rather just set this file as the best file of its group?' + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result == QW.QDialog.Accepted: + + self._SetDuplicatesFocusedKing( silent = True ) + + + return + + + media_pairs = [ ( better_media, worse_media ) for worse_media in worse_flat_media ] + + message = 'Are you sure you want to set the focused file as better than the {} other files in the selection?'.format( HydrusNumbers.ToHumanInt( len( worse_flat_media ) ) ) + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result == QW.QDialog.Accepted: + + self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs, silent = True, duplicate_content_merge_options = duplicate_content_merge_options ) + + + else: + + ClientGUIDialogsMessage.ShowWarning( self, 'No file is focused, so cannot set the focused file as better!' ) + + return + + + + def _SetDuplicatesFocusedKing( self, silent = False ): + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + focused_hash = media.GetHash() + + # TODO: when media knows its duplicate gubbins, we can test num dupe files and if it is king already and stuff easier here + + do_it = False + + if silent: + + do_it = True + + else: + + message = 'Are you sure you want to set the focused file as the best file of its duplicate group?' + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result == QW.QDialog.Accepted: + + do_it = True + + + + if do_it: + + CG.client_controller.WriteSynchronous( 'duplicate_set_king', focused_hash ) + + + else: + + ClientGUIDialogsMessage.ShowWarning( self, 'No file is focused, so cannot set the focused file as king!' ) + + return + + + + def _SetDuplicatesPotential( self ): + + media_group = self._GetSelectedFlatMedia() + + self._SetDuplicates( HC.DUPLICATE_POTENTIAL, media_group = media_group ) + + + def _SetFocusedMedia( self, media ): + + if media is None and self._focused_media is not None: + + next_best_media = self._focused_media + + i = self._sorted_media.index( next_best_media ) + + while next_best_media in self._selected_media: + + if i == 0: + + next_best_media = None + + break + + + i -= 1 + + next_best_media = self._sorted_media[ i ] + + + self._next_best_media_if_focuses_removed = next_best_media + + else: + + self._next_best_media_if_focuses_removed = None + + + publish_media = None + + self._focused_media = media + self._last_hit_media = media + + if self._focused_media is not None: + + publish_media = self._focused_media.GetDisplayMedia() + + + if publish_media is None: + + self.focusMediaCleared.emit() + + else: + + self.focusMediaChanged.emit( publish_media ) + + + + def _ScrollToMedia( self, media ): + + pass + + + def _ShowSelectionInNewPage( self ): + + hashes = self._GetSelectedHashes( ordered = True ) + + if len( hashes ) > 0: + + media_sort = self._management_controller.GetVariable( 'media_sort' ) + + if self._management_controller.HasVariable( 'media_collect' ): + + media_collect = self._management_controller.GetVariable( 'media_collect' ) + + else: + + media_collect = ClientMedia.MediaCollect() + + + ClientGUIMediaSimpleActions.ShowFilesInNewPage( hashes, self._location_context, media_sort = media_sort, media_collect = media_collect ) + + + + def _StartShiftSelect( self, media ): + + self._shift_select_started_with_this_media = media + self._media_added_in_current_shift_select = set() + + + def _Undelete( self ): + + media = self._GetSelectedFlatMedia() + + ClientGUIMediaModalActions.UndeleteMedia( self, media ) + + + def _UpdateBackgroundColour( self ): + + self.widget().update() + + + def _UploadDirectory( self, file_service_key ): + + hashes = self._GetSelectedHashes() + + if hashes is not None and len( hashes ) > 0: + + ipfs_service = CG.client_controller.services_manager.GetService( file_service_key ) + + + with ClientGUIDialogs.DialogTextEntry( self, 'Enter a note to describe this directory.' ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + note = dlg.GetValue() + + CG.client_controller.CallToThread( ipfs_service.PinDirectory, hashes, note ) + + + + + def _UploadFiles( self, file_service_key ): + + hashes = self._GetSelectedHashes( is_not_in_file_service_key = file_service_key ) + + if hashes is not None and len( hashes ) > 0: + + CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PEND, hashes ) ) ) + + + + def AddMediaResults( self, page_key, media_results ): + + if page_key == self._page_key: + + CG.client_controller.pub( 'refresh_page_name', self._page_key ) + + result = ClientMedia.ListeningMediaList.AddMediaResults( self, media_results ) + + self.newMediaAdded.emit() + + CG.client_controller.pub( 'notify_new_pages_count' ) + + return result + + + + def CleanBeforeDestroy( self ): + + self.Clear() + + + def ClearPageKey( self ): + + self._page_key = b'dead media panel page key' + + + def Collect( self, media_collect = None ): + + self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) ) + + ClientMedia.ListeningMediaList.Collect( self, media_collect = media_collect ) + + self._RecalculateVirtualSize() + + self.Sort() + + + def GetColour( self, colour_type ): + + if CG.client_controller.new_options.GetBoolean( 'override_stylesheet_colours' ): + + bg_colour = CG.client_controller.new_options.GetColour( colour_type ) + + else: + + bg_colour = self._qss_colours.get( colour_type, QG.QColor( 127, 127, 127 ) ) + + + return bg_colour + + + def GetTotalFileSize( self ): + + return 0 + + + def LaunchMediaViewerOnFocus( self, page_key ): + + if page_key == self._page_key: + + self._LaunchMediaViewer() + + + + def PageHidden( self ): + + pass + + + def PageShown( self ): + + self._PublishSelectionChange() + + + def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): + + command_processed = True + + if command.IsSimpleCommand(): + + action = command.GetSimpleAction() + + if action == CAC.SIMPLE_COPY_FILE_BITMAP: + + if not self._HasFocusSingleton(): + + return + + + focus_singleton = self._GetFocusSingleton() + + bitmap_type = command.GetSimpleData() + + ClientGUIMediaSimpleActions.CopyMediaBitmap( focus_singleton, bitmap_type ) + + elif action == CAC.SIMPLE_COPY_FILES: + + file_command_target = command.GetSimpleData() + + medias = self._GetMediasForFileCommandTarget( file_command_target ) + + if len( medias ) > 0: + + ClientGUIMediaSimpleActions.CopyFilesToClipboard( medias ) + + + elif action == CAC.SIMPLE_COPY_FILE_PATHS: + + file_command_target = command.GetSimpleData() + + medias = self._GetMediasForFileCommandTarget( file_command_target ) + + if len( medias ) > 0: + + ClientGUIMediaSimpleActions.CopyFilePathsToClipboard( medias ) + + + elif action == CAC.SIMPLE_COPY_FILE_HASHES: + + ( file_command_target, hash_type ) = command.GetSimpleData() + + medias = self._GetMediasForFileCommandTarget( file_command_target ) + + if len( medias ) > 0: + + ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, medias ) + + + elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES: + + hacky_ipfs_dict = command.GetSimpleData() + + file_command_target = hacky_ipfs_dict[ 'file_command_target' ] + ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ] + + medias = self._GetMediasForFileCommandTarget( file_command_target ) + + if len( medias ) > 0: + + ClientGUIMediaSimpleActions.CopyServiceFilenamesToClipboard( ipfs_service_key, medias ) + + + elif action == CAC.SIMPLE_COPY_FILE_ID: + + file_command_target = command.GetSimpleData() + + medias = self._GetMediasForFileCommandTarget( file_command_target ) + + if len( medias ) > 0: + + ClientGUIMediaSimpleActions.CopyFileIdsToClipboard( medias ) + + + elif action == CAC.SIMPLE_COPY_URLS: + + ordered_selected_media = self._GetSelectedMediaOrdered() + + if len( ordered_selected_media ) > 0: + + ClientGUIMediaSimpleActions.CopyMediaURLs( ordered_selected_media ) + + + elif action == CAC.SIMPLE_REARRANGE_THUMBNAILS: + + ordered_selected_media = self._GetSelectedMediaOrdered() + + ( rearrange_type, rearrange_data ) = command.GetSimpleData() + + insertion_index = None + + if rearrange_type == CAC.REARRANGE_THUMBNAILS_TYPE_FIXED: + + insertion_index = rearrange_data + + elif rearrange_type == CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND: + + rearrange_command = rearrange_data + + if rearrange_command == CAC.MOVE_HOME: + + insertion_index = 0 + + elif rearrange_command == CAC.MOVE_END: + + insertion_index = len( self._sorted_media ) + + else: + + if len( self._selected_media ) > 0: + + if rearrange_command in ( CAC.MOVE_LEFT, CAC.MOVE_RIGHT ): + + ordered_selected_media = self._GetSelectedMediaOrdered() + + earliest_index = self._sorted_media.index( ordered_selected_media[0] ) + + if rearrange_command == CAC.MOVE_LEFT: + + if earliest_index > 0: + + insertion_index = earliest_index - 1 + + + elif rearrange_command == CAC.MOVE_RIGHT: + + insertion_index = earliest_index + 1 + + + elif rearrange_command == CAC.MOVE_TO_FOCUS: + + if self._focused_media is not None: + + focus_index = self._sorted_media.index( self._focused_media ) + + insertion_index = focus_index + + + + + + + if insertion_index is None: + + return + + + self.MoveMedia( ordered_selected_media, insertion_index = insertion_index ) + + elif action == CAC.SIMPLE_SHOW_DUPLICATES: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + duplicate_type = command.GetSimpleData() + + ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FOCUSED_FALSE_POSITIVES: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.ClearFalsePositives( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FALSE_POSITIVES: + + hashes = self._GetSelectedHashes() + + if len( hashes ) > 0: + + ClientGUIDuplicates.ClearFalsePositives( self, hashes ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_FOCUSED_ALTERNATE_GROUP: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.DissolveAlternateGroup( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_ALTERNATE_GROUP: + + hashes = self._GetSelectedHashes() + + if len( hashes ) > 0: + + ClientGUIDuplicates.DissolveAlternateGroup( self, hashes ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_FOCUSED_DUPLICATE_GROUP: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.DissolveDuplicateGroup( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_DUPLICATE_GROUP: + + hashes = self._GetSelectedHashes() + + if len( hashes ) > 0: + + ClientGUIDuplicates.DissolveDuplicateGroup( self, hashes ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_FROM_ALTERNATE_GROUP: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.RemoveFromAlternateGroup( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_FROM_DUPLICATE_GROUP: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.RemoveFromDuplicateGroup( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_RESET_FOCUSED_POTENTIAL_SEARCH: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.ResetPotentialSearch( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_RESET_POTENTIAL_SEARCH: + + hashes = self._GetSelectedHashes() + + if len( hashes ) > 0: + + ClientGUIDuplicates.ResetPotentialSearch( self, hashes ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_POTENTIALS: + + if self._HasFocusSingleton(): + + media = self._GetFocusSingleton() + + hash = media.GetHash() + + ClientGUIDuplicates.RemovePotentials( self, ( hash, ) ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_POTENTIALS: + + hashes = self._GetSelectedHashes() + + if len( hashes ) > 0: + + ClientGUIDuplicates.RemovePotentials( self, hashes ) + + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE: + + self._SetDuplicates( HC.DUPLICATE_ALTERNATE ) + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE_COLLECTIONS: + + self._SetCollectionsAsAlternate() + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_CUSTOM: + + self._SetDuplicatesCustom() + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_FOCUSED_BETTER: + + self._SetDuplicatesFocusedBetter() + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_FOCUSED_KING: + + self._SetDuplicatesFocusedKing() + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_POTENTIAL: + + self._SetDuplicatesPotential() + + elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_SAME_QUALITY: + + self._SetDuplicates( HC.DUPLICATE_SAME_QUALITY ) + + elif action in ( CAC.SIMPLE_EXPORT_FILES, CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT ): + + do_export_and_then_quit = action == CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT + + if len( self._selected_media ) > 0: + + medias = self._GetSelectedMediaOrdered() + + flat_media = ClientMedia.FlattenMedia( medias ) + + ClientGUIMediaModalActions.ExportFiles( self, flat_media, do_export_and_then_quit = do_export_and_then_quit ) + + + elif action == CAC.SIMPLE_MANAGE_FILE_RATINGS: + + self._ManageRatings() + + elif action == CAC.SIMPLE_MANAGE_FILE_TAGS: + + self._ManageTags() + + elif action == CAC.SIMPLE_MANAGE_FILE_URLS: + + self._ManageURLs() + + elif action == CAC.SIMPLE_MANAGE_FILE_NOTES: + + self._ManageNotes() + + elif action == CAC.SIMPLE_MANAGE_FILE_TIMESTAMPS: + + self._ManageTimestamps() + + elif action == CAC.SIMPLE_OPEN_KNOWN_URL: + + self._OpenKnownURL() + + elif action == CAC.SIMPLE_ARCHIVE_FILE: + + self._Archive() + + elif action == CAC.SIMPLE_DELETE_FILE: + + self._Delete() + + elif action == CAC.SIMPLE_UNDELETE_FILE: + + self._Undelete() + + elif action == CAC.SIMPLE_INBOX_FILE: + + self._Inbox() + + elif action == CAC.SIMPLE_REMOVE_FILE_FROM_VIEW: + + self._Remove( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) ) + + elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER: + + self._LaunchMediaViewer() + + elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM: + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + it_worked = ClientGUIMediaSimpleActions.OpenExternally( focused_singleton ) + + if it_worked: + + self.focusMediaPaused.emit() + + + + elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER: + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + it_worked = ClientGUIMediaSimpleActions.OpenFileLocation( focused_singleton ) + + if it_worked: + + self.focusMediaPaused.emit() + + + + elif action == CAC.SIMPLE_NATIVE_OPEN_FILE_WITH_DIALOG: + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + it_worked = ClientGUIMediaSimpleActions.OpenFileWithDialog( focused_singleton ) + + if it_worked: + + self.focusMediaPaused.emit() + + + + elif action == CAC.SIMPLE_NATIVE_OPEN_FILE_PROPERTIES: + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + it_worked = ClientGUIMediaSimpleActions.OpenNativeFileProperties( focused_singleton ) + + if it_worked: + + self.focusMediaPaused.emit() + + + + elif action == CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER: + + if self._HasFocusSingleton(): + + focused_singleton = self._GetFocusSingleton() + + it_worked = ClientGUIMediaSimpleActions.OpenInWebBrowser( focused_singleton ) + + if it_worked: + + self.focusMediaPaused.emit() + + + + elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE: + + self._ShowSelectionInNewPage() + + elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE: + + hashes = self._GetSelectedHashes( ordered = True ) + + ClientGUIMediaSimpleActions.ShowFilesInNewDuplicatesFilterPage( hashes, self._location_context ) + + elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES: + + media = self._GetSelectedFlatMedia() + + hamming_distance = command.GetSimpleData() + + ClientGUIMediaSimpleActions.ShowSimilarFilesInNewPage( media, self._location_context, hamming_distance ) + + elif action == CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER: + + self._ArchiveDeleteFilter() + + elif action == CAC.SIMPLE_MAC_QUICKLOOK: + + self._MacQuicklook() + + else: + + command_processed = False + + + elif command.IsContentCommand(): + + command_processed = ClientGUIMediaModalActions.ApplyContentApplicationCommandToMedia( self, command, self._GetSelectedFlatMedia() ) + + else: + + command_processed = False + + + return command_processed + + + def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ): + + ClientMedia.ListeningMediaList.ProcessContentUpdatePackage( self, content_update_package ) + + we_were_file_or_tag_affected = False + + for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): + + for content_update in content_updates: + + hashes = content_update.GetHashes() + + if self._HasHashes( hashes ): + + affected_media = self._GetMedia( hashes ) + + self._RedrawMedia( affected_media ) + + if content_update.GetDataType() in ( HC.CONTENT_TYPE_FILES, HC.CONTENT_TYPE_MAPPINGS ): + + we_were_file_or_tag_affected = True + + + + + + if we_were_file_or_tag_affected: + + self._PublishSelectionChange( tags_changed = True ) + + + + def ProcessServiceUpdates( self, service_keys_to_service_updates: typing.Dict[ bytes, typing.Collection[ ClientServices.ServiceUpdate ] ] ): + + ClientMedia.ListeningMediaList.ProcessServiceUpdates( self, service_keys_to_service_updates ) + + for ( service_key, service_updates ) in service_keys_to_service_updates.items(): + + for service_update in service_updates: + + ( action, row ) = service_update.ToTuple() + + if action in ( HC.SERVICE_UPDATE_DELETE_PENDING, HC.SERVICE_UPDATE_RESET ): + + self._RecalculateVirtualSize() + + + self._PublishSelectionChange( tags_changed = True ) + + + + + def PublishSelectionChange( self ): + + self._PublishSelectionChange() + + + def RemoveMedia( self, page_key, hashes ): + + if page_key == self._page_key: + + self._RemoveMediaByHashes( hashes ) + + + + def SelectByTags( self, page_key, tag_service_key, and_or_or, tags ): + + if page_key == self._page_key: + + self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_TAGS, ( tag_service_key, and_or_or, tags ) ) ) + + self.setFocus( QC.Qt.OtherFocusReason ) + + + + def SetDuplicateStatusForAll( self, duplicate_type ): + + media_group = ClientMedia.FlattenMedia( self._sorted_media ) + + return self._SetDuplicates( duplicate_type, media_group = media_group ) + + + def SetEmptyPageStatusOverride( self, value: str ): + + self._empty_page_status_override = value + + + def SetFocusedMedia( self, media ): + + pass + + + def get_hmrp_background( self ): + + return self._qss_colours[ CC.COLOUR_THUMBGRID_BACKGROUND ] + + + def get_hmrp_thumbnail_local_background_normal( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND ] + + + def get_hmrp_thumbnail_local_background_selected( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] + + + def get_hmrp_thumbnail_local_border_normal( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BORDER ] + + + def get_hmrp_thumbnail_local_border_selected( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BORDER_SELECTED ] + + + def get_hmrp_thumbnail_not_local_background_normal( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] + + + def get_hmrp_thumbnail_not_local_background_selected( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] + + + def get_hmrp_thumbnail_not_local_border_normal( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE ] + + + def get_hmrp_thumbnail_not_local_border_selected( self ): + + return self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] + + + def set_hmrp_background( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMBGRID_BACKGROUND ] = colour + + + def set_hmrp_thumbnail_local_background_normal( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND ] = colour + + + def set_hmrp_thumbnail_local_background_selected( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] = colour + + + def set_hmrp_thumbnail_local_border_normal( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BORDER ] = colour + + + def set_hmrp_thumbnail_local_border_selected( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BORDER_SELECTED ] = colour + + + def set_hmrp_thumbnail_not_local_background_normal( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] = colour + + + def set_hmrp_thumbnail_not_local_background_selected( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] = colour + + + def set_hmrp_thumbnail_not_local_border_normal( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE ] = colour + + + def set_hmrp_thumbnail_not_local_border_selected( self, colour ): + + self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] = colour + + + hmrp_background = QC.Property( QG.QColor, get_hmrp_background, set_hmrp_background ) + hmrp_thumbnail_local_background_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_local_background_normal, set_hmrp_thumbnail_local_background_normal ) + hmrp_thumbnail_local_background_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_local_background_selected, set_hmrp_thumbnail_local_background_selected ) + hmrp_thumbnail_local_border_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_local_border_normal, set_hmrp_thumbnail_local_border_normal ) + hmrp_thumbnail_local_border_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_local_border_selected, set_hmrp_thumbnail_local_border_selected ) + hmrp_thumbnail_not_local_background_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_background_normal, set_hmrp_thumbnail_not_local_background_normal ) + hmrp_thumbnail_not_local_background_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_background_selected, set_hmrp_thumbnail_not_local_background_selected ) + hmrp_thumbnail_not_local_border_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_border_normal, set_hmrp_thumbnail_not_local_border_normal ) + hmrp_thumbnail_not_local_border_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_border_selected, set_hmrp_thumbnail_not_local_border_selected ) + + class _InnerWidget( QW.QWidget ): + + def __init__( self, parent ): + + super().__init__( parent ) + + self._parent = parent + + + def paintEvent( self, event ): + + painter = QG.QPainter( self ) + + bg_colour = self._parent.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) + + painter.setBackground( QG.QBrush( bg_colour ) ) + + painter.eraseRect( painter.viewport() ) + + background_pixmap = CG.client_controller.bitmap_manager.GetMediaBackgroundPixmap() + + if background_pixmap is not None: + + my_size = QP.ScrollAreaVisibleRect( self._parent ).size() + + pixmap_size = background_pixmap.size() + + painter.drawPixmap( my_size.width() - pixmap_size.width(), my_size.height() - pixmap_size.height(), background_pixmap ) + + + + diff --git a/hydrus/client/gui/pages/ClientGUIMediaResultsPanelLoading.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelLoading.py new file mode 100644 index 000000000..0d738291b --- /dev/null +++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelLoading.py @@ -0,0 +1,53 @@ +from hydrus.core import HydrusConstants as HC +from hydrus.core import HydrusNumbers + +from hydrus.client import ClientGlobals as CG +from hydrus.client.gui.pages import ClientGUIManagementController +from hydrus.client.gui.pages import ClientGUIMediaResultsPanel + +class MediaResultsPanelLoading( ClientGUIMediaResultsPanel.MediaResultsPanel ): + + def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController ): + + self._current = None + self._max = None + + super().__init__( parent, page_key, management_controller, [] ) + + CG.client_controller.sub( self, 'SetNumQueryResults', 'set_num_query_results' ) + + + def _GetPrettyStatusForStatusBar( self ): + + s = 'Loading' + HC.UNICODE_ELLIPSIS + + if self._current is not None: + + s += ' ' + HydrusNumbers.ToHumanInt( self._current ) + + if self._max is not None: + + s += ' of ' + HydrusNumbers.ToHumanInt( self._max ) + + + + return s + + + def GetSortedMedia( self ): + + return [] + + + def SetNumQueryResults( self, page_key, num_current, num_max ): + + if page_key == self._page_key: + + self._current = num_current + + self._max = num_max + + self._PublishSelectionChange() + + + diff --git a/hydrus/client/gui/pages/ClientGUIMediaResultsPanelMenus.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelMenus.py new file mode 100644 index 000000000..2bfbce395 --- /dev/null +++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelMenus.py @@ -0,0 +1,254 @@ +import typing + +from qtpy import QtWidgets as QW + +from hydrus.core import HydrusExceptions + +from hydrus.client import ClientApplicationCommand as CAC +from hydrus.client import ClientLocation +from hydrus.client.gui import ClientGUIMenus +from hydrus.client.media import ClientMedia +from hydrus.client.media import ClientMediaFileFilter +from hydrus.client.gui.pages import ClientGUIMediaResultsPanel + +def AddRearrangeMenu( win: ClientGUIMediaResultsPanel.MediaResultsPanel, menu: QW.QMenu, selected_media: typing.Set[ ClientMedia.Media ], sorted_media: ClientMedia.SortedList, focused_media: typing.Optional[ ClientMedia.Media ], selection_is_contiguous: bool, earliest_index: int ): + + if len( selected_media ) == 0 or len( selected_media ) == len( sorted_media ): + + return + + + rearrange_menu = ClientGUIMenus.GenerateMenu( menu ) + + if earliest_index > 0: + + ClientGUIMenus.AppendMenuItem( + rearrange_menu, + 'to start', + 'Move the selected thumbnails to the start of the media list.', + win.ProcessApplicationCommand, + CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_HOME ) ) + ) + + ClientGUIMenus.AppendMenuItem( + rearrange_menu, + 'back one', + 'Move the selected thumbnails back one position.', + win.ProcessApplicationCommand, + CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_LEFT ) ) + ) + + + if focused_media is not None: + + try: + + focused_index = sorted_media.index( focused_media ) + + if focused_index != earliest_index or not selection_is_contiguous: + + ClientGUIMenus.AppendMenuItem( + rearrange_menu, + 'to here', + 'Move the selected thumbnails to the focused position (most likely the one you clicked on).', + win.ProcessApplicationCommand, + CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_TO_FOCUS ) ) + ) + + + except HydrusExceptions.DataMissing: + + pass + + + + if earliest_index + len( selected_media ) < len( sorted_media ): + + ClientGUIMenus.AppendMenuItem( + rearrange_menu, + 'forward one', + 'Move the selected thumbnails forward one position.', + win.ProcessApplicationCommand, + CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_RIGHT ) ) + ) + + ClientGUIMenus.AppendMenuItem( + rearrange_menu, + 'to end', + 'Move the selected thumbnails to the end of the media list.', + win.ProcessApplicationCommand, + CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_END ) ) + ) + + + ClientGUIMenus.AppendMenu( menu, rearrange_menu, 'rearrange' ) + + +def AddRemoveMenu( win: ClientGUIMediaResultsPanel.MediaResultsPanel, menu: QW.QMenu, filter_counts, all_specific_file_domains, has_local_and_remote ): + + file_filter_all = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) + + if file_filter_all.GetCount( win, filter_counts ) > 0: + + remove_menu = ClientGUIMenus.GenerateMenu( menu ) + + # + + file_filter_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) + + file_filter_inbox = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) + + file_filter_archive = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) + + file_filter_not_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NOT_SELECTED ) + + # + + selected_count = file_filter_selected.GetCount( win, filter_counts ) + + if 0 < selected_count < file_filter_all.GetCount( win, filter_counts ): + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_selected.ToStringWithCount( win, filter_counts ), 'Remove all the selected files from the current view.', win._Remove, file_filter_selected ) + + + if file_filter_all.GetCount( win, filter_counts ) > 0: + + ClientGUIMenus.AppendSeparator( remove_menu ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_all.ToStringWithCount( win, filter_counts ), 'Remove all the files from the current view.', win._Remove, file_filter_all ) + + + if file_filter_inbox.GetCount( win, filter_counts ) > 0 and file_filter_archive.GetCount( win, filter_counts ) > 0: + + ClientGUIMenus.AppendSeparator( remove_menu ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_inbox.ToStringWithCount( win, filter_counts ), 'Remove all the inbox files from the current view.', win._Remove, file_filter_inbox ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_archive.ToStringWithCount( win, filter_counts ), 'Remove all the archived files from the current view.', win._Remove, file_filter_archive ) + + + if len( all_specific_file_domains ) > 1: + + ClientGUIMenus.AppendSeparator( remove_menu ) + + all_specific_file_domains = ClientLocation.SortFileServiceKeysNicely( all_specific_file_domains ) + + all_specific_file_domains = ClientLocation.FilterOutRedundantMetaServices( all_specific_file_domains ) + + for file_service_key in all_specific_file_domains: + + file_filter = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_FILE_SERVICE, file_service_key ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter.ToStringWithCount( win, filter_counts ), 'Remove all the files that are in this file domain.', win._Remove, file_filter ) + + + + if has_local_and_remote: + + file_filter_local = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_LOCAL ) + file_filter_remote = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_REMOTE ) + + ClientGUIMenus.AppendSeparator( remove_menu ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_local.ToStringWithCount( win, filter_counts ), 'Remove all the files that are in this client.', win._Remove, file_filter_local ) + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_remote.ToStringWithCount( win, filter_counts ), 'Remove all the files that are not in this client.', win._Remove, file_filter_remote ) + + + not_selected_count = file_filter_not_selected.GetCount( win, filter_counts ) + + if not_selected_count > 0 and selected_count > 0: + + ClientGUIMenus.AppendSeparator( remove_menu ) + + ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_not_selected.ToStringWithCount( win, filter_counts ), 'Remove all the not selected files from the current view.', win._Remove, file_filter_not_selected ) + + + ClientGUIMenus.AppendMenu( menu, remove_menu, 'remove' ) + + + +def AddSelectMenu( win: ClientGUIMediaResultsPanel.MediaResultsPanel, menu, filter_counts, all_specific_file_domains, has_local_and_remote ): + + file_filter_all = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) + + if file_filter_all.GetCount( win, filter_counts ) > 0: + + select_menu = ClientGUIMenus.GenerateMenu( menu ) + + # + + file_filter_inbox = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) + + file_filter_archive = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) + + file_filter_not_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NOT_SELECTED ) + + file_filter_none = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) + + # + + if file_filter_all.GetCount( win, filter_counts ) > 0: + + ClientGUIMenus.AppendSeparator( select_menu ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_all.ToStringWithCount( win, filter_counts ), 'Select all the files in the current view.', win._Select, file_filter_all ) + + + if file_filter_inbox.GetCount( win, filter_counts ) > 0 and file_filter_archive.GetCount( win, filter_counts ) > 0: + + ClientGUIMenus.AppendSeparator( select_menu ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_inbox.ToStringWithCount( win, filter_counts ), 'Select all the inbox files in the current view.', win._Select, file_filter_inbox ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_archive.ToStringWithCount( win, filter_counts ), 'Select all the archived files in the current view.', win._Select, file_filter_archive ) + + + if len( all_specific_file_domains ) > 1: + + ClientGUIMenus.AppendSeparator( select_menu ) + + all_specific_file_domains = ClientLocation.SortFileServiceKeysNicely( all_specific_file_domains ) + + all_specific_file_domains = ClientLocation.FilterOutRedundantMetaServices( all_specific_file_domains ) + + for file_service_key in all_specific_file_domains: + + file_filter = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_FILE_SERVICE, file_service_key ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter.ToStringWithCount( win, filter_counts ), 'Select all the files in this file domain.', win._Select, file_filter ) + + + + if has_local_and_remote: + + file_filter_local = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_LOCAL ) + file_filter_remote = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_REMOTE ) + + ClientGUIMenus.AppendSeparator( select_menu ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_local.ToStringWithCount( win, filter_counts ), 'Select all the files that are in this client.', win._Select, file_filter_local ) + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_remote.ToStringWithCount( win, filter_counts ), 'Select all the files that are not in this client.', win._Select, file_filter_remote ) + + + file_filter_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) + selected_count = file_filter_selected.GetCount( win, filter_counts ) + + not_selected_count = file_filter_not_selected.GetCount( win, filter_counts ) + + if selected_count > 0: + + if not_selected_count > 0: + + ClientGUIMenus.AppendSeparator( select_menu ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_not_selected.ToStringWithCount( win, filter_counts ), 'Swap what is and is not selected.', win._Select, file_filter_not_selected ) + + + ClientGUIMenus.AppendSeparator( select_menu ) + + ClientGUIMenus.AppendMenuItem( select_menu, file_filter_none.ToStringWithCount( win, filter_counts ), 'Deselect everything selected.', win._Select, file_filter_none ) + + + ClientGUIMenus.AppendMenu( menu, select_menu, 'select' ) + + diff --git a/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelSortCollect.py similarity index 99% rename from hydrus/client/gui/pages/ClientGUIResultsSortCollect.py rename to hydrus/client/gui/pages/ClientGUIMediaResultsPanelSortCollect.py index 4354f32ed..5b74e83ee 100644 --- a/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py +++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelSortCollect.py @@ -46,7 +46,7 @@ def __init__( self, parent, media_collect ): # TODO: Rewrite this garbage! A custom paintEvent, r u serious??? - QW.QComboBox.__init__( self, parent ) + super().__init__( parent ) self.view().pressed.connect( self._HandleItemPressed ) diff --git a/hydrus/client/gui/pages/ClientGUIMediaResultsPanelThumbnails.py b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelThumbnails.py new file mode 100644 index 000000000..4e70bc0cf --- /dev/null +++ b/hydrus/client/gui/pages/ClientGUIMediaResultsPanelThumbnails.py @@ -0,0 +1,2520 @@ +import random +import typing + +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW +from qtpy import QtGui as QG + +from hydrus.core import HydrusConstants as HC +from hydrus.core import HydrusData +from hydrus.core import HydrusExceptions +from hydrus.core import HydrusGlobals as HG +from hydrus.core import HydrusLists +from hydrus.core import HydrusNumbers +from hydrus.core import HydrusTime + +from hydrus.client import ClientApplicationCommand as CAC +from hydrus.client import ClientConstants as CC +from hydrus.client import ClientData +from hydrus.client import ClientFiles +from hydrus.client import ClientGlobals as CG +from hydrus.client.gui import ClientGUIDragDrop +from hydrus.client.gui import ClientGUICore as CGC +from hydrus.client.gui import ClientGUIFunctions +from hydrus.client.gui import ClientGUIMenus +from hydrus.client.gui import QtPorting as QP +from hydrus.client.gui.media import ClientGUIMediaSimpleActions +from hydrus.client.gui.media import ClientGUIMediaModalActions +from hydrus.client.gui.media import ClientGUIMediaMenus +from hydrus.client.gui.pages import ClientGUIManagementController +from hydrus.client.gui.pages import ClientGUIMediaResultsPanel +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelMenus +from hydrus.client.media import ClientMedia +from hydrus.client.media import ClientMediaFileFilter +from hydrus.client.metadata import ClientTags + +FRAME_DURATION_60FPS = 1.0 / 60 + +class ThumbnailWaitingToBeDrawn( object ): + + def __init__( self, hash, thumbnail, thumbnail_index, bitmap ): + + self.hash = hash + self.thumbnail = thumbnail + self.thumbnail_index = thumbnail_index + self.bitmap = bitmap + + self._draw_complete = False + + + def DrawComplete( self ) -> bool: + + return self._draw_complete + + + def DrawDue( self ) -> bool: + + return True + + + def DrawToPainter( self, x: int, y: int, painter: QG.QPainter ): + + painter.drawImage( x, y, self.bitmap ) + + self._draw_complete = True + + + +class ThumbnailWaitingToBeDrawnAnimated( ThumbnailWaitingToBeDrawn ): + + FADE_DURATION_S = 0.5 + + def __init__( self, hash, thumbnail, thumbnail_index, bitmap ): + + super().__init__( hash, thumbnail, thumbnail_index, bitmap ) + + self.num_frames_drawn = 0 + self.num_frames_to_draw = max( int( self.FADE_DURATION_S // FRAME_DURATION_60FPS ), 1 ) + + opacity_factor = max( 0.05, 1 / ( self.num_frames_to_draw / 3 ) ) + + self.alpha_bmp = QP.AdjustOpacity( self.bitmap, opacity_factor ) + + self.animation_started_precise = HydrusTime.GetNowPrecise() + + + def _GetNumFramesOutstanding( self ): + + now_precise = HydrusTime.GetNowPrecise() + + num_frames_to_now = int( ( now_precise - self.animation_started_precise ) // FRAME_DURATION_60FPS ) + + return min( num_frames_to_now, self.num_frames_to_draw - self.num_frames_drawn ) + + + def DrawDue( self ) -> bool: + + return self._GetNumFramesOutstanding() > 0 + + + def DrawToPainter( self, x: int, y: int, painter: QG.QPainter ): + + num_frames_to_draw = self._GetNumFramesOutstanding() + + if self.num_frames_drawn + num_frames_to_draw >= self.num_frames_to_draw: + + painter.drawImage( x, y, self.bitmap ) + + self.num_frames_drawn = self.num_frames_to_draw + self._draw_complete = True + + else: + + for i in range( num_frames_to_draw ): + + painter.drawImage( x, y, self.alpha_bmp ) + + + self.num_frames_drawn += num_frames_to_draw + + + + +class MediaResultsPanelThumbnails( ClientGUIMediaResultsPanel.MediaResultsPanel ): + + def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController, media_results ): + + self._clean_canvas_pages = {} + self._dirty_canvas_pages = [] + self._num_rows_per_canvas_page = 1 + self._num_rows_per_actual_page = 1 + + self._last_size = QC.QSize( 20, 20 ) + self._num_columns = 1 + + self._drag_init_coordinates = None + self._drag_click_timestamp_ms = 0 + self._drag_prefire_event_count = 0 + self._hashes_to_thumbnails_waiting_to_be_drawn: typing.Dict[ bytes, ThumbnailWaitingToBeDrawn ] = {} + self._hashes_faded = set() + + super().__init__( parent, page_key, management_controller, media_results ) + + self._last_device_pixel_ratio = self.devicePixelRatio() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + thumbnail_scroll_rate = float( CG.client_controller.new_options.GetString( 'thumbnail_scroll_rate' ) ) + + self.verticalScrollBar().setSingleStep( int( round( thumbnail_span_height * thumbnail_scroll_rate ) ) ) + + self._widget_event_filter = QP.WidgetEventFilter( self.widget() ) + self._widget_event_filter.EVT_LEFT_DCLICK( self.EventMouseFullScreen ) + self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventMouseFullScreen ) + + # notice this is on widget, not myself. fails to set up scrollbars if just moved up + # there's a job in qt to-do to sort all this out and fix other scroll issues + self._widget_event_filter.EVT_SIZE( self.EventResize ) + + self.widget().setMinimumSize( 50, 50 ) + + self._UpdateScrollBars() + + CG.client_controller.sub( self, 'MaintainPageCache', 'memory_maintenance_pulse' ) + CG.client_controller.sub( self, 'NotifyNewFileInfo', 'new_file_info' ) + CG.client_controller.sub( self, 'NewThumbnails', 'new_thumbnails' ) + CG.client_controller.sub( self, 'ThumbnailsReset', 'notify_complete_thumbnail_reset' ) + CG.client_controller.sub( self, 'RedrawAllThumbnails', 'refresh_all_tag_presentation_gui' ) + CG.client_controller.sub( self, 'WaterfallThumbnails', 'waterfall_thumbnails' ) + + + def _CalculateVisiblePageIndices( self ): + + y_start = self._GetYStart() + + earliest_y = y_start + + last_y = earliest_y + QP.ScrollAreaVisibleRect( self ).size().height() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + page_height = self._num_rows_per_canvas_page * thumbnail_span_height + + first_visible_page_index = earliest_y // page_height + + last_visible_page_index = last_y // page_height + + page_indices = list( range( first_visible_page_index, last_visible_page_index + 1 ) ) + + return page_indices + + + def _CreateNewDirtyPage( self ): + + my_width = self.size().width() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + dpr = self.devicePixelRatio() + + canvas_width = int( my_width * dpr ) + canvas_height = int( self._num_rows_per_canvas_page * thumbnail_span_height * dpr ) + + canvas_page = CG.client_controller.bitmap_manager.GetQtImage( canvas_width, canvas_height, 32 ) + + canvas_page.setDevicePixelRatio( dpr ) + + self._dirty_canvas_pages.append( canvas_page ) + + + def _DeleteAllDirtyPages( self ): + + self._dirty_canvas_pages = [] + + + def _DirtyAllPages( self ): + + clean_indices = list( self._clean_canvas_pages.keys() ) + + for clean_index in clean_indices: + + self._DirtyPage( clean_index ) + + + + def _DirtyPage( self, clean_index ): + + canvas_page = self._clean_canvas_pages[ clean_index ] + + del self._clean_canvas_pages[ clean_index ] + + thumbnails = [ thumbnail for ( thumbnail_index, thumbnail ) in self._GetThumbnailsFromPageIndex( clean_index ) ] + + if len( thumbnails ) > 0: + + CG.client_controller.GetCache( 'thumbnail' ).CancelWaterfall( self._page_key, thumbnails ) + + + self._dirty_canvas_pages.append( canvas_page ) + + + def _DrawCanvasPage( self, page_index, canvas_page ): + + painter = QG.QPainter( canvas_page ) + + new_options = CG.client_controller.new_options + + bg_colour = self.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) + + if HG.thumbnail_debug_mode and page_index % 2 == 0: + + bg_colour = ClientGUIFunctions.GetLighterDarkerColour( bg_colour ) + + + if new_options.GetNoneableString( 'media_background_bmp_path' ) is not None: + + comp_mode = painter.compositionMode() + + painter.setCompositionMode( QG.QPainter.CompositionMode_Source ) + + painter.setBackground( QG.QBrush( QC.Qt.transparent ) ) + + painter.eraseRect( painter.viewport() ) + + painter.setCompositionMode( comp_mode ) + + else: + + painter.setBackground( QG.QBrush( bg_colour ) ) + + painter.eraseRect( painter.viewport() ) + + + # + + page_thumbnails = self._GetThumbnailsFromPageIndex( page_index ) + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + thumbnails_to_render_later = [] + + thumbnail_cache = CG.client_controller.GetCache( 'thumbnail' ) + + thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) + + for ( thumbnail_index, thumbnail ) in page_thumbnails: + + display_media = thumbnail.GetDisplayMedia() + + if display_media is None: + + continue + + + hash = display_media.GetHash() + + if hash in self._hashes_faded and thumbnail_cache.HasThumbnailCached( thumbnail ): + + self._StopFading( hash ) + + thumbnail_col = thumbnail_index % self._num_columns + + thumbnail_row = thumbnail_index // self._num_columns + + x = thumbnail_col * thumbnail_span_width + thumbnail_margin + + y = ( thumbnail_row - ( page_index * self._num_rows_per_canvas_page ) ) * thumbnail_span_height + thumbnail_margin + + painter.drawImage( x, y, thumbnail.GetQtImage( self, self.devicePixelRatio() ) ) + + else: + + thumbnails_to_render_later.append( thumbnail ) + + + + if len( thumbnails_to_render_later ) > 0: + + CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails_to_render_later ) + + + + def _FadeThumbnails( self, thumbnails ): + + if len( thumbnails ) == 0: + + return + + + if not CG.client_controller.gui.IsCurrentPage( self._page_key ): + + self._DirtyAllPages() + + return + + + now_precise = HydrusTime.GetNowPrecise() + + for thumbnail in thumbnails: + + display_media = thumbnail.GetDisplayMedia() + + if display_media is None: + + continue + + + try: + + thumbnail_index = self._sorted_media.index( thumbnail ) + + except HydrusExceptions.DataMissing: + + # probably means a collect happened during an ongoing waterfall or whatever + + continue + + + if self._GetPageIndexFromThumbnailIndex( thumbnail_index ) not in self._clean_canvas_pages: + + continue + + + hash = display_media.GetHash() + + self._hashes_faded.add( hash ) + + self._StopFading( hash ) + + bitmap = thumbnail.GetQtImage( self, self.devicePixelRatio() ) + + fade_thumbnails = CG.client_controller.new_options.GetBoolean( 'fade_thumbnails' ) + + if fade_thumbnails: + + thumbnail_draw_object = ThumbnailWaitingToBeDrawnAnimated( hash, thumbnail, thumbnail_index, bitmap ) + + else: + + thumbnail_draw_object = ThumbnailWaitingToBeDrawn( hash, thumbnail, thumbnail_index, bitmap ) + + + self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] = thumbnail_draw_object + + + CG.client_controller.gui.RegisterAnimationUpdateWindow( self ) + + + def _GenerateMediaCollection( self, media_results ): + + return ThumbnailMediaCollection( self._location_context, media_results ) + + + def _GenerateMediaSingleton( self, media_result ): + + return ThumbnailMediaSingleton( media_result ) + + + def _GetMediaCoordinates( self, media ): + + try: index = self._sorted_media.index( media ) + except: return ( -1, -1 ) + + row = index // self._num_columns + column = index % self._num_columns + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) + + ( x, y ) = ( column * thumbnail_span_width + thumbnail_margin, row * thumbnail_span_height + thumbnail_margin ) + + return ( x, y ) + + + def _GetPageIndexFromThumbnailIndex( self, thumbnail_index ): + + thumbnails_per_page = self._num_columns * self._num_rows_per_canvas_page + + page_index = thumbnail_index // thumbnails_per_page + + return page_index + + + def _GetThumbnailSpanDimensions( self ): + + thumbnail_border = CG.client_controller.new_options.GetInteger( 'thumbnail_border' ) + thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) + + return ClientData.AddPaddingToDimensions( HC.options[ 'thumbnail_dimensions' ], ( thumbnail_border + thumbnail_margin ) * 2 ) + + + def _GetThumbnailUnderMouse( self, mouse_event ): + + pos = mouse_event.position().toPoint() + + x = pos.x() + y = pos.y() + + ( t_span_x, t_span_y ) = self._GetThumbnailSpanDimensions() + + x_mod = x % t_span_x + y_mod = y % t_span_y + + thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) + + if x_mod <= thumbnail_margin or y_mod <= thumbnail_margin or x_mod > t_span_x - thumbnail_margin or y_mod > t_span_y - thumbnail_margin: + + return None + + + column_index = x // t_span_x + row_index = y // t_span_y + + if column_index >= self._num_columns: + + return None + + + thumbnail_index = self._num_columns * row_index + column_index + + if thumbnail_index < 0: + + return None + + + if thumbnail_index >= len( self._sorted_media ): + + return None + + + return self._sorted_media[ thumbnail_index ] + + + def _GetThumbnailsFromPageIndex( self, page_index ): + + num_thumbnails_per_page = self._num_columns * self._num_rows_per_canvas_page + + start_index = num_thumbnails_per_page * page_index + + if start_index <= len( self._sorted_media ): + + end_index = min( len( self._sorted_media ), start_index + num_thumbnails_per_page ) + + thumbnails = [ ( index, self._sorted_media[ index ] ) for index in range( start_index, end_index ) ] + + else: + + thumbnails = [] + + + return thumbnails + + + def _GetYStart( self ): + + visible_rect = QP.ScrollAreaVisibleRect( self ) + + visible_rect_y = visible_rect.y() + + visible_rect_height = visible_rect.height() + + my_virtual_size = self.widget().size() + + my_virtual_height = my_virtual_size.height() + + max_y = my_virtual_height - visible_rect_height + + y_start = max( 0, visible_rect_y ) + + y_start = min( y_start, max_y ) + + return y_start + + + def _MediaIsInCleanPage( self, thumbnail ): + + try: + + index = self._sorted_media.index( thumbnail ) + + except HydrusExceptions.DataMissing: + + return False + + + if self._GetPageIndexFromThumbnailIndex( index ) in self._clean_canvas_pages: + + return True + + else: + + return False + + + + def _MediaIsVisible( self, media ): + + if media is not None: + + ( x, y ) = self._GetMediaCoordinates( media ) + + visible_rect = QP.ScrollAreaVisibleRect( self ) + + visible_rect_y = visible_rect.y() + + visible_rect_height = visible_rect.height() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + bottom_edge_below_top_of_view = visible_rect_y < y + thumbnail_span_height + top_edge_above_bottom_of_view = y < visible_rect_y + visible_rect_height + + is_visible = bottom_edge_below_top_of_view and top_edge_above_bottom_of_view + + return is_visible + + + return True + + + def _MoveThumbnailFocus( self, rows, columns, shift ): + + if self._last_hit_media is not None: + + media_to_use = self._last_hit_media + + elif self._next_best_media_if_focuses_removed is not None: + + media_to_use = self._next_best_media_if_focuses_removed + + if columns == -1: # treat it as if the focused area is between this and the next + + columns = 0 + + + elif len( self._sorted_media ) > 0: + + media_to_use = self._sorted_media[ 0 ] + + else: + + media_to_use = None + + + if media_to_use is not None: + + try: + + current_position = self._sorted_media.index( media_to_use ) + + except HydrusExceptions.DataMissing: + + self._SetFocusedMedia( None ) + + return + + + new_position = current_position + columns + ( self._num_columns * rows ) + + if new_position < 0: + + new_position = 0 + + elif new_position > len( self._sorted_media ) - 1: + + new_position = len( self._sorted_media ) - 1 + + + new_media = self._sorted_media[ new_position ] + + self._HitMedia( new_media, False, shift ) + + self._ScrollToMedia( new_media ) + + + + def _NotifyThumbnailsHaveMoved( self ): + + self._DirtyAllPages() + + self.widget().update() + + + def _RecalculateVirtualSize( self, called_from_resize_event = False ): + + my_size = QP.ScrollAreaVisibleRect( self ).size() + + my_width = my_size.width() + my_height = my_size.height() + + if my_width > 0 and my_height > 0: + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + num_media = len( self._sorted_media ) + + num_rows = max( 1, num_media // self._num_columns ) + + if num_media % self._num_columns > 0: + + num_rows += 1 + + + virtual_width = my_width + + virtual_height = num_rows * thumbnail_span_height + + yUnit = self.verticalScrollBar().singleStep() + + excess = virtual_height % yUnit + + if excess > 0: # we want virtual height to fit exactly into scroll units, even if that puts some padding below bottom row + + top_up = yUnit - excess + + virtual_height += top_up + + + virtual_height = max( virtual_height, my_height ) + + virtual_size = QC.QSize( virtual_width, virtual_height ) + + if virtual_size != self.widget().size(): + + self.widget().resize( QC.QSize( virtual_width, virtual_height ) ) + + if not called_from_resize_event: + + self._UpdateScrollBars() # would lead to infinite recursion if called from a resize event + + + + + + def _RedrawMedia( self, thumbnails ): + + visible_thumbnails = [ thumbnail for thumbnail in thumbnails if self._MediaIsInCleanPage( thumbnail ) ] + + thumbnail_cache = CG.client_controller.GetCache( 'thumbnail' ) + + thumbnails_to_render_now = [] + thumbnails_to_render_later = [] + + for thumbnail in visible_thumbnails: + + if thumbnail_cache.HasThumbnailCached( thumbnail ): + + thumbnails_to_render_now.append( thumbnail ) + + else: + + thumbnails_to_render_later.append( thumbnail ) + + + + if len( thumbnails_to_render_now ) > 0: + + self._FadeThumbnails( thumbnails_to_render_now ) + + + if len( thumbnails_to_render_later ) > 0: + + CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails_to_render_later ) + + + + def _ReinitialisePageCacheIfNeeded( self ): + + old_num_rows = self._num_rows_per_canvas_page + old_num_columns = self._num_columns + + old_width = self._last_size.width() + old_height = self._last_size.height() + + my_size = QP.ScrollAreaVisibleRect( self ).size() + + my_width = my_size.width() + my_height = my_size.height() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + num_rows = ( my_height // thumbnail_span_height ) + + self._num_rows_per_actual_page = max( 1, num_rows ) + self._num_rows_per_canvas_page = max( 1, num_rows // 2 ) + + self._num_columns = max( 1, my_width // thumbnail_span_width ) + + dimensions_changed = old_width != my_width or old_height != my_height + thumb_layout_changed = old_num_columns != self._num_columns or old_num_rows != self._num_rows_per_canvas_page + + if dimensions_changed or thumb_layout_changed: + + width_got_bigger = old_width < my_width + + if thumb_layout_changed or width_got_bigger: + + self._DirtyAllPages() + + self._DeleteAllDirtyPages() + + + self.widget().update() + + + + def _RemoveMediaDirectly( self, singleton_media, collected_media ): + + if self._focused_media is not None: + + if self._focused_media in singleton_media or self._focused_media in collected_media: + + self._SetFocusedMedia( None ) + + + + super()._RemoveMediaDirectly( singleton_media, collected_media ) + + self._EndShiftSelect() + + self._RecalculateVirtualSize() + + self._DirtyAllPages() + + self._PublishSelectionChange() + + CG.client_controller.pub( 'refresh_page_name', self._page_key ) + + CG.client_controller.pub( 'notify_new_pages_count' ) + + self.widget().update() + + + def _ScrollEnd( self, shift = False ): + + if len( self._sorted_media ) > 0: + + end_media = self._sorted_media[ -1 ] + + self._HitMedia( end_media, False, shift ) + + self._ScrollToMedia( end_media ) + + + + def _ScrollHome( self, shift = False ): + + if len( self._sorted_media ) > 0: + + home_media = self._sorted_media[ 0 ] + + self._HitMedia( home_media, False, shift ) + + self._ScrollToMedia( home_media ) + + + + def _ScrollToMedia( self, media ): + + if media is not None: + + ( x, y ) = self._GetMediaCoordinates( media ) + + visible_rect = QP.ScrollAreaVisibleRect( self ) + + visible_rect_y = visible_rect.y() + + visible_rect_height = visible_rect.height() + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + new_options = CG.client_controller.new_options + + percent_visible = new_options.GetInteger( 'thumbnail_visibility_scroll_percent' ) / 100 + + if y < visible_rect_y: + + self.ensureVisible( 0, y, 0, 0 ) + + elif y > visible_rect_y + visible_rect_height - ( thumbnail_span_height * percent_visible ): + + self.ensureVisible( 0, y + thumbnail_span_height ) + + + + + def _StopFading( self, hash ): + + if hash in self._hashes_to_thumbnails_waiting_to_be_drawn: + + del self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] + + + + def _UpdateBackgroundColour( self ): + + super()._UpdateBackgroundColour() + + self._DirtyAllPages() + + self._DeleteAllDirtyPages() + + self.widget().update() + + + def _UpdateScrollBars( self ): + + # The following call is officially a no-op since this property is already true, but it also triggers an update + # of the scroll area's scrollbars which we need. + # We need this since we are intercepting & doing work in resize events which causes + # event propagation between the scroll area and the scrolled widget to not work properly (since we are suppressing resize events of the scrolled widget - otherwise we would get an infinite loop). + # Probably the best would be to change how this work and not intercept any resize events. + # Originally this was wx event handling which got ported to Qt more or less unchanged, hence the hackiness. + + self.setWidgetResizable( True ) + + + def AddMediaResults( self, page_key, media_results ): + + if page_key == self._page_key: + + thumbnails = super().AddMediaResults( page_key, media_results ) + + if len( thumbnails ) > 0: + + self._RecalculateVirtualSize() + + CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails ) + + if len( self._selected_media ) == 0: + + self._PublishSelectionIncrement( thumbnails ) + + else: + + self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) + + + + + + def contextMenuEvent( self, event ): + + if event.reason() == QG.QContextMenuEvent.Keyboard: + + self.ShowMenu() + + + + def EventMouseFullScreen( self, event ): + + t = self._GetThumbnailUnderMouse( event ) + + if t is not None: + + locations_manager = t.GetLocationsManager() + + if locations_manager.IsLocal(): + + self._LaunchMediaViewer( t ) + + else: + + can_download = not locations_manager.GetCurrent().isdisjoint( CG.client_controller.services_manager.GetRemoteFileServiceKeys() ) + + if can_download: + + self._DownloadHashes( t.GetHashes() ) + + + + + + def EventResize( self, event ): + + self._ReinitialisePageCacheIfNeeded() + + self._RecalculateVirtualSize( called_from_resize_event = True ) + + self._last_size = QP.ScrollAreaVisibleRect( self ).size() + + + def GetTotalFileSize( self ): + + return sum( ( m.GetSize() for m in self._sorted_media ) ) + + + def MaintainPageCache( self ): + + if not CG.client_controller.gui.IsCurrentPage( self._page_key ): + + self._DirtyAllPages() + + + self._DeleteAllDirtyPages() + + + def mouseMoveEvent( self, event ): + + if event.buttons() & QC.Qt.LeftButton: + + we_started_dragging_on_this_panel = self._drag_init_coordinates is not None + + if we_started_dragging_on_this_panel: + + old_drag_pos = self._drag_init_coordinates + + global_mouse_pos = QG.QCursor.pos() + + delta_pos = global_mouse_pos - old_drag_pos + + total_absolute_pixels_moved = delta_pos.manhattanLength() + + we_moved = total_absolute_pixels_moved > 0 + + if we_moved: + + self._drag_prefire_event_count += 1 + + + # prefire deal here is mpv lags on initial click, which can cause a drag (and hence an immediate pause) event by accident when mouserelease isn't processed quick + # so now we'll say we can't start a drag unless we get a smooth ramp to our pixel delta threshold + clean_drag_started = self._drag_prefire_event_count >= 10 + prob_not_an_accidental_click = HydrusTime.TimeHasPassedMS( self._drag_click_timestamp_ms + 100 ) + + if clean_drag_started and prob_not_an_accidental_click: + + media = self._GetSelectedFlatMedia( discriminant = CC.DISCRIMINANT_LOCAL ) + + if len( media ) > 0: + + alt_down = event.modifiers() & QC.Qt.AltModifier + + result = ClientGUIDragDrop.DoFileExportDragDrop( self, self._page_key, media, alt_down ) + + if result not in ( QC.Qt.IgnoreAction, ): + + self.focusMediaPaused.emit() + + + + + + else: + + self._drag_init_coordinates = None + self._drag_prefire_event_count = 0 + self._drag_click_timestamp_ms = 0 + + + event.ignore() + + + def mouseReleaseEvent( self, event ): + + if event.button() != QC.Qt.RightButton: + + QW.QScrollArea.mouseReleaseEvent( self, event ) + + return + + + self.ShowMenu() + + + def MoveMedia( self, medias: typing.List[ ClientMedia.Media ], insertion_index: int ): + + super().MoveMedia( medias, insertion_index ) + + self._NotifyThumbnailsHaveMoved() + + self._ScrollToMedia( medias[0] ) + + + def NewThumbnails( self, hashes ): + + affected_thumbnails = self._GetMedia( hashes ) + + if len( affected_thumbnails ) > 0: + + self._RedrawMedia( affected_thumbnails ) + + + + def NotifyNewFileInfo( self, hashes ): + + def qt_do_update( hashes_to_media_results ): + + affected_media = self._GetMedia( set( hashes_to_media_results.keys() ) ) + + for media in affected_media: + + media.UpdateFileInfo( hashes_to_media_results ) + + + self._RedrawMedia( affected_media ) + + + def do_it( win, callable, affected_hashes ): + + media_results = CG.client_controller.Read( 'media_results', affected_hashes ) + + hashes_to_media_results = { media_result.GetHash() : media_result for media_result in media_results } + + CG.client_controller.CallAfterQtSafe( win, 'new file info notification', qt_do_update, hashes_to_media_results ) + + + affected_hashes = self._hashes.intersection( hashes ) + + CG.client_controller.CallToThread( do_it, self, do_it, affected_hashes ) + + + def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): + + command_processed = True + + if command.IsSimpleCommand(): + + action = command.GetSimpleAction() + + if action == CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS: + + ( move_direction, selection_status ) = command.GetSimpleData() + + shift = selection_status == CAC.SELECTION_STATUS_SHIFT + + if move_direction in ( CAC.MOVE_HOME, CAC.MOVE_END ): + + if move_direction == CAC.MOVE_HOME: + + self._ScrollHome( shift ) + + else: # MOVE_END + + self._ScrollEnd( shift ) + + + elif move_direction in ( CAC.MOVE_PAGE_UP, CAC.MOVE_PAGE_DOWN ): + + if move_direction == CAC.MOVE_PAGE_UP: + + direction = -1 + + else: # MOVE_PAGE_DOWN + + direction = 1 + + + self._MoveThumbnailFocus( self._num_rows_per_actual_page * direction, 0, shift ) + + else: + + if move_direction == CAC.MOVE_LEFT: + + rows = 0 + columns = -1 + + elif move_direction == CAC.MOVE_RIGHT: + + rows = 0 + columns = 1 + + elif move_direction == CAC.MOVE_UP: + + rows = -1 + columns = 0 + + elif move_direction == CAC.MOVE_DOWN: + + rows = 1 + columns = 0 + + else: + + raise NotImplementedError() + + + self._MoveThumbnailFocus( rows, columns, shift ) + + + elif action == CAC.SIMPLE_SELECT_FILES: + + file_filter = command.GetSimpleData() + + self._Select( file_filter ) + + else: + + command_processed = False + + + else: + + command_processed = False + + + if not command_processed: + + return super().ProcessApplicationCommand( command ) + + else: + + return command_processed + + + + def RedrawAllThumbnails( self ): + + self._DirtyAllPages() + + for m in self._collected_media: + + m.RecalcInternals() + + + for thumbnail in self._sorted_media: + + thumbnail.ClearTagSummaryCaches() + + + self.widget().update() + + + def SetFocusedMedia( self, media ): + + super().SetFocusedMedia( media ) + + if media is None: + + self._SetFocusedMedia( None ) + + else: + + try: + + my_media = self._GetMedia( media.GetHashes() )[0] + + self._HitMedia( my_media, False, False ) + + self._ScrollToMedia( self._focused_media ) + + except: + + pass + + + + + def showEvent( self, event ): + + self._UpdateScrollBars() + + + def ShowMenu( self, do_not_show_just_return = False ): + + flat_selected_medias = ClientMedia.FlattenMedia( self._selected_media ) + + all_locations_managers = [ media.GetLocationsManager() for media in ClientMedia.FlattenMedia( self._sorted_media ) ] + selected_locations_managers = [ media.GetLocationsManager() for media in flat_selected_medias ] + + selection_has_local_file_domain = True in ( locations_manager.IsLocal() and not locations_manager.IsTrashed() for locations_manager in selected_locations_managers ) + selection_has_trash = True in ( locations_manager.IsTrashed() for locations_manager in selected_locations_managers ) + selection_has_inbox = True in ( media.HasInbox() for media in self._selected_media ) + selection_has_archive = True in ( media.HasArchive() and media.GetLocationsManager().IsLocal() for media in self._selected_media ) + selection_has_deletion_record = True in ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY in locations_manager.GetDeleted() for locations_manager in selected_locations_managers ) + + all_file_domains = HydrusLists.MassUnion( locations_manager.GetCurrent() for locations_manager in all_locations_managers ) + all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } ) + + some_downloading = True in ( locations_manager.IsDownloading() for locations_manager in selected_locations_managers ) + + has_local = True in ( locations_manager.IsLocal() for locations_manager in all_locations_managers ) + has_remote = True in ( locations_manager.IsRemote() for locations_manager in all_locations_managers ) + + num_files = self.GetNumFiles() + num_selected = self._GetNumSelected() + num_inbox = self.GetNumInbox() + num_archive = self.GetNumArchive() + + any_selected = num_selected > 0 + multiple_selected = num_selected > 1 + + menu = ClientGUIMenus.GenerateMenu( self.window() ) + + # variables + + collections_selected = True in ( media.IsCollection() for media in self._selected_media ) + + services_manager = CG.client_controller.services_manager + + services = services_manager.GetServices() + + file_repositories = [ service for service in services if service.GetServiceType() == HC.FILE_REPOSITORY ] + + ipfs_services = [ service for service in services if service.GetServiceType() == HC.IPFS ] + + local_ratings_services = [ service for service in services if service.GetServiceType() in HC.RATINGS_SERVICES ] + + i_can_post_ratings = len( local_ratings_services ) > 0 + + local_media_file_service_keys = { service.GetServiceKey() for service in services if service.GetServiceType() == HC.LOCAL_FILE_DOMAIN } + + file_repository_service_keys = { repository.GetServiceKey() for repository in file_repositories } + upload_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_CREATE ) } + petition_resolve_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_MODERATE ) } + petition_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_PETITION ) } - petition_resolve_permission_file_service_keys + user_manage_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_MODERATE ) } + ipfs_service_keys = { service.GetServiceKey() for service in ipfs_services } + + if multiple_selected: + + download_phrase = 'download all possible selected' + rescind_download_phrase = 'cancel downloads for all possible selected' + upload_phrase = 'upload all possible selected to' + rescind_upload_phrase = 'rescind pending selected uploads to' + petition_phrase = 'petition all possible selected for removal from' + rescind_petition_phrase = 'rescind selected petitions for' + remote_delete_phrase = 'delete all possible selected from' + modify_account_phrase = 'modify the accounts that uploaded selected to' + + pin_phrase = 'pin all to' + rescind_pin_phrase = 'rescind pin to' + unpin_phrase = 'unpin all from' + rescind_unpin_phrase = 'rescind unpin from' + + archive_phrase = 'archive selected' + inbox_phrase = 're-inbox selected' + local_delete_phrase = 'delete selected' + delete_physically_phrase = 'delete selected physically now' + undelete_phrase = 'undelete selected' + clear_deletion_phrase = 'clear deletion record for selected' + + else: + + download_phrase = 'download' + rescind_download_phrase = 'cancel download' + upload_phrase = 'upload to' + rescind_upload_phrase = 'rescind pending upload to' + petition_phrase = 'petition for removal from' + rescind_petition_phrase = 'rescind petition for' + remote_delete_phrase = 'delete from' + modify_account_phrase = 'modify the account that uploaded this to' + + pin_phrase = 'pin to' + rescind_pin_phrase = 'rescind pin to' + unpin_phrase = 'unpin from' + rescind_unpin_phrase = 'rescind unpin from' + + archive_phrase = 'archive' + inbox_phrase = 're-inbox' + local_delete_phrase = 'delete' + delete_physically_phrase = 'delete physically now' + undelete_phrase = 'undelete' + clear_deletion_phrase = 'clear deletion record' + + + # info about the files + + remote_service_keys = CG.client_controller.services_manager.GetRemoteFileServiceKeys() + + groups_of_current_remote_service_keys = [ locations_manager.GetCurrent().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] + groups_of_pending_remote_service_keys = [ locations_manager.GetPending().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] + groups_of_petitioned_remote_service_keys = [ locations_manager.GetPetitioned().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] + groups_of_deleted_remote_service_keys = [ locations_manager.GetDeleted().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] + + current_remote_service_keys = HydrusLists.MassUnion( groups_of_current_remote_service_keys ) + pending_remote_service_keys = HydrusLists.MassUnion( groups_of_pending_remote_service_keys ) + petitioned_remote_service_keys = HydrusLists.MassUnion( groups_of_petitioned_remote_service_keys ) + deleted_remote_service_keys = HydrusLists.MassUnion( groups_of_deleted_remote_service_keys ) + + common_current_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_current_remote_service_keys ) + common_pending_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_pending_remote_service_keys ) + common_petitioned_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_petitioned_remote_service_keys ) + common_deleted_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_deleted_remote_service_keys ) + + disparate_current_remote_service_keys = current_remote_service_keys - common_current_remote_service_keys + disparate_pending_remote_service_keys = pending_remote_service_keys - common_pending_remote_service_keys + disparate_petitioned_remote_service_keys = petitioned_remote_service_keys - common_petitioned_remote_service_keys + disparate_deleted_remote_service_keys = deleted_remote_service_keys - common_deleted_remote_service_keys + + pending_file_service_keys = pending_remote_service_keys.intersection( file_repository_service_keys ) + petitioned_file_service_keys = petitioned_remote_service_keys.intersection( file_repository_service_keys ) + + common_current_file_service_keys = common_current_remote_service_keys.intersection( file_repository_service_keys ) + common_pending_file_service_keys = common_pending_remote_service_keys.intersection( file_repository_service_keys ) + common_petitioned_file_service_keys = common_petitioned_remote_service_keys.intersection( file_repository_service_keys ) + common_deleted_file_service_keys = common_deleted_remote_service_keys.intersection( file_repository_service_keys ) + + disparate_current_file_service_keys = disparate_current_remote_service_keys.intersection( file_repository_service_keys ) + disparate_pending_file_service_keys = disparate_pending_remote_service_keys.intersection( file_repository_service_keys ) + disparate_petitioned_file_service_keys = disparate_petitioned_remote_service_keys.intersection( file_repository_service_keys ) + disparate_deleted_file_service_keys = disparate_deleted_remote_service_keys.intersection( file_repository_service_keys ) + + pending_ipfs_service_keys = pending_remote_service_keys.intersection( ipfs_service_keys ) + petitioned_ipfs_service_keys = petitioned_remote_service_keys.intersection( ipfs_service_keys ) + + common_current_ipfs_service_keys = common_current_remote_service_keys.intersection( ipfs_service_keys ) + common_pending_ipfs_service_keys = common_pending_file_service_keys.intersection( ipfs_service_keys ) + common_petitioned_ipfs_service_keys = common_petitioned_remote_service_keys.intersection( ipfs_service_keys ) + + disparate_current_ipfs_service_keys = disparate_current_remote_service_keys.intersection( ipfs_service_keys ) + disparate_pending_ipfs_service_keys = disparate_pending_remote_service_keys.intersection( ipfs_service_keys ) + disparate_petitioned_ipfs_service_keys = disparate_petitioned_remote_service_keys.intersection( ipfs_service_keys ) + + # valid commands for the files + + current_file_service_keys = set() + + uploadable_file_service_keys = set() + + downloadable_file_service_keys = set() + + petitionable_file_service_keys = set() + + deletable_file_service_keys = set() + + modifyable_file_service_keys = set() + + pinnable_ipfs_service_keys = set() + + unpinnable_ipfs_service_keys = set() + + remote_file_service_keys = ipfs_service_keys.union( file_repository_service_keys ) + + for locations_manager in selected_locations_managers: + + current = locations_manager.GetCurrent() + deleted = locations_manager.GetDeleted() + pending = locations_manager.GetPending() + petitioned = locations_manager.GetPetitioned() + + # ALL + + current_file_service_keys.update( current ) + + # FILE REPOS + + # we can upload (set pending) to a repo_id when we have permission, a file is local, not current, not pending, and either ( not deleted or we_can_overrule ) + + if locations_manager.IsLocal(): + + cannot_upload_to = current.union( pending ).union( deleted.difference( petition_resolve_permission_file_service_keys ) ) + + can_upload_to = upload_permission_file_service_keys.difference( cannot_upload_to ) + + uploadable_file_service_keys.update( can_upload_to ) + + + # we can download (set pending to local) when we have permission, a file is not local and not already downloading and current + + if not locations_manager.IsLocal() and not locations_manager.IsDownloading(): + + downloadable_file_service_keys.update( remote_file_service_keys.intersection( current ) ) + + + # we can petition when we have permission and a file is current and it is not already petitioned + + petitionable_file_service_keys.update( ( petition_permission_file_service_keys & current ) - petitioned ) + + # we can delete remote when we have permission and a file is current and it is not already petitioned + + deletable_file_service_keys.update( ( petition_resolve_permission_file_service_keys & current ) - petitioned ) + + # we can modify users when we have permission and the file is current or deleted + + modifyable_file_service_keys.update( user_manage_permission_file_service_keys & ( current | deleted ) ) + + # IPFS + + # we can pin if a file is local, not current, not pending + + if locations_manager.IsLocal(): + + pinnable_ipfs_service_keys.update( ipfs_service_keys - current - pending ) + + + # we can unpin a file if it is current and not petitioned + + unpinnable_ipfs_service_keys.update( ( ipfs_service_keys & current ) - petitioned ) + + + # do the actual menu + + selection_info_menu = ClientGUIMenus.GenerateMenu( menu ) + + selected_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._selected_media ) + + selection_info_menu_label = f'{selected_files_string}, {self._GetPrettyTotalSize( only_selected = True )}' + + if multiple_selected: + + pretty_total_duration = self._GetPrettyTotalDuration( only_selected = True ) + + if pretty_total_duration != '': + + selection_info_menu_label += ', {}'.format( pretty_total_duration ) + + + else: + + # TODO: move away from this hell function GetPrettyInfoLines and set the timestamp tooltips to the be the full ISO time + + if self._HasFocusSingleton(): + + focus_singleton = self._GetFocusSingleton() + + pretty_info_lines = list( focus_singleton.GetPrettyInfoLines() ) + + ClientGUIMediaMenus.AddPrettyInfoLines( selection_info_menu, pretty_info_lines ) + + + + ClientGUIMenus.AppendSeparator( selection_info_menu ) + + ClientGUIMediaMenus.AddFileViewingStatsMenu( selection_info_menu, self._selected_media ) + + if len( disparate_current_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_current_file_service_keys, 'some uploaded to' ) + + + if multiple_selected and len( common_current_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_current_file_service_keys, 'selected uploaded to' ) + + + if len( disparate_pending_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_pending_file_service_keys, 'some pending to' ) + + + if len( common_pending_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_pending_file_service_keys, 'pending to' ) + + + if len( disparate_petitioned_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_petitioned_file_service_keys, 'some petitioned for removal from' ) + + + if len( common_petitioned_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_petitioned_file_service_keys, 'petitioned for removal from' ) + + + if len( disparate_deleted_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_deleted_file_service_keys, 'some deleted from' ) + + + if len( common_deleted_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_deleted_file_service_keys, 'deleted from' ) + + + if len( disparate_current_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_current_ipfs_service_keys, 'some pinned to' ) + + + if multiple_selected and len( common_current_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_current_ipfs_service_keys, 'selected pinned to' ) + + + if len( disparate_pending_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_pending_ipfs_service_keys, 'some to be pinned to' ) + + + if len( common_pending_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_pending_ipfs_service_keys, 'to be pinned to' ) + + + if len( disparate_petitioned_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_petitioned_ipfs_service_keys, 'some to be unpinned from' ) + + + if len( common_petitioned_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_petitioned_ipfs_service_keys, unpin_phrase ) + + + if any_selected: + + if len( selection_info_menu.actions() ) == 0: + + selection_info_menu.deleteLater() + + ClientGUIMenus.AppendMenuLabel( menu, selection_info_menu_label ) + + else: + + ClientGUIMenus.AppendMenu( menu, selection_info_menu, selection_info_menu_label ) + + + ClientGUIMenus.AppendSeparator( menu ) + + + ClientGUIMenus.AppendMenuItem( menu, 'refresh', 'Refresh the current search.', self.refreshQuery.emit ) + + if len( self._sorted_media ) > 0: + + ClientGUIMenus.AppendSeparator( menu ) + + filter_counts = {} + + filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) ] = num_files + filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) ] = num_inbox + filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) ] = num_archive + filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) ] = num_selected + + has_local_and_remote = has_local and has_remote + + ClientGUIMediaResultsPanelMenus.AddSelectMenu( self, menu, filter_counts, all_specific_file_domains, has_local_and_remote ) + ClientGUIMediaResultsPanelMenus.AddRemoveMenu( self, menu, filter_counts, all_specific_file_domains, has_local_and_remote ) + + if len( self._selected_media ) > 0: + + ordered_selected_media = self._GetSelectedMediaOrdered() + + try: + + earliest_index = self._sorted_media.index( ordered_selected_media[0] ) + + selection_is_contiguous = any_selected and self._sorted_media.index( ordered_selected_media[-1] ) - earliest_index == num_selected - 1 + + ClientGUIMediaResultsPanelMenus.AddRearrangeMenu( self, menu, self._selected_media, self._sorted_media, self._focused_media, selection_is_contiguous, earliest_index ) + + except HydrusExceptions.DataMissing: + + pass + + + + ClientGUIMenus.AppendSeparator( menu ) + + if has_local: + + ClientGUIMenus.AppendMenuItem( menu, 'archive/delete filter', 'Launch a special media viewer that will quickly archive or delete the selected media. Check the help if you are unfamiliar with this mode!', self._ArchiveDeleteFilter ) + + + + if selection_has_inbox: + + ClientGUIMenus.AppendMenuItem( menu, archive_phrase, 'Archive the selected files.', self._Archive ) + + + if selection_has_archive: + + ClientGUIMenus.AppendMenuItem( menu, inbox_phrase, 'Put the selected files back in the inbox.', self._Inbox ) + + + ClientGUIMenus.AppendSeparator( menu ) + + user_command_deletable_file_service_keys = local_media_file_service_keys.union( [ CC.LOCAL_UPDATE_SERVICE_KEY ] ) + + local_file_service_keys_we_are_in = sorted( current_file_service_keys.intersection( user_command_deletable_file_service_keys ), key = CG.client_controller.services_manager.GetName ) + + if len( local_file_service_keys_we_are_in ) > 0: + + delete_menu = ClientGUIMenus.GenerateMenu( menu ) + + for file_service_key in local_file_service_keys_we_are_in: + + service_name = CG.client_controller.services_manager.GetName( file_service_key ) + + ClientGUIMenus.AppendMenuItem( delete_menu, f'from {service_name}', f'Delete the selected files from {service_name}.', self._Delete, file_service_key ) + + + ClientGUIMenus.AppendMenu( menu, delete_menu, local_delete_phrase ) + + + if selection_has_trash: + + if selection_has_local_file_domain: + + ClientGUIMenus.AppendMenuItem( menu, 'delete trash physically now', 'Completely delete the selected trashed files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY, only_those_in_file_service_key = CC.TRASH_SERVICE_KEY ) + + + ClientGUIMenus.AppendMenuItem( menu, delete_physically_phrase, 'Completely delete the selected files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) + ClientGUIMenus.AppendMenuItem( menu, undelete_phrase, 'Restore the selected files back to \'my files\'.', self._Undelete ) + + + if selection_has_deletion_record: + + ClientGUIMenus.AppendMenuItem( menu, clear_deletion_phrase, 'Clear the deletion record for these files, allowing them to reimport even if previously deleted files are set to be discarded.', self._ClearDeleteRecord ) + + + ClientGUIMenus.AppendSeparator( menu ) + + if any_selected: + + manage_menu = ClientGUIMenus.GenerateMenu( menu ) + + ClientGUIMenus.AppendMenuItem( manage_menu, 'tags', 'Manage tags for the selected files.', self._ManageTags ) + + if i_can_post_ratings: + + ClientGUIMenus.AppendMenuItem( manage_menu, 'ratings', 'Manage ratings for the selected files.', self._ManageRatings ) + + + num_notes = 0 + + if self._HasFocusSingleton(): + + focus_singleton = self._GetFocusSingleton() + + num_notes = focus_singleton.GetNotesManager().GetNumNotes() + + + notes_str = 'notes' + + if num_notes > 0: + + notes_str = '{} ({})'.format( notes_str, HydrusNumbers.ToHumanInt( num_notes ) ) + + + ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage notes for the focused file.', self._ManageNotes ) + + ClientGUIMenus.AppendMenuItem( manage_menu, 'times', 'Edit the timestamps for your files.', self._ManageTimestamps ) + ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaModalActions.SetFilesForcedFiletypes, self, self._selected_media ) + + if self._HasFocusSingleton(): + + focus_singleton = self._GetFocusSingleton() + + ClientGUIMediaMenus.AddDuplicatesMenu( self, manage_menu, self._location_context, focus_singleton, num_selected, collections_selected ) + + + regen_menu = ClientGUIMenus.GenerateMenu( manage_menu ) + + for job_type in ClientFiles.ALL_REGEN_JOBS_IN_HUMAN_ORDER: + + ClientGUIMenus.AppendMenuItem( regen_menu, ClientFiles.regen_file_enum_to_str_lookup[ job_type ], ClientFiles.regen_file_enum_to_description_lookup[ job_type ], self._RegenerateFileData, job_type ) + + + ClientGUIMenus.AppendMenu( manage_menu, regen_menu, 'maintenance' ) + + ClientGUIMediaMenus.AddManageFileViewingStatsMenu( self, manage_menu, flat_selected_medias ) + + ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' ) + + ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( flat_selected_medias ) + + len_interesting_local_service_keys = 0 + + len_interesting_local_service_keys += len( local_duplicable_to_file_service_keys ) + len_interesting_local_service_keys += len( local_moveable_from_and_to_file_service_keys ) + + # + + len_interesting_remote_service_keys = 0 + + len_interesting_remote_service_keys += len( downloadable_file_service_keys ) + len_interesting_remote_service_keys += len( uploadable_file_service_keys ) + len_interesting_remote_service_keys += len( pending_file_service_keys ) + len_interesting_remote_service_keys += len( petitionable_file_service_keys ) + len_interesting_remote_service_keys += len( petitioned_file_service_keys ) + len_interesting_remote_service_keys += len( deletable_file_service_keys ) + len_interesting_remote_service_keys += len( modifyable_file_service_keys ) + len_interesting_remote_service_keys += len( pinnable_ipfs_service_keys ) + len_interesting_remote_service_keys += len( pending_ipfs_service_keys ) + len_interesting_remote_service_keys += len( unpinnable_ipfs_service_keys ) + len_interesting_remote_service_keys += len( petitioned_ipfs_service_keys ) + + if multiple_selected: + + len_interesting_remote_service_keys += len( ipfs_service_keys ) + + + if len_interesting_local_service_keys > 0 or len_interesting_remote_service_keys > 0: + + locations_menu = ClientGUIMenus.GenerateMenu( menu ) + + ClientGUIMenus.AppendMenu( menu, locations_menu, 'locations' ) + + if len_interesting_local_service_keys > 0: + + ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, locations_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand ) + + + if len_interesting_remote_service_keys > 0: + + ClientGUIMenus.AppendSeparator( locations_menu ) + + if len( downloadable_file_service_keys ) > 0: + + ClientGUIMenus.AppendMenuItem( locations_menu, download_phrase, 'Download all possible selected files.', self._DownloadSelected ) + + + if some_downloading: + + ClientGUIMenus.AppendMenuItem( locations_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self._RescindDownloadSelected ) + + + if len( uploadable_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles ) + + + if len( pending_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles ) + + + if len( petitionable_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles ) + + + if len( petitioned_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles ) + + + if len( deletable_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete ) + + + if len( modifyable_file_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders ) + + + if len( pinnable_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles ) + + + if len( pending_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles ) + + + if len( unpinnable_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles ) + + + if len( petitioned_ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles ) + + + if multiple_selected and len( ipfs_service_keys ) > 0: + + ClientGUIMediaMenus.AddServiceKeysToMenu( locations_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory ) + + + + + # + + ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._focused_media, num_selected, selected_media = self._selected_media ) + + ClientGUIMediaMenus.AddOpenMenu( self, menu, self._focused_media, self._selected_media ) + + ClientGUIMediaMenus.AddShareMenu( self, menu, self._focused_media, self._selected_media ) + + + if not do_not_show_just_return: + + CGC.core().PopupMenu( self, menu ) + + + else: + + return menu + + + + def Sort( self, media_sort = None ): + + super().Sort( media_sort ) + + self._NotifyThumbnailsHaveMoved() + + + def ThumbnailsReset( self ): + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + thumbnail_scroll_rate = float( CG.client_controller.new_options.GetString( 'thumbnail_scroll_rate' ) ) + + self.verticalScrollBar().setSingleStep( int( round( thumbnail_span_height * thumbnail_scroll_rate ) ) ) + + self._hashes_to_thumbnails_waiting_to_be_drawn = {} + self._hashes_faded = set() + + self._ReinitialisePageCacheIfNeeded() + + self._RecalculateVirtualSize() + + self.RedrawAllThumbnails() + + + def TIMERAnimationUpdate( self ): + + loop_should_break_time = HydrusTime.GetNowPrecise() + ( FRAME_DURATION_60FPS / 2 ) + + ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() + + thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) + + hashes = list( self._hashes_to_thumbnails_waiting_to_be_drawn.keys() ) + + page_indices_to_painters = {} + + page_height = self._num_rows_per_canvas_page * thumbnail_span_height + + for hash in HydrusData.IterateListRandomlyAndFast( hashes ): + + thumbnail_draw_object = self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] + + delete_entry = False + + if thumbnail_draw_object.DrawDue(): + + thumbnail_index = thumbnail_draw_object.thumbnail_index + + try: + + expected_thumbnail = self._sorted_media[ thumbnail_index ] + + except: + + expected_thumbnail = None + + + page_index = self._GetPageIndexFromThumbnailIndex( thumbnail_index ) + + if expected_thumbnail != thumbnail_draw_object.thumbnail: + + delete_entry = True + + elif page_index not in self._clean_canvas_pages: + + delete_entry = True + + else: + + thumbnail_col = thumbnail_index % self._num_columns + + thumbnail_row = thumbnail_index // self._num_columns + + x = thumbnail_col * thumbnail_span_width + thumbnail_margin + + y = ( thumbnail_row - ( page_index * self._num_rows_per_canvas_page ) ) * thumbnail_span_height + thumbnail_margin + + if page_index not in page_indices_to_painters: + + canvas_page = self._clean_canvas_pages[ page_index ] + + painter = QG.QPainter( canvas_page ) + + page_indices_to_painters[ page_index ] = painter + + + painter = page_indices_to_painters[ page_index ] + + thumbnail_draw_object.DrawToPainter( x, y, painter ) + + # + + page_virtual_y = page_height * page_index + + self.widget().update( QC.QRect( x, page_virtual_y + y, thumbnail_span_width - thumbnail_margin, thumbnail_span_height - thumbnail_margin ) ) + + + + if thumbnail_draw_object.DrawComplete() or delete_entry: + + del self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] + + + if HydrusTime.TimeHasPassedPrecise( loop_should_break_time ): + + break + + + + if len( self._hashes_to_thumbnails_waiting_to_be_drawn ) == 0: + + CG.client_controller.gui.UnregisterAnimationUpdateWindow( self ) + + + + + def WaterfallThumbnails( self, page_key, thumbnails ): + + if self._page_key == page_key: + + self._FadeThumbnails( thumbnails ) + + + + class _InnerWidget( QW.QWidget ): + + def __init__( self, parent ): + + super().__init__( parent ) + + self._parent = parent + + + def mousePressEvent( self, event ): + + self._parent._drag_init_coordinates = QG.QCursor.pos() + self._parent._drag_click_timestamp_ms = HydrusTime.GetNowMS() + + thumb = self._parent._GetThumbnailUnderMouse( event ) + + right_on_whitespace = event.button() == QC.Qt.RightButton and thumb is None + + if not right_on_whitespace: + + self._parent._HitMedia( thumb, event.modifiers() & QC.Qt.ControlModifier, event.modifiers() & QC.Qt.ShiftModifier ) + + + # this specifically does not scroll to media, as for clicking (esp. double-clicking attempts), the scroll can be jarring + + + def paintEvent( self, event ): + + if self._parent.devicePixelRatio() != self._parent._last_device_pixel_ratio: + + self._parent._last_device_pixel_ratio = self._parent.devicePixelRatio() + + self._parent._DirtyAllPages() + self._parent._DeleteAllDirtyPages() + + + painter = QG.QPainter( self ) + + ( thumbnail_span_width, thumbnail_span_height ) = self._parent._GetThumbnailSpanDimensions() + + page_height = self._parent._num_rows_per_canvas_page * thumbnail_span_height + + page_indices_to_display = self._parent._CalculateVisiblePageIndices() + + earliest_page_index_to_display = min( page_indices_to_display ) + last_page_index_to_display = max( page_indices_to_display ) + + page_indices_to_draw = list( page_indices_to_display ) + + if earliest_page_index_to_display > 0: + + page_indices_to_draw.append( earliest_page_index_to_display - 1 ) + + + page_indices_to_draw.append( last_page_index_to_display + 1 ) + + page_indices_to_draw.sort() + + potential_clean_indices_to_steal = [ page_index for page_index in self._parent._clean_canvas_pages.keys() if page_index not in page_indices_to_draw ] + + random.shuffle( potential_clean_indices_to_steal ) + + y_start = self._parent._GetYStart() + + bg_colour = self._parent.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) + + painter.setBackground( QG.QBrush( bg_colour ) ) + + painter.eraseRect( painter.viewport() ) + + background_pixmap = CG.client_controller.bitmap_manager.GetMediaBackgroundPixmap() + + if background_pixmap is not None: + + my_size = QP.ScrollAreaVisibleRect( self._parent ).size() + + pixmap_size = background_pixmap.size() + + painter.drawPixmap( my_size.width() - pixmap_size.width(), my_size.height() - pixmap_size.height(), background_pixmap ) + + + for page_index in page_indices_to_draw: + + if page_index not in self._parent._clean_canvas_pages: + + if len( self._parent._dirty_canvas_pages ) == 0: + + if len( potential_clean_indices_to_steal ) > 0: + + index_to_steal = potential_clean_indices_to_steal.pop() + + self._parent._DirtyPage( index_to_steal ) + + else: + + self._parent._CreateNewDirtyPage() + + + + canvas_page = self._parent._dirty_canvas_pages.pop() + + self._parent._DrawCanvasPage( page_index, canvas_page ) + + self._parent._clean_canvas_pages[ page_index ] = canvas_page + + + if page_index in page_indices_to_display: + + canvas_page = self._parent._clean_canvas_pages[ page_index ] + + page_virtual_y = page_height * page_index + + painter.drawImage( 0, page_virtual_y, canvas_page ) + + + + + + +class Selectable( object ): + + def __init__( self, *args, **kwargs ): + + self._selected = False + + super().__init__( *args, **kwargs ) + + + def Deselect( self ): self._selected = False + + def IsSelected( self ): return self._selected + + def Select( self ): self._selected = True + + +class Thumbnail( Selectable ): + + def __init__( self, *args, **kwargs ): + + super().__init__( *args, **kwargs ) + + self._last_tags = None + + self._last_upper_summary = None + self._last_lower_summary = None + + + def ClearTagSummaryCaches( self ): + + self._last_tags = None + + self._last_upper_summary = None + self._last_lower_summary = None + + + def GetQtImage( self, media_panel: ClientGUIMediaResultsPanel.MediaResultsPanel, device_pixel_ratio ) -> QG.QImage: + + # we probably don't really want to say DPR as a param here, but instead ask for a qt_image in a certain resolution? + # or just give the qt_image to be drawn to? + # or just give a painter and a rect and draw to that or something + # we don't really want to mess around with DPR here, we just want to draw thumbs + # that said, this works after a medium-high headache getting it there, so let's not get ahead of ourselves + + thumbnail_hydrus_bmp = CG.client_controller.GetCache( 'thumbnail' ).GetThumbnail( self ) + + thumbnail_border = CG.client_controller.new_options.GetInteger( 'thumbnail_border' ) + + ( width, height ) = ClientData.AddPaddingToDimensions( HC.options[ 'thumbnail_dimensions' ], thumbnail_border * 2 ) + + qt_image_width = int( width * device_pixel_ratio ) + + qt_image_height = int( height * device_pixel_ratio ) + + qt_image = CG.client_controller.bitmap_manager.GetQtImage( qt_image_width, qt_image_height, 24 ) + + qt_image.setDevicePixelRatio( device_pixel_ratio ) + + inbox = self.HasInbox() + + local = self.GetLocationsManager().IsLocal() + + # + # BAD FONT QUALITY AT 100% UI Scale (semi fixed now, look at the bottom) + # + # Ok I have spent hours on this now trying to figure it out and can't, so I'll just write about it for when I come back + # So, if you boot with two monitors at 100% UI scale, the text here on a QImage is ugly, but on QWidget it is fine + # If you boot with one monitor at 125%, the text is beautiful on QImage both screens + # My current assumption is booting Qt with unusual UI scales triggers some extra init and that spills over to QImage QPainter initialisation + # + # I checked painter hints, font stuff, fontinfo and fontmetrics, and the only difference was with fontmetrics, on all-100% vs one >100%: + # minLeftBearing: -1, -7 + # minRightBearing: -1, -8 + # xHeight: 3, 6 + # + # The fontmetric produced a text size one pixel less wide on the both-100% run, so it is calculating different + # However these differences are global to the program so don't explain why painting on a QImage specifically has bad font rather than QWidget + # The ugly font is anti-aliased, but it looks like not drawn with sub-pixel calculations, like ClearType isn't kicking in or something + # If I blow the font size up to 72, there is still a difference in screenshots between the all-100% and some >100% boot. + # So, maybe if the program boots with any weird UI scale going on, Qt kicks in a different renderer for all QImages, the same renderer for QWidgets, perhaps more expensively + # Or this is just some weird bug + # Or I am still missing some flag + # + # bit like this https://stackoverflow.com/questions/31043332/qt-antialiasing-of-vertical-text-rendered-using-qpainter + # + # EDIT: OK, I 'fixed' it with setStyleStrategy( preferantialias ), which has no change in 125%, but in all-100% it draws something different but overall better quality + # Note you can't setStyleStrategy on the font when it is in the QPainter. either it gets set read only or there is some other voodoo going on + # It does look very slightly weird, but it is a step up so I won't complain. it really seems like the isolated QPainter of only-100% world has some different initialisation. it just can't find the nice font renderer + # + # 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! + # + # UPDATE 3a: Qt 6.4.x did not magically fix it. It draws much nicer, but still a different font weight/metrics compared to media viewer background, say. + # The PreferAntialias flag on 6.4.x seems to draw very very close to our ideal, so let's be happy with it for now. + + painter = QG.QPainter( qt_image ) + + painter.setRenderHint( QG.QPainter.TextAntialiasing, True ) # is true already in tests, is supposed to be 'the way' to fix the ugly text issue + painter.setRenderHint( QG.QPainter.Antialiasing, True ) # seems to do nothing, it only affects primitives? + painter.setRenderHint( QG.QPainter.SmoothPixmapTransform, True ) # makes the thumb QImage scale up and down prettily when we need it, either because it is too small or DPR gubbins + + new_options = CG.client_controller.new_options + + if not local: + + if self._selected: + + background_colour_type = CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED + + else: + + background_colour_type = CC.COLOUR_THUMB_BACKGROUND_REMOTE + + + else: + + if self._selected: + + background_colour_type = CC.COLOUR_THUMB_BACKGROUND_SELECTED + + else: + + background_colour_type = CC.COLOUR_THUMB_BACKGROUND + + + + # the painter isn't getting QSS style from the qt_image, we need to set the font explitly to get font size changes from QSS etc.. + + f = QG.QFont( CG.client_controller.gui.font() ) + + # this line magically fixes the bad text, as above + f.setStyleStrategy( QG.QFont.PreferAntialias ) + + painter.setFont( f ) + + bg_color = media_panel.GetColour( background_colour_type ) + + painter.fillRect( thumbnail_border, thumbnail_border, width - ( thumbnail_border * 2 ), height - ( thumbnail_border * 2 ), bg_color ) + + raw_thumbnail_qt_image = thumbnail_hydrus_bmp.GetQtImage() + + thumbnail_dpr_percent = CG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' ) + + if thumbnail_dpr_percent != 100: + + thumbnail_dpr = thumbnail_dpr_percent / 100 + + raw_thumbnail_qt_image.setDevicePixelRatio( thumbnail_dpr ) + + # qt_image.deviceIndepedentSize isn't supported in Qt5 lmao + device_independent_thumb_size = raw_thumbnail_qt_image.size() / thumbnail_dpr + + else: + + device_independent_thumb_size = raw_thumbnail_qt_image.size() + + + x_offset = ( width - device_independent_thumb_size.width() ) // 2 + + y_offset = ( height - device_independent_thumb_size.height() ) // 2 + + painter.drawImage( x_offset, y_offset, raw_thumbnail_qt_image ) + + TEXT_BORDER = 1 + + new_options = CG.client_controller.new_options + + tags = self.GetTagsManager().GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA ) + + if len( tags ) > 0: + + upper_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_top' ) + lower_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_bottom_right' ) + + if self._last_tags is not None and self._last_tags == tags: + + upper_summary = self._last_upper_summary + lower_summary = self._last_lower_summary + + else: + + upper_summary = upper_tag_summary_generator.GenerateSummary( tags ) + + lower_summary = lower_tag_summary_generator.GenerateSummary( tags ) + + self._last_tags = set( tags ) + + self._last_upper_summary = upper_summary + self._last_lower_summary = lower_summary + + + if len( upper_summary ) > 0 or len( lower_summary ) > 0: + + if len( upper_summary ) > 0: + + text_colour_with_alpha = upper_tag_summary_generator.GetTextColour() + + background_colour_with_alpha = upper_tag_summary_generator.GetBackgroundColour() + + ( text_size, upper_summary ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, upper_summary ) + + box_x = thumbnail_border + box_y = thumbnail_border + box_width = width - ( thumbnail_border * 2 ) + box_height = text_size.height() + 2 + + painter.fillRect( box_x, box_y, box_width, box_height, background_colour_with_alpha ) + + text_x = ( width - text_size.width() ) // 2 + text_y = box_y + TEXT_BORDER + + painter.setPen( QG.QPen( text_colour_with_alpha ) ) + + ClientGUIFunctions.DrawText( painter, text_x, text_y, upper_summary ) + + + if len( lower_summary ) > 0: + + text_colour_with_alpha = lower_tag_summary_generator.GetTextColour() + + background_colour_with_alpha = lower_tag_summary_generator.GetBackgroundColour() + + ( text_size, lower_summary ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, lower_summary ) + + text_width = text_size.width() + text_height = text_size.height() + + box_width = text_width + ( TEXT_BORDER * 2 ) + box_height = text_height + ( TEXT_BORDER * 2 ) + box_x = width - box_width - thumbnail_border + box_y = height - text_height - thumbnail_border + + painter.fillRect( box_x, box_y, box_width, box_height, background_colour_with_alpha ) + + text_x = box_x + TEXT_BORDER + text_y = box_y + TEXT_BORDER + + painter.setPen( QG.QPen( text_colour_with_alpha ) ) + + ClientGUIFunctions.DrawText( painter, text_x, text_y, lower_summary ) + + + + + if thumbnail_border > 0: + + if not local: + + if self._selected: + + border_colour_type = CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED + + else: + + border_colour_type = CC.COLOUR_THUMB_BORDER_REMOTE + + + else: + + if self._selected: + + border_colour_type = CC.COLOUR_THUMB_BORDER_SELECTED + + else: + + border_colour_type = CC.COLOUR_THUMB_BORDER + + + + # I had a hell of a time getting a transparent box to draw right with a pen border without crazy +1px in the params for reasons I did not understand + # so I just decided four rects is neater and fine and actually prob faster in some cases + + # _____ ______ _____ ______ ________________ + # ___________(_)___ _________ /_______ _______ ______ __ /______ ___ /_________ /__ /__ / + # ___ __ \_ /__ |/_/ _ \_ /__ ___/ __ __ `/ __ \ _ __/ __ \ __ __ \ _ \_ /__ /__ / + # __ /_/ / / __> < / __/ / _(__ ) _ /_/ // /_/ / / /_ / /_/ / _ / / / __/ / _ / /_/ + # _ .___//_/ /_/|_| \___//_/ /____/ _\__, / \____/ \__/ \____/ /_/ /_/\___//_/ /_/ (_) + # /_/ /____/ + + bd_colour = media_panel.GetColour( border_colour_type ) + + painter.setBrush( QG.QBrush( bd_colour ) ) + painter.setPen( QG.QPen( QC.Qt.NoPen ) ) + + rectangles = [] + + side_height = height - ( thumbnail_border * 2 ) + rectangles.append( QC.QRectF( 0, 0, width, thumbnail_border ) ) # top + rectangles.append( QC.QRectF( 0, height - thumbnail_border, width, thumbnail_border ) ) # bottom + rectangles.append( QC.QRectF( 0, thumbnail_border, thumbnail_border, side_height ) ) # left + rectangles.append( QC.QRectF( width - thumbnail_border, thumbnail_border, thumbnail_border, side_height ) ) # right + + painter.drawRects( rectangles ) + + + ICON_MARGIN = 1 + + locations_manager = self.GetLocationsManager() + + icons_to_draw = [] + + if locations_manager.IsDownloading(): + + icons_to_draw.append( CC.global_pixmaps().downloading ) + + + if self.HasNotes(): + + icons_to_draw.append( CC.global_pixmaps().notes ) + + + if locations_manager.IsTrashed() or CC.COMBINED_LOCAL_FILE_SERVICE_KEY in locations_manager.GetDeleted(): + + icons_to_draw.append( CC.global_pixmaps().trash ) + + + if inbox: + + icons_to_draw.append( CC.global_pixmaps().inbox ) + + + if len( icons_to_draw ) > 0: + + icon_x = - ( thumbnail_border + ICON_MARGIN ) + + for icon in icons_to_draw: + + icon_x -= icon.width() + + painter.drawPixmap( width + icon_x, thumbnail_border, icon ) + + icon_x -= 2 * ICON_MARGIN + + + + if self.IsCollection(): + + icon = CC.global_pixmaps().collection + + icon_x = thumbnail_border + ICON_MARGIN + icon_y = ( height - 1 ) - thumbnail_border - ICON_MARGIN - icon.height() + + painter.drawPixmap( icon_x, icon_y, icon ) + + num_files_str = HydrusNumbers.ToHumanInt( self.GetNumFiles() ) + + ( text_size, num_files_str ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, num_files_str ) + + text_width = text_size.width() + text_height = text_size.height() + + box_width = text_width + ( ICON_MARGIN * 2 ) + box_x = icon_x + icon.width() + ICON_MARGIN + box_height = text_height + ( ICON_MARGIN * 2 ) + box_y = ( height - 1 ) - box_height + + painter.fillRect( box_x, height - text_height - 3, box_width, box_height, CC.COLOUR_UNSELECTED ) + + painter.setPen( QG.QPen( CC.COLOUR_SELECTED_DARK ) ) + + text_x = box_x + ICON_MARGIN + text_y = box_y + ICON_MARGIN + + ClientGUIFunctions.DrawText( painter, text_x, text_y, num_files_str ) + + + # top left icons + + icons_to_draw = [] + + if self.HasAudio(): + + icons_to_draw.append( CC.global_pixmaps().sound ) + + elif self.HasDuration(): + + icons_to_draw.append( CC.global_pixmaps().play ) + + + services_manager = CG.client_controller.services_manager + + remote_file_service_keys = CG.client_controller.services_manager.GetRemoteFileServiceKeys() + + current = locations_manager.GetCurrent().intersection( remote_file_service_keys ) + pending = locations_manager.GetPending().intersection( remote_file_service_keys ) + petitioned = locations_manager.GetPetitioned().intersection( remote_file_service_keys ) + + current_to_display = current.difference( petitioned ) + + # + + service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in current_to_display ] + + if HC.FILE_REPOSITORY in service_types: + + icons_to_draw.append( CC.global_pixmaps().file_repository ) + + + if HC.IPFS in service_types: + + icons_to_draw.append( CC.global_pixmaps().ipfs ) + + + # + + service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in pending ] + + if HC.FILE_REPOSITORY in service_types: + + icons_to_draw.append( CC.global_pixmaps().file_repository_pending ) + + + if HC.IPFS in service_types: + + icons_to_draw.append( CC.global_pixmaps().ipfs_pending ) + + + # + + service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in petitioned ] + + if HC.FILE_REPOSITORY in service_types: + + icons_to_draw.append( CC.global_pixmaps().file_repository_petitioned ) + + + if HC.IPFS in service_types: + + icons_to_draw.append( CC.global_pixmaps().ipfs_petitioned ) + + + top_left_x = thumbnail_border + ICON_MARGIN + + for icon_to_draw in icons_to_draw: + + painter.drawPixmap( top_left_x, thumbnail_border + ICON_MARGIN, icon_to_draw ) + + top_left_x += icon_to_draw.width() + ( ICON_MARGIN * 2 ) + + + return qt_image + + + +# TODO: This is another area of OOD inheritance garbage. just rewrite the whole damn thing, stop trying to do everything in one class, decouple and you'll lose the linter freakout over GetQtImage's references and related __init__ headaches +class ThumbnailMediaCollection( Thumbnail, ClientMedia.MediaCollection ): + + def __init__( self, location_context, media_results ): + + super().__init__( location_context, media_results ) + + +class ThumbnailMediaSingleton( Thumbnail, ClientMedia.MediaSingleton ): + + def __init__( self, media_result ): + + super().__init__( media_result ) + + diff --git a/hydrus/client/gui/pages/ClientGUINewPageChooser.py b/hydrus/client/gui/pages/ClientGUINewPageChooser.py index d8b580095..1dd4425a4 100644 --- a/hydrus/client/gui/pages/ClientGUINewPageChooser.py +++ b/hydrus/client/gui/pages/ClientGUINewPageChooser.py @@ -18,7 +18,7 @@ class DialogPageChooser( ClientGUIDialogs.Dialog ): def __init__( self, parent, controller ): - ClientGUIDialogs.Dialog.__init__( self, parent, 'new page', position = 'center' ) + super().__init__( parent, 'new page', position = 'center' ) self._controller = controller diff --git a/hydrus/client/gui/pages/ClientGUIPages.py b/hydrus/client/gui/pages/ClientGUIPages.py index f9586e1e6..fd8cda35d 100644 --- a/hydrus/client/gui/pages/ClientGUIPages.py +++ b/hydrus/client/gui/pages/ClientGUIPages.py @@ -33,7 +33,8 @@ from hydrus.client.gui.pages import ClientGUIManagementController from hydrus.client.gui.pages import ClientGUIManagementPanels from hydrus.client.gui.pages import ClientGUINewPageChooser -from hydrus.client.gui.pages import ClientGUIResults +from hydrus.client.gui.pages import ClientGUIMediaResultsPanel +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelThumbnails from hydrus.client.gui.pages import ClientGUISession from hydrus.client.gui.pages import ClientGUISessionLegacy # to get serialisable data types loaded from hydrus.client.search import ClientSearchFileSearchContext @@ -100,7 +101,7 @@ def __init__( self, parent, controller, management_controller: ClientGUIManageme self._management_panel.locationChanged.connect( self._preview_canvas.SetLocationContext ) # this is the only place we _do_ want to set the split as the parent of the thumbnail panel. doing it on init avoids init flicker - self._media_panel = self._management_panel.GetDefaultEmptyMediaPanel( self._management_media_split ) + self._media_panel = self._management_panel.GetDefaultEmptyMediaResultsPanel( self._management_media_split ) self._management_media_split.addWidget( self._search_preview_split ) self._management_media_split.addWidget( self._media_panel ) @@ -144,14 +145,14 @@ def __init__( self, parent, controller, management_controller: ClientGUIManageme self._current_session_page_container_hashes_hash = self._GetCurrentSessionPageHashesHash() self._current_session_page_container_timestamp = 0 - self._ConnectMediaPanelSignals() + self._ConnectMediaResultsPanelSignals() self.SetSplitterPositions() self._search_preview_split.splitterMoved.connect( self._PreviewSplitterMoved ) - def _ConnectMediaPanelSignals( self ): + def _ConnectMediaResultsPanelSignals( self ): self._media_panel.refreshQuery.connect( self.RefreshQuery ) self._media_panel.focusMediaChanged.connect( self._preview_canvas.SetMedia ) @@ -159,7 +160,7 @@ def _ConnectMediaPanelSignals( self ): self._media_panel.focusMediaPaused.connect( self._preview_canvas.PauseMedia ) self._media_panel.statusTextChanged.connect( self._SetPrettyStatus ) - self._management_panel.ConnectMediaPanelSignals( self._media_panel ) + self._management_panel.ConnectMediaResultsPanelSignals( self._media_panel ) def _GetCurrentSessionPageHashesHash( self ): @@ -198,7 +199,7 @@ def _SetPrettyStatus( self, status: str ): self._controller.gui.SetStatusBarDirty() - def _SwapMediaPanel( self, new_panel: ClientGUIResults.MediaPanel ): + def _SwapMediaResultsPanel( self, new_panel: ClientGUIMediaResultsPanel.MediaResultsPanel ): """ Yo, it is important that the new_panel here starts with a parent _other_ than the splitter! The page itself is usually fine. If we give it the splitter as parent, you can get a frame of unusual layout flicker, usually a page-wide autocomplete input. Re-parent it here and we are fine. @@ -258,7 +259,7 @@ def _SwapMediaPanel( self, new_panel: ClientGUIResults.MediaPanel ): self._management_media_split.setSizes( previous_sizes ) - self._ConnectMediaPanelSignals() + self._ConnectMediaResultsPanelSignals() self._controller.pub( 'refresh_page_name', self._page_key ) @@ -396,7 +397,7 @@ def GetMedia( self ): return self._media_panel.GetSortedMedia() - def GetMediaPanel( self ): + def GetMediaResultsPanel( self ): return self._media_panel @@ -802,9 +803,9 @@ def publish_callable( media_results ): self._SetPrettyStatus( '' ) - media_panel = ClientGUIResults.MediaPanelThumbnails( self, self._page_key, self._management_controller, media_results ) + media_panel = ClientGUIMediaResultsPanelThumbnails.MediaResultsPanelThumbnails( self, self._page_key, self._management_controller, media_results ) - self._SwapMediaPanel( media_panel ) + self._SwapMediaResultsPanel( media_panel ) if len( self._pre_initialisation_media_results ) > 0: @@ -845,9 +846,9 @@ def Start( self ): - def SwapMediaPanel( self, new_panel ): + def SwapMediaResultsPanel( self, new_panel ): - self._SwapMediaPanel( new_panel ) + self._SwapMediaResultsPanel( new_panel ) def TestAbleToClose( self ): @@ -890,7 +891,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ): def __init__( self, parent, controller, name ): - QP.TabWidgetWithDnD.__init__( self, parent ) + super().__init__( parent ) self._parent_notebook = parent @@ -2803,7 +2804,7 @@ def MediaDragAndDropDropped( self, source_page_key, hashes ): if not ctrl_down: - source_page.GetMediaPanel().RemoveMedia( source_page.GetPageKey(), hashes ) + source_page.GetMediaResultsPanel().RemoveMedia( source_page.GetPageKey(), hashes ) @@ -3090,7 +3091,7 @@ def NewPageQuery( media_sort = page.GetSort() - page.GetMediaPanel().Sort( media_sort ) + page.GetMediaResultsPanel().Sort( media_sort ) return page diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py deleted file mode 100644 index b43dd575a..000000000 --- a/hydrus/client/gui/pages/ClientGUIResults.py +++ /dev/null @@ -1,5367 +0,0 @@ -import collections -import itertools -import random -import time -import typing - -from qtpy import QtCore as QC -from qtpy import QtWidgets as QW -from qtpy import QtGui as QG - -from hydrus.core import HydrusConstants as HC -from hydrus.core import HydrusData -from hydrus.core import HydrusExceptions -from hydrus.core import HydrusGlobals as HG -from hydrus.core import HydrusLists -from hydrus.core import HydrusNumbers -from hydrus.core import HydrusPaths -from hydrus.core import HydrusTime -from hydrus.core.networking import HydrusNetwork - -from hydrus.client import ClientApplicationCommand as CAC -from hydrus.client import ClientConstants as CC -from hydrus.client import ClientData -from hydrus.client import ClientFiles -from hydrus.client import ClientGlobals as CG -from hydrus.client import ClientLocation -from hydrus.client import ClientPaths -from hydrus.client import ClientServices -from hydrus.client.gui import ClientGUIDragDrop -from hydrus.client.gui import ClientGUICore as CGC -from hydrus.client.gui import ClientGUIDialogs -from hydrus.client.gui import ClientGUIDialogsManage -from hydrus.client.gui import ClientGUIDialogsMessage -from hydrus.client.gui import ClientGUIDialogsQuick -from hydrus.client.gui import ClientGUIDuplicates -from hydrus.client.gui import ClientGUIFunctions -from hydrus.client.gui import ClientGUIMenus -from hydrus.client.gui import ClientGUIShortcuts -from hydrus.client.gui import ClientGUITags -from hydrus.client.gui import ClientGUITopLevelWindowsPanels -from hydrus.client.gui import QtPorting as QP -from hydrus.client.gui.canvas import ClientGUICanvas -from hydrus.client.gui.canvas import ClientGUICanvasFrame -from hydrus.client.gui.media import ClientGUIMediaSimpleActions -from hydrus.client.gui.media import ClientGUIMediaModalActions -from hydrus.client.gui.media import ClientGUIMediaMenus -from hydrus.client.gui.networking import ClientGUIHydrusNetwork -from hydrus.client.gui.pages import ClientGUIManagementController -from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit -from hydrus.client.media import ClientMedia -from hydrus.client.media import ClientMediaFileFilter -from hydrus.client.metadata import ClientContentUpdates -from hydrus.client.metadata import ClientTags - -MAC_QUARTZ_OK = True - -if HC.PLATFORM_MACOS: - - try: - - from hydrus.client import ClientMacIntegration - - except: - - MAC_QUARTZ_OK = False - - - -FRAME_DURATION_60FPS = 1.0 / 60 - -class ThumbnailWaitingToBeDrawn( object ): - - def __init__( self, hash, thumbnail, thumbnail_index, bitmap ): - - self.hash = hash - self.thumbnail = thumbnail - self.thumbnail_index = thumbnail_index - self.bitmap = bitmap - - self._draw_complete = False - - - def DrawComplete( self ) -> bool: - - return self._draw_complete - - - def DrawDue( self ) -> bool: - - return True - - - def DrawToPainter( self, x: int, y: int, painter: QG.QPainter ): - - painter.drawImage( x, y, self.bitmap ) - - self._draw_complete = True - - - -class ThumbnailWaitingToBeDrawnAnimated( ThumbnailWaitingToBeDrawn ): - - FADE_DURATION_S = 0.5 - - def __init__( self, hash, thumbnail, thumbnail_index, bitmap ): - - ThumbnailWaitingToBeDrawn.__init__( self, hash, thumbnail, thumbnail_index, bitmap ) - - self.num_frames_drawn = 0 - self.num_frames_to_draw = max( int( self.FADE_DURATION_S // FRAME_DURATION_60FPS ), 1 ) - - opacity_factor = max( 0.05, 1 / ( self.num_frames_to_draw / 3 ) ) - - self.alpha_bmp = QP.AdjustOpacity( self.bitmap, opacity_factor ) - - self.animation_started_precise = HydrusTime.GetNowPrecise() - - - def _GetNumFramesOutstanding( self ): - - now_precise = HydrusTime.GetNowPrecise() - - num_frames_to_now = int( ( now_precise - self.animation_started_precise ) // FRAME_DURATION_60FPS ) - - return min( num_frames_to_now, self.num_frames_to_draw - self.num_frames_drawn ) - - - def DrawDue( self ) -> bool: - - return self._GetNumFramesOutstanding() > 0 - - - def DrawToPainter( self, x: int, y: int, painter: QG.QPainter ): - - num_frames_to_draw = self._GetNumFramesOutstanding() - - if self.num_frames_drawn + num_frames_to_draw >= self.num_frames_to_draw: - - painter.drawImage( x, y, self.bitmap ) - - self.num_frames_drawn = self.num_frames_to_draw - self._draw_complete = True - - else: - - for i in range( num_frames_to_draw ): - - painter.drawImage( x, y, self.alpha_bmp ) - - - self.num_frames_drawn += num_frames_to_draw - - - - -class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMediaList, QW.QScrollArea ): - - selectedMediaTagPresentationChanged = QC.Signal( list, bool ) - selectedMediaTagPresentationIncremented = QC.Signal( list ) - statusTextChanged = QC.Signal( str ) - - focusMediaChanged = QC.Signal( ClientMedia.Media ) - focusMediaCleared = QC.Signal() - focusMediaPaused = QC.Signal() - refreshQuery = QC.Signal() - - newMediaAdded = QC.Signal() - - def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController, media_results ): - - self._qss_colours = { - CC.COLOUR_THUMBGRID_BACKGROUND : QG.QColor( 255, 255, 255 ), - CC.COLOUR_THUMB_BACKGROUND : QG.QColor( 255, 255, 255 ), - CC.COLOUR_THUMB_BACKGROUND_SELECTED : QG.QColor( 217, 242, 255 ), - CC.COLOUR_THUMB_BACKGROUND_REMOTE : QG.QColor( 32, 32, 36 ), - CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED : QG.QColor( 64, 64, 72 ), - CC.COLOUR_THUMB_BORDER : QG.QColor( 223, 227, 230 ), - CC.COLOUR_THUMB_BORDER_SELECTED : QG.QColor( 1, 17, 26 ), - CC.COLOUR_THUMB_BORDER_REMOTE : QG.QColor( 248, 208, 204 ), - CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED : QG.QColor( 227, 66, 52 ) - } - - self._page_key = page_key - self._management_controller = management_controller - - # TODO: BRUH REWRITE THIS GARBAGE - # we don't really want to be messing around with *args, **kwargs in __init__/super() gubbins, and this is highlighted as we move to super() and see this is all a mess!! - # obviously decouple the list from the panel here so we aren't trying to do everything in one class - super().__init__( self._management_controller.GetLocationContext(), media_results, parent ) - - self.setObjectName( 'HydrusMediaList' ) - - self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken ) - self.setLineWidth( 2 ) - - self.resize( QC.QSize( 20, 20 ) ) - self.setWidget( QW.QWidget( self ) ) - self.setWidgetResizable( True ) - - self._UpdateBackgroundColour() - - self.verticalScrollBar().setSingleStep( 50 ) - - self._focused_media = None - self._last_hit_media = None - self._next_best_media_if_focuses_removed = None - self._shift_select_started_with_this_media = None - self._media_added_in_current_shift_select = set() - - self._empty_page_status_override = None - - CG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' ) - CG.client_controller.sub( self, 'RemoveMedia', 'remove_media' ) - CG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' ) - CG.client_controller.sub( self, 'SelectByTags', 'select_files_with_tags' ) - CG.client_controller.sub( self, 'LaunchMediaViewerOnFocus', 'launch_media_viewer' ) - - self._had_changes_to_tag_presentation_while_hidden = False - - self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'media', 'thumbnails' ] ) - - self.setWidget( self._InnerWidget( self ) ) - self.setWidgetResizable( True ) - - - def __bool__( self ): - - return QP.isValid( self ) - - - def _Archive( self ): - - hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_INBOX ) - - if len( hashes ) > 0: - - if HC.options[ 'confirm_archive' ]: - - if len( hashes ) > 1: - - message = 'Archive ' + HydrusNumbers.ToHumanInt( len( hashes ) ) + ' files?' - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result != QW.QDialog.Accepted: - - return - - - - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, hashes ) ) ) - - - - def _ArchiveDeleteFilter( self ): - - if len( self._selected_media ) == 0: - - media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH, selected_media = set( self._sorted_media ), for_media_viewer = True ) - - else: - - media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL_BUT_NOT_IN_TRASH, selected_media = set( self._selected_media ), for_media_viewer = True ) - - - if len( media_results ) > 0: - - self.SetFocusedMedia( None ) - - canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() ) - - canvas_window = ClientGUICanvas.CanvasMediaListFilterArchiveDelete( canvas_frame, self._page_key, self._location_context, media_results ) - - canvas_frame.SetCanvas( canvas_window ) - - canvas_window.exitFocusMedia.connect( self.SetFocusedMedia ) - - - - def _ClearDeleteRecord( self ): - - media = self._GetSelectedFlatMedia() - - ClientGUIMediaModalActions.ClearDeleteRecord( self, media ) - - - def _Delete( self, file_service_key = None, only_those_in_file_service_key = None ): - - if file_service_key is None: - - if len( self._location_context.current_service_keys ) == 1: - - ( possible_suggested_file_service_key, ) = self._location_context.current_service_keys - - if CG.client_controller.services_manager.GetServiceType( possible_suggested_file_service_key ) in HC.SPECIFIC_LOCAL_FILE_SERVICES + ( HC.FILE_REPOSITORY, ): - - file_service_key = possible_suggested_file_service_key - - - - - media_to_delete = ClientMedia.FlattenMedia( self._selected_media ) - - if only_those_in_file_service_key is not None: - - media_to_delete = ClientMedia.FlattenMedia( media_to_delete ) - - media_to_delete = [ m for m in media_to_delete if only_those_in_file_service_key in m.GetLocationsManager().GetCurrent() ] - - - if file_service_key is None or CG.client_controller.services_manager.GetServiceType( file_service_key ) in HC.LOCAL_FILE_SERVICES: - - default_reason = 'Deleted from Media Page.' - - else: - - default_reason = 'admin' - - - try: - - ( hashes_physically_deleted, content_update_packages ) = ClientGUIDialogsQuick.GetDeleteFilesJobs( self, media_to_delete, default_reason, suggested_file_service_key = file_service_key ) - - except HydrusExceptions.CancelledException: - - return - - - if len( hashes_physically_deleted ) > 0: - - self._RemoveMediaByHashes( hashes_physically_deleted ) - - - def do_it( content_update_packages ): - - for content_update_package in content_update_packages: - - CG.client_controller.WriteSynchronous( 'content_updates', content_update_package ) - - - - CG.client_controller.CallToThread( do_it, content_update_packages ) - - - def _DeselectSelect( self, media_to_deselect, media_to_select ): - - if len( media_to_deselect ) > 0: - - for m in media_to_deselect: m.Deselect() - - self._RedrawMedia( media_to_deselect ) - - self._selected_media.difference_update( media_to_deselect ) - - - if len( media_to_select ) > 0: - - for m in media_to_select: m.Select() - - self._RedrawMedia( media_to_select ) - - self._selected_media.update( media_to_select ) - - - self._PublishSelectionChange() - - - def _DownloadSelected( self ): - - hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL ) - - self._DownloadHashes( hashes ) - - - def _DownloadHashes( self, hashes ): - - CG.client_controller.quick_download_manager.DownloadFiles( hashes ) - - - def _EndShiftSelect( self ): - - self._shift_select_started_with_this_media = None - self._media_added_in_current_shift_select = set() - - - def _GetFocusSingleton( self ) -> ClientMedia.MediaSingleton: - - if self._focused_media is not None: - - media_singleton = self._focused_media.GetDisplayMedia() - - if media_singleton is not None: - - return media_singleton - - - - raise HydrusExceptions.DataMissing( 'No media singleton!' ) - - - def _GetMediasForFileCommandTarget( self, file_command_target: int ) -> typing.Collection[ ClientMedia.MediaSingleton ]: - - if file_command_target == CAC.FILE_COMMAND_TARGET_FOCUSED_FILE: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - return [ media.GetDisplayMedia() ] - - - elif file_command_target == CAC.FILE_COMMAND_TARGET_SELECTED_FILES: - - if len( self._selected_media ) > 0: - - medias = self._GetSelectedMediaOrdered() - - return ClientMedia.FlattenMedia( medias ) - - - - return [] - - - def _GetNumSelected( self ): - - return sum( [ media.GetNumFiles() for media in self._selected_media ] ) - - - def _GetPrettyStatusForStatusBar( self ) -> str: - - num_files = len( self._hashes ) - - if self._empty_page_status_override is not None: - - if num_files == 0: - - return self._empty_page_status_override - - else: - - # user has dragged files onto this page or similar - - self._empty_page_status_override = None - - - - num_selected = self._GetNumSelected() - - num_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._sorted_media ) - selected_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._selected_media ) - - s = num_files_string # 23 files - - if num_selected == 0: - - if num_files > 0: - - pretty_total_size = self._GetPrettyTotalSize() - - s += ' - totalling ' + pretty_total_size - - pretty_total_duration = self._GetPrettyTotalDuration() - - if pretty_total_duration != '': - - s += ', {}'.format( pretty_total_duration ) - - - - else: - - s += ' - ' - - # if 1 selected, we show the whole mime string, so no need to specify - if num_selected == 1 or selected_files_string == num_files_string: - - selected_files_string = HydrusNumbers.ToHumanInt( num_selected ) - - - if num_selected == 1: # 23 files - 1 video selected, file_info - - ( selected_media, ) = self._selected_media - - pretty_info_lines = [ line for line in selected_media.GetPrettyInfoLines( only_interesting_lines = True ) if isinstance( line, str ) ] - - s += '{} selected, {}'.format( selected_files_string, ', '.join( pretty_info_lines ) ) - - else: # 23 files - 5 selected, selection_info - - num_inbox = sum( ( media.GetNumInbox() for media in self._selected_media ) ) - - if num_inbox == num_selected: - - inbox_phrase = 'all in inbox' - - elif num_inbox == 0: - - inbox_phrase = 'all archived' - - else: - - inbox_phrase = '{} in inbox and {} archived'.format( HydrusNumbers.ToHumanInt( num_inbox ), HydrusNumbers.ToHumanInt( num_selected - num_inbox ) ) - - - pretty_total_size = self._GetPrettyTotalSize( only_selected = True ) - - s += '{} selected, {}, totalling {}'.format( selected_files_string, inbox_phrase, pretty_total_size ) - - pretty_total_duration = self._GetPrettyTotalDuration( only_selected = True ) - - if pretty_total_duration != '': - - s += ', {}'.format( pretty_total_duration ) - - - - - return s - - - def _GetPrettyTotalDuration( self, only_selected = False ): - - if only_selected: - - media_source = self._selected_media - - else: - - media_source = self._sorted_media - - - if len( media_source ) == 0 or False in ( media.HasDuration() for media in media_source ): - - return '' - - - total_duration = sum( ( media.GetDurationMS() for media in media_source ) ) - - return HydrusTime.MillisecondsDurationToPrettyTime( total_duration ) - - - def _GetPrettyTotalSize( self, only_selected = False ): - - if only_selected: - - media_source = self._selected_media - - else: - - media_source = self._sorted_media - - - total_size = sum( [ media.GetSize() for media in media_source ] ) - - unknown_size = False in ( media.IsSizeDefinite() for media in media_source ) - - if total_size == 0: - - if unknown_size: - - return 'unknown size' - - else: - - return HydrusData.ToHumanBytes( 0 ) - - - else: - - if unknown_size: - - return HydrusData.ToHumanBytes( total_size ) + ' + some unknown size' - - else: - - return HydrusData.ToHumanBytes( total_size ) - - - - - def _GetSelectedHashes( self, is_in_file_service_key = None, discriminant = None, is_not_in_file_service_key = None, ordered = False ): - - if ordered: - - result = [] - - for media in self._GetSelectedMediaOrdered(): - - result.extend( media.GetHashes( is_in_file_service_key, discriminant, is_not_in_file_service_key, ordered ) ) - - - else: - - result = set() - - for media in self._selected_media: - - result.update( media.GetHashes( is_in_file_service_key, discriminant, is_not_in_file_service_key, ordered ) ) - - - - return result - - - def _GetSelectedCollections( self ): - - sorted_selected_collections = [ media for media in self._sorted_media if media.IsCollection() and media in self._selected_media ] - - return sorted_selected_collections - - - def _GetSelectedFlatMedia( self, is_in_file_service_key = None, discriminant = None, is_not_in_file_service_key = None ): - - # this now always delivers sorted results - - sorted_selected_media = [ media for media in self._sorted_media if media in self._selected_media ] - - flat_media = ClientMedia.FlattenMedia( sorted_selected_media ) - - flat_media = [ media for media in flat_media if media.MatchesDiscriminant( is_in_file_service_key = is_in_file_service_key, discriminant = discriminant, is_not_in_file_service_key = is_not_in_file_service_key ) ] - - return flat_media - - - def _GetSelectedMediaOrdered( self ): - - # note that this is fast because sorted_media is custom - return sorted( self._selected_media, key = lambda m: self._sorted_media.index( m ) ) - - - def _GetSortedSelectedMimeDescriptors( self ): - - def GetDescriptor( plural, classes, num_collections ): - - suffix = 's' if plural else '' - - if len( classes ) == 0: - - return 'file' + suffix - - - if len( classes ) == 1: - - ( mime, ) = classes - - if mime == HC.APPLICATION_HYDRUS_CLIENT_COLLECTION: - - collections_suffix = 's' if num_collections > 1 else '' - - return 'file{} in {} collection{}'.format( suffix, HydrusNumbers.ToHumanInt( num_collections ), collections_suffix ) - - else: - - return HC.mime_string_lookup[ mime ] + suffix - - - - if len( classes.difference( HC.IMAGES ) ) == 0: - - return 'image' + suffix - - elif len( classes.difference( HC.ANIMATIONS ) ) == 0: - - return 'animation' + suffix - - elif len( classes.difference( HC.VIDEO ) ) == 0: - - return 'video' + suffix - - elif len( classes.difference( HC.AUDIO ) ) == 0: - - return 'audio file' + suffix - - else: - - return 'file' + suffix - - - - if len( self._sorted_media ) > 1000: - - sorted_mime_descriptor = 'files' - - else: - - sorted_mimes = { media.GetMime() for media in self._sorted_media } - - if HC.APPLICATION_HYDRUS_CLIENT_COLLECTION in sorted_mimes: - - num_collections = len( [ media for media in self._sorted_media if isinstance( media, ClientMedia.MediaCollection ) ] ) - - else: - - num_collections = 0 - - - plural = len( self._sorted_media ) > 1 or sum( ( m.GetNumFiles() for m in self._sorted_media ) ) > 1 - - sorted_mime_descriptor = GetDescriptor( plural, sorted_mimes, num_collections ) - - - if len( self._selected_media ) > 1000: - - selected_mime_descriptor = 'files' - - else: - - selected_mimes = { media.GetMime() for media in self._selected_media } - - if HC.APPLICATION_HYDRUS_CLIENT_COLLECTION in selected_mimes: - - num_collections = len( [ media for media in self._selected_media if isinstance( media, ClientMedia.MediaCollection ) ] ) - - else: - - num_collections = 0 - - - plural = len( self._selected_media ) > 1 or sum( ( m.GetNumFiles() for m in self._selected_media ) ) > 1 - - selected_mime_descriptor = GetDescriptor( plural, selected_mimes, num_collections ) - - - return ( sorted_mime_descriptor, selected_mime_descriptor ) - - - def _HasFocusSingleton( self ) -> bool: - - try: - - media = self._GetFocusSingleton() - - return True - - except HydrusExceptions.DataMissing: - - return False - - - - def _HitMedia( self, media, ctrl, shift ): - - if media is None: - - if not ctrl and not shift: - - self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) ) - self._SetFocusedMedia( None ) - self._EndShiftSelect() - - - else: - - if ctrl and not shift: - - if media.IsSelected(): - - self._DeselectSelect( ( media, ), () ) - - if self._focused_media == media: - - self._SetFocusedMedia( None ) - - - self._EndShiftSelect() - - else: - - self._DeselectSelect( (), ( media, ) ) - - focus_it = False - - if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_ctrl_click' ): - - if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_ctrl_click_only_static' ): - - focus_it = media.GetDurationMS() is None - - else: - - focus_it = True - - - - if focus_it: - - self._SetFocusedMedia( media ) - - else: - - self._last_hit_media = media - - - self._StartShiftSelect( media ) - - - elif shift and self._shift_select_started_with_this_media is not None: - - start_index = self._sorted_media.index( self._shift_select_started_with_this_media ) - - end_index = self._sorted_media.index( media ) - - if start_index < end_index: - - media_from_start_of_shift_to_end = set( self._sorted_media[ start_index : end_index + 1 ] ) - - else: - - media_from_start_of_shift_to_end = set( self._sorted_media[ end_index : start_index + 1 ] ) - - - media_to_deselect = [ m for m in self._media_added_in_current_shift_select if m not in media_from_start_of_shift_to_end ] - media_to_select = [ m for m in media_from_start_of_shift_to_end if not m.IsSelected() ] - - self._media_added_in_current_shift_select.difference_update( media_to_deselect ) - self._media_added_in_current_shift_select.update( media_to_select ) - - self._DeselectSelect( media_to_deselect, media_to_select ) - - focus_it = False - - if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_shift_click' ): - - if CG.client_controller.new_options.GetBoolean( 'focus_preview_on_shift_click_only_static' ): - - focus_it = media.GetDurationMS() is None - - else: - - focus_it = True - - - - if focus_it: - - self._SetFocusedMedia( media ) - - else: - - self._last_hit_media = media - - - else: - - if not media.IsSelected(): - - self._DeselectSelect( self._selected_media, ( media, ) ) - - else: - - self._PublishSelectionChange() - - - self._SetFocusedMedia( media ) - self._StartShiftSelect( media ) - - - - - def _Inbox( self ): - - hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_ARCHIVE, is_in_file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) - - if len( hashes ) > 0: - - if HC.options[ 'confirm_archive' ]: - - if len( hashes ) > 1: - - message = 'Send {} files to inbox?'.format( HydrusNumbers.ToHumanInt( len( hashes ) ) ) - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result != QW.QDialog.Accepted: - - return - - - - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_INBOX, hashes ) ) ) - - - - def _LaunchMediaViewer( self, first_media = None ): - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - if not media.GetLocationsManager().IsLocal(): - - return - - - new_options = CG.client_controller.new_options - - ( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( media.GetMime() ) - - if media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY: - - hash = media.GetHash() - mime = media.GetMime() - - client_files_manager = CG.client_controller.client_files_manager - - path = client_files_manager.GetFilePath( hash, mime ) - - new_options = CG.client_controller.new_options - - launch_path = new_options.GetMimeLaunch( mime ) - - HydrusPaths.LaunchFile( path, launch_path ) - - return - - elif media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW: - - return - - - - media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, for_media_viewer = True ) - - if len( media_results ) > 0: - - if first_media is None and self._focused_media is not None: - - first_media = self._focused_media - - - if first_media is not None: - - first_media = first_media.GetDisplayMedia() - - - if first_media is not None and first_media.GetLocationsManager().IsLocal(): - - first_hash = first_media.GetHash() - - else: - - first_hash = None - - - self.SetFocusedMedia( None ) - - canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() ) - - canvas_window = ClientGUICanvas.CanvasMediaListBrowser( canvas_frame, self._page_key, self._location_context, media_results, first_hash ) - - canvas_frame.SetCanvas( canvas_window ) - - canvas_window.exitFocusMedia.connect( self.SetFocusedMedia ) - - - - def _ManageNotes( self ): - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - ClientGUIMediaModalActions.EditFileNotes( self, media ) - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def _ManageRatings( self ): - - flat_media = ClientMedia.FlattenMedia( self._selected_media ) - - if len( flat_media ) > 0: - - if len( CG.client_controller.services_manager.GetServices( HC.RATINGS_SERVICES ) ) > 0: - - with ClientGUIDialogsManage.DialogManageRatings( self, flat_media ) as dlg: - - dlg.exec() - - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - - def _ManageTags( self ): - - flat_media = ClientMedia.FlattenMedia( self._GetSelectedMediaOrdered() ) - - if len( flat_media ) > 0: - - num_files = self._GetNumSelected() - - title = 'manage tags for ' + HydrusNumbers.ToHumanInt( num_files ) + ' files' - frame_key = 'manage_tags_dialog' - - with ClientGUITopLevelWindowsPanels.DialogManage( self, title, frame_key ) as dlg: - - panel = ClientGUITags.ManageTagsPanel( dlg, self._location_context, CC.TAG_PRESENTATION_SEARCH_PAGE_MANAGE_TAGS, flat_media ) - - dlg.SetPanel( panel ) - - dlg.exec() - - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def _ManageTimestamps( self ): - - ordered_selected_media = self._GetSelectedMediaOrdered() - - ordered_selected_flat_media = ClientMedia.FlattenMedia( ordered_selected_media ) - - if len( ordered_selected_flat_media ) > 0: - - ClientGUIMediaModalActions.EditFileTimestamps( self, ordered_selected_flat_media ) - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def _ManageURLs( self ): - - flat_media = ClientMedia.FlattenMedia( self._selected_media ) - - if len( flat_media ) > 0: - - num_files = self._GetNumSelected() - - title = 'manage urls for {} files'.format( num_files ) - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg: - - panel = ClientGUIScrolledPanelsEdit.EditURLsPanel( dlg, flat_media ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - pending_content_updates = panel.GetValue() - - if len( pending_content_updates ) > 0: - - content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, pending_content_updates ) - - CG.client_controller.Write( 'content_updates', content_update_package ) - - - - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def _MediaIsVisible( self, media ): - - return True - - - def _ModifyUploaders( self, file_service_key ): - - hashes = self._GetSelectedHashes() - - contents = [ HydrusNetwork.Content( HC.CONTENT_TYPE_FILES, ( hash, ) ) for hash in hashes ] - - if len( contents ) > 0: - - subject_account_identifiers = [ HydrusNetwork.AccountIdentifier( content = content ) for content in contents ] - - frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'manage accounts' ) - - panel = ClientGUIHydrusNetwork.ModifyAccountsPanel( frame, file_service_key, subject_account_identifiers ) - - frame.SetPanel( panel ) - - - - def _OpenFileInWebBrowser( self ): - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - if focused_singleton.GetLocationsManager().IsLocal(): - - hash = focused_singleton.GetHash() - mime = focused_singleton.GetMime() - - client_files_manager = CG.client_controller.client_files_manager - - path = client_files_manager.GetFilePath( hash, mime ) - - self.focusMediaPaused.emit() - - ClientPaths.LaunchPathInWebBrowser( path ) - - - - - def _MacQuicklook( self ): - - if HC.PLATFORM_MACOS and self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - if focused_singleton.GetLocationsManager().IsLocal(): - - hash = focused_singleton.GetHash() - mime = focused_singleton.GetMime() - - client_files_manager = CG.client_controller.client_files_manager - - path = client_files_manager.GetFilePath( hash, mime ) - - self.focusMediaPaused.emit() - - if not MAC_QUARTZ_OK: - - HydrusData.ShowText( 'Sorry, could not do the Quick Look integration--it looks like your venv does not support it. If you are running from source, try rebuilding it!' ) - - - ClientMacIntegration.show_quicklook_for_path( path ) - - - - - def _OpenKnownURL( self ): - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - ClientGUIMediaModalActions.DoOpenKnownURLFromShortcut( self, focused_singleton ) - - - - def _PetitionFiles( self, remote_service_key ): - - hashes = self._GetSelectedHashes() - - if hashes is not None and len( hashes ) > 0: - - remote_service = CG.client_controller.services_manager.GetService( remote_service_key ) - - service_type = remote_service.GetServiceType() - - if service_type == HC.FILE_REPOSITORY: - - if len( hashes ) == 1: - - message = 'Enter a reason for this file to be removed from {}.'.format( remote_service.GetName() ) - - else: - - message = 'Enter a reason for these {} files to be removed from {}.'.format( HydrusNumbers.ToHumanInt( len( hashes ) ), remote_service.GetName() ) - - - with ClientGUIDialogs.DialogTextEntry( self, message ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - reason = dlg.GetValue() - - content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PETITION, hashes, reason = reason ) - - content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( remote_service_key, content_update ) - - CG.client_controller.Write( 'content_updates', content_update_package ) - - - - self.setFocus( QC.Qt.OtherFocusReason ) - - elif service_type == HC.IPFS: - - content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PETITION, hashes, reason = 'ipfs' ) - - content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( remote_service_key, content_update ) - - CG.client_controller.Write( 'content_updates', content_update_package ) - - - - - def _PublishSelectionChange( self, tags_changed = False ): - - if CG.client_controller.gui.IsCurrentPage( self._page_key ): - - if len( self._selected_media ) == 0: - - tags_media = self._sorted_media - - else: - - tags_media = self._selected_media - - - tags_media = list( tags_media ) - - tags_changed = tags_changed or self._had_changes_to_tag_presentation_while_hidden - - self.selectedMediaTagPresentationChanged.emit( tags_media, tags_changed ) - - self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) - - if tags_changed: - - self._had_changes_to_tag_presentation_while_hidden = False - - - elif tags_changed: - - self._had_changes_to_tag_presentation_while_hidden = True - - - - def _PublishSelectionIncrement( self, medias ): - - if CG.client_controller.gui.IsCurrentPage( self._page_key ): - - medias = list( medias ) - - self.selectedMediaTagPresentationIncremented.emit( medias ) - - self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) - - else: - - self._had_changes_to_tag_presentation_while_hidden = True - - - - def _RecalculateVirtualSize( self, called_from_resize_event = False ): - - pass - - - def _RedrawMedia( self, media ): - - pass - - - def _Remove( self, file_filter: ClientMediaFileFilter.FileFilter ): - - hashes = file_filter.GetMediaListHashes( self ) - - if len( hashes ) > 0: - - self._RemoveMediaByHashes( hashes ) - - - - def _RegenerateFileData( self, job_type ): - - flat_media = self._GetSelectedFlatMedia() - - num_files = len( flat_media ) - - if num_files > 0: - - if job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_METADATA: - - message = 'This will reparse the {} selected files\' metadata.'.format( HydrusNumbers.ToHumanInt( num_files ) ) - message += '\n' * 2 - message += 'If the files were imported before some more recent improvement in the parsing code (such as EXIF rotation or bad video resolution or duration or frame count calculation), this will update them.' - - elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL: - - message = 'This will force-regenerate the {} selected files\' thumbnails.'.format( HydrusNumbers.ToHumanInt( num_files ) ) - - elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL: - - message = 'This will regenerate the {} selected files\' thumbnails, but only if they are the wrong size.'.format( HydrusNumbers.ToHumanInt( num_files ) ) - - else: - - message = ClientFiles.regen_file_enum_to_description_lookup[ job_type ] - - - do_it_now = True - - if num_files > 50: - - message += '\n' * 2 - message += 'You have selected {} files, so this job may take some time. You can run it all now or schedule it to the overall file maintenance queue for later spread-out processing.'.format( HydrusNumbers.ToHumanInt( num_files ) ) - - yes_tuples = [] - - yes_tuples.append( ( 'do it now', 'now' ) ) - yes_tuples.append( ( 'do it later', 'later' ) ) - - try: - - result = ClientGUIDialogsQuick.GetYesYesNo( self, message, yes_tuples = yes_tuples, no_label = 'forget it' ) - - except HydrusExceptions.CancelledException: - - return - - - do_it_now = result == 'now' - - else: - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result != QW.QDialog.Accepted: - - return - - - - if do_it_now: - - self._SetFocusedMedia( None ) - - time.sleep( 0.1 ) - - CG.client_controller.CallToThread( CG.client_controller.files_maintenance_manager.RunJobImmediately, flat_media, job_type ) - - else: - - hashes = { media.GetHash() for media in flat_media } - - CG.client_controller.CallToThread( CG.client_controller.files_maintenance_manager.ScheduleJob, hashes, job_type ) - - - - - def _RescindDownloadSelected( self ): - - hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL ) - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, hashes ) ) ) - - - def _RescindPetitionFiles( self, file_service_key ): - - hashes = self._GetSelectedHashes() - - if hashes is not None and len( hashes ) > 0: - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PETITION, hashes ) ) ) - - - - def _RescindUploadFiles( self, file_service_key ): - - hashes = self._GetSelectedHashes() - - if hashes is not None and len( hashes ) > 0: - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_RESCIND_PEND, hashes ) ) ) - - - - def _Select( self, file_filter: ClientMediaFileFilter.FileFilter ): - - matching_media = file_filter.GetMediaListMedia( self ) - - media_to_deselect = self._selected_media.difference( matching_media ) - media_to_select = matching_media.difference( self._selected_media ) - - move_focus = self._focused_media in media_to_deselect or self._focused_media is None - - if move_focus or self._shift_select_started_with_this_media in media_to_deselect: - - self._EndShiftSelect() - - - self._DeselectSelect( media_to_deselect, media_to_select ) - - if move_focus: - - if len( self._selected_media ) == 0: - - self._SetFocusedMedia( None ) - - else: - - # let's not focus if one of the selectees is already visible - - media_visible = True in ( self._MediaIsVisible( media ) for media in self._selected_media ) - - if not media_visible: - - for m in self._sorted_media: - - if m in self._selected_media: - - ctrl = False - shift = False - - self._HitMedia( m, ctrl, shift ) - - self._ScrollToMedia( m ) - - break - - - - - - - - def _SetCollectionsAsAlternate( self ): - - collections = self._GetSelectedCollections() - - if len( collections ) > 0: - - message = 'Are you sure you want to set files in the selected collections as alternates? Each collection will be considered a separate group of alternates.' - message += '\n' * 2 - message += 'Be careful applying this to large groups--any more than a few dozen files, and the client could hang a long time.' - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result == QW.QDialog.Accepted: - - for collection in collections: - - media_group = collection.GetFlatMedia() - - self._SetDuplicates( HC.DUPLICATE_ALTERNATE, media_group = media_group, silent = True ) - - - - - - def _SetDuplicates( self, duplicate_type, media_pairs = None, media_group = None, duplicate_content_merge_options = None, silent = False ): - - if duplicate_type == HC.DUPLICATE_POTENTIAL: - - yes_no_text = 'queue all possible and valid pair combinations into the duplicate filter' - - elif duplicate_content_merge_options is None: - - yes_no_text = 'apply "{}"'.format( HC.duplicate_type_string_lookup[ duplicate_type ] ) - - if duplicate_type in [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] or ( CG.client_controller.new_options.GetBoolean( 'advanced_mode' ) and duplicate_type == HC.DUPLICATE_ALTERNATE ): - - yes_no_text += ' (with default duplicate metadata merge options)' - - new_options = CG.client_controller.new_options - - duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type ) - - - else: - - yes_no_text = 'apply "{}" (with custom duplicate metadata merge options)'.format( HC.duplicate_type_string_lookup[ duplicate_type ] ) - - - file_deletion_reason = 'Deleted from duplicate action on Media Page ({}).'.format( yes_no_text ) - - if media_pairs is None: - - if media_group is None: - - flat_media = self._GetSelectedFlatMedia() - - else: - - flat_media = ClientMedia.FlattenMedia( media_group ) - - - num_files_str = HydrusNumbers.ToHumanInt( len( flat_media ) ) - - if len( flat_media ) < 2: - - return False - - - if duplicate_type in ( HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_POTENTIAL ): - - media_pairs = list( itertools.combinations( flat_media, 2 ) ) - - else: - - first_media = flat_media[0] - - media_pairs = [ ( first_media, other_media ) for other_media in flat_media if other_media != first_media ] - - - else: - - num_files_str = HydrusNumbers.ToHumanInt( len( self._GetSelectedFlatMedia() ) ) - - - if len( media_pairs ) == 0: - - return False - - - if not silent: - - yes_label = 'yes' - no_label = 'no' - - if len( media_pairs ) > 1 and duplicate_type in ( HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_ALTERNATE ): - - media_pairs_str = HydrusNumbers.ToHumanInt( len( media_pairs ) ) - - message = 'Are you sure you want to {} for the {} selected files? The relationship will be applied between every pair combination in the file selection ({} pairs).'.format( yes_no_text, num_files_str, media_pairs_str ) - - if len( media_pairs ) > 100: - - if duplicate_type == HC.DUPLICATE_FALSE_POSITIVE: - - message = 'False positive records are complicated, and setting that relationship for {} files ({} pairs) at once is likely a mistake.'.format( num_files_str, media_pairs_str ) - message += '\n' * 2 - message += 'Are you sure all of these files are all potential duplicates and that they are all false positive matches with each other? If not, I recommend you step back for now.' - - yes_label = 'I know what I am doing' - no_label = 'step back for now' - - elif duplicate_type == HC.DUPLICATE_ALTERNATE: - - message = 'Are you certain all these {} files are alternates with every other member of the selection, and that none are duplicates?'.format( num_files_str ) - message += '\n' * 2 - message += 'If some of them may be duplicates, I recommend you either deselect the possible duplicates and try again, or just leave this group to be processed in the normal duplicate filter.' - - yes_label = 'they are all alternates' - no_label = 'some may be duplicates' - - - - else: - - message = 'Are you sure you want to ' + yes_no_text + ' for the {} selected files?'.format( num_files_str ) - - - result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = yes_label, no_label = no_label ) - - if result != QW.QDialog.Accepted: - - return False - - - - pair_info = [] - - # there's an issue here in that one decision will affect the next. if we say 'copy tags both sides' and say A > B & C, then B's tags, merged with A, should soon merge with C - # therefore, we need to update the media objects as we go here, which means we need duplicates to force content updates on - # this is a little hacky, so maybe a big rewrite here would be nice - - # There's a second issue, wew, in that in order to propagate C back to B, we need to do the whole thing twice! wow! - # some service_key_to_content_updates preservation gubbins is needed as a result - - hashes_to_duplicated_media = {} - hash_pairs_to_content_update_packages = collections.defaultdict( list ) - - for is_first_run in ( True, False ): - - for ( first_media, second_media ) in media_pairs: - - first_hash = first_media.GetHash() - second_hash = second_media.GetHash() - - if first_hash not in hashes_to_duplicated_media: - - hashes_to_duplicated_media[ first_hash ] = first_media.Duplicate() - - - first_duplicated_media = hashes_to_duplicated_media[ first_hash ] - - if second_hash not in hashes_to_duplicated_media: - - hashes_to_duplicated_media[ second_hash ] = second_media.Duplicate() - - - second_duplicated_media = hashes_to_duplicated_media[ second_hash ] - - content_update_packages = hash_pairs_to_content_update_packages[ ( first_hash, second_hash ) ] - - if duplicate_content_merge_options is not None: - - do_not_do_deletes = is_first_run - - # so the important part of this mess is here. we send the duplicated media, which is keeping up with content updates, to the method here - # original 'first_media' is not changed, and won't be until the database Write clears and publishes everything - content_update_packages.append( duplicate_content_merge_options.ProcessPairIntoContentUpdatePackage( first_duplicated_media, second_duplicated_media, file_deletion_reason = file_deletion_reason, do_not_do_deletes = do_not_do_deletes ) ) - - - for content_update_package in content_update_packages: - - for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): - - for content_update in content_updates: - - hashes = content_update.GetHashes() - - if first_hash in hashes: - - first_duplicated_media.GetMediaResult().ProcessContentUpdate( service_key, content_update ) - - - if second_hash in hashes: - - second_duplicated_media.GetMediaResult().ProcessContentUpdate( service_key, content_update ) - - - - - - if is_first_run: - - continue - - - pair_info.append( ( duplicate_type, first_hash, second_hash, content_update_packages ) ) - - - - if len( pair_info ) > 0: - - CG.client_controller.WriteSynchronous( 'duplicate_pair_status', pair_info ) - - return True - - - return False - - - def _SetDuplicatesCustom( self ): - - duplicate_types = [ HC.DUPLICATE_BETTER, HC.DUPLICATE_SAME_QUALITY ] - - if CG.client_controller.new_options.GetBoolean( 'advanced_mode' ): - - duplicate_types.append( HC.DUPLICATE_ALTERNATE ) - - - choice_tuples = [ ( HC.duplicate_type_string_lookup[ duplicate_type ], duplicate_type ) for duplicate_type in duplicate_types ] - - try: - - duplicate_type = ClientGUIDialogsQuick.SelectFromList( self, 'select duplicate type', choice_tuples ) - - except HydrusExceptions.CancelledException: - - return - - - new_options = CG.client_controller.new_options - - duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type ) - - with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit duplicate merge options' ) as dlg: - - panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options, for_custom_action = True ) - - dlg.SetPanel( panel ) - - if dlg.exec() == QW.QDialog.Accepted: - - duplicate_content_merge_options = panel.GetValue() - - if duplicate_type == HC.DUPLICATE_BETTER: - - self._SetDuplicatesFocusedBetter( duplicate_content_merge_options = duplicate_content_merge_options ) - - else: - - self._SetDuplicates( duplicate_type, duplicate_content_merge_options = duplicate_content_merge_options ) - - - - - - def _SetDuplicatesFocusedBetter( self, duplicate_content_merge_options = None ): - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - focused_hash = focused_singleton.GetHash() - - flat_media = self._GetSelectedFlatMedia() - - ( better_media, ) = [ media for media in flat_media if media.GetHash() == focused_hash ] - - worse_flat_media = [ media for media in flat_media if media.GetHash() != focused_hash ] - - if len( worse_flat_media ) == 0: - - message = 'Since you only selected one file, would you rather just set this file as the best file of its group?' - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result == QW.QDialog.Accepted: - - self._SetDuplicatesFocusedKing( silent = True ) - - - return - - - media_pairs = [ ( better_media, worse_media ) for worse_media in worse_flat_media ] - - message = 'Are you sure you want to set the focused file as better than the {} other files in the selection?'.format( HydrusNumbers.ToHumanInt( len( worse_flat_media ) ) ) - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result == QW.QDialog.Accepted: - - self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs, silent = True, duplicate_content_merge_options = duplicate_content_merge_options ) - - - else: - - ClientGUIDialogsMessage.ShowWarning( self, 'No file is focused, so cannot set the focused file as better!' ) - - return - - - - def _SetDuplicatesFocusedKing( self, silent = False ): - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - focused_hash = media.GetHash() - - # TODO: when media knows its duplicate gubbins, we can test num dupe files and if it is king already and stuff easier here - - do_it = False - - if silent: - - do_it = True - - else: - - message = 'Are you sure you want to set the focused file as the best file of its duplicate group?' - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result == QW.QDialog.Accepted: - - do_it = True - - - - if do_it: - - CG.client_controller.WriteSynchronous( 'duplicate_set_king', focused_hash ) - - - else: - - ClientGUIDialogsMessage.ShowWarning( self, 'No file is focused, so cannot set the focused file as king!' ) - - return - - - - def _SetDuplicatesPotential( self ): - - media_group = self._GetSelectedFlatMedia() - - self._SetDuplicates( HC.DUPLICATE_POTENTIAL, media_group = media_group ) - - - def _SetFocusedMedia( self, media ): - - if media is None and self._focused_media is not None: - - next_best_media = self._focused_media - - i = self._sorted_media.index( next_best_media ) - - while next_best_media in self._selected_media: - - if i == 0: - - next_best_media = None - - break - - - i -= 1 - - next_best_media = self._sorted_media[ i ] - - - self._next_best_media_if_focuses_removed = next_best_media - - else: - - self._next_best_media_if_focuses_removed = None - - - publish_media = None - - self._focused_media = media - self._last_hit_media = media - - if self._focused_media is not None: - - publish_media = self._focused_media.GetDisplayMedia() - - - if publish_media is None: - - self.focusMediaCleared.emit() - - else: - - self.focusMediaChanged.emit( publish_media ) - - - - def _ScrollToMedia( self, media ): - - pass - - - def _ShowSelectionInNewPage( self ): - - hashes = self._GetSelectedHashes( ordered = True ) - - if len( hashes ) > 0: - - media_sort = self._management_controller.GetVariable( 'media_sort' ) - - if self._management_controller.HasVariable( 'media_collect' ): - - media_collect = self._management_controller.GetVariable( 'media_collect' ) - - else: - - media_collect = ClientMedia.MediaCollect() - - - ClientGUIMediaSimpleActions.ShowFilesInNewPage( hashes, self._location_context, media_sort = media_sort, media_collect = media_collect ) - - - - def _StartShiftSelect( self, media ): - - self._shift_select_started_with_this_media = media - self._media_added_in_current_shift_select = set() - - - def _Undelete( self ): - - media = self._GetSelectedFlatMedia() - - ClientGUIMediaModalActions.UndeleteMedia( self, media ) - - - def _UpdateBackgroundColour( self ): - - self.widget().update() - - - def _UploadDirectory( self, file_service_key ): - - hashes = self._GetSelectedHashes() - - if hashes is not None and len( hashes ) > 0: - - ipfs_service = CG.client_controller.services_manager.GetService( file_service_key ) - - - with ClientGUIDialogs.DialogTextEntry( self, 'Enter a note to describe this directory.' ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - note = dlg.GetValue() - - CG.client_controller.CallToThread( ipfs_service.PinDirectory, hashes, note ) - - - - - def _UploadFiles( self, file_service_key ): - - hashes = self._GetSelectedHashes( is_not_in_file_service_key = file_service_key ) - - if hashes is not None and len( hashes ) > 0: - - CG.client_controller.Write( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( file_service_key, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_PEND, hashes ) ) ) - - - - def AddMediaResults( self, page_key, media_results ): - - if page_key == self._page_key: - - CG.client_controller.pub( 'refresh_page_name', self._page_key ) - - result = ClientMedia.ListeningMediaList.AddMediaResults( self, media_results ) - - self.newMediaAdded.emit() - - CG.client_controller.pub( 'notify_new_pages_count' ) - - return result - - - - def CleanBeforeDestroy( self ): - - self.Clear() - - - def ClearPageKey( self ): - - self._page_key = b'dead media panel page key' - - - def Collect( self, media_collect = None ): - - self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) ) - - ClientMedia.ListeningMediaList.Collect( self, media_collect = media_collect ) - - self._RecalculateVirtualSize() - - self.Sort() - - - def GetColour( self, colour_type ): - - if CG.client_controller.new_options.GetBoolean( 'override_stylesheet_colours' ): - - bg_colour = CG.client_controller.new_options.GetColour( colour_type ) - - else: - - bg_colour = self._qss_colours.get( colour_type, QG.QColor( 127, 127, 127 ) ) - - - return bg_colour - - - def GetTotalFileSize( self ): - - return 0 - - - def LaunchMediaViewerOnFocus( self, page_key ): - - if page_key == self._page_key: - - self._LaunchMediaViewer() - - - - def PageHidden( self ): - - pass - - - def PageShown( self ): - - self._PublishSelectionChange() - - - def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): - - command_processed = True - - if command.IsSimpleCommand(): - - action = command.GetSimpleAction() - - if action == CAC.SIMPLE_COPY_FILE_BITMAP: - - if not self._HasFocusSingleton(): - - return - - - focus_singleton = self._GetFocusSingleton() - - bitmap_type = command.GetSimpleData() - - ClientGUIMediaSimpleActions.CopyMediaBitmap( focus_singleton, bitmap_type ) - - elif action == CAC.SIMPLE_COPY_FILES: - - file_command_target = command.GetSimpleData() - - medias = self._GetMediasForFileCommandTarget( file_command_target ) - - if len( medias ) > 0: - - ClientGUIMediaSimpleActions.CopyFilesToClipboard( medias ) - - - elif action == CAC.SIMPLE_COPY_FILE_PATHS: - - file_command_target = command.GetSimpleData() - - medias = self._GetMediasForFileCommandTarget( file_command_target ) - - if len( medias ) > 0: - - ClientGUIMediaSimpleActions.CopyFilePathsToClipboard( medias ) - - - elif action == CAC.SIMPLE_COPY_FILE_HASHES: - - ( file_command_target, hash_type ) = command.GetSimpleData() - - medias = self._GetMediasForFileCommandTarget( file_command_target ) - - if len( medias ) > 0: - - ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, medias ) - - - elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES: - - hacky_ipfs_dict = command.GetSimpleData() - - file_command_target = hacky_ipfs_dict[ 'file_command_target' ] - ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ] - - medias = self._GetMediasForFileCommandTarget( file_command_target ) - - if len( medias ) > 0: - - ClientGUIMediaSimpleActions.CopyServiceFilenamesToClipboard( ipfs_service_key, medias ) - - - elif action == CAC.SIMPLE_COPY_FILE_ID: - - file_command_target = command.GetSimpleData() - - medias = self._GetMediasForFileCommandTarget( file_command_target ) - - if len( medias ) > 0: - - ClientGUIMediaSimpleActions.CopyFileIdsToClipboard( medias ) - - - elif action == CAC.SIMPLE_COPY_URLS: - - ordered_selected_media = self._GetSelectedMediaOrdered() - - if len( ordered_selected_media ) > 0: - - ClientGUIMediaSimpleActions.CopyMediaURLs( ordered_selected_media ) - - - elif action == CAC.SIMPLE_REARRANGE_THUMBNAILS: - - ordered_selected_media = self._GetSelectedMediaOrdered() - - ( rearrange_type, rearrange_data ) = command.GetSimpleData() - - insertion_index = None - - if rearrange_type == CAC.REARRANGE_THUMBNAILS_TYPE_FIXED: - - insertion_index = rearrange_data - - elif rearrange_type == CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND: - - rearrange_command = rearrange_data - - if rearrange_command == CAC.MOVE_HOME: - - insertion_index = 0 - - elif rearrange_command == CAC.MOVE_END: - - insertion_index = len( self._sorted_media ) - - else: - - if len( self._selected_media ) > 0: - - if rearrange_command in ( CAC.MOVE_LEFT, CAC.MOVE_RIGHT ): - - ordered_selected_media = self._GetSelectedMediaOrdered() - - earliest_index = self._sorted_media.index( ordered_selected_media[0] ) - - if rearrange_command == CAC.MOVE_LEFT: - - if earliest_index > 0: - - insertion_index = earliest_index - 1 - - - elif rearrange_command == CAC.MOVE_RIGHT: - - insertion_index = earliest_index + 1 - - - elif rearrange_command == CAC.MOVE_TO_FOCUS: - - if self._focused_media is not None: - - focus_index = self._sorted_media.index( self._focused_media ) - - insertion_index = focus_index - - - - - - - if insertion_index is None: - - return - - - self.MoveMedia( ordered_selected_media, insertion_index = insertion_index ) - - elif action == CAC.SIMPLE_SHOW_DUPLICATES: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - duplicate_type = command.GetSimpleData() - - ClientGUIMediaSimpleActions.ShowDuplicatesInNewPage( self._location_context, hash, duplicate_type ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FOCUSED_FALSE_POSITIVES: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.ClearFalsePositives( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_CLEAR_FALSE_POSITIVES: - - hashes = self._GetSelectedHashes() - - if len( hashes ) > 0: - - ClientGUIDuplicates.ClearFalsePositives( self, hashes ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_FOCUSED_ALTERNATE_GROUP: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.DissolveAlternateGroup( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_ALTERNATE_GROUP: - - hashes = self._GetSelectedHashes() - - if len( hashes ) > 0: - - ClientGUIDuplicates.DissolveAlternateGroup( self, hashes ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_FOCUSED_DUPLICATE_GROUP: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.DissolveDuplicateGroup( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_DISSOLVE_DUPLICATE_GROUP: - - hashes = self._GetSelectedHashes() - - if len( hashes ) > 0: - - ClientGUIDuplicates.DissolveDuplicateGroup( self, hashes ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_FROM_ALTERNATE_GROUP: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.RemoveFromAlternateGroup( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_FROM_DUPLICATE_GROUP: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.RemoveFromDuplicateGroup( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_RESET_FOCUSED_POTENTIAL_SEARCH: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.ResetPotentialSearch( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_RESET_POTENTIAL_SEARCH: - - hashes = self._GetSelectedHashes() - - if len( hashes ) > 0: - - ClientGUIDuplicates.ResetPotentialSearch( self, hashes ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_FOCUSED_POTENTIALS: - - if self._HasFocusSingleton(): - - media = self._GetFocusSingleton() - - hash = media.GetHash() - - ClientGUIDuplicates.RemovePotentials( self, ( hash, ) ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_REMOVE_POTENTIALS: - - hashes = self._GetSelectedHashes() - - if len( hashes ) > 0: - - ClientGUIDuplicates.RemovePotentials( self, hashes ) - - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE: - - self._SetDuplicates( HC.DUPLICATE_ALTERNATE ) - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE_COLLECTIONS: - - self._SetCollectionsAsAlternate() - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_CUSTOM: - - self._SetDuplicatesCustom() - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_FOCUSED_BETTER: - - self._SetDuplicatesFocusedBetter() - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_FOCUSED_KING: - - self._SetDuplicatesFocusedKing() - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_POTENTIAL: - - self._SetDuplicatesPotential() - - elif action == CAC.SIMPLE_DUPLICATE_MEDIA_SET_SAME_QUALITY: - - self._SetDuplicates( HC.DUPLICATE_SAME_QUALITY ) - - elif action in ( CAC.SIMPLE_EXPORT_FILES, CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT ): - - do_export_and_then_quit = action == CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT - - if len( self._selected_media ) > 0: - - medias = self._GetSelectedMediaOrdered() - - flat_media = ClientMedia.FlattenMedia( medias ) - - ClientGUIMediaModalActions.ExportFiles( self, flat_media, do_export_and_then_quit = do_export_and_then_quit ) - - - elif action == CAC.SIMPLE_MANAGE_FILE_RATINGS: - - self._ManageRatings() - - elif action == CAC.SIMPLE_MANAGE_FILE_TAGS: - - self._ManageTags() - - elif action == CAC.SIMPLE_MANAGE_FILE_URLS: - - self._ManageURLs() - - elif action == CAC.SIMPLE_MANAGE_FILE_NOTES: - - self._ManageNotes() - - elif action == CAC.SIMPLE_MANAGE_FILE_TIMESTAMPS: - - self._ManageTimestamps() - - elif action == CAC.SIMPLE_OPEN_KNOWN_URL: - - self._OpenKnownURL() - - elif action == CAC.SIMPLE_ARCHIVE_FILE: - - self._Archive() - - elif action == CAC.SIMPLE_DELETE_FILE: - - self._Delete() - - elif action == CAC.SIMPLE_UNDELETE_FILE: - - self._Undelete() - - elif action == CAC.SIMPLE_INBOX_FILE: - - self._Inbox() - - elif action == CAC.SIMPLE_REMOVE_FILE_FROM_VIEW: - - self._Remove( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) ) - - elif action == CAC.SIMPLE_LAUNCH_MEDIA_VIEWER: - - self._LaunchMediaViewer() - - elif action == CAC.SIMPLE_OPEN_FILE_IN_EXTERNAL_PROGRAM: - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - it_worked = ClientGUIMediaSimpleActions.OpenExternally( focused_singleton ) - - if it_worked: - - self.focusMediaPaused.emit() - - - - elif action == CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER: - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - it_worked = ClientGUIMediaSimpleActions.OpenFileLocation( focused_singleton ) - - if it_worked: - - self.focusMediaPaused.emit() - - - - elif action == CAC.SIMPLE_NATIVE_OPEN_FILE_WITH_DIALOG: - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - it_worked = ClientGUIMediaSimpleActions.OpenFileWithDialog( focused_singleton ) - - if it_worked: - - self.focusMediaPaused.emit() - - - - elif action == CAC.SIMPLE_NATIVE_OPEN_FILE_PROPERTIES: - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - it_worked = ClientGUIMediaSimpleActions.OpenNativeFileProperties( focused_singleton ) - - if it_worked: - - self.focusMediaPaused.emit() - - - - elif action == CAC.SIMPLE_OPEN_FILE_IN_WEB_BROWSER: - - if self._HasFocusSingleton(): - - focused_singleton = self._GetFocusSingleton() - - it_worked = ClientGUIMediaSimpleActions.OpenInWebBrowser( focused_singleton ) - - if it_worked: - - self.focusMediaPaused.emit() - - - - elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE: - - self._ShowSelectionInNewPage() - - elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE: - - hashes = self._GetSelectedHashes( ordered = True ) - - ClientGUIMediaSimpleActions.ShowFilesInNewDuplicatesFilterPage( hashes, self._location_context ) - - elif action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES: - - media = self._GetSelectedFlatMedia() - - hamming_distance = command.GetSimpleData() - - ClientGUIMediaSimpleActions.ShowSimilarFilesInNewPage( media, self._location_context, hamming_distance ) - - elif action == CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER: - - self._ArchiveDeleteFilter() - - elif action == CAC.SIMPLE_MAC_QUICKLOOK: - - self._MacQuicklook() - - else: - - command_processed = False - - - elif command.IsContentCommand(): - - command_processed = ClientGUIMediaModalActions.ApplyContentApplicationCommandToMedia( self, command, self._GetSelectedFlatMedia() ) - - else: - - command_processed = False - - - return command_processed - - - def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpdates.ContentUpdatePackage ): - - ClientMedia.ListeningMediaList.ProcessContentUpdatePackage( self, content_update_package ) - - we_were_file_or_tag_affected = False - - for ( service_key, content_updates ) in content_update_package.IterateContentUpdates(): - - for content_update in content_updates: - - hashes = content_update.GetHashes() - - if self._HasHashes( hashes ): - - affected_media = self._GetMedia( hashes ) - - self._RedrawMedia( affected_media ) - - if content_update.GetDataType() in ( HC.CONTENT_TYPE_FILES, HC.CONTENT_TYPE_MAPPINGS ): - - we_were_file_or_tag_affected = True - - - - - - if we_were_file_or_tag_affected: - - self._PublishSelectionChange( tags_changed = True ) - - - - def ProcessServiceUpdates( self, service_keys_to_service_updates: typing.Dict[ bytes, typing.Collection[ ClientServices.ServiceUpdate ] ] ): - - ClientMedia.ListeningMediaList.ProcessServiceUpdates( self, service_keys_to_service_updates ) - - for ( service_key, service_updates ) in service_keys_to_service_updates.items(): - - for service_update in service_updates: - - ( action, row ) = service_update.ToTuple() - - if action in ( HC.SERVICE_UPDATE_DELETE_PENDING, HC.SERVICE_UPDATE_RESET ): - - self._RecalculateVirtualSize() - - - self._PublishSelectionChange( tags_changed = True ) - - - - - def PublishSelectionChange( self ): - - self._PublishSelectionChange() - - - def RemoveMedia( self, page_key, hashes ): - - if page_key == self._page_key: - - self._RemoveMediaByHashes( hashes ) - - - - def SelectByTags( self, page_key, tag_service_key, and_or_or, tags ): - - if page_key == self._page_key: - - self._Select( ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_TAGS, ( tag_service_key, and_or_or, tags ) ) ) - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def SetDuplicateStatusForAll( self, duplicate_type ): - - media_group = ClientMedia.FlattenMedia( self._sorted_media ) - - return self._SetDuplicates( duplicate_type, media_group = media_group ) - - - def SetEmptyPageStatusOverride( self, value: str ): - - self._empty_page_status_override = value - - - def SetFocusedMedia( self, media ): - - pass - - - def get_hmrp_background( self ): - - return self._qss_colours[ CC.COLOUR_THUMBGRID_BACKGROUND ] - - - def get_hmrp_thumbnail_local_background_normal( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND ] - - - def get_hmrp_thumbnail_local_background_selected( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] - - - def get_hmrp_thumbnail_local_border_normal( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BORDER ] - - - def get_hmrp_thumbnail_local_border_selected( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BORDER_SELECTED ] - - - def get_hmrp_thumbnail_not_local_background_normal( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] - - - def get_hmrp_thumbnail_not_local_background_selected( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] - - - def get_hmrp_thumbnail_not_local_border_normal( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE ] - - - def get_hmrp_thumbnail_not_local_border_selected( self ): - - return self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] - - - def set_hmrp_background( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMBGRID_BACKGROUND ] = colour - - - def set_hmrp_thumbnail_local_background_normal( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND ] = colour - - - def set_hmrp_thumbnail_local_background_selected( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_SELECTED ] = colour - - - def set_hmrp_thumbnail_local_border_normal( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BORDER ] = colour - - - def set_hmrp_thumbnail_local_border_selected( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BORDER_SELECTED ] = colour - - - def set_hmrp_thumbnail_not_local_background_normal( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE ] = colour - - - def set_hmrp_thumbnail_not_local_background_selected( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED ] = colour - - - def set_hmrp_thumbnail_not_local_border_normal( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE ] = colour - - - def set_hmrp_thumbnail_not_local_border_selected( self, colour ): - - self._qss_colours[ CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED ] = colour - - - hmrp_background = QC.Property( QG.QColor, get_hmrp_background, set_hmrp_background ) - hmrp_thumbnail_local_background_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_local_background_normal, set_hmrp_thumbnail_local_background_normal ) - hmrp_thumbnail_local_background_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_local_background_selected, set_hmrp_thumbnail_local_background_selected ) - hmrp_thumbnail_local_border_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_local_border_normal, set_hmrp_thumbnail_local_border_normal ) - hmrp_thumbnail_local_border_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_local_border_selected, set_hmrp_thumbnail_local_border_selected ) - hmrp_thumbnail_not_local_background_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_background_normal, set_hmrp_thumbnail_not_local_background_normal ) - hmrp_thumbnail_not_local_background_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_background_selected, set_hmrp_thumbnail_not_local_background_selected ) - hmrp_thumbnail_not_local_border_normal = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_border_normal, set_hmrp_thumbnail_not_local_border_normal ) - hmrp_thumbnail_not_local_border_selected = QC.Property( QG.QColor, get_hmrp_thumbnail_not_local_border_selected, set_hmrp_thumbnail_not_local_border_selected ) - - class _InnerWidget( QW.QWidget ): - - def __init__( self, parent ): - - super().__init__( parent ) - - self._parent = parent - - - def paintEvent( self, event ): - - painter = QG.QPainter( self ) - - bg_colour = self._parent.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) - - painter.setBackground( QG.QBrush( bg_colour ) ) - - painter.eraseRect( painter.viewport() ) - - background_pixmap = CG.client_controller.bitmap_manager.GetMediaBackgroundPixmap() - - if background_pixmap is not None: - - my_size = QP.ScrollAreaVisibleRect( self._parent ).size() - - pixmap_size = background_pixmap.size() - - painter.drawPixmap( my_size.width() - pixmap_size.width(), my_size.height() - pixmap_size.height(), background_pixmap ) - - - - -class MediaPanelLoading( MediaPanel ): - - def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController ): - - self._current = None - self._max = None - - MediaPanel.__init__( self, parent, page_key, management_controller, [] ) - - CG.client_controller.sub( self, 'SetNumQueryResults', 'set_num_query_results' ) - - - def _GetPrettyStatusForStatusBar( self ): - - s = 'Loading' + HC.UNICODE_ELLIPSIS - - if self._current is not None: - - s += ' ' + HydrusNumbers.ToHumanInt( self._current ) - - if self._max is not None: - - s += ' of ' + HydrusNumbers.ToHumanInt( self._max ) - - - - return s - - - def GetSortedMedia( self ): - - return [] - - - def SetNumQueryResults( self, page_key, num_current, num_max ): - - if page_key == self._page_key: - - self._current = num_current - - self._max = num_max - - self._PublishSelectionChange() - - - -class MediaPanelThumbnails( MediaPanel ): - - def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController, media_results ): - - self._clean_canvas_pages = {} - self._dirty_canvas_pages = [] - self._num_rows_per_canvas_page = 1 - self._num_rows_per_actual_page = 1 - - self._last_size = QC.QSize( 20, 20 ) - self._num_columns = 1 - - self._drag_init_coordinates = None - self._drag_click_timestamp_ms = 0 - self._drag_prefire_event_count = 0 - self._hashes_to_thumbnails_waiting_to_be_drawn: typing.Dict[ bytes, ThumbnailWaitingToBeDrawn ] = {} - self._hashes_faded = set() - - super().__init__( parent, page_key, management_controller, media_results ) - - self._last_device_pixel_ratio = self.devicePixelRatio() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - thumbnail_scroll_rate = float( CG.client_controller.new_options.GetString( 'thumbnail_scroll_rate' ) ) - - self.verticalScrollBar().setSingleStep( int( round( thumbnail_span_height * thumbnail_scroll_rate ) ) ) - - self._widget_event_filter = QP.WidgetEventFilter( self.widget() ) - self._widget_event_filter.EVT_LEFT_DCLICK( self.EventMouseFullScreen ) - self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventMouseFullScreen ) - - # notice this is on widget, not myself. fails to set up scrollbars if just moved up - # there's a job in qt to-do to sort all this out and fix other scroll issues - self._widget_event_filter.EVT_SIZE( self.EventResize ) - - self.widget().setMinimumSize( 50, 50 ) - - self._UpdateScrollBars() - - CG.client_controller.sub( self, 'MaintainPageCache', 'memory_maintenance_pulse' ) - CG.client_controller.sub( self, 'NotifyNewFileInfo', 'new_file_info' ) - CG.client_controller.sub( self, 'NewThumbnails', 'new_thumbnails' ) - CG.client_controller.sub( self, 'ThumbnailsReset', 'notify_complete_thumbnail_reset' ) - CG.client_controller.sub( self, 'RedrawAllThumbnails', 'refresh_all_tag_presentation_gui' ) - CG.client_controller.sub( self, 'WaterfallThumbnails', 'waterfall_thumbnails' ) - - - def _CalculateVisiblePageIndices( self ): - - y_start = self._GetYStart() - - earliest_y = y_start - - last_y = earliest_y + QP.ScrollAreaVisibleRect( self ).size().height() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - page_height = self._num_rows_per_canvas_page * thumbnail_span_height - - first_visible_page_index = earliest_y // page_height - - last_visible_page_index = last_y // page_height - - page_indices = list( range( first_visible_page_index, last_visible_page_index + 1 ) ) - - return page_indices - - - def _CreateNewDirtyPage( self ): - - my_width = self.size().width() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - dpr = self.devicePixelRatio() - - canvas_width = int( my_width * dpr ) - canvas_height = int( self._num_rows_per_canvas_page * thumbnail_span_height * dpr ) - - canvas_page = CG.client_controller.bitmap_manager.GetQtImage( canvas_width, canvas_height, 32 ) - - canvas_page.setDevicePixelRatio( dpr ) - - self._dirty_canvas_pages.append( canvas_page ) - - - def _DeleteAllDirtyPages( self ): - - self._dirty_canvas_pages = [] - - - def _DirtyAllPages( self ): - - clean_indices = list( self._clean_canvas_pages.keys() ) - - for clean_index in clean_indices: - - self._DirtyPage( clean_index ) - - - - def _DirtyPage( self, clean_index ): - - canvas_page = self._clean_canvas_pages[ clean_index ] - - del self._clean_canvas_pages[ clean_index ] - - thumbnails = [ thumbnail for ( thumbnail_index, thumbnail ) in self._GetThumbnailsFromPageIndex( clean_index ) ] - - if len( thumbnails ) > 0: - - CG.client_controller.GetCache( 'thumbnail' ).CancelWaterfall( self._page_key, thumbnails ) - - - self._dirty_canvas_pages.append( canvas_page ) - - - def _DrawCanvasPage( self, page_index, canvas_page ): - - painter = QG.QPainter( canvas_page ) - - new_options = CG.client_controller.new_options - - bg_colour = self.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) - - if HG.thumbnail_debug_mode and page_index % 2 == 0: - - bg_colour = ClientGUIFunctions.GetLighterDarkerColour( bg_colour ) - - - if new_options.GetNoneableString( 'media_background_bmp_path' ) is not None: - - comp_mode = painter.compositionMode() - - painter.setCompositionMode( QG.QPainter.CompositionMode_Source ) - - painter.setBackground( QG.QBrush( QC.Qt.transparent ) ) - - painter.eraseRect( painter.viewport() ) - - painter.setCompositionMode( comp_mode ) - - else: - - painter.setBackground( QG.QBrush( bg_colour ) ) - - painter.eraseRect( painter.viewport() ) - - - # - - page_thumbnails = self._GetThumbnailsFromPageIndex( page_index ) - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - thumbnails_to_render_later = [] - - thumbnail_cache = CG.client_controller.GetCache( 'thumbnail' ) - - thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) - - for ( thumbnail_index, thumbnail ) in page_thumbnails: - - display_media = thumbnail.GetDisplayMedia() - - if display_media is None: - - continue - - - hash = display_media.GetHash() - - if hash in self._hashes_faded and thumbnail_cache.HasThumbnailCached( thumbnail ): - - self._StopFading( hash ) - - thumbnail_col = thumbnail_index % self._num_columns - - thumbnail_row = thumbnail_index // self._num_columns - - x = thumbnail_col * thumbnail_span_width + thumbnail_margin - - y = ( thumbnail_row - ( page_index * self._num_rows_per_canvas_page ) ) * thumbnail_span_height + thumbnail_margin - - painter.drawImage( x, y, thumbnail.GetQtImage( self, self.devicePixelRatio() ) ) - - else: - - thumbnails_to_render_later.append( thumbnail ) - - - - if len( thumbnails_to_render_later ) > 0: - - CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails_to_render_later ) - - - - def _FadeThumbnails( self, thumbnails ): - - if len( thumbnails ) == 0: - - return - - - if not CG.client_controller.gui.IsCurrentPage( self._page_key ): - - self._DirtyAllPages() - - return - - - now_precise = HydrusTime.GetNowPrecise() - - for thumbnail in thumbnails: - - display_media = thumbnail.GetDisplayMedia() - - if display_media is None: - - continue - - - try: - - thumbnail_index = self._sorted_media.index( thumbnail ) - - except HydrusExceptions.DataMissing: - - # probably means a collect happened during an ongoing waterfall or whatever - - continue - - - if self._GetPageIndexFromThumbnailIndex( thumbnail_index ) not in self._clean_canvas_pages: - - continue - - - hash = display_media.GetHash() - - self._hashes_faded.add( hash ) - - self._StopFading( hash ) - - bitmap = thumbnail.GetQtImage( self, self.devicePixelRatio() ) - - fade_thumbnails = CG.client_controller.new_options.GetBoolean( 'fade_thumbnails' ) - - if fade_thumbnails: - - thumbnail_draw_object = ThumbnailWaitingToBeDrawnAnimated( hash, thumbnail, thumbnail_index, bitmap ) - - else: - - thumbnail_draw_object = ThumbnailWaitingToBeDrawn( hash, thumbnail, thumbnail_index, bitmap ) - - - self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] = thumbnail_draw_object - - - CG.client_controller.gui.RegisterAnimationUpdateWindow( self ) - - - def _GenerateMediaCollection( self, media_results ): - - return ThumbnailMediaCollection( self._location_context, media_results ) - - - def _GenerateMediaSingleton( self, media_result ): - - return ThumbnailMediaSingleton( media_result ) - - - def _GetMediaCoordinates( self, media ): - - try: index = self._sorted_media.index( media ) - except: return ( -1, -1 ) - - row = index // self._num_columns - column = index % self._num_columns - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) - - ( x, y ) = ( column * thumbnail_span_width + thumbnail_margin, row * thumbnail_span_height + thumbnail_margin ) - - return ( x, y ) - - - def _GetPageIndexFromThumbnailIndex( self, thumbnail_index ): - - thumbnails_per_page = self._num_columns * self._num_rows_per_canvas_page - - page_index = thumbnail_index // thumbnails_per_page - - return page_index - - - def _GetThumbnailSpanDimensions( self ): - - thumbnail_border = CG.client_controller.new_options.GetInteger( 'thumbnail_border' ) - thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) - - return ClientData.AddPaddingToDimensions( HC.options[ 'thumbnail_dimensions' ], ( thumbnail_border + thumbnail_margin ) * 2 ) - - - def _GetThumbnailUnderMouse( self, mouse_event ): - - pos = mouse_event.position().toPoint() - - x = pos.x() - y = pos.y() - - ( t_span_x, t_span_y ) = self._GetThumbnailSpanDimensions() - - x_mod = x % t_span_x - y_mod = y % t_span_y - - thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) - - if x_mod <= thumbnail_margin or y_mod <= thumbnail_margin or x_mod > t_span_x - thumbnail_margin or y_mod > t_span_y - thumbnail_margin: - - return None - - - column_index = x // t_span_x - row_index = y // t_span_y - - if column_index >= self._num_columns: - - return None - - - thumbnail_index = self._num_columns * row_index + column_index - - if thumbnail_index < 0: - - return None - - - if thumbnail_index >= len( self._sorted_media ): - - return None - - - return self._sorted_media[ thumbnail_index ] - - - def _GetThumbnailsFromPageIndex( self, page_index ): - - num_thumbnails_per_page = self._num_columns * self._num_rows_per_canvas_page - - start_index = num_thumbnails_per_page * page_index - - if start_index <= len( self._sorted_media ): - - end_index = min( len( self._sorted_media ), start_index + num_thumbnails_per_page ) - - thumbnails = [ ( index, self._sorted_media[ index ] ) for index in range( start_index, end_index ) ] - - else: - - thumbnails = [] - - - return thumbnails - - - def _GetYStart( self ): - - visible_rect = QP.ScrollAreaVisibleRect( self ) - - visible_rect_y = visible_rect.y() - - visible_rect_height = visible_rect.height() - - my_virtual_size = self.widget().size() - - my_virtual_height = my_virtual_size.height() - - max_y = my_virtual_height - visible_rect_height - - y_start = max( 0, visible_rect_y ) - - y_start = min( y_start, max_y ) - - return y_start - - - def _MediaIsInCleanPage( self, thumbnail ): - - try: - - index = self._sorted_media.index( thumbnail ) - - except HydrusExceptions.DataMissing: - - return False - - - if self._GetPageIndexFromThumbnailIndex( index ) in self._clean_canvas_pages: - - return True - - else: - - return False - - - - def _MediaIsVisible( self, media ): - - if media is not None: - - ( x, y ) = self._GetMediaCoordinates( media ) - - visible_rect = QP.ScrollAreaVisibleRect( self ) - - visible_rect_y = visible_rect.y() - - visible_rect_height = visible_rect.height() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - bottom_edge_below_top_of_view = visible_rect_y < y + thumbnail_span_height - top_edge_above_bottom_of_view = y < visible_rect_y + visible_rect_height - - is_visible = bottom_edge_below_top_of_view and top_edge_above_bottom_of_view - - return is_visible - - - return True - - - def _MoveThumbnailFocus( self, rows, columns, shift ): - - if self._last_hit_media is not None: - - media_to_use = self._last_hit_media - - elif self._next_best_media_if_focuses_removed is not None: - - media_to_use = self._next_best_media_if_focuses_removed - - if columns == -1: # treat it as if the focused area is between this and the next - - columns = 0 - - - elif len( self._sorted_media ) > 0: - - media_to_use = self._sorted_media[ 0 ] - - else: - - media_to_use = None - - - if media_to_use is not None: - - try: - - current_position = self._sorted_media.index( media_to_use ) - - except HydrusExceptions.DataMissing: - - self._SetFocusedMedia( None ) - - return - - - new_position = current_position + columns + ( self._num_columns * rows ) - - if new_position < 0: - - new_position = 0 - - elif new_position > len( self._sorted_media ) - 1: - - new_position = len( self._sorted_media ) - 1 - - - new_media = self._sorted_media[ new_position ] - - self._HitMedia( new_media, False, shift ) - - self._ScrollToMedia( new_media ) - - - - def _NotifyThumbnailsHaveMoved( self ): - - self._DirtyAllPages() - - self.widget().update() - - - def _RecalculateVirtualSize( self, called_from_resize_event = False ): - - my_size = QP.ScrollAreaVisibleRect( self ).size() - - my_width = my_size.width() - my_height = my_size.height() - - if my_width > 0 and my_height > 0: - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - num_media = len( self._sorted_media ) - - num_rows = max( 1, num_media // self._num_columns ) - - if num_media % self._num_columns > 0: - - num_rows += 1 - - - virtual_width = my_width - - virtual_height = num_rows * thumbnail_span_height - - yUnit = self.verticalScrollBar().singleStep() - - excess = virtual_height % yUnit - - if excess > 0: # we want virtual height to fit exactly into scroll units, even if that puts some padding below bottom row - - top_up = yUnit - excess - - virtual_height += top_up - - - virtual_height = max( virtual_height, my_height ) - - virtual_size = QC.QSize( virtual_width, virtual_height ) - - if virtual_size != self.widget().size(): - - self.widget().resize( QC.QSize( virtual_width, virtual_height ) ) - - if not called_from_resize_event: - - self._UpdateScrollBars() # would lead to infinite recursion if called from a resize event - - - - - - def _RedrawMedia( self, thumbnails ): - - visible_thumbnails = [ thumbnail for thumbnail in thumbnails if self._MediaIsInCleanPage( thumbnail ) ] - - thumbnail_cache = CG.client_controller.GetCache( 'thumbnail' ) - - thumbnails_to_render_now = [] - thumbnails_to_render_later = [] - - for thumbnail in visible_thumbnails: - - if thumbnail_cache.HasThumbnailCached( thumbnail ): - - thumbnails_to_render_now.append( thumbnail ) - - else: - - thumbnails_to_render_later.append( thumbnail ) - - - - if len( thumbnails_to_render_now ) > 0: - - self._FadeThumbnails( thumbnails_to_render_now ) - - - if len( thumbnails_to_render_later ) > 0: - - CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails_to_render_later ) - - - - def _ReinitialisePageCacheIfNeeded( self ): - - old_num_rows = self._num_rows_per_canvas_page - old_num_columns = self._num_columns - - old_width = self._last_size.width() - old_height = self._last_size.height() - - my_size = QP.ScrollAreaVisibleRect( self ).size() - - my_width = my_size.width() - my_height = my_size.height() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - num_rows = ( my_height // thumbnail_span_height ) - - self._num_rows_per_actual_page = max( 1, num_rows ) - self._num_rows_per_canvas_page = max( 1, num_rows // 2 ) - - self._num_columns = max( 1, my_width // thumbnail_span_width ) - - dimensions_changed = old_width != my_width or old_height != my_height - thumb_layout_changed = old_num_columns != self._num_columns or old_num_rows != self._num_rows_per_canvas_page - - if dimensions_changed or thumb_layout_changed: - - width_got_bigger = old_width < my_width - - if thumb_layout_changed or width_got_bigger: - - self._DirtyAllPages() - - self._DeleteAllDirtyPages() - - - self.widget().update() - - - - def _RemoveMediaDirectly( self, singleton_media, collected_media ): - - if self._focused_media is not None: - - if self._focused_media in singleton_media or self._focused_media in collected_media: - - self._SetFocusedMedia( None ) - - - - MediaPanel._RemoveMediaDirectly( self, singleton_media, collected_media ) - - self._EndShiftSelect() - - self._RecalculateVirtualSize() - - self._DirtyAllPages() - - self._PublishSelectionChange() - - CG.client_controller.pub( 'refresh_page_name', self._page_key ) - - CG.client_controller.pub( 'notify_new_pages_count' ) - - self.widget().update() - - - def _ScrollEnd( self, shift = False ): - - if len( self._sorted_media ) > 0: - - end_media = self._sorted_media[ -1 ] - - self._HitMedia( end_media, False, shift ) - - self._ScrollToMedia( end_media ) - - - - def _ScrollHome( self, shift = False ): - - if len( self._sorted_media ) > 0: - - home_media = self._sorted_media[ 0 ] - - self._HitMedia( home_media, False, shift ) - - self._ScrollToMedia( home_media ) - - - - def _ScrollToMedia( self, media ): - - if media is not None: - - ( x, y ) = self._GetMediaCoordinates( media ) - - visible_rect = QP.ScrollAreaVisibleRect( self ) - - visible_rect_y = visible_rect.y() - - visible_rect_height = visible_rect.height() - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - new_options = CG.client_controller.new_options - - percent_visible = new_options.GetInteger( 'thumbnail_visibility_scroll_percent' ) / 100 - - if y < visible_rect_y: - - self.ensureVisible( 0, y, 0, 0 ) - - elif y > visible_rect_y + visible_rect_height - ( thumbnail_span_height * percent_visible ): - - self.ensureVisible( 0, y + thumbnail_span_height ) - - - - - def _StopFading( self, hash ): - - if hash in self._hashes_to_thumbnails_waiting_to_be_drawn: - - del self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] - - - - def _UpdateBackgroundColour( self ): - - MediaPanel._UpdateBackgroundColour( self ) - - self._DirtyAllPages() - - self._DeleteAllDirtyPages() - - self.widget().update() - - - def _UpdateScrollBars( self ): - - # The following call is officially a no-op since this property is already true, but it also triggers an update - # of the scroll area's scrollbars which we need. - # We need this since we are intercepting & doing work in resize events which causes - # event propagation between the scroll area and the scrolled widget to not work properly (since we are suppressing resize events of the scrolled widget - otherwise we would get an infinite loop). - # Probably the best would be to change how this work and not intercept any resize events. - # Originally this was wx event handling which got ported to Qt more or less unchanged, hence the hackiness. - - self.setWidgetResizable( True ) - - - def AddMediaResults( self, page_key, media_results ): - - if page_key == self._page_key: - - thumbnails = MediaPanel.AddMediaResults( self, page_key, media_results ) - - if len( thumbnails ) > 0: - - self._RecalculateVirtualSize() - - CG.client_controller.GetCache( 'thumbnail' ).Waterfall( self._page_key, thumbnails ) - - if len( self._selected_media ) == 0: - - self._PublishSelectionIncrement( thumbnails ) - - else: - - self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() ) - - - - - - def contextMenuEvent( self, event ): - - if event.reason() == QG.QContextMenuEvent.Keyboard: - - self.ShowMenu() - - - - def EventMouseFullScreen( self, event ): - - t = self._GetThumbnailUnderMouse( event ) - - if t is not None: - - locations_manager = t.GetLocationsManager() - - if locations_manager.IsLocal(): - - self._LaunchMediaViewer( t ) - - else: - - can_download = not locations_manager.GetCurrent().isdisjoint( CG.client_controller.services_manager.GetRemoteFileServiceKeys() ) - - if can_download: - - self._DownloadHashes( t.GetHashes() ) - - - - - - def EventResize( self, event ): - - self._ReinitialisePageCacheIfNeeded() - - self._RecalculateVirtualSize( called_from_resize_event = True ) - - self._last_size = QP.ScrollAreaVisibleRect( self ).size() - - - def GetTotalFileSize( self ): - - return sum( ( m.GetSize() for m in self._sorted_media ) ) - - - def MaintainPageCache( self ): - - if not CG.client_controller.gui.IsCurrentPage( self._page_key ): - - self._DirtyAllPages() - - - self._DeleteAllDirtyPages() - - - def mouseMoveEvent( self, event ): - - if event.buttons() & QC.Qt.LeftButton: - - we_started_dragging_on_this_panel = self._drag_init_coordinates is not None - - if we_started_dragging_on_this_panel: - - old_drag_pos = self._drag_init_coordinates - - global_mouse_pos = QG.QCursor.pos() - - delta_pos = global_mouse_pos - old_drag_pos - - total_absolute_pixels_moved = delta_pos.manhattanLength() - - we_moved = total_absolute_pixels_moved > 0 - - if we_moved: - - self._drag_prefire_event_count += 1 - - - # prefire deal here is mpv lags on initial click, which can cause a drag (and hence an immediate pause) event by accident when mouserelease isn't processed quick - # so now we'll say we can't start a drag unless we get a smooth ramp to our pixel delta threshold - clean_drag_started = self._drag_prefire_event_count >= 10 - prob_not_an_accidental_click = HydrusTime.TimeHasPassedMS( self._drag_click_timestamp_ms + 100 ) - - if clean_drag_started and prob_not_an_accidental_click: - - media = self._GetSelectedFlatMedia( discriminant = CC.DISCRIMINANT_LOCAL ) - - if len( media ) > 0: - - alt_down = event.modifiers() & QC.Qt.AltModifier - - result = ClientGUIDragDrop.DoFileExportDragDrop( self, self._page_key, media, alt_down ) - - if result not in ( QC.Qt.IgnoreAction, ): - - self.focusMediaPaused.emit() - - - - - - else: - - self._drag_init_coordinates = None - self._drag_prefire_event_count = 0 - self._drag_click_timestamp_ms = 0 - - - event.ignore() - - - def mouseReleaseEvent( self, event ): - - if event.button() != QC.Qt.RightButton: - - QW.QScrollArea.mouseReleaseEvent( self, event ) - - return - - - self.ShowMenu() - - - def MoveMedia( self, medias: typing.List[ ClientMedia.Media ], insertion_index: int ): - - MediaPanel.MoveMedia( self, medias, insertion_index ) - - self._NotifyThumbnailsHaveMoved() - - self._ScrollToMedia( medias[0] ) - - - def NewThumbnails( self, hashes ): - - affected_thumbnails = self._GetMedia( hashes ) - - if len( affected_thumbnails ) > 0: - - self._RedrawMedia( affected_thumbnails ) - - - - def NotifyNewFileInfo( self, hashes ): - - def qt_do_update( hashes_to_media_results ): - - affected_media = self._GetMedia( set( hashes_to_media_results.keys() ) ) - - for media in affected_media: - - media.UpdateFileInfo( hashes_to_media_results ) - - - self._RedrawMedia( affected_media ) - - - def do_it( win, callable, affected_hashes ): - - media_results = CG.client_controller.Read( 'media_results', affected_hashes ) - - hashes_to_media_results = { media_result.GetHash() : media_result for media_result in media_results } - - CG.client_controller.CallAfterQtSafe( win, 'new file info notification', qt_do_update, hashes_to_media_results ) - - - affected_hashes = self._hashes.intersection( hashes ) - - CG.client_controller.CallToThread( do_it, self, do_it, affected_hashes ) - - - def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): - - command_processed = True - - if command.IsSimpleCommand(): - - action = command.GetSimpleAction() - - if action == CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS: - - ( move_direction, selection_status ) = command.GetSimpleData() - - shift = selection_status == CAC.SELECTION_STATUS_SHIFT - - if move_direction in ( CAC.MOVE_HOME, CAC.MOVE_END ): - - if move_direction == CAC.MOVE_HOME: - - self._ScrollHome( shift ) - - else: # MOVE_END - - self._ScrollEnd( shift ) - - - elif move_direction in ( CAC.MOVE_PAGE_UP, CAC.MOVE_PAGE_DOWN ): - - if move_direction == CAC.MOVE_PAGE_UP: - - direction = -1 - - else: # MOVE_PAGE_DOWN - - direction = 1 - - - self._MoveThumbnailFocus( self._num_rows_per_actual_page * direction, 0, shift ) - - else: - - if move_direction == CAC.MOVE_LEFT: - - rows = 0 - columns = -1 - - elif move_direction == CAC.MOVE_RIGHT: - - rows = 0 - columns = 1 - - elif move_direction == CAC.MOVE_UP: - - rows = -1 - columns = 0 - - elif move_direction == CAC.MOVE_DOWN: - - rows = 1 - columns = 0 - - else: - - raise NotImplementedError() - - - self._MoveThumbnailFocus( rows, columns, shift ) - - - elif action == CAC.SIMPLE_SELECT_FILES: - - file_filter = command.GetSimpleData() - - self._Select( file_filter ) - - else: - - command_processed = False - - - else: - - command_processed = False - - - if not command_processed: - - return MediaPanel.ProcessApplicationCommand( self, command ) - - else: - - return command_processed - - - - def RedrawAllThumbnails( self ): - - self._DirtyAllPages() - - for m in self._collected_media: - - m.RecalcInternals() - - - for thumbnail in self._sorted_media: - - thumbnail.ClearTagSummaryCaches() - - - self.widget().update() - - - def SetFocusedMedia( self, media ): - - MediaPanel.SetFocusedMedia( self, media ) - - if media is None: - - self._SetFocusedMedia( None ) - - else: - - try: - - my_media = self._GetMedia( media.GetHashes() )[0] - - self._HitMedia( my_media, False, False ) - - self._ScrollToMedia( self._focused_media ) - - except: - - pass - - - - - def showEvent( self, event ): - - self._UpdateScrollBars() - - - def ShowMenu( self, do_not_show_just_return = False ): - - flat_selected_medias = ClientMedia.FlattenMedia( self._selected_media ) - - all_locations_managers = [ media.GetLocationsManager() for media in ClientMedia.FlattenMedia( self._sorted_media ) ] - selected_locations_managers = [ media.GetLocationsManager() for media in flat_selected_medias ] - - selection_has_local_file_domain = True in ( locations_manager.IsLocal() and not locations_manager.IsTrashed() for locations_manager in selected_locations_managers ) - selection_has_trash = True in ( locations_manager.IsTrashed() for locations_manager in selected_locations_managers ) - selection_has_inbox = True in ( media.HasInbox() for media in self._selected_media ) - selection_has_archive = True in ( media.HasArchive() and media.GetLocationsManager().IsLocal() for media in self._selected_media ) - selection_has_deletion_record = True in ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY in locations_manager.GetDeleted() for locations_manager in selected_locations_managers ) - - all_file_domains = HydrusLists.MassUnion( locations_manager.GetCurrent() for locations_manager in all_locations_managers ) - all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } ) - - some_downloading = True in ( locations_manager.IsDownloading() for locations_manager in selected_locations_managers ) - - has_local = True in ( locations_manager.IsLocal() for locations_manager in all_locations_managers ) - has_remote = True in ( locations_manager.IsRemote() for locations_manager in all_locations_managers ) - - num_files = self.GetNumFiles() - num_selected = self._GetNumSelected() - num_inbox = self.GetNumInbox() - num_archive = self.GetNumArchive() - - any_selected = num_selected > 0 - multiple_selected = num_selected > 1 - - menu = ClientGUIMenus.GenerateMenu( self.window() ) - - # variables - - collections_selected = True in ( media.IsCollection() for media in self._selected_media ) - - services_manager = CG.client_controller.services_manager - - services = services_manager.GetServices() - - file_repositories = [ service for service in services if service.GetServiceType() == HC.FILE_REPOSITORY ] - - ipfs_services = [ service for service in services if service.GetServiceType() == HC.IPFS ] - - local_ratings_services = [ service for service in services if service.GetServiceType() in HC.RATINGS_SERVICES ] - - i_can_post_ratings = len( local_ratings_services ) > 0 - - local_media_file_service_keys = { service.GetServiceKey() for service in services if service.GetServiceType() == HC.LOCAL_FILE_DOMAIN } - - file_repository_service_keys = { repository.GetServiceKey() for repository in file_repositories } - upload_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_CREATE ) } - petition_resolve_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_MODERATE ) } - petition_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_FILES, HC.PERMISSION_ACTION_PETITION ) } - petition_resolve_permission_file_service_keys - user_manage_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_MODERATE ) } - ipfs_service_keys = { service.GetServiceKey() for service in ipfs_services } - - if multiple_selected: - - download_phrase = 'download all possible selected' - rescind_download_phrase = 'cancel downloads for all possible selected' - upload_phrase = 'upload all possible selected to' - rescind_upload_phrase = 'rescind pending selected uploads to' - petition_phrase = 'petition all possible selected for removal from' - rescind_petition_phrase = 'rescind selected petitions for' - remote_delete_phrase = 'delete all possible selected from' - modify_account_phrase = 'modify the accounts that uploaded selected to' - - pin_phrase = 'pin all to' - rescind_pin_phrase = 'rescind pin to' - unpin_phrase = 'unpin all from' - rescind_unpin_phrase = 'rescind unpin from' - - archive_phrase = 'archive selected' - inbox_phrase = 're-inbox selected' - local_delete_phrase = 'delete selected' - delete_physically_phrase = 'delete selected physically now' - undelete_phrase = 'undelete selected' - clear_deletion_phrase = 'clear deletion record for selected' - - else: - - download_phrase = 'download' - rescind_download_phrase = 'cancel download' - upload_phrase = 'upload to' - rescind_upload_phrase = 'rescind pending upload to' - petition_phrase = 'petition for removal from' - rescind_petition_phrase = 'rescind petition for' - remote_delete_phrase = 'delete from' - modify_account_phrase = 'modify the account that uploaded this to' - - pin_phrase = 'pin to' - rescind_pin_phrase = 'rescind pin to' - unpin_phrase = 'unpin from' - rescind_unpin_phrase = 'rescind unpin from' - - archive_phrase = 'archive' - inbox_phrase = 're-inbox' - local_delete_phrase = 'delete' - delete_physically_phrase = 'delete physically now' - undelete_phrase = 'undelete' - clear_deletion_phrase = 'clear deletion record' - - - # info about the files - - remote_service_keys = CG.client_controller.services_manager.GetRemoteFileServiceKeys() - - groups_of_current_remote_service_keys = [ locations_manager.GetCurrent().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] - groups_of_pending_remote_service_keys = [ locations_manager.GetPending().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] - groups_of_petitioned_remote_service_keys = [ locations_manager.GetPetitioned().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] - groups_of_deleted_remote_service_keys = [ locations_manager.GetDeleted().intersection( remote_service_keys ) for locations_manager in selected_locations_managers ] - - current_remote_service_keys = HydrusLists.MassUnion( groups_of_current_remote_service_keys ) - pending_remote_service_keys = HydrusLists.MassUnion( groups_of_pending_remote_service_keys ) - petitioned_remote_service_keys = HydrusLists.MassUnion( groups_of_petitioned_remote_service_keys ) - deleted_remote_service_keys = HydrusLists.MassUnion( groups_of_deleted_remote_service_keys ) - - common_current_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_current_remote_service_keys ) - common_pending_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_pending_remote_service_keys ) - common_petitioned_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_petitioned_remote_service_keys ) - common_deleted_remote_service_keys = HydrusLists.IntelligentMassIntersect( groups_of_deleted_remote_service_keys ) - - disparate_current_remote_service_keys = current_remote_service_keys - common_current_remote_service_keys - disparate_pending_remote_service_keys = pending_remote_service_keys - common_pending_remote_service_keys - disparate_petitioned_remote_service_keys = petitioned_remote_service_keys - common_petitioned_remote_service_keys - disparate_deleted_remote_service_keys = deleted_remote_service_keys - common_deleted_remote_service_keys - - pending_file_service_keys = pending_remote_service_keys.intersection( file_repository_service_keys ) - petitioned_file_service_keys = petitioned_remote_service_keys.intersection( file_repository_service_keys ) - - common_current_file_service_keys = common_current_remote_service_keys.intersection( file_repository_service_keys ) - common_pending_file_service_keys = common_pending_remote_service_keys.intersection( file_repository_service_keys ) - common_petitioned_file_service_keys = common_petitioned_remote_service_keys.intersection( file_repository_service_keys ) - common_deleted_file_service_keys = common_deleted_remote_service_keys.intersection( file_repository_service_keys ) - - disparate_current_file_service_keys = disparate_current_remote_service_keys.intersection( file_repository_service_keys ) - disparate_pending_file_service_keys = disparate_pending_remote_service_keys.intersection( file_repository_service_keys ) - disparate_petitioned_file_service_keys = disparate_petitioned_remote_service_keys.intersection( file_repository_service_keys ) - disparate_deleted_file_service_keys = disparate_deleted_remote_service_keys.intersection( file_repository_service_keys ) - - pending_ipfs_service_keys = pending_remote_service_keys.intersection( ipfs_service_keys ) - petitioned_ipfs_service_keys = petitioned_remote_service_keys.intersection( ipfs_service_keys ) - - common_current_ipfs_service_keys = common_current_remote_service_keys.intersection( ipfs_service_keys ) - common_pending_ipfs_service_keys = common_pending_file_service_keys.intersection( ipfs_service_keys ) - common_petitioned_ipfs_service_keys = common_petitioned_remote_service_keys.intersection( ipfs_service_keys ) - - disparate_current_ipfs_service_keys = disparate_current_remote_service_keys.intersection( ipfs_service_keys ) - disparate_pending_ipfs_service_keys = disparate_pending_remote_service_keys.intersection( ipfs_service_keys ) - disparate_petitioned_ipfs_service_keys = disparate_petitioned_remote_service_keys.intersection( ipfs_service_keys ) - - # valid commands for the files - - current_file_service_keys = set() - - uploadable_file_service_keys = set() - - downloadable_file_service_keys = set() - - petitionable_file_service_keys = set() - - deletable_file_service_keys = set() - - modifyable_file_service_keys = set() - - pinnable_ipfs_service_keys = set() - - unpinnable_ipfs_service_keys = set() - - remote_file_service_keys = ipfs_service_keys.union( file_repository_service_keys ) - - for locations_manager in selected_locations_managers: - - current = locations_manager.GetCurrent() - deleted = locations_manager.GetDeleted() - pending = locations_manager.GetPending() - petitioned = locations_manager.GetPetitioned() - - # ALL - - current_file_service_keys.update( current ) - - # FILE REPOS - - # we can upload (set pending) to a repo_id when we have permission, a file is local, not current, not pending, and either ( not deleted or we_can_overrule ) - - if locations_manager.IsLocal(): - - cannot_upload_to = current.union( pending ).union( deleted.difference( petition_resolve_permission_file_service_keys ) ) - - can_upload_to = upload_permission_file_service_keys.difference( cannot_upload_to ) - - uploadable_file_service_keys.update( can_upload_to ) - - - # we can download (set pending to local) when we have permission, a file is not local and not already downloading and current - - if not locations_manager.IsLocal() and not locations_manager.IsDownloading(): - - downloadable_file_service_keys.update( remote_file_service_keys.intersection( current ) ) - - - # we can petition when we have permission and a file is current and it is not already petitioned - - petitionable_file_service_keys.update( ( petition_permission_file_service_keys & current ) - petitioned ) - - # we can delete remote when we have permission and a file is current and it is not already petitioned - - deletable_file_service_keys.update( ( petition_resolve_permission_file_service_keys & current ) - petitioned ) - - # we can modify users when we have permission and the file is current or deleted - - modifyable_file_service_keys.update( user_manage_permission_file_service_keys & ( current | deleted ) ) - - # IPFS - - # we can pin if a file is local, not current, not pending - - if locations_manager.IsLocal(): - - pinnable_ipfs_service_keys.update( ipfs_service_keys - current - pending ) - - - # we can unpin a file if it is current and not petitioned - - unpinnable_ipfs_service_keys.update( ( ipfs_service_keys & current ) - petitioned ) - - - # do the actual menu - - selection_info_menu = ClientGUIMenus.GenerateMenu( menu ) - - selected_files_string = ClientMedia.GetMediasFiletypeSummaryString( self._selected_media ) - - selection_info_menu_label = f'{selected_files_string}, {self._GetPrettyTotalSize( only_selected = True )}' - - if multiple_selected: - - pretty_total_duration = self._GetPrettyTotalDuration( only_selected = True ) - - if pretty_total_duration != '': - - selection_info_menu_label += ', {}'.format( pretty_total_duration ) - - - else: - - # TODO: move away from this hell function GetPrettyInfoLines and set the timestamp tooltips to the be the full ISO time - - if self._HasFocusSingleton(): - - focus_singleton = self._GetFocusSingleton() - - pretty_info_lines = list( focus_singleton.GetPrettyInfoLines() ) - - ClientGUIMediaMenus.AddPrettyInfoLines( selection_info_menu, pretty_info_lines ) - - - - ClientGUIMenus.AppendSeparator( selection_info_menu ) - - ClientGUIMediaMenus.AddFileViewingStatsMenu( selection_info_menu, self._selected_media ) - - if len( disparate_current_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_current_file_service_keys, 'some uploaded to' ) - - - if multiple_selected and len( common_current_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_current_file_service_keys, 'selected uploaded to' ) - - - if len( disparate_pending_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_pending_file_service_keys, 'some pending to' ) - - - if len( common_pending_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_pending_file_service_keys, 'pending to' ) - - - if len( disparate_petitioned_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_petitioned_file_service_keys, 'some petitioned for removal from' ) - - - if len( common_petitioned_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_petitioned_file_service_keys, 'petitioned for removal from' ) - - - if len( disparate_deleted_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_deleted_file_service_keys, 'some deleted from' ) - - - if len( common_deleted_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_deleted_file_service_keys, 'deleted from' ) - - - if len( disparate_current_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_current_ipfs_service_keys, 'some pinned to' ) - - - if multiple_selected and len( common_current_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_current_ipfs_service_keys, 'selected pinned to' ) - - - if len( disparate_pending_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_pending_ipfs_service_keys, 'some to be pinned to' ) - - - if len( common_pending_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_pending_ipfs_service_keys, 'to be pinned to' ) - - - if len( disparate_petitioned_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, disparate_petitioned_ipfs_service_keys, 'some to be unpinned from' ) - - - if len( common_petitioned_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeyLabelsToMenu( selection_info_menu, common_petitioned_ipfs_service_keys, unpin_phrase ) - - - if any_selected: - - if len( selection_info_menu.actions() ) == 0: - - selection_info_menu.deleteLater() - - ClientGUIMenus.AppendMenuLabel( menu, selection_info_menu_label ) - - else: - - ClientGUIMenus.AppendMenu( menu, selection_info_menu, selection_info_menu_label ) - - - ClientGUIMenus.AppendSeparator( menu ) - - - ClientGUIMenus.AppendMenuItem( menu, 'refresh', 'Refresh the current search.', self.refreshQuery.emit ) - - if len( self._sorted_media ) > 0: - - ClientGUIMenus.AppendSeparator( menu ) - - filter_counts = {} - - filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) ] = num_files - filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) ] = num_inbox - filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) ] = num_archive - filter_counts[ ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) ] = num_selected - - has_local_and_remote = has_local and has_remote - - AddSelectMenu( self, menu, filter_counts, all_specific_file_domains, has_local_and_remote ) - AddRemoveMenu( self, menu, filter_counts, all_specific_file_domains, has_local_and_remote ) - - if len( self._selected_media ) > 0: - - ordered_selected_media = self._GetSelectedMediaOrdered() - - try: - - earliest_index = self._sorted_media.index( ordered_selected_media[0] ) - - selection_is_contiguous = any_selected and self._sorted_media.index( ordered_selected_media[-1] ) - earliest_index == num_selected - 1 - - AddMoveMenu( self, menu, self._selected_media, self._sorted_media, self._focused_media, selection_is_contiguous, earliest_index ) - - except HydrusExceptions.DataMissing: - - pass - - - - ClientGUIMenus.AppendSeparator( menu ) - - if has_local: - - ClientGUIMenus.AppendMenuItem( menu, 'archive/delete filter', 'Launch a special media viewer that will quickly archive or delete the selected media. Check the help if you are unfamiliar with this mode!', self._ArchiveDeleteFilter ) - - - - if selection_has_inbox: - - ClientGUIMenus.AppendMenuItem( menu, archive_phrase, 'Archive the selected files.', self._Archive ) - - - if selection_has_archive: - - ClientGUIMenus.AppendMenuItem( menu, inbox_phrase, 'Put the selected files back in the inbox.', self._Inbox ) - - - ClientGUIMenus.AppendSeparator( menu ) - - user_command_deletable_file_service_keys = local_media_file_service_keys.union( [ CC.LOCAL_UPDATE_SERVICE_KEY ] ) - - local_file_service_keys_we_are_in = sorted( current_file_service_keys.intersection( user_command_deletable_file_service_keys ), key = CG.client_controller.services_manager.GetName ) - - if len( local_file_service_keys_we_are_in ) > 0: - - delete_menu = ClientGUIMenus.GenerateMenu( menu ) - - for file_service_key in local_file_service_keys_we_are_in: - - service_name = CG.client_controller.services_manager.GetName( file_service_key ) - - ClientGUIMenus.AppendMenuItem( delete_menu, f'from {service_name}', f'Delete the selected files from {service_name}.', self._Delete, file_service_key ) - - - ClientGUIMenus.AppendMenu( menu, delete_menu, local_delete_phrase ) - - - if selection_has_trash: - - if selection_has_local_file_domain: - - ClientGUIMenus.AppendMenuItem( menu, 'delete trash physically now', 'Completely delete the selected trashed files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY, only_those_in_file_service_key = CC.TRASH_SERVICE_KEY ) - - - ClientGUIMenus.AppendMenuItem( menu, delete_physically_phrase, 'Completely delete the selected files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) - ClientGUIMenus.AppendMenuItem( menu, undelete_phrase, 'Restore the selected files back to \'my files\'.', self._Undelete ) - - - if selection_has_deletion_record: - - ClientGUIMenus.AppendMenuItem( menu, clear_deletion_phrase, 'Clear the deletion record for these files, allowing them to reimport even if previously deleted files are set to be discarded.', self._ClearDeleteRecord ) - - - ClientGUIMenus.AppendSeparator( menu ) - - if any_selected: - - manage_menu = ClientGUIMenus.GenerateMenu( menu ) - - ClientGUIMenus.AppendMenuItem( manage_menu, 'tags', 'Manage tags for the selected files.', self._ManageTags ) - - if i_can_post_ratings: - - ClientGUIMenus.AppendMenuItem( manage_menu, 'ratings', 'Manage ratings for the selected files.', self._ManageRatings ) - - - num_notes = 0 - - if self._HasFocusSingleton(): - - focus_singleton = self._GetFocusSingleton() - - num_notes = focus_singleton.GetNotesManager().GetNumNotes() - - - notes_str = 'notes' - - if num_notes > 0: - - notes_str = '{} ({})'.format( notes_str, HydrusNumbers.ToHumanInt( num_notes ) ) - - - ClientGUIMenus.AppendMenuItem( manage_menu, notes_str, 'Manage notes for the focused file.', self._ManageNotes ) - - ClientGUIMenus.AppendMenuItem( manage_menu, 'times', 'Edit the timestamps for your files.', self._ManageTimestamps ) - ClientGUIMenus.AppendMenuItem( manage_menu, 'force filetype', 'Force your files to appear as a different filetype.', ClientGUIMediaModalActions.SetFilesForcedFiletypes, self, self._selected_media ) - - if self._HasFocusSingleton(): - - focus_singleton = self._GetFocusSingleton() - - ClientGUIMediaMenus.AddDuplicatesMenu( self, manage_menu, self._location_context, focus_singleton, num_selected, collections_selected ) - - - regen_menu = ClientGUIMenus.GenerateMenu( manage_menu ) - - for job_type in ClientFiles.ALL_REGEN_JOBS_IN_HUMAN_ORDER: - - ClientGUIMenus.AppendMenuItem( regen_menu, ClientFiles.regen_file_enum_to_str_lookup[ job_type ], ClientFiles.regen_file_enum_to_description_lookup[ job_type ], self._RegenerateFileData, job_type ) - - - ClientGUIMenus.AppendMenu( manage_menu, regen_menu, 'maintenance' ) - - ClientGUIMediaMenus.AddManageFileViewingStatsMenu( self, manage_menu, flat_selected_medias ) - - ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' ) - - ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaSimpleActions.GetLocalFileActionServiceKeys( flat_selected_medias ) - - len_interesting_local_service_keys = 0 - - len_interesting_local_service_keys += len( local_duplicable_to_file_service_keys ) - len_interesting_local_service_keys += len( local_moveable_from_and_to_file_service_keys ) - - # - - len_interesting_remote_service_keys = 0 - - len_interesting_remote_service_keys += len( downloadable_file_service_keys ) - len_interesting_remote_service_keys += len( uploadable_file_service_keys ) - len_interesting_remote_service_keys += len( pending_file_service_keys ) - len_interesting_remote_service_keys += len( petitionable_file_service_keys ) - len_interesting_remote_service_keys += len( petitioned_file_service_keys ) - len_interesting_remote_service_keys += len( deletable_file_service_keys ) - len_interesting_remote_service_keys += len( modifyable_file_service_keys ) - len_interesting_remote_service_keys += len( pinnable_ipfs_service_keys ) - len_interesting_remote_service_keys += len( pending_ipfs_service_keys ) - len_interesting_remote_service_keys += len( unpinnable_ipfs_service_keys ) - len_interesting_remote_service_keys += len( petitioned_ipfs_service_keys ) - - if multiple_selected: - - len_interesting_remote_service_keys += len( ipfs_service_keys ) - - - if len_interesting_local_service_keys > 0 or len_interesting_remote_service_keys > 0: - - files_menu = ClientGUIMenus.GenerateMenu( menu ) - - ClientGUIMenus.AppendMenu( menu, files_menu, 'files' ) - - if len_interesting_local_service_keys > 0: - - ClientGUIMediaMenus.AddLocalFilesMoveAddToMenu( self, files_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand ) - - - if len_interesting_remote_service_keys > 0: - - ClientGUIMenus.AppendSeparator( files_menu ) - - if len( downloadable_file_service_keys ) > 0: - - ClientGUIMenus.AppendMenuItem( files_menu, download_phrase, 'Download all possible selected files.', self._DownloadSelected ) - - - if some_downloading: - - ClientGUIMenus.AppendMenuItem( files_menu, rescind_download_phrase, 'Stop downloading any of the selected files.', self._RescindDownloadSelected ) - - - if len( uploadable_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles ) - - - if len( pending_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles ) - - - if len( petitionable_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles ) - - - if len( petitioned_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles ) - - - if len( deletable_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete ) - - - if len( modifyable_file_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders ) - - - if len( pinnable_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles ) - - - if len( pending_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles ) - - - if len( unpinnable_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles ) - - - if len( petitioned_ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles ) - - - if multiple_selected and len( ipfs_service_keys ) > 0: - - ClientGUIMediaMenus.AddServiceKeysToMenu( files_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory ) - - - - - # - - ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._focused_media, num_selected, selected_media = self._selected_media ) - - ClientGUIMediaMenus.AddOpenMenu( self, menu, self._focused_media, self._selected_media ) - - ClientGUIMediaMenus.AddShareMenu( self, menu, self._focused_media, self._selected_media ) - - - if not do_not_show_just_return: - - CGC.core().PopupMenu( self, menu ) - - - else: - - return menu - - - - def Sort( self, media_sort = None ): - - MediaPanel.Sort( self, media_sort ) - - self._NotifyThumbnailsHaveMoved() - - - def ThumbnailsReset( self ): - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - thumbnail_scroll_rate = float( CG.client_controller.new_options.GetString( 'thumbnail_scroll_rate' ) ) - - self.verticalScrollBar().setSingleStep( int( round( thumbnail_span_height * thumbnail_scroll_rate ) ) ) - - self._hashes_to_thumbnails_waiting_to_be_drawn = {} - self._hashes_faded = set() - - self._ReinitialisePageCacheIfNeeded() - - self._RecalculateVirtualSize() - - self.RedrawAllThumbnails() - - - def TIMERAnimationUpdate( self ): - - loop_should_break_time = HydrusTime.GetNowPrecise() + ( FRAME_DURATION_60FPS / 2 ) - - ( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions() - - thumbnail_margin = CG.client_controller.new_options.GetInteger( 'thumbnail_margin' ) - - hashes = list( self._hashes_to_thumbnails_waiting_to_be_drawn.keys() ) - - page_indices_to_painters = {} - - page_height = self._num_rows_per_canvas_page * thumbnail_span_height - - for hash in HydrusData.IterateListRandomlyAndFast( hashes ): - - thumbnail_draw_object = self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] - - delete_entry = False - - if thumbnail_draw_object.DrawDue(): - - thumbnail_index = thumbnail_draw_object.thumbnail_index - - try: - - expected_thumbnail = self._sorted_media[ thumbnail_index ] - - except: - - expected_thumbnail = None - - - page_index = self._GetPageIndexFromThumbnailIndex( thumbnail_index ) - - if expected_thumbnail != thumbnail_draw_object.thumbnail: - - delete_entry = True - - elif page_index not in self._clean_canvas_pages: - - delete_entry = True - - else: - - thumbnail_col = thumbnail_index % self._num_columns - - thumbnail_row = thumbnail_index // self._num_columns - - x = thumbnail_col * thumbnail_span_width + thumbnail_margin - - y = ( thumbnail_row - ( page_index * self._num_rows_per_canvas_page ) ) * thumbnail_span_height + thumbnail_margin - - if page_index not in page_indices_to_painters: - - canvas_page = self._clean_canvas_pages[ page_index ] - - painter = QG.QPainter( canvas_page ) - - page_indices_to_painters[ page_index ] = painter - - - painter = page_indices_to_painters[ page_index ] - - thumbnail_draw_object.DrawToPainter( x, y, painter ) - - # - - page_virtual_y = page_height * page_index - - self.widget().update( QC.QRect( x, page_virtual_y + y, thumbnail_span_width - thumbnail_margin, thumbnail_span_height - thumbnail_margin ) ) - - - - if thumbnail_draw_object.DrawComplete() or delete_entry: - - del self._hashes_to_thumbnails_waiting_to_be_drawn[ hash ] - - - if HydrusTime.TimeHasPassedPrecise( loop_should_break_time ): - - break - - - - if len( self._hashes_to_thumbnails_waiting_to_be_drawn ) == 0: - - CG.client_controller.gui.UnregisterAnimationUpdateWindow( self ) - - - - - def WaterfallThumbnails( self, page_key, thumbnails ): - - if self._page_key == page_key: - - self._FadeThumbnails( thumbnails ) - - - - class _InnerWidget( QW.QWidget ): - - def __init__( self, parent ): - - super().__init__( parent ) - - self._parent = parent - - - def mousePressEvent( self, event ): - - self._parent._drag_init_coordinates = QG.QCursor.pos() - self._parent._drag_click_timestamp_ms = HydrusTime.GetNowMS() - - thumb = self._parent._GetThumbnailUnderMouse( event ) - - right_on_whitespace = event.button() == QC.Qt.RightButton and thumb is None - - if not right_on_whitespace: - - self._parent._HitMedia( thumb, event.modifiers() & QC.Qt.ControlModifier, event.modifiers() & QC.Qt.ShiftModifier ) - - - # this specifically does not scroll to media, as for clicking (esp. double-clicking attempts), the scroll can be jarring - - - def paintEvent( self, event ): - - if self._parent.devicePixelRatio() != self._parent._last_device_pixel_ratio: - - self._parent._last_device_pixel_ratio = self._parent.devicePixelRatio() - - self._parent._DirtyAllPages() - self._parent._DeleteAllDirtyPages() - - - painter = QG.QPainter( self ) - - ( thumbnail_span_width, thumbnail_span_height ) = self._parent._GetThumbnailSpanDimensions() - - page_height = self._parent._num_rows_per_canvas_page * thumbnail_span_height - - page_indices_to_display = self._parent._CalculateVisiblePageIndices() - - earliest_page_index_to_display = min( page_indices_to_display ) - last_page_index_to_display = max( page_indices_to_display ) - - page_indices_to_draw = list( page_indices_to_display ) - - if earliest_page_index_to_display > 0: - - page_indices_to_draw.append( earliest_page_index_to_display - 1 ) - - - page_indices_to_draw.append( last_page_index_to_display + 1 ) - - page_indices_to_draw.sort() - - potential_clean_indices_to_steal = [ page_index for page_index in self._parent._clean_canvas_pages.keys() if page_index not in page_indices_to_draw ] - - random.shuffle( potential_clean_indices_to_steal ) - - y_start = self._parent._GetYStart() - - bg_colour = self._parent.GetColour( CC.COLOUR_THUMBGRID_BACKGROUND ) - - painter.setBackground( QG.QBrush( bg_colour ) ) - - painter.eraseRect( painter.viewport() ) - - background_pixmap = CG.client_controller.bitmap_manager.GetMediaBackgroundPixmap() - - if background_pixmap is not None: - - my_size = QP.ScrollAreaVisibleRect( self._parent ).size() - - pixmap_size = background_pixmap.size() - - painter.drawPixmap( my_size.width() - pixmap_size.width(), my_size.height() - pixmap_size.height(), background_pixmap ) - - - for page_index in page_indices_to_draw: - - if page_index not in self._parent._clean_canvas_pages: - - if len( self._parent._dirty_canvas_pages ) == 0: - - if len( potential_clean_indices_to_steal ) > 0: - - index_to_steal = potential_clean_indices_to_steal.pop() - - self._parent._DirtyPage( index_to_steal ) - - else: - - self._parent._CreateNewDirtyPage() - - - - canvas_page = self._parent._dirty_canvas_pages.pop() - - self._parent._DrawCanvasPage( page_index, canvas_page ) - - self._parent._clean_canvas_pages[ page_index ] = canvas_page - - - if page_index in page_indices_to_display: - - canvas_page = self._parent._clean_canvas_pages[ page_index ] - - page_virtual_y = page_height * page_index - - painter.drawImage( 0, page_virtual_y, canvas_page ) - - - - - - -def AddMoveMenu( win: MediaPanel, menu: QW.QMenu, selected_media: typing.Set[ ClientMedia.Media ], sorted_media: ClientMedia.SortedList, focused_media: typing.Optional[ ClientMedia.Media ], selection_is_contiguous: bool, earliest_index: int ): - - if len( selected_media ) == 0 or len( selected_media ) == len( sorted_media ): - - return - - - move_menu = ClientGUIMenus.GenerateMenu( menu ) - - if earliest_index > 0: - - ClientGUIMenus.AppendMenuItem( - move_menu, - 'to start', - 'Move the selected thumbnails to the start of the media list.', - win.ProcessApplicationCommand, - CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_HOME ) ) - ) - - ClientGUIMenus.AppendMenuItem( - move_menu, - 'back one', - 'Move the selected thumbnails back one position.', - win.ProcessApplicationCommand, - CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_LEFT ) ) - ) - - - if focused_media is not None: - - try: - - focused_index = sorted_media.index( focused_media ) - - if focused_index != earliest_index or not selection_is_contiguous: - - ClientGUIMenus.AppendMenuItem( - move_menu, - 'to here', - 'Move the selected thumbnails to the focused position (most likely the one you clicked on).', - win.ProcessApplicationCommand, - CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_TO_FOCUS ) ) - ) - - - except HydrusExceptions.DataMissing: - - pass - - - - if earliest_index + len( selected_media ) < len( sorted_media ): - - ClientGUIMenus.AppendMenuItem( - move_menu, - 'forward one', - 'Move the selected thumbnails forward one position.', - win.ProcessApplicationCommand, - CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_RIGHT ) ) - ) - - ClientGUIMenus.AppendMenuItem( - move_menu, - 'to end', - 'Move the selected thumbnails to the end of the media list.', - win.ProcessApplicationCommand, - CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_REARRANGE_THUMBNAILS, ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, CAC.MOVE_END ) ) - ) - - - ClientGUIMenus.AppendMenu( menu, move_menu, 'move' ) - - -def AddRemoveMenu( win: MediaPanel, menu: QW.QMenu, filter_counts, all_specific_file_domains, has_local_and_remote ): - - file_filter_all = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) - - if file_filter_all.GetCount( win, filter_counts ) > 0: - - remove_menu = ClientGUIMenus.GenerateMenu( menu ) - - # - - file_filter_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) - - file_filter_inbox = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) - - file_filter_archive = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) - - file_filter_not_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NOT_SELECTED ) - - # - - selected_count = file_filter_selected.GetCount( win, filter_counts ) - - if 0 < selected_count < file_filter_all.GetCount( win, filter_counts ): - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_selected.ToStringWithCount( win, filter_counts ), 'Remove all the selected files from the current view.', win._Remove, file_filter_selected ) - - - if file_filter_all.GetCount( win, filter_counts ) > 0: - - ClientGUIMenus.AppendSeparator( remove_menu ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_all.ToStringWithCount( win, filter_counts ), 'Remove all the files from the current view.', win._Remove, file_filter_all ) - - - if file_filter_inbox.GetCount( win, filter_counts ) > 0 and file_filter_archive.GetCount( win, filter_counts ) > 0: - - ClientGUIMenus.AppendSeparator( remove_menu ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_inbox.ToStringWithCount( win, filter_counts ), 'Remove all the inbox files from the current view.', win._Remove, file_filter_inbox ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_archive.ToStringWithCount( win, filter_counts ), 'Remove all the archived files from the current view.', win._Remove, file_filter_archive ) - - - if len( all_specific_file_domains ) > 1: - - ClientGUIMenus.AppendSeparator( remove_menu ) - - all_specific_file_domains = ClientLocation.SortFileServiceKeysNicely( all_specific_file_domains ) - - all_specific_file_domains = ClientLocation.FilterOutRedundantMetaServices( all_specific_file_domains ) - - for file_service_key in all_specific_file_domains: - - file_filter = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_FILE_SERVICE, file_service_key ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter.ToStringWithCount( win, filter_counts ), 'Remove all the files that are in this file domain.', win._Remove, file_filter ) - - - - if has_local_and_remote: - - file_filter_local = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_LOCAL ) - file_filter_remote = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_REMOTE ) - - ClientGUIMenus.AppendSeparator( remove_menu ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_local.ToStringWithCount( win, filter_counts ), 'Remove all the files that are in this client.', win._Remove, file_filter_local ) - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_remote.ToStringWithCount( win, filter_counts ), 'Remove all the files that are not in this client.', win._Remove, file_filter_remote ) - - - not_selected_count = file_filter_not_selected.GetCount( win, filter_counts ) - - if not_selected_count > 0 and selected_count > 0: - - ClientGUIMenus.AppendSeparator( remove_menu ) - - ClientGUIMenus.AppendMenuItem( remove_menu, file_filter_not_selected.ToStringWithCount( win, filter_counts ), 'Remove all the not selected files from the current view.', win._Remove, file_filter_not_selected ) - - - ClientGUIMenus.AppendMenu( menu, remove_menu, 'remove' ) - - - -def AddSelectMenu( win: MediaPanel, menu, filter_counts, all_specific_file_domains, has_local_and_remote ): - - file_filter_all = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ALL ) - - if file_filter_all.GetCount( win, filter_counts ) > 0: - - select_menu = ClientGUIMenus.GenerateMenu( menu ) - - # - - file_filter_inbox = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_INBOX ) - - file_filter_archive = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_ARCHIVE ) - - file_filter_not_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NOT_SELECTED ) - - file_filter_none = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_NONE ) - - # - - if file_filter_all.GetCount( win, filter_counts ) > 0: - - ClientGUIMenus.AppendSeparator( select_menu ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_all.ToStringWithCount( win, filter_counts ), 'Select all the files in the current view.', win._Select, file_filter_all ) - - - if file_filter_inbox.GetCount( win, filter_counts ) > 0 and file_filter_archive.GetCount( win, filter_counts ) > 0: - - ClientGUIMenus.AppendSeparator( select_menu ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_inbox.ToStringWithCount( win, filter_counts ), 'Select all the inbox files in the current view.', win._Select, file_filter_inbox ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_archive.ToStringWithCount( win, filter_counts ), 'Select all the archived files in the current view.', win._Select, file_filter_archive ) - - - if len( all_specific_file_domains ) > 1: - - ClientGUIMenus.AppendSeparator( select_menu ) - - all_specific_file_domains = ClientLocation.SortFileServiceKeysNicely( all_specific_file_domains ) - - all_specific_file_domains = ClientLocation.FilterOutRedundantMetaServices( all_specific_file_domains ) - - for file_service_key in all_specific_file_domains: - - file_filter = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_FILE_SERVICE, file_service_key ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter.ToStringWithCount( win, filter_counts ), 'Select all the files in this file domain.', win._Select, file_filter ) - - - - if has_local_and_remote: - - file_filter_local = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_LOCAL ) - file_filter_remote = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_REMOTE ) - - ClientGUIMenus.AppendSeparator( select_menu ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_local.ToStringWithCount( win, filter_counts ), 'Select all the files that are in this client.', win._Select, file_filter_local ) - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_remote.ToStringWithCount( win, filter_counts ), 'Select all the files that are not in this client.', win._Select, file_filter_remote ) - - - file_filter_selected = ClientMediaFileFilter.FileFilter( ClientMediaFileFilter.FILE_FILTER_SELECTED ) - selected_count = file_filter_selected.GetCount( win, filter_counts ) - - not_selected_count = file_filter_not_selected.GetCount( win, filter_counts ) - - if selected_count > 0: - - if not_selected_count > 0: - - ClientGUIMenus.AppendSeparator( select_menu ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_not_selected.ToStringWithCount( win, filter_counts ), 'Swap what is and is not selected.', win._Select, file_filter_not_selected ) - - - ClientGUIMenus.AppendSeparator( select_menu ) - - ClientGUIMenus.AppendMenuItem( select_menu, file_filter_none.ToStringWithCount( win, filter_counts ), 'Deselect everything selected.', win._Select, file_filter_none ) - - - ClientGUIMenus.AppendMenu( menu, select_menu, 'select' ) - - -class Selectable( object ): - - def __init__( self, *args, **kwargs ): - - self._selected = False - - super().__init__( *args, **kwargs ) - - - def Deselect( self ): self._selected = False - - def IsSelected( self ): return self._selected - - def Select( self ): self._selected = True - -class Thumbnail( Selectable ): - - def __init__( self, *args, **kwargs ): - - super().__init__( *args, **kwargs ) - - self._last_tags = None - - self._last_upper_summary = None - self._last_lower_summary = None - - - def ClearTagSummaryCaches( self ): - - self._last_tags = None - - self._last_upper_summary = None - self._last_lower_summary = None - - - def GetQtImage( self, media_panel: MediaPanel, device_pixel_ratio ) -> QG.QImage: - - # we probably don't really want to say DPR as a param here, but instead ask for a qt_image in a certain resolution? - # or just give the qt_image to be drawn to? - # or just give a painter and a rect and draw to that or something - # we don't really want to mess around with DPR here, we just want to draw thumbs - # that said, this works after a medium-high headache getting it there, so let's not get ahead of ourselves - - thumbnail_hydrus_bmp = CG.client_controller.GetCache( 'thumbnail' ).GetThumbnail( self ) - - thumbnail_border = CG.client_controller.new_options.GetInteger( 'thumbnail_border' ) - - ( width, height ) = ClientData.AddPaddingToDimensions( HC.options[ 'thumbnail_dimensions' ], thumbnail_border * 2 ) - - qt_image_width = int( width * device_pixel_ratio ) - - qt_image_height = int( height * device_pixel_ratio ) - - qt_image = CG.client_controller.bitmap_manager.GetQtImage( qt_image_width, qt_image_height, 24 ) - - qt_image.setDevicePixelRatio( device_pixel_ratio ) - - inbox = self.HasInbox() - - local = self.GetLocationsManager().IsLocal() - - # - # BAD FONT QUALITY AT 100% UI Scale (semi fixed now, look at the bottom) - # - # Ok I have spent hours on this now trying to figure it out and can't, so I'll just write about it for when I come back - # So, if you boot with two monitors at 100% UI scale, the text here on a QImage is ugly, but on QWidget it is fine - # If you boot with one monitor at 125%, the text is beautiful on QImage both screens - # My current assumption is booting Qt with unusual UI scales triggers some extra init and that spills over to QImage QPainter initialisation - # - # I checked painter hints, font stuff, fontinfo and fontmetrics, and the only difference was with fontmetrics, on all-100% vs one >100%: - # minLeftBearing: -1, -7 - # minRightBearing: -1, -8 - # xHeight: 3, 6 - # - # The fontmetric produced a text size one pixel less wide on the both-100% run, so it is calculating different - # However these differences are global to the program so don't explain why painting on a QImage specifically has bad font rather than QWidget - # The ugly font is anti-aliased, but it looks like not drawn with sub-pixel calculations, like ClearType isn't kicking in or something - # If I blow the font size up to 72, there is still a difference in screenshots between the all-100% and some >100% boot. - # So, maybe if the program boots with any weird UI scale going on, Qt kicks in a different renderer for all QImages, the same renderer for QWidgets, perhaps more expensively - # Or this is just some weird bug - # Or I am still missing some flag - # - # bit like this https://stackoverflow.com/questions/31043332/qt-antialiasing-of-vertical-text-rendered-using-qpainter - # - # EDIT: OK, I 'fixed' it with setStyleStrategy( preferantialias ), which has no change in 125%, but in all-100% it draws something different but overall better quality - # Note you can't setStyleStrategy on the font when it is in the QPainter. either it gets set read only or there is some other voodoo going on - # It does look very slightly weird, but it is a step up so I won't complain. it really seems like the isolated QPainter of only-100% world has some different initialisation. it just can't find the nice font renderer - # - # 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! - # - # UPDATE 3a: Qt 6.4.x did not magically fix it. It draws much nicer, but still a different font weight/metrics compared to media viewer background, say. - # The PreferAntialias flag on 6.4.x seems to draw very very close to our ideal, so let's be happy with it for now. - - painter = QG.QPainter( qt_image ) - - painter.setRenderHint( QG.QPainter.TextAntialiasing, True ) # is true already in tests, is supposed to be 'the way' to fix the ugly text issue - painter.setRenderHint( QG.QPainter.Antialiasing, True ) # seems to do nothing, it only affects primitives? - painter.setRenderHint( QG.QPainter.SmoothPixmapTransform, True ) # makes the thumb QImage scale up and down prettily when we need it, either because it is too small or DPR gubbins - - new_options = CG.client_controller.new_options - - if not local: - - if self._selected: - - background_colour_type = CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED - - else: - - background_colour_type = CC.COLOUR_THUMB_BACKGROUND_REMOTE - - - else: - - if self._selected: - - background_colour_type = CC.COLOUR_THUMB_BACKGROUND_SELECTED - - else: - - background_colour_type = CC.COLOUR_THUMB_BACKGROUND - - - - # the painter isn't getting QSS style from the qt_image, we need to set the font explitly to get font size changes from QSS etc.. - - f = QG.QFont( CG.client_controller.gui.font() ) - - # this line magically fixes the bad text, as above - f.setStyleStrategy( QG.QFont.PreferAntialias ) - - painter.setFont( f ) - - bg_color = media_panel.GetColour( background_colour_type ) - - painter.fillRect( thumbnail_border, thumbnail_border, width - ( thumbnail_border * 2 ), height - ( thumbnail_border * 2 ), bg_color ) - - raw_thumbnail_qt_image = thumbnail_hydrus_bmp.GetQtImage() - - thumbnail_dpr_percent = CG.client_controller.new_options.GetInteger( 'thumbnail_dpr_percent' ) - - if thumbnail_dpr_percent != 100: - - thumbnail_dpr = thumbnail_dpr_percent / 100 - - raw_thumbnail_qt_image.setDevicePixelRatio( thumbnail_dpr ) - - # qt_image.deviceIndepedentSize isn't supported in Qt5 lmao - device_independent_thumb_size = raw_thumbnail_qt_image.size() / thumbnail_dpr - - else: - - device_independent_thumb_size = raw_thumbnail_qt_image.size() - - - x_offset = ( width - device_independent_thumb_size.width() ) // 2 - - y_offset = ( height - device_independent_thumb_size.height() ) // 2 - - painter.drawImage( x_offset, y_offset, raw_thumbnail_qt_image ) - - TEXT_BORDER = 1 - - new_options = CG.client_controller.new_options - - tags = self.GetTagsManager().GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA ) - - if len( tags ) > 0: - - upper_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_top' ) - lower_tag_summary_generator = new_options.GetTagSummaryGenerator( 'thumbnail_bottom_right' ) - - if self._last_tags is not None and self._last_tags == tags: - - upper_summary = self._last_upper_summary - lower_summary = self._last_lower_summary - - else: - - upper_summary = upper_tag_summary_generator.GenerateSummary( tags ) - - lower_summary = lower_tag_summary_generator.GenerateSummary( tags ) - - self._last_tags = set( tags ) - - self._last_upper_summary = upper_summary - self._last_lower_summary = lower_summary - - - if len( upper_summary ) > 0 or len( lower_summary ) > 0: - - if len( upper_summary ) > 0: - - text_colour_with_alpha = upper_tag_summary_generator.GetTextColour() - - background_colour_with_alpha = upper_tag_summary_generator.GetBackgroundColour() - - ( text_size, upper_summary ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, upper_summary ) - - box_x = thumbnail_border - box_y = thumbnail_border - box_width = width - ( thumbnail_border * 2 ) - box_height = text_size.height() + 2 - - painter.fillRect( box_x, box_y, box_width, box_height, background_colour_with_alpha ) - - text_x = ( width - text_size.width() ) // 2 - text_y = box_y + TEXT_BORDER - - painter.setPen( QG.QPen( text_colour_with_alpha ) ) - - ClientGUIFunctions.DrawText( painter, text_x, text_y, upper_summary ) - - - if len( lower_summary ) > 0: - - text_colour_with_alpha = lower_tag_summary_generator.GetTextColour() - - background_colour_with_alpha = lower_tag_summary_generator.GetBackgroundColour() - - ( text_size, lower_summary ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, lower_summary ) - - text_width = text_size.width() - text_height = text_size.height() - - box_width = text_width + ( TEXT_BORDER * 2 ) - box_height = text_height + ( TEXT_BORDER * 2 ) - box_x = width - box_width - thumbnail_border - box_y = height - text_height - thumbnail_border - - painter.fillRect( box_x, box_y, box_width, box_height, background_colour_with_alpha ) - - text_x = box_x + TEXT_BORDER - text_y = box_y + TEXT_BORDER - - painter.setPen( QG.QPen( text_colour_with_alpha ) ) - - ClientGUIFunctions.DrawText( painter, text_x, text_y, lower_summary ) - - - - - if thumbnail_border > 0: - - if not local: - - if self._selected: - - border_colour_type = CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED - - else: - - border_colour_type = CC.COLOUR_THUMB_BORDER_REMOTE - - - else: - - if self._selected: - - border_colour_type = CC.COLOUR_THUMB_BORDER_SELECTED - - else: - - border_colour_type = CC.COLOUR_THUMB_BORDER - - - - # I had a hell of a time getting a transparent box to draw right with a pen border without crazy +1px in the params for reasons I did not understand - # so I just decided four rects is neater and fine and actually prob faster in some cases - - # _____ ______ _____ ______ ________________ - # ___________(_)___ _________ /_______ _______ ______ __ /______ ___ /_________ /__ /__ / - # ___ __ \_ /__ |/_/ _ \_ /__ ___/ __ __ `/ __ \ _ __/ __ \ __ __ \ _ \_ /__ /__ / - # __ /_/ / / __> < / __/ / _(__ ) _ /_/ // /_/ / / /_ / /_/ / _ / / / __/ / _ / /_/ - # _ .___//_/ /_/|_| \___//_/ /____/ _\__, / \____/ \__/ \____/ /_/ /_/\___//_/ /_/ (_) - # /_/ /____/ - - bd_colour = media_panel.GetColour( border_colour_type ) - - painter.setBrush( QG.QBrush( bd_colour ) ) - painter.setPen( QG.QPen( QC.Qt.NoPen ) ) - - rectangles = [] - - side_height = height - ( thumbnail_border * 2 ) - rectangles.append( QC.QRectF( 0, 0, width, thumbnail_border ) ) # top - rectangles.append( QC.QRectF( 0, height - thumbnail_border, width, thumbnail_border ) ) # bottom - rectangles.append( QC.QRectF( 0, thumbnail_border, thumbnail_border, side_height ) ) # left - rectangles.append( QC.QRectF( width - thumbnail_border, thumbnail_border, thumbnail_border, side_height ) ) # right - - painter.drawRects( rectangles ) - - - ICON_MARGIN = 1 - - locations_manager = self.GetLocationsManager() - - icons_to_draw = [] - - if locations_manager.IsDownloading(): - - icons_to_draw.append( CC.global_pixmaps().downloading ) - - - if self.HasNotes(): - - icons_to_draw.append( CC.global_pixmaps().notes ) - - - if locations_manager.IsTrashed() or CC.COMBINED_LOCAL_FILE_SERVICE_KEY in locations_manager.GetDeleted(): - - icons_to_draw.append( CC.global_pixmaps().trash ) - - - if inbox: - - icons_to_draw.append( CC.global_pixmaps().inbox ) - - - if len( icons_to_draw ) > 0: - - icon_x = - ( thumbnail_border + ICON_MARGIN ) - - for icon in icons_to_draw: - - icon_x -= icon.width() - - painter.drawPixmap( width + icon_x, thumbnail_border, icon ) - - icon_x -= 2 * ICON_MARGIN - - - - if self.IsCollection(): - - icon = CC.global_pixmaps().collection - - icon_x = thumbnail_border + ICON_MARGIN - icon_y = ( height - 1 ) - thumbnail_border - ICON_MARGIN - icon.height() - - painter.drawPixmap( icon_x, icon_y, icon ) - - num_files_str = HydrusNumbers.ToHumanInt( self.GetNumFiles() ) - - ( text_size, num_files_str ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, num_files_str ) - - text_width = text_size.width() - text_height = text_size.height() - - box_width = text_width + ( ICON_MARGIN * 2 ) - box_x = icon_x + icon.width() + ICON_MARGIN - box_height = text_height + ( ICON_MARGIN * 2 ) - box_y = ( height - 1 ) - box_height - - painter.fillRect( box_x, height - text_height - 3, box_width, box_height, CC.COLOUR_UNSELECTED ) - - painter.setPen( QG.QPen( CC.COLOUR_SELECTED_DARK ) ) - - text_x = box_x + ICON_MARGIN - text_y = box_y + ICON_MARGIN - - ClientGUIFunctions.DrawText( painter, text_x, text_y, num_files_str ) - - - # top left icons - - icons_to_draw = [] - - if self.HasAudio(): - - icons_to_draw.append( CC.global_pixmaps().sound ) - - elif self.HasDuration(): - - icons_to_draw.append( CC.global_pixmaps().play ) - - - services_manager = CG.client_controller.services_manager - - remote_file_service_keys = CG.client_controller.services_manager.GetRemoteFileServiceKeys() - - current = locations_manager.GetCurrent().intersection( remote_file_service_keys ) - pending = locations_manager.GetPending().intersection( remote_file_service_keys ) - petitioned = locations_manager.GetPetitioned().intersection( remote_file_service_keys ) - - current_to_display = current.difference( petitioned ) - - # - - service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in current_to_display ] - - if HC.FILE_REPOSITORY in service_types: - - icons_to_draw.append( CC.global_pixmaps().file_repository ) - - - if HC.IPFS in service_types: - - icons_to_draw.append( CC.global_pixmaps().ipfs ) - - - # - - service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in pending ] - - if HC.FILE_REPOSITORY in service_types: - - icons_to_draw.append( CC.global_pixmaps().file_repository_pending ) - - - if HC.IPFS in service_types: - - icons_to_draw.append( CC.global_pixmaps().ipfs_pending ) - - - # - - service_types = [ services_manager.GetService( service_key ).GetServiceType() for service_key in petitioned ] - - if HC.FILE_REPOSITORY in service_types: - - icons_to_draw.append( CC.global_pixmaps().file_repository_petitioned ) - - - if HC.IPFS in service_types: - - icons_to_draw.append( CC.global_pixmaps().ipfs_petitioned ) - - - top_left_x = thumbnail_border + ICON_MARGIN - - for icon_to_draw in icons_to_draw: - - painter.drawPixmap( top_left_x, thumbnail_border + ICON_MARGIN, icon_to_draw ) - - top_left_x += icon_to_draw.width() + ( ICON_MARGIN * 2 ) - - - return qt_image - - - -# TODO: This is another area of OOD inheritance garbage. just rewrite the whole damn thing, stop trying to do everything in one class, decouple and you'll lose the linter freakout over GetQtImage's references and related __init__ headaches -class ThumbnailMediaCollection( Thumbnail, ClientMedia.MediaCollection ): - - def __init__( self, location_context, media_results ): - - super().__init__( location_context, media_results ) - - -class ThumbnailMediaSingleton( Thumbnail, ClientMedia.MediaSingleton ): - - def __init__( self, media_result ): - - super().__init__( media_result ) - - diff --git a/hydrus/client/gui/pages/ClientGUISession.py b/hydrus/client/gui/pages/ClientGUISession.py index 55df34e50..c6edad8d8 100644 --- a/hydrus/client/gui/pages/ClientGUISession.py +++ b/hydrus/client/gui/pages/ClientGUISession.py @@ -15,7 +15,7 @@ class GUISessionContainer( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name, top_notebook_container = None, hashes_to_page_data = None, skipped_unchanged_page_hashes = None ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) if top_notebook_container is None: @@ -129,7 +129,7 @@ class GUISessionContainerPageNotebook( GUISessionContainerPage ): def __init__( self, name, page_containers = None ): - GUISessionContainerPage.__init__( self, name ) + super().__init__( name ) if page_containers is None: @@ -173,7 +173,7 @@ class GUISessionContainerPageSingle( GUISessionContainerPage ): def __init__( self, name, page_data_hash = None ): - GUISessionContainerPage.__init__( self, name ) + super().__init__( name ) if page_data_hash is None: diff --git a/hydrus/client/gui/pages/ClientGUISessionLegacy.py b/hydrus/client/gui/pages/ClientGUISessionLegacy.py index 684e8e8fd..39da12b28 100644 --- a/hydrus/client/gui/pages/ClientGUISessionLegacy.py +++ b/hydrus/client/gui/pages/ClientGUISessionLegacy.py @@ -12,7 +12,7 @@ class GUISessionLegacy( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._page_tuples = [] diff --git a/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py index 7604198a8..9d5bd26cf 100644 --- a/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py +++ b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py @@ -39,7 +39,7 @@ from hydrus.client.gui.lists import ClientGUIListConstants as CGLC from hydrus.client.gui.lists import ClientGUIListCtrl from hydrus.client.gui.metadata import ClientGUITime -from hydrus.client.gui.pages import ClientGUIResultsSortCollect +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelSortCollect from hydrus.client.gui.panels import ClientGUIScrolledPanels from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit from hydrus.client.gui.search import ClientGUIACDropdown @@ -56,7 +56,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) self._original_options = dict( HC.options ) @@ -3534,7 +3534,7 @@ def __init__( self, parent, new_options ): default_sort = self._new_options.GetDefaultSort() - self._default_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self._file_sort_panel, media_sort = default_sort ) + self._default_media_sort = ClientGUIMediaResultsPanelSortCollect.MediaSortControl( self._file_sort_panel, media_sort = default_sort ) if self._default_media_sort.GetSort() != default_sort: @@ -3545,7 +3545,7 @@ def __init__( self, parent, new_options ): fallback_sort = self._new_options.GetFallbackSort() - self._fallback_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self._file_sort_panel, media_sort = fallback_sort ) + self._fallback_media_sort = ClientGUIMediaResultsPanelSortCollect.MediaSortControl( self._file_sort_panel, media_sort = fallback_sort ) if self._fallback_media_sort.GetSort() != fallback_sort: @@ -3556,7 +3556,7 @@ def __init__( self, parent, new_options ): self._save_page_sort_on_change = QW.QCheckBox( self._file_sort_panel ) - self._default_media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self._file_sort_panel ) + self._default_media_collect = ClientGUIMediaResultsPanelSortCollect.MediaCollectControl( self._file_sort_panel ) namespace_sorting_box = ClientGUICommon.StaticBox( self._file_sort_panel, 'namespace file sorting' ) diff --git a/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py b/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py index 0677a38b4..ec496dc90 100644 --- a/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py +++ b/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py @@ -19,7 +19,7 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPhysical.FilesStorageSubfolder ] ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) # TODO: This needs another pass as we move to multiple locations and other tech # if someone has f10 and we are expecting 16 lots of f10x, or vice versa, (e.g. on an out of sync db recovery, not uncommon) we'll need to handle that diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanels.py b/hydrus/client/gui/panels/ClientGUIScrolledPanels.py index 6a7b3aa44..00b917016 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanels.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanels.py @@ -60,7 +60,7 @@ class ResizingScrolledPanel( QW.QScrollArea ): def __init__( self, parent ): - QW.QScrollArea.__init__( self, parent ) + super().__init__( parent ) self.setWidget( QW.QWidget( self ) ) diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsButtonQuestions.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsButtonQuestions.py index e678bcdc1..ae479da73 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsButtonQuestions.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsButtonQuestions.py @@ -10,7 +10,7 @@ class QuestionYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ): def __init__( self, parent, message, yes_label = 'yes', no_label = 'no' ): - ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent ) + super().__init__( parent ) self._yes = ClientGUICommon.BetterButton( self, yes_label, self.parentWidget().done, QW.QDialog.Accepted ) self._yes.setObjectName( 'HydrusAccept' ) @@ -43,7 +43,7 @@ class QuestionYesYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ): def __init__( self, parent, message, yes_tuples = None, no_label = 'no' ): - ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent ) + super().__init__( parent ) if yes_tuples is None: diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsCommitFiltering.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsCommitFiltering.py index 7e8e2ec65..9ca97dcd9 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsCommitFiltering.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsCommitFiltering.py @@ -61,7 +61,7 @@ class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.Resizing def __init__( self, parent, label ): - ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent ) + super().__init__( parent ) self._commit = ClientGUICommon.BetterButton( self, 'commit and continue', self.parentWidget().done, QW.QDialog.Accepted ) self._commit.setObjectName( 'HydrusAccept' ) @@ -94,7 +94,7 @@ class QuestionArchiveDeleteFinishFilteringPanel( ClientGUIScrolledPanels.Resizin def __init__( self, parent, kept_label: typing.Optional[ str ], deletion_options ): - ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent ) + super().__init__( parent ) self._location_context = ClientLocation.LocationContext() # empty @@ -256,7 +256,7 @@ class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPane def __init__( self, parent, label ): - ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent ) + super().__init__( parent ) self._commit = ClientGUICommon.BetterButton( self, 'commit', self.parentWidget().done, QW.QDialog.Accepted ) self._commit.setObjectName( 'HydrusAccept' ) diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py index 9f31695a1..544ffb988 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py @@ -61,7 +61,7 @@ def __init__( url_class_keys_to_note_import_options ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._url_classes = url_classes self._parsers = parsers @@ -509,7 +509,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, media, default_reason, suggested_file_service_key = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._default_reason = default_reason @@ -1164,7 +1164,7 @@ class EditDuplicateContentMergeOptionsPanel( ClientGUIScrolledPanels.EditPanel ) def __init__( self, parent: QW.QWidget, duplicate_action, duplicate_content_merge_options: ClientDuplicates.DuplicateContentMergeOptions, for_custom_action = False ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._duplicate_action = duplicate_action @@ -1735,7 +1735,7 @@ class EditFilesForcedFiletypePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, original_mimes_count: typing.Dict[ int, int ], forced_mimes_count: typing.Dict[ int, int ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) total_file_count = sum( original_mimes_count.values() ) total_forced_mimes_count = sum( forced_mimes_count.values() ) @@ -2232,7 +2232,7 @@ class EditFrameLocationPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, info ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_info = info @@ -2328,7 +2328,7 @@ class EditMediaViewOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, info ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_info = info @@ -2622,7 +2622,7 @@ class EditRegexFavourites( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, regex_favourites ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) regex_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py index cd0fd7100..96b9d9494 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py @@ -67,7 +67,7 @@ class AboutPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, name, version, description_versions, description_availability, license_text, developers, site ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) icon_label = ClientGUICommon.BetterStaticText( self ) icon_label.setPixmap( CG.client_controller.frame_icon_pixmap ) @@ -134,7 +134,7 @@ def __init__( self, parent, controller ): self._new_options = self._controller.new_options - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._client_files_subfolders = CG.client_controller.Read( 'client_files_subfolders' ) @@ -950,7 +950,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, network_engine ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._network_engine = network_engine @@ -1479,7 +1479,7 @@ class ReviewFileEmbeddedMetadata( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, mime: int, exif_dict: typing.Optional[ dict ], file_text: typing.Optional[ str ], extra_rows: typing.List[ typing.Tuple[ str, str ] ] ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1646,7 +1646,7 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._job_status = ClientThreading.JobStatus() @@ -1829,7 +1829,7 @@ class ReviewFileMaintenance( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, stats ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._hash_ids = None self._job_types_to_due_counts = {} @@ -2261,7 +2261,7 @@ class ReviewHowBonedAmI( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._update_job = None self._job_status = ClientThreading.JobStatus() @@ -2803,7 +2803,7 @@ def __init__( self, parent, paths = None ): paths = [] - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self.widget().installEventFilter( ClientGUIDragDrop.FileDropTarget( self.widget(), filenames_callable = self._AddPathsToList ) ) @@ -3528,7 +3528,7 @@ class ReviewDeferredDeleteTableData( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, controller ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._controller = controller @@ -3690,7 +3690,7 @@ class ReviewThreads( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, controller ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._notebook = ClientGUICommon.BetterNotebook( self ) @@ -3716,7 +3716,7 @@ class ReviewVacuumData( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, controller, vacuum_data ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._controller = controller self._vacuum_data = vacuum_data diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsSelectFromList.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsSelectFromList.py index 9d7f03119..7b9a535eb 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsSelectFromList.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsSelectFromList.py @@ -13,7 +13,7 @@ class EditSelectFromListPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, choice_tuples: list, value_to_select = None, sort_tuples = True ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._list = ClientGUIListBoxes.BetterQListWidget( self ) self._list.itemDoubleClicked.connect( self.EventSelect ) @@ -111,7 +111,7 @@ class EditSelectFromListButtonsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, choices, message = '' ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._data = None @@ -164,7 +164,7 @@ class EditSelectMultiple( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, choice_tuples: list ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._checkboxes = ClientGUICommon.BetterCheckBoxList( self ) diff --git a/hydrus/client/gui/parsing/ClientGUIParsing.py b/hydrus/client/gui/parsing/ClientGUIParsing.py index c2a46c7dd..e9cb341ce 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsing.py +++ b/hydrus/client/gui/parsing/ClientGUIParsing.py @@ -46,7 +46,7 @@ class DownloaderExportPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, network_engine ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._network_engine = network_engine @@ -490,7 +490,7 @@ def __init__( self, parent: QW.QWidget, content_parser: ClientParsing.ContentPar self._original_content_parser = content_parser - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1023,7 +1023,7 @@ class EditContentParsersPanel( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ], permitted_content_types ): - ClientGUICommon.StaticBox.__init__( self, parent, 'content parsers' ) + super().__init__( parent, 'content parsers' ) self._test_data_callable = test_data_callable self._permitted_content_types = permitted_content_types @@ -1168,7 +1168,7 @@ def __init__( self, parent, parser: ClientParsing.PageParser, formula = None, te self._original_parser = parser - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) if test_data is None: @@ -1649,7 +1649,7 @@ class EditParsersPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, parsers ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) diff --git a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py index b21d333e2..338f8021d 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py +++ b/hydrus/client/gui/parsing/ClientGUIParsingFormulae.py @@ -28,7 +28,7 @@ class EditSpecificFormulaPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, collapse_newlines: bool ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._collapse_newlines = collapse_newlines @@ -39,19 +39,19 @@ def GetValue( self ): -class EditCompoundFormulaPanel( EditSpecificFormulaPanel ): +class EditZipperFormulaPanel( EditSpecificFormulaPanel ): - def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaCompound, test_data: ClientParsing.ParsingTestData ): + def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaZipper, test_data: ClientParsing.ParsingTestData ): - EditSpecificFormulaPanel.__init__( self, parent, collapse_newlines ) + super().__init__( parent, collapse_newlines ) # menu_items = [] - page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_COMPOUND_FORMULA ) + page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_ZIPPER_FORMULA ) - menu_items.append( ( 'normal', 'open the compound formula help', 'Open the help page for compound formulae in your web browser.', page_func ) ) + menu_items.append( ( 'normal', 'open the zipper formula help', 'Open the help page for zipper formulae in your web browser.', page_func ) ) help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items ) @@ -90,7 +90,7 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client sub_phrase = formula.GetSubstitutionPhrase() string_processor = formula.GetStringProcessor() - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) # @@ -199,7 +199,7 @@ def Delete( self ): if self._formulae.count() == 1: - ClientGUIDialogsMessage.ShowWarning( self, 'A compound formula needs at least one sub-formula!' ) + ClientGUIDialogsMessage.ShowWarning( self, 'A zipper formula needs at least one sub-formula!' ) else: @@ -243,7 +243,7 @@ def GetValue( self ): string_processor = self._string_processor_button.GetValue() - formula = ClientParsing.ParseFormulaCompound( formulae, sub_phrase, string_processor ) + formula = ClientParsing.ParseFormulaZipper( formulae, sub_phrase, string_processor ) return formula @@ -288,7 +288,7 @@ class EditContextVariableFormulaPanel( EditSpecificFormulaPanel ): def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaContextVariable, test_data: ClientParsing.ParsingTestData ): - EditSpecificFormulaPanel.__init__( self, parent, collapse_newlines ) + super().__init__( parent, collapse_newlines ) # @@ -319,7 +319,7 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client variable_name = formula.GetVariableName() string_processor = formula.GetStringProcessor() - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) # @@ -381,7 +381,7 @@ class EditFormulaPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, formula: ClientParsing.ParseFormula, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._current_formula = formula self._test_data_callable = test_data_callable @@ -441,46 +441,35 @@ def _ChangeFormulaType( self ): new_json = ClientParsing.ParseFormulaJSON() - new_compound = ClientParsing.ParseFormulaCompound() + new_zipper = ClientParsing.ParseFormulaZipper() new_context_variable = ClientParsing.ParseFormulaContextVariable() + new_nested = ClientParsing.ParseFormulaNested() - if isinstance( self._current_formula, ClientParsing.ParseFormulaHTML ): + choice_tuples = [] + + if not isinstance( self._current_formula, ClientParsing.ParseFormulaHTML ): - order = ( 'json', 'compound', 'context_variable' ) + choice_tuples.append( ( 'change to a new HTML formula', new_html ) ) - elif isinstance( self._current_formula, ClientParsing.ParseFormulaJSON ): + + if not isinstance( self._current_formula, ClientParsing.ParseFormulaJSON ): - order = ( 'html', 'compound', 'context_variable' ) + choice_tuples.append( ( 'change to a new JSON formula', new_json ) ) - elif isinstance( self._current_formula, ClientParsing.ParseFormulaCompound ): + + if not isinstance( self._current_formula, ClientParsing.ParseFormulaNested ): - order = ( 'html', 'json', 'context_variable' ) + choice_tuples.append( ( 'change to a new NESTED formula', new_nested ) ) - elif isinstance( self._current_formula, ClientParsing.ParseFormulaContextVariable ): + + if not isinstance( self._current_formula, ClientParsing.ParseFormulaZipper ): - order = ( 'html', 'json', 'compound', 'context_variable' ) + choice_tuples.append( ( 'change to a new ZIPPER formula', new_zipper ) ) - choice_tuples = [] - - for formula_type in order: + if not isinstance( self._current_formula, ClientParsing.ParseFormulaContextVariable ): - if formula_type == 'html': - - choice_tuples.append( ( 'change to a new HTML formula', new_html ) ) - - elif formula_type == 'json': - - choice_tuples.append( ( 'change to a new JSON formula', new_json ) ) - - elif formula_type == 'compound': - - choice_tuples.append( ( 'change to a new COMPOUND formula', new_compound ) ) - - elif formula_type == 'context_variable': - - choice_tuples.append( ( 'change to a new CONTEXT VARIABLE formula', new_context_variable ) ) - + choice_tuples.append( ( 'change to a new CONTEXT VARIABLE formula', new_context_variable ) ) try: @@ -505,9 +494,13 @@ def _EditFormula( self ): panel_class = EditJSONFormulaPanel - elif isinstance( self._current_formula, ClientParsing.ParseFormulaCompound ): + elif isinstance( self._current_formula, ClientParsing.ParseFormulaNested ): + + panel_class = EditNestedFormulaPanel + + elif isinstance( self._current_formula, ClientParsing.ParseFormulaZipper ): - panel_class = EditCompoundFormulaPanel + panel_class = EditZipperFormulaPanel elif isinstance( self._current_formula, ClientParsing.ParseFormulaContextVariable ): @@ -570,7 +563,7 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, tag_rule ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) ( rule_type, tag_name, tag_attributes, tag_index, tag_depth, should_test_tag_string, tag_string_string_match ) = tag_rule.ToTuple() @@ -763,7 +756,7 @@ class EditHTMLFormulaPanel( EditSpecificFormulaPanel ): def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaHTML, test_data: ClientParsing.ParsingTestData ): - EditSpecificFormulaPanel.__init__( self, parent, collapse_newlines ) + super().__init__( parent, collapse_newlines ) # @@ -819,7 +812,7 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client attribute_to_fetch = formula.GetAttributeToFetch() string_processor = formula.GetStringProcessor() - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) # @@ -1038,13 +1031,14 @@ class EditJSONParsingRulePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, rule: ClientParsing.ParseRuleHTML ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._parse_rule_type = ClientGUICommon.BetterChoice( self ) self._parse_rule_type.addItem( 'dictionary entry', ClientParsing.JSON_PARSE_RULE_TYPE_DICT_KEY ) self._parse_rule_type.addItem( 'all dictionary/list items', ClientParsing.JSON_PARSE_RULE_TYPE_ALL_ITEMS ) self._parse_rule_type.addItem( 'indexed item', ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM ) + self._parse_rule_type.addItem( 'de-minify json', ClientParsing.JSON_PARSE_RULE_TYPE_DEMINIFY_JSON ) string_match = ClientStrings.StringMatch( match_type = ClientStrings.STRING_MATCH_FIXED, match_value = 'posts', example_string = 'posts' ) @@ -1067,6 +1061,10 @@ def __init__( self, parent: QW.QWidget, rule: ClientParsing.ParseRuleHTML ): self._string_match.SetValue( parse_rule ) + elif parse_rule_type == ClientParsing.JSON_PARSE_RULE_TYPE_DEMINIFY_JSON: + + self._index.setValue( parse_rule ) + self._UpdateHideShow() @@ -1106,6 +1104,10 @@ def _UpdateHideShow( self ): self._index.setEnabled( True ) + elif parse_rule_type == ClientParsing.JSON_PARSE_RULE_TYPE_DEMINIFY_JSON: + + self._index.setEnabled( True ) + def GetValue( self ): @@ -1124,6 +1126,10 @@ def GetValue( self ): parse_rule = None + elif parse_rule_type == ClientParsing.JSON_PARSE_RULE_TYPE_DEMINIFY_JSON: + + parse_rule = self._index.value() + return ( parse_rule_type, parse_rule ) @@ -1132,7 +1138,7 @@ class EditJSONFormulaPanel( EditSpecificFormulaPanel ): def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaJSON, test_data: ClientParsing.ParsingTestData ): - EditSpecificFormulaPanel.__init__( self, parent, collapse_newlines ) + super().__init__( parent, collapse_newlines ) # @@ -1182,7 +1188,7 @@ def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: Client content_to_fetch = formula.GetContentToFetch() string_processor = formula.GetStringProcessor() - self._string_processor_button = ClientGUIStringControls.StringProcessorButton( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) # @@ -1372,3 +1378,137 @@ def MoveUp( self ): self._parse_rules.insertItem( selection - 1, item ) + + +class EditNestedFormulaPanel( EditSpecificFormulaPanel ): + + def __init__( self, parent: QW.QWidget, collapse_newlines: bool, formula: ClientParsing.ParseFormulaNested, test_data: ClientParsing.ParsingTestData ): + + super().__init__( parent, collapse_newlines ) + + # + + menu_items = [] + + page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_NESTED_FORMULA ) + + menu_items.append( ( 'normal', 'open the nested formula help', 'Open the help page for nested formulae in your web browser.', page_func ) ) + + help_button = ClientGUIMenuButton.MenuBitmapButton( self, CC.global_pixmaps().help, menu_items ) + + help_hbox = ClientGUICommon.WrapInText( help_button, self, 'help for this panel -->', object_name = 'HydrusIndeterminate' ) + + # + + test_panel = ClientGUICommon.StaticBox( self, 'test' ) + + self._test_panel = ClientGUIParsingTest.TestPanelFormula( test_panel, self.GetValue, test_data = test_data ) + + self._test_panel.SetCollapseNewlines( self._collapse_newlines ) + + # + + main_formula = formula.GetMainFormula() + sub_formula = formula.GetSubFormula() + + edit_panel = ClientGUICommon.StaticBox( self, 'edit' ) + + main_panel = ClientGUICommon.StaticBox( edit_panel, 'first formula' ) + + self._main_formula_panel = EditFormulaPanel( main_panel, main_formula, test_data_callable = self._test_panel.GetTestData ) + + sub_panel = ClientGUICommon.StaticBox( edit_panel, 'second formula' ) + + self._sub_formula_panel = EditFormulaPanel( sub_panel, sub_formula, test_data_callable = self._GetSubTestData ) + + string_processor = formula.GetStringProcessor() + + self._string_processor_button = ClientGUIStringControls.StringProcessorWidget( edit_panel, string_processor, self._test_panel.GetTestDataForStringProcessor ) + + # + + main_panel.Add( self._main_formula_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + sub_panel.Add( self._sub_formula_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + label = 'Whatever is parsed by the first formula is sent to the second. Use this if you have JSON embedded in HTML or vice versa. Even if the thing you want to parse is very simple, it can be worth doing it this proper way to ensure html entities etc.. are decoded properly and naturally!' + + st = ClientGUICommon.BetterStaticText( edit_panel, label ) + + st.setWordWrap( True ) + + edit_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR ) + edit_panel.Add( main_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + edit_panel.Add( sub_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + if collapse_newlines: + + label = 'Newlines are removed from parsed strings right after parsing, before string processing.' + + else: + + label = 'Newlines are not collapsed here (probably a note parser)' + + + edit_panel.Add( ClientGUICommon.BetterStaticText( edit_panel, label, ellipsize_end = True ), CC.FLAGS_EXPAND_PERPENDICULAR ) + edit_panel.Add( self._string_processor_button, CC.FLAGS_EXPAND_PERPENDICULAR ) + + # + + test_panel.Add( self._test_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + # + + hbox = QP.HBoxLayout() + + QP.AddToLayout( hbox, edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( hbox, test_panel, CC.FLAGS_EXPAND_BOTH_WAYS ) + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, help_hbox, CC.FLAGS_ON_RIGHT ) + QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) + + self.widget().setLayout( vbox ) + + + def _GetSubTestData( self ) -> ClientParsing.ParsingTestData: + + test_data = self._test_panel.GetTestData() + + example_parsing_context = test_data.parsing_context + + main_formula = self._main_formula_panel.GetValue() + + main_texts = [ '' ] + + try: + + for text in test_data.texts: + + main_texts = main_formula.Parse( example_parsing_context, text, self._collapse_newlines ) + + if len( main_texts ) > 0: + + break + + + + except: + + main_texts = [ '' ] + + + return ClientParsing.ParsingTestData( example_parsing_context, main_texts ) + + + def GetValue( self ): + + main_formula = self._main_formula_panel.GetValue() + sub_formula = self._sub_formula_panel.GetValue() + string_processor = self._string_processor_button.GetValue() + + formula = ClientParsing.ParseFormulaNested( main_formula = main_formula, sub_formula = sub_formula, string_processor = string_processor ) + + return formula + + diff --git a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py index 25589bf64..0f605c1dc 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py +++ b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py @@ -298,7 +298,7 @@ class EditParseNodeContentLinkPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, node, referral_url = None, example_data = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) if referral_url is None: @@ -550,7 +550,7 @@ class EditParsingScriptFileLookupPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, script ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) ( name, url, query_type, file_identifier_type, file_identifier_string_converter, file_identifier_arg_name, static_args, children ) = script.ToTuple() @@ -870,7 +870,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PARSING_SCRIPTS.ID, self._ConvertScriptToTuples ) diff --git a/hydrus/client/gui/parsing/ClientGUIParsingTest.py b/hydrus/client/gui/parsing/ClientGUIParsingTest.py index e4159979f..5487d6796 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsingTest.py +++ b/hydrus/client/gui/parsing/ClientGUIParsingTest.py @@ -431,6 +431,7 @@ def TestParse( self ): + class TestPanelFormula( TestPanel ): def GetTestDataForStringProcessor( self ): @@ -459,7 +460,7 @@ def __init__( self, parent, object_callable, pre_parsing_converter_callable, tes self._pre_parsing_converter_callable = pre_parsing_converter_callable - TestPanel.__init__( self, parent, object_callable, test_data = test_data ) + super().__init__( parent, object_callable, test_data = test_data ) post_conversion_panel = QW.QWidget( self._data_preview_notebook ) @@ -587,7 +588,7 @@ 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 ) + super().__init__( parent, object_callable, pre_parsing_converter_callable, test_data = test_data ) self._formula_callable = formula_callable diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py index c2045bf49..1dbd6245f 100644 --- a/hydrus/client/gui/search/ClientGUIACDropdown.py +++ b/hydrus/client/gui/search/ClientGUIACDropdown.py @@ -33,7 +33,7 @@ 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.pages import ClientGUIResultsSortCollect +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelSortCollect from hydrus.client.gui.panels import ClientGUIScrolledPanels from hydrus.client.gui.search import ClientGUILocation from hydrus.client.gui.search import ClientGUISearch @@ -604,7 +604,7 @@ class ListBoxTagsPredicatesAC( ClientGUIListBoxes.ListBoxTagsPredicates ): def __init__( self, parent, callable, float_mode, service_key, **kwargs ): - ClientGUIListBoxes.ListBoxTagsPredicates.__init__( self, parent, **kwargs ) + super().__init__( parent, **kwargs ) self._callable = callable self._float_mode = float_mode @@ -760,7 +760,7 @@ class ListBoxTagsStringsAC( ClientGUIListBoxes.ListBoxTagsStrings ): def __init__( self, parent, callable, service_key, float_mode, **kwargs ): - ClientGUIListBoxes.ListBoxTagsStrings.__init__( self, parent, service_key = service_key, sort_tags = False, **kwargs ) + super().__init__( parent, service_key = service_key, sort_tags = False, **kwargs ) self._callable = callable self._float_mode = float_mode @@ -1234,10 +1234,15 @@ def eventFilter( self, watched, event ): send_input_to_current_list = False ctrl = event.modifiers() & QC.Qt.ControlModifier + # previous/next hardcoded shortcuts, should obviously be migrated to a user-customised shortcut set in future! crazy_n_p_hardcodes = ctrl and key in ( ord( 'P' ), ord( 'p' ), ord( 'N' ), ord( 'n' ) ) - if key in ( QC.Qt.Key_Up, QC.Qt.Key_Down, QC.Qt.Key_PageDown, QC.Qt.Key_PageUp, QC.Qt.Key_Home, QC.Qt.Key_End ) or crazy_n_p_hardcodes: + we_copying = ctrl and key in( ord( 'C' ), ord( 'c' ), QC.Qt.Key_Insert ) + + we_copying_the_list = we_copying and self._text_ctrl.selectedText() == '' + + if key in ( QC.Qt.Key_Up, QC.Qt.Key_Down, QC.Qt.Key_PageDown, QC.Qt.Key_PageUp, QC.Qt.Key_Home, QC.Qt.Key_End ) or crazy_n_p_hardcodes or we_copying_the_list: send_input_to_current_list = True @@ -1585,7 +1590,7 @@ def __init__( self, parent: QW.QWidget, broadcast_call, float_mode: bool, locati self._tags_to_child_predicates_cache = dict() self._children_need_updating = True - ListBoxTagsPredicatesAC.__init__( self, parent, broadcast_call, float_mode, tag_service_key, tag_display_type = tag_display_type, height_num_chars = height_num_chars ) + super().__init__( parent, broadcast_call, float_mode, tag_service_key, tag_display_type = tag_display_type, height_num_chars = height_num_chars ) def NotifyNeedsUpdating( self ): @@ -1744,7 +1749,7 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta self._current_context_tags = {} self._tag_service_key = tag_service_key - AutoCompleteDropdown.__init__( self, parent ) + super().__init__( parent ) self._location_context_button = ClientGUILocation.LocationSearchContextButton( self._dropdown_window, location_context, is_paired_with_tag_domain = True ) self._location_context_button.setMinimumWidth( 20 ) @@ -2034,7 +2039,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ): searchChanged = QC.Signal( ClientSearchFileSearchContext.FileSearchContext ) searchCancelled = QC.Signal() - def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearchFileSearchContext.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, only_allow_local_file_domains = False, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ): + def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearchFileSearchContext.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, only_allow_local_file_domains = False, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ): self._page_key = page_key @@ -2061,7 +2066,7 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea self._file_search_context = file_search_context - AutoCompleteDropdownTags.__init__( self, parent, location_context, tag_context.service_key ) + super().__init__( parent, location_context, tag_context.service_key ) self._location_context_button.SetOnlyLocalFileDomainsAllowed( only_allow_local_file_domains ) self._location_context_button.SetAllKnownFilesAllowed( allow_all_known_files, True ) @@ -2071,6 +2076,9 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea self._paste_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().paste, self._Paste ) self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'You can paste a newline-separated list of regular tags and/or system predicates.' ) ) + self._empty_search_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().clear_highlight, self._ClearSearch ) + self._empty_search_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Clear the search back to an empty page.' ) ) + self._favourite_searches_button = ClientGUICommon.BetterBitmapButton( self._text_input_panel, CC.global_pixmaps().star, self._FavouriteSearchesMenu ) self._favourite_searches_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Load or save a favourite search.' ) ) @@ -2078,9 +2086,10 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea self._cancel_search_button.hide() + QP.AddToLayout( self._text_input_hbox, self._cancel_search_button, CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( self._text_input_hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( self._text_input_hbox, self._favourite_searches_button, CC.FLAGS_CENTER_PERPENDICULAR ) - QP.AddToLayout( self._text_input_hbox, self._cancel_search_button, CC.FLAGS_CENTER_PERPENDICULAR ) + QP.AddToLayout( self._text_input_hbox, self._empty_search_button, CC.FLAGS_CENTER_PERPENDICULAR ) # @@ -2256,6 +2265,44 @@ def _CancelORConstruction( self ): self._ClearInput() + def _ClearSearch( self ): + + location_context = CG.client_controller.new_options.GetDefaultLocalLocationContext() + + tag_context = ClientSearchTagContext.TagContext() + + predicates = [] + + file_search_context = ClientSearchFileSearchContext.FileSearchContext( location_context = location_context, tag_context = tag_context, predicates = predicates ) + + synchronised = True + media_sort = None + media_collect = None + + self.blockSignals( True ) + + self.SetFileSearchContext( file_search_context ) + + if media_sort is not None and self._media_sort_widget is not None: + + self._media_sort_widget.SetSort( media_sort ) + + + if media_collect is not None and self._media_collect_widget is not None: + + self._media_collect_widget.SetCollect( media_collect ) + + + self._search_pause_play.SetOnOff( synchronised ) + + self.blockSignals( False ) + + self.locationChanged.emit( self._location_context_button.GetValue() ) + self.tagServiceChanged.emit( self._tag_service_key ) + + self._SignalNewSearchState() + + def _CreateNewOR( self ): predicates = { ClientSearchPredicate.Predicate( ClientSearchPredicate.PREDICATE_TYPE_OR_CONTAINER, value = [ ] ) } @@ -2832,7 +2879,7 @@ class ListBoxTagsActiveSearchPredicates( ClientGUIListBoxes.ListBoxTagsPredicate def __init__( self, parent: AutoCompleteDropdownTagsRead, page_key, file_search_context: ClientSearchFileSearchContext.FileSearchContext ): - ClientGUIListBoxes.ListBoxTagsPredicates.__init__( self, parent, height_num_chars = 6 ) + super().__init__( parent, height_num_chars = 6 ) self._my_ac_parent = parent @@ -3116,7 +3163,7 @@ def __init__( self, parent, chosen_tag_callable, location_context, tag_service_k ( location_context, tag_service_key ) = tag_autocomplete_options.GetWriteAutocompleteSearchDomain( location_context ) - AutoCompleteDropdownTags.__init__( self, parent, location_context, tag_service_key ) + super().__init__( parent, location_context, tag_service_key ) self._location_context_button.SetAllKnownFilesAllowed( True, False ) @@ -3305,7 +3352,7 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, initial_string = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._input_text = QW.QLineEdit( self ) diff --git a/hydrus/client/gui/search/ClientGUILocation.py b/hydrus/client/gui/search/ClientGUILocation.py index 1b0811365..137686fde 100644 --- a/hydrus/client/gui/search/ClientGUILocation.py +++ b/hydrus/client/gui/search/ClientGUILocation.py @@ -19,7 +19,7 @@ class EditMultipleLocationContextPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext, all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_location_context = location_context self._all_known_files_allowed = all_known_files_allowed @@ -138,7 +138,7 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio self._location_context = ClientLocation.LocationContext() self._is_paired_with_tag_domain = is_paired_with_tag_domain - ClientGUICommon.BetterButton.__init__( self, parent, 'initialising', self._EditLocation ) + super().__init__( parent, 'initialising', self._EditLocation ) self._all_known_files_allowed = True self._all_known_files_allowed_only_in_advanced_mode = False diff --git a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py index 3576aa8d9..665e1bad6 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py @@ -420,7 +420,7 @@ class PanelPredicateSystemRating( PanelPredicateSystemMultiple ): def __init__( self, parent, predicates ): - PanelPredicateSystemMultiple.__init__( self, parent ) + super().__init__( parent ) # diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py index 62be4ea71..729b46cde 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py @@ -99,7 +99,7 @@ def __init__( self, parent ): ( '+/- a month of', HC.UNICODE_APPROX_EQUAL ) ] - ClientGUICommon.BetterRadioBox.__init__( self, parent, choice_tuples, vertical = True ) + super().__init__( parent, choice_tuples, vertical = True ) @@ -113,7 +113,7 @@ def __init__( self, parent ): ( '+/- 15% of', HC.UNICODE_APPROX_EQUAL ) ] - ClientGUICommon.BetterRadioBox.__init__( self, parent, choice_tuples, vertical = True ) + super().__init__( parent, choice_tuples, vertical = True ) @@ -123,7 +123,7 @@ def __init__( self, parent: QW.QWidget, predicate: ClientSearchPredicate.Predica self._predicate = predicate - ClientGUICommon.BetterButton.__init__( self, parent, 'predicate', self._ButtonHit ) + super().__init__( parent, 'predicate', self._ButtonHit ) self._UpdateLabel() @@ -359,7 +359,7 @@ class PanelPredicateSystemDate( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = TimeDateOperator( self ) @@ -524,7 +524,7 @@ class PanelPredicateSystemAgeDelta( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = TimeDeltaOperator( self ) @@ -586,7 +586,7 @@ class PanelPredicateSystemLastViewedDelta( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = TimeDeltaOperator( self ) @@ -645,7 +645,7 @@ class PanelPredicateSystemArchivedDelta( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = TimeDeltaOperator( self ) @@ -704,7 +704,7 @@ class PanelPredicateSystemModifiedDelta( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = TimeDeltaOperator( self ) @@ -762,7 +762,7 @@ class PanelPredicateSystemDuplicateRelationships( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', '>' ] @@ -820,7 +820,7 @@ class PanelPredicateSystemDuration( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -874,7 +874,7 @@ class PanelPredicateSystemFileService( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = ClientGUICommon.BetterRadioBox( self, [ ( 'is', True ), ( 'is not', False ) ], vertical = True ) @@ -939,7 +939,7 @@ class PanelPredicateSystemFileViewingStatsViews( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._viewing_locations = ClientGUICommon.BetterCheckBoxList( self ) @@ -1015,7 +1015,7 @@ class PanelPredicateSystemFileViewingStatsViewtime( PanelPredicateSystemSingle ) def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._viewing_locations = ClientGUICommon.BetterCheckBoxList( self ) @@ -1091,7 +1091,7 @@ class PanelPredicateSystemFramerate( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -1144,7 +1144,7 @@ class PanelPredicateSystemHash( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._sign = ClientGUICommon.BetterRadioBox( self, [ ( 'is', True ), ( 'is not', False ) ], vertical = True ) @@ -1217,7 +1217,7 @@ class PanelPredicateSystemHasNoteName( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._operator = ClientGUICommon.BetterChoice( self ) @@ -1276,7 +1276,7 @@ class PanelPredicateSystemHeight( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -1331,7 +1331,7 @@ class PanelPredicateSystemKnownURLsExactURL( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._operator = ClientGUICommon.BetterChoice( self ) @@ -1403,7 +1403,7 @@ class PanelPredicateSystemKnownURLsDomain( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._operator = ClientGUICommon.BetterChoice( self ) @@ -1477,7 +1477,7 @@ class PanelPredicateSystemKnownURLsRegex( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._operator = ClientGUICommon.BetterChoice( self ) @@ -1563,7 +1563,7 @@ class PanelPredicateSystemKnownURLsURLClass( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._operator = ClientGUICommon.BetterChoice( self ) @@ -1640,7 +1640,7 @@ class PanelPredicateSystemLimit( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._limit = ClientGUICommon.BetterSpinBox( self, min = 1, max=1000000, width = 60 ) @@ -1684,7 +1684,7 @@ class PanelPredicateSystemMime( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._mimes = ClientGUIOptionsPanels.OptionsPanelMimesTree( self, HC.SEARCHABLE_MIMES ) @@ -1735,7 +1735,7 @@ class PanelPredicateSystemNumPixels( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) choices = [ '<', HC.UNICODE_APPROX_EQUAL, '=', HC.UNICODE_NOT_EQUAL, '>' ] @@ -1795,7 +1795,7 @@ class PanelPredicateSystemNumFrames( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -1850,7 +1850,7 @@ class PanelPredicateSystemNumTags( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._namespace = QW.QLineEdit( self ) self._namespace.setPlaceholderText( 'Leave empty for unnamespaced, \'*\' for all namespaces' ) @@ -1939,7 +1939,7 @@ class PanelPredicateSystemNumNotes( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -1993,7 +1993,7 @@ class PanelPredicateSystemNumURLs( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -2047,7 +2047,7 @@ class PanelPredicateSystemNumWords( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, @@ -2103,7 +2103,7 @@ class PanelPredicateSystemRatio( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) choices = ['=','wider than','taller than',HC.UNICODE_APPROX_EQUAL,HC.UNICODE_NOT_EQUAL] @@ -2163,7 +2163,7 @@ class PanelPredicateSystemSimilarToData( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._clear_button = ClientGUICommon.BetterButton( self, 'clear', self._Clear ) @@ -2369,7 +2369,7 @@ class PanelPredicateSystemSimilarToFiles( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._hashes = QW.QPlainTextEdit( self ) @@ -2446,7 +2446,7 @@ class PanelPredicateSystemSize( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) choices = ['<',HC.UNICODE_APPROX_EQUAL,'=',HC.UNICODE_NOT_EQUAL,'>'] @@ -2501,7 +2501,7 @@ class PanelPredicateSystemTagAsNumber( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) self._namespace = QW.QLineEdit( self ) @@ -2558,7 +2558,7 @@ class PanelPredicateSystemWidth( PanelPredicateSystemSingle ): def __init__( self, parent, predicate ): - PanelPredicateSystemSingle.__init__( self, parent ) + super().__init__( parent ) allowed_operators = [ ClientNumberTest.NUMBER_TEST_OPERATOR_LESS_THAN, diff --git a/hydrus/client/gui/search/ClientGUISearch.py b/hydrus/client/gui/search/ClientGUISearch.py index a6d1690af..b3b513133 100644 --- a/hydrus/client/gui/search/ClientGUISearch.py +++ b/hydrus/client/gui/search/ClientGUISearch.py @@ -194,7 +194,7 @@ class EditPredicatesPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, predicates: typing.Collection[ ClientSearchPredicate.Predicate ], empty_file_search_context: typing.Optional[ ClientSearchFileSearchContext.FileSearchContext ] = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) predicates = list( predicates ) @@ -471,7 +471,7 @@ class FleshOutPredicatePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, predicate: ClientSearchPredicate.Predicate ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) predicate_type = predicate.GetType() @@ -1012,7 +1012,7 @@ def __init__( self, parent: QW.QWidget, tag_context: ClientSearchTagContext.TagC self._use_short_label = use_short_label - ClientGUICommon.BetterButton.__init__( self, parent, 'initialising', self._Edit ) + super().__init__( parent, 'initialising', self._Edit ) self.SetValue( tag_context ) diff --git a/hydrus/client/gui/search/ClientGUISearchPanels.py b/hydrus/client/gui/search/ClientGUISearchPanels.py index 4bf02c851..eab7a1b28 100644 --- a/hydrus/client/gui/search/ClientGUISearchPanels.py +++ b/hydrus/client/gui/search/ClientGUISearchPanels.py @@ -16,7 +16,7 @@ from hydrus.client.gui import QtPorting as QP from hydrus.client.gui.lists import ClientGUIListConstants as CGLC from hydrus.client.gui.lists import ClientGUIListCtrl -from hydrus.client.gui.pages import ClientGUIResultsSortCollect +from hydrus.client.gui.pages import ClientGUIMediaResultsPanelSortCollect from hydrus.client.gui.panels import ClientGUIScrolledPanels from hydrus.client.gui.widgets import ClientGUICommon from hydrus.client.search import ClientSearchFileSearchContext @@ -25,7 +25,7 @@ class EditFavouriteSearchPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, existing_folders_to_names, foldername, name, file_search_context, synchronised, media_sort, media_collect ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._existing_folders_to_names = existing_folders_to_names self._original_folder_and_name = ( foldername, name ) @@ -33,8 +33,8 @@ def __init__( self, parent, existing_folders_to_names, foldername, name, file_se self._foldername = QW.QLineEdit( self ) self._name = QW.QLineEdit( self ) - self._media_sort = ClientGUIResultsSortCollect.MediaSortControl( self, media_sort = media_sort ) - self._media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self ) + self._media_sort = ClientGUIMediaResultsPanelSortCollect.MediaSortControl( self, media_sort = media_sort ) + self._media_collect = ClientGUIMediaResultsPanelSortCollect.MediaCollectControl( self ) page_key = HydrusData.GenerateKey() @@ -173,7 +173,7 @@ class EditFavouriteSearchesPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, favourite_searches_rows, initial_search_row_to_edit = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._favourite_searches_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py index a31ff3415..ba6823125 100644 --- a/hydrus/client/gui/services/ClientGUIClientsideServices.py +++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py @@ -54,7 +54,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ): def __init__( self, parent, auto_account_creation_service_key = None ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_MANAGE_SERVICES.ID, self._ConvertServiceToListCtrlTuples ) @@ -328,7 +328,7 @@ class EditClientServicePanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, service, auto_account_creation_service_key = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) duplicate_service = service.Duplicate() @@ -430,7 +430,7 @@ class EditServiceSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, name ): - ClientGUICommon.StaticBox.__init__( self, parent, 'name' ) + super().__init__( parent, 'name' ) self._name = QW.QLineEdit( self ) @@ -463,7 +463,7 @@ class EditServiceRemoteSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service_type, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'network connection' ) + super().__init__( parent, 'network connection' ) self._service_type = service_type @@ -646,7 +646,7 @@ class EditServiceRestrictedSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service_key, remote_panel: EditServiceRemoteSubPanel, service_type, dictionary, auto_account_creation_service_key = None ): - ClientGUICommon.StaticBox.__init__( self, parent, 'hydrus network' ) + super().__init__( parent, 'hydrus network' ) self._service_key = service_key self._remote_panel = remote_panel @@ -1112,7 +1112,7 @@ class EditServiceClientServerSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service_type, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'client api' ) + super().__init__( parent, 'client api' ) self._client_server_options_panel = ClientGUICommon.StaticBox( self, 'options' ) @@ -1278,7 +1278,7 @@ class EditServiceTagSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'tags' ) + super().__init__( parent, 'tags' ) self._st = ClientGUICommon.BetterStaticText( self ) @@ -1301,7 +1301,7 @@ class EditServiceRatingsSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service_type, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'rating colours' ) + super().__init__( parent, 'rating colours' ) self._colour_ctrls = {} @@ -1406,7 +1406,7 @@ class EditServiceStarRatingsSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'rating shape' ) + super().__init__( parent, 'rating shape' ) self._shape = ClientGUICommon.BetterChoice( self ) @@ -1444,7 +1444,7 @@ class EditServiceRatingsNumericalSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'numerical ratings' ) + super().__init__( parent, 'numerical ratings' ) self._num_stars = ClientGUICommon.BetterSpinBox( self, min=1, max=20 ) self._allow_zero = QW.QCheckBox( self ) @@ -1489,7 +1489,7 @@ class EditServiceIPFSSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'ipfs' ) + super().__init__( parent, 'ipfs' ) interaction_panel = ClientGUIPanels.IPFSDaemonStatusAndInteractionPanel( self, self.parentWidget().GetValue ) @@ -1835,7 +1835,7 @@ class ReviewServiceSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'name and type' ) + super().__init__( parent, 'name and type' ) self._service = service @@ -1883,7 +1883,7 @@ class ReviewServiceClientAPISubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'client api' ) + super().__init__( parent, 'client api' ) self._service = service @@ -2169,7 +2169,7 @@ class ReviewServiceCombinedLocalFilesSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'combined local files' ) + super().__init__( parent, 'combined local files' ) self._service = service @@ -2269,7 +2269,7 @@ class ReviewServiceFileSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'files' ) + super().__init__( parent, 'files' ) self._service = service @@ -2341,7 +2341,7 @@ class ReviewServiceRemoteSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'this client\'s network use' ) + super().__init__( parent, 'this client\'s network use' ) self._service = service @@ -2447,7 +2447,7 @@ class ReviewServiceRestrictedSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'hydrus service account - shared by all clients using the same access key' ) + super().__init__( parent, 'hydrus service account - shared by all clients using the same access key' ) self._service = service @@ -3457,7 +3457,7 @@ class ReviewServiceIPFSSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'ipfs' ) + super().__init__( parent, 'ipfs' ) self._service = service @@ -3685,7 +3685,7 @@ class ReviewServiceRatingSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'ratings' ) + super().__init__( parent, 'ratings' ) self._service = service @@ -3775,7 +3775,7 @@ class ReviewServiceTagSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'tags' ) + super().__init__( parent, 'tags' ) self._service = service @@ -3858,7 +3858,7 @@ class ReviewServiceTrashSubPanel( ClientGUICommon.StaticBox ): def __init__( self, parent, service ): - ClientGUICommon.StaticBox.__init__( self, parent, 'trash' ) + super().__init__( parent, 'trash' ) self._service = service @@ -3984,7 +3984,7 @@ def __init__( self, parent, controller ): self._controller = controller - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._notebook = ClientGUICommon.BetterNotebook( self ) @@ -4051,7 +4051,7 @@ def _InitialiseServices( self ): elif service_type == HC.FILE_REPOSITORY: service_type_name = 'file repositories' elif service_type == HC.MESSAGE_DEPOT: service_type_name = 'message depots' elif service_type == HC.SERVER_ADMIN: service_type_name = 'administrative servers' - elif service_type in HC.LOCAL_FILE_SERVICES: service_type_name = 'files' + elif service_type in HC.LOCAL_FILE_SERVICES: service_type_name = 'locations' elif service_type == HC.LOCAL_TAG: service_type_name = 'tags' elif service_type == HC.LOCAL_RATING_LIKE: service_type_name = 'like/dislike ratings' elif service_type == HC.LOCAL_RATING_NUMERICAL: service_type_name = 'numerical ratings' diff --git a/hydrus/client/gui/services/ClientGUIModalClientsideServiceActions.py b/hydrus/client/gui/services/ClientGUIModalClientsideServiceActions.py index 3d45d672e..6f2433402 100644 --- a/hydrus/client/gui/services/ClientGUIModalClientsideServiceActions.py +++ b/hydrus/client/gui/services/ClientGUIModalClientsideServiceActions.py @@ -27,7 +27,7 @@ def __init__( self, parent, service_key: bytes, tags: typing.Collection[ str ] ) self._service_key = service_key - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) # what about a listboxtags that has an auto-async thing to produce a count suffix, mate? surely this is doable in some way self._tags_to_remove = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, service_key = service_key ) diff --git a/hydrus/client/gui/services/ClientGUIServersideServices.py b/hydrus/client/gui/services/ClientGUIServersideServices.py index 1b4c7cf5d..fa1caa884 100644 --- a/hydrus/client/gui/services/ClientGUIServersideServices.py +++ b/hydrus/client/gui/services/ClientGUIServersideServices.py @@ -25,7 +25,7 @@ class EditServersideService( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, serverside_service: HydrusNetwork.ServerService ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) duplicate_serverside_service = serverside_service.Duplicate() @@ -86,7 +86,7 @@ class _ServicePanel( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget, name: str, port: int, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'basic information' ) + super().__init__( parent, 'basic information' ) self._name = QW.QLineEdit( self ) self._port = ClientGUICommon.BetterSpinBox( self, min=1, max=65535 ) @@ -171,7 +171,7 @@ class _ServiceFileRepositoryPanel( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'file repository' ) + super().__init__( parent, 'file repository' ) self._log_uploader_ips = QW.QCheckBox( self ) self._max_storage = ClientGUIBytes.NoneableBytesControl( self, 5 * ( 1024 ** 3 ) ) @@ -214,7 +214,7 @@ class _ServiceServerAdminPanel( ClientGUICommon.StaticBox ): def __init__( self, parent: QW.QWidget, dictionary ): - ClientGUICommon.StaticBox.__init__( self, parent, 'server-wide bandwidth' ) + super().__init__( parent, 'server-wide bandwidth' ) self._bandwidth_tracker_st = ClientGUICommon.BetterStaticText( self ) @@ -254,7 +254,7 @@ def __init__( self, parent, service_key ): self._clientside_admin_service = CG.client_controller.services_manager.GetService( service_key ) - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) + super().__init__( parent ) self._deletee_service_keys = [] diff --git a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py index b7c2a4505..c8db86a05 100644 --- a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py +++ b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py @@ -983,7 +983,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, command: CAC.ApplicationCommand, shortcuts_name: str ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # diff --git a/hydrus/client/gui/widgets/ClientGUIBandwidth.py b/hydrus/client/gui/widgets/ClientGUIBandwidth.py index 4a9691bf8..1b07e4113 100644 --- a/hydrus/client/gui/widgets/ClientGUIBandwidth.py +++ b/hydrus/client/gui/widgets/ClientGUIBandwidth.py @@ -22,7 +22,7 @@ class BandwidthRulesCtrl( ClientGUICommon.StaticBox ): def __init__( self, parent, bandwidth_rules ): - ClientGUICommon.StaticBox.__init__( self, parent, 'bandwidth rules' ) + super().__init__( parent, 'bandwidth rules' ) listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -185,7 +185,7 @@ class _EditPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, rule ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._bandwidth_type = ClientGUICommon.BetterChoice( self ) diff --git a/hydrus/client/gui/widgets/ClientGUIColourPicker.py b/hydrus/client/gui/widgets/ClientGUIColourPicker.py index 6ac6201d8..631425a5b 100644 --- a/hydrus/client/gui/widgets/ClientGUIColourPicker.py +++ b/hydrus/client/gui/widgets/ClientGUIColourPicker.py @@ -119,7 +119,7 @@ class ColourPickerButton( QW.QPushButton ): def __init__( self, parent = None ): - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) self._colour = QG.QColor( 0, 0, 0, 0 ) diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py index c5914ccbe..284898cae 100644 --- a/hydrus/client/gui/widgets/ClientGUICommon.py +++ b/hydrus/client/gui/widgets/ClientGUICommon.py @@ -330,7 +330,7 @@ class BetterCheckBoxList( QW.QListWidget ): def __init__( self, parent: QW.QWidget ): - QW.QListWidget.__init__( self, parent ) + super().__init__( parent ) self.itemClicked.connect( self._ItemCheckStateChanged ) @@ -442,7 +442,7 @@ class BetterChoice( QW.QComboBox ): def __init__( self, *args, **kwargs ): - QW.QComboBox.__init__( self, *args, **kwargs ) + super().__init__( *args, **kwargs ) self.setMaxVisibleItems( 32 ) @@ -554,7 +554,7 @@ class BetterSpinBox( QW.QSpinBox ): def __init__( self, parent: QW.QWidget, initial = None, min = None, max = None, width = None ): - QW.QSpinBox.__init__( self, parent ) + super().__init__( parent ) if min is not None: @@ -581,7 +581,7 @@ class ButtonWithMenuArrow( QW.QToolButton ): def __init__( self, parent: QW.QWidget, action: QW.QAction ): - QW.QToolButton.__init__( self, parent ) + super().__init__( parent ) self.setPopupMode( QW.QToolButton.MenuButtonPopup ) @@ -639,7 +639,7 @@ class BetterRadioBox( QW.QFrame ): def __init__( self, parent, choice_tuples, vertical = False ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) @@ -750,7 +750,7 @@ def __init__( self, parent, label = None, tooltip_label = False, **kwargs ): ellipsize_end = 'ellipsize_end' in kwargs and kwargs[ 'ellipsize_end' ] - QP.EllipsizedLabel.__init__( self, parent, ellipsize_end = ellipsize_end ) + super().__init__( parent, ellipsize_end = ellipsize_end ) # otherwise by default html in 'this is a
      parsing step' stuff renders fully lmaoooo self.setTextFormat( QC.Qt.PlainText ) @@ -800,7 +800,7 @@ class BetterHyperLink( BetterStaticText ): def __init__( self, parent, label, url ): - BetterStaticText.__init__( self, parent, label ) + super().__init__( parent, label ) self._url = url @@ -844,7 +844,7 @@ class BufferedWindow( QW.QWidget ): def __init__( self, *args, **kwargs ): - QW.QWidget.__init__( self, *args ) + super().__init__( *args ) if 'size' in kwargs: @@ -875,7 +875,7 @@ def __init__( self, parent, pixmap: QG.QPixmap, click_callable = None ): device_independant_size = pixmap.size() / pixmap.devicePixelRatio() - BufferedWindow.__init__( self, parent, size = device_independant_size ) + super().__init__( parent, size = device_independant_size ) self._pixmap = pixmap self._click_callable = click_callable @@ -945,7 +945,7 @@ class CheckboxManagerBoolean( CheckboxManager ): def __init__( self, obj, name ): - CheckboxManager.__init__( self ) + super().__init__() self._obj = obj self._name = name @@ -977,7 +977,7 @@ class CheckboxManagerCalls( CheckboxManager ): def __init__( self, invert_call, value_call ): - CheckboxManager.__init__( self ) + super().__init__() self._invert_call = invert_call self._value_call = value_call @@ -997,7 +997,7 @@ class CheckboxManagerOptions( CheckboxManager ): def __init__( self, boolean_name ): - CheckboxManager.__init__( self ) + super().__init__() self._boolean_name = boolean_name @@ -1029,7 +1029,7 @@ class ExportPatternButton( BetterButton ): def __init__( self, parent ): - BetterButton.__init__( self, parent, 'pattern shortcuts', self._Hit ) + super().__init__( parent, 'pattern shortcuts', self._Hit ) def _Hit( self ): @@ -1062,7 +1062,7 @@ class Gauge( QW.QProgressBar ): def __init__( self, *args, **kwargs ): - QW.QProgressBar.__init__( self, *args, **kwargs ) + super().__init__( *args, **kwargs ) self._actual_value = None self._actual_range = None @@ -1519,7 +1519,7 @@ def __init__( self, parent, on_label, off_label = None, start_on = True ): if start_on: label = on_label else: label = off_label - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) QW.QPushButton.setText( self, label ) self.setObjectName( 'HydrusOnOffButton' ) @@ -1580,7 +1580,7 @@ class StaticBox( QW.QFrame ): def __init__( self, parent, title ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) self._spacer = QW.QSpacerItem( 0, 0, QW.QSizePolicy.Minimum, QW.QSizePolicy.MinimumExpanding ) @@ -1623,7 +1623,7 @@ class TextCatchEnterEventFilter( QC.QObject ): def __init__( self, parent, callable, *args, **kwargs ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._callable = HydrusData.Call( callable, *args, **kwargs ) diff --git a/hydrus/client/gui/widgets/ClientGUIRegex.py b/hydrus/client/gui/widgets/ClientGUIRegex.py index 03ac5e5a2..af9739b19 100644 --- a/hydrus/client/gui/widgets/ClientGUIRegex.py +++ b/hydrus/client/gui/widgets/ClientGUIRegex.py @@ -19,7 +19,7 @@ class RegexButton( ClientGUICommon.BetterButton ): def __init__( self, parent, show_group_menu = False ): - ClientGUICommon.BetterButton.__init__( self, parent, '.*', self._ShowMenu ) + super().__init__( parent, '.*', self._ShowMenu ) self._show_group_menu = show_group_menu diff --git a/hydrus/client/importing/ClientImportLocal.py b/hydrus/client/importing/ClientImportLocal.py index 5868bd1ea..fd0cd8e9f 100644 --- a/hydrus/client/importing/ClientImportLocal.py +++ b/hydrus/client/importing/ClientImportLocal.py @@ -529,7 +529,7 @@ def __init__( action_locations = {} - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._path = path self._file_import_options = file_import_options diff --git a/hydrus/client/importing/ClientImportSubscriptionLegacy.py b/hydrus/client/importing/ClientImportSubscriptionLegacy.py index 298a181c5..a0ddfa807 100644 --- a/hydrus/client/importing/ClientImportSubscriptionLegacy.py +++ b/hydrus/client/importing/ClientImportSubscriptionLegacy.py @@ -474,7 +474,7 @@ class SubscriptionLegacy( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name, gug_key_and_name = None ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) if gug_key_and_name is None: diff --git a/hydrus/client/importing/ClientImportSubscriptionQuery.py b/hydrus/client/importing/ClientImportSubscriptionQuery.py index ff4dce2cd..aacd9be96 100644 --- a/hydrus/client/importing/ClientImportSubscriptionQuery.py +++ b/hydrus/client/importing/ClientImportSubscriptionQuery.py @@ -32,7 +32,7 @@ class SubscriptionQueryLogContainer( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._gallery_seed_log = ClientImportGallerySeeds.GallerySeedLog() self._file_seed_cache = ClientImportFileSeeds.FileSeedCache() diff --git a/hydrus/client/importing/ClientImportSubscriptions.py b/hydrus/client/importing/ClientImportSubscriptions.py index 43eb82a4c..985b19254 100644 --- a/hydrus/client/importing/ClientImportSubscriptions.py +++ b/hydrus/client/importing/ClientImportSubscriptions.py @@ -36,7 +36,7 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name, gug_key_and_name = None ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) if gug_key_and_name is None: diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py index a0c33b24f..235dd5103 100644 --- a/hydrus/client/media/ClientMedia.py +++ b/hydrus/client/media/ClientMedia.py @@ -1239,8 +1239,11 @@ def ProcessContentUpdatePackage( self, full_content_update_package: ClientConten # physically_deleted = service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY - possibly_trashed = service_key in local_file_domains and action == HC.CONTENT_UPDATE_DELETE - deleted_from_our_domain = self._location_context.IsOneDomain() and service_key in self._location_context.current_service_keys + possibly_trashed = ( service_key in local_file_domains or service_key == CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) and action == HC.CONTENT_UPDATE_DELETE + + deleted_specifically_from_our_domain = self._location_context.IsOneDomain() and service_key in self._location_context.current_service_keys + deleted_implicitly_from_our_domain = set( self._location_context.current_service_keys ).issubset( local_file_domains ) and service_key == CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY + deleted_from_our_domain = deleted_specifically_from_our_domain or deleted_implicitly_from_our_domain we_are_looking_at_trash = self._location_context.IsOneDomain() and CC.TRASH_SERVICE_KEY in self._location_context.current_service_keys our_view_is_all_local = self._location_context.IncludesCurrent() and not self._location_context.IncludesDeleted() and self._location_context.current_service_keys.issubset( all_local_file_services ) diff --git a/hydrus/client/metadata/ClientMetadataMigrationImporters.py b/hydrus/client/metadata/ClientMetadataMigrationImporters.py index e884bc88d..08061cd74 100644 --- a/hydrus/client/metadata/ClientMetadataMigrationImporters.py +++ b/hydrus/client/metadata/ClientMetadataMigrationImporters.py @@ -776,6 +776,8 @@ def Import( self, actual_file_path: str ) -> typing.Collection[ str ]: raise Exception( f'Could not import from {path} (from file path {actual_file_path}: {e}' ) + raw_text = HydrusText.CleanseImportText( raw_text ) + rows = HydrusText.DeserialiseNewlinedTexts( raw_text ) if self._separator != '\n': diff --git a/hydrus/client/networking/ClientNetworkingBandwidth.py b/hydrus/client/networking/ClientNetworkingBandwidth.py index ba2eb6bf8..a969c1e11 100644 --- a/hydrus/client/networking/ClientNetworkingBandwidth.py +++ b/hydrus/client/networking/ClientNetworkingBandwidth.py @@ -31,7 +31,7 @@ def __init__( self, name, network_context = None, bandwidth_tracker = None ): bandwidth_tracker = HydrusNetworking.BandwidthTracker() - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self.network_context = network_context self.bandwidth_tracker = bandwidth_tracker diff --git a/hydrus/client/networking/ClientNetworkingGUG.py b/hydrus/client/networking/ClientNetworkingGUG.py index 28944282c..b687ca89c 100644 --- a/hydrus/client/networking/ClientNetworkingGUG.py +++ b/hydrus/client/networking/ClientNetworkingGUG.py @@ -44,7 +44,7 @@ def __init__( self, name, gug_key = None, url_template = None, replacement_phras example_search_text = 'blue_eyes blonde_hair' - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._gallery_url_generator_key = gug_key self._url_template = url_template @@ -244,7 +244,7 @@ def __init__( self, name, gug_key = None, initial_search_text = None, gug_keys_a gug_keys_and_names = [] - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._gallery_url_generator_key = gug_key self._initial_search_text = initial_search_text diff --git a/hydrus/client/networking/ClientNetworkingJobs.py b/hydrus/client/networking/ClientNetworkingJobs.py index e55080f75..4f9edbb40 100644 --- a/hydrus/client/networking/ClientNetworkingJobs.py +++ b/hydrus/client/networking/ClientNetworkingJobs.py @@ -2077,7 +2077,7 @@ def __init__( self, downloader_page_key, method, url, body = None, referral_url self._downloader_page_key = downloader_page_key - NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path ) + super().__init__( method, url, body = body, referral_url = referral_url, temp_path = temp_path ) def _GenerateNetworkContexts( self ): @@ -2097,7 +2097,7 @@ def __init__( self, subscription_key, method, url, body = None, referral_url = N self._subscription_key = subscription_key - NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path ) + super().__init__( method, url, body = body, referral_url = referral_url, temp_path = temp_path ) def _GenerateNetworkContexts( self ): @@ -2162,7 +2162,7 @@ def __init__( self, service_key, method, url, body = None, referral_url = None, self._service_key = service_key - NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path, file_body_path = file_body_path ) + super().__init__( method, url, body = body, referral_url = referral_url, temp_path = temp_path, file_body_path = file_body_path ) def _GenerateNetworkContexts( self ): @@ -2247,7 +2247,7 @@ def __init__( self, url, body = None, referral_url = None, temp_path = None ): method = 'POST' - NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path ) + super().__init__( method, url, body = body, referral_url = referral_url, temp_path = temp_path ) self.OnlyTryConnectionOnce() self.OverrideBandwidth() @@ -2268,7 +2268,7 @@ def __init__( self, watcher_key, method, url, body = None, referral_url = None, self._watcher_key = watcher_key - NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path ) + super().__init__( method, url, body = body, referral_url = referral_url, temp_path = temp_path ) def _GenerateNetworkContexts( self ): diff --git a/hydrus/client/networking/ClientNetworkingLogin.py b/hydrus/client/networking/ClientNetworkingLogin.py index 3e53c6209..8f56a9b04 100644 --- a/hydrus/client/networking/ClientNetworkingLogin.py +++ b/hydrus/client/networking/ClientNetworkingLogin.py @@ -873,7 +873,7 @@ def __init__( self, name = 'username', credential_type = CREDENTIAL_TYPE_TEXT, s string_match = ClientStrings.StringMatch() - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._credential_type = credential_type self._string_match = string_match @@ -977,7 +977,7 @@ class LoginProcessDomain( LoginProcess ): def __init__( self, engine, network_context, login_script, credentials ): - LoginProcess.__init__( self, engine, network_context, login_script ) + super().__init__( engine, network_context, login_script ) self.credentials = credentials @@ -1114,7 +1114,7 @@ def __init__( self, name = 'login script', login_script_key = None, required_coo example_domains_info = [] - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._login_script_key = HydrusData.GenerateKey() self._required_cookies_info = required_cookies_info # string match : string match @@ -1537,7 +1537,7 @@ class LoginStep( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name = 'hit home page to establish session', scheme = 'https', method = 'GET', subdomain = None, path = '/' ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._scheme = scheme self._method = method diff --git a/hydrus/client/networking/ClientNetworkingSessions.py b/hydrus/client/networking/ClientNetworkingSessions.py index f8db73614..c68071931 100644 --- a/hydrus/client/networking/ClientNetworkingSessions.py +++ b/hydrus/client/networking/ClientNetworkingSessions.py @@ -40,7 +40,7 @@ def __init__( self, name, network_context = None, session = None ): network_context = ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self.network_context = network_context self.session = session diff --git a/hydrus/client/networking/ClientNetworkingURLClass.py b/hydrus/client/networking/ClientNetworkingURLClass.py index 151ca240b..c24804732 100644 --- a/hydrus/client/networking/ClientNetworkingURLClass.py +++ b/hydrus/client/networking/ClientNetworkingURLClass.py @@ -321,7 +321,7 @@ def __init__( path_components = HydrusSerialisable.SerialisableList( path_components ) parameters = HydrusSerialisable.SerialisableList( parameters ) - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._url_class_key = url_class_key self._url_type = url_type diff --git a/hydrus/client/networking/api/ClientLocalServer.py b/hydrus/client/networking/api/ClientLocalServer.py index 6474d4253..5ce4543c6 100644 --- a/hydrus/client/networking/api/ClientLocalServer.py +++ b/hydrus/client/networking/api/ClientLocalServer.py @@ -30,7 +30,7 @@ def __init__( self, service, allow_non_local_connections ): self._client_requests_domain = HydrusServer.LOCAL_DOMAIN - HydrusServer.HydrusService.__init__( self, service ) + super().__init__( service ) @@ -99,6 +99,7 @@ def _InitRoot( self ): get_files.putChild( b'file_hashes', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesFileHashes( self._service, self._client_requests_domain ) ) get_files.putChild( b'file', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetFile( self._service, self._client_requests_domain ) ) get_files.putChild( b'file_path', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetFilePath( self._service, self._client_requests_domain) ) + get_files.putChild( b'local_file_storage_locations', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetLocalFileStorageLocations( self._service, self._client_requests_domain ) ) get_files.putChild( b'thumbnail', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( self._service, self._client_requests_domain ) ) get_files.putChild( b'thumbnail_path', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetThumbnailPath( self._service, self._client_requests_domain) ) get_files.putChild( b'render', ClientLocalServerResourcesGetFiles.HydrusResourceClientAPIRestrictedGetFilesGetRenderedFile( self._service, self._client_requests_domain) ) diff --git a/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py b/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py index 7cae37406..d139bf010 100644 --- a/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py +++ b/hydrus/client/networking/api/ClientLocalServerResourcesGetFiles.py @@ -2,6 +2,7 @@ import time from hydrus.core import HydrusConstants as HC +from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusTags from hydrus.core import HydrusTime @@ -808,7 +809,42 @@ def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ): -class HydrusResourceClientAPIRestrictedGetFilesGetFilePath( HydrusResourceClientAPIRestrictedGetFilesSearchFiles ): +class HydrusResourceClientAPIRestrictedGetFilesGetLocalFileStorageLocations( HydrusResourceClientAPIRestrictedGetFilesGetLocalPath ): + + def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): + + all_subfolders = CG.client_controller.client_files_manager.GetAllSubfolders() + + base_locations_to_subfolders = HydrusData.BuildKeyToListDict( [ ( subfolder.base_location, subfolder ) for subfolder in all_subfolders ] ) + + locations_list = [] + + for ( base_location, subfolders ) in sorted( base_locations_to_subfolders.items(), key = lambda b_l_s: b_l_s[0].path ): + + locations_structure = { + "path" : base_location.path, + "ideal_weight" : base_location.ideal_weight, + "max_num_bytes" : base_location.max_num_bytes, + "prefixes" : sorted( [ subfolder.prefix for subfolder in subfolders ] ) + } + + locations_list.append( locations_structure ) + + + body_dict = { + 'locations' : locations_list + } + + mime = request.preferred_mime + body = ClientLocalServerCore.Dumps( body_dict, mime ) + + response_context = HydrusServerResources.ResponseContext( 200, mime = mime, body = body ) + + return response_context + + + +class HydrusResourceClientAPIRestrictedGetFilesGetFilePath( HydrusResourceClientAPIRestrictedGetFilesGetLocalPath ): def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): @@ -849,7 +885,7 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): -class HydrusResourceClientAPIRestrictedGetFilesGetThumbnailPath( HydrusResourceClientAPIRestrictedGetFilesSearchFiles ): +class HydrusResourceClientAPIRestrictedGetFilesGetThumbnailPath( HydrusResourceClientAPIRestrictedGetFilesGetLocalPath ): def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): diff --git a/hydrus/client/search/ClientSearchAutocomplete.py b/hydrus/client/search/ClientSearchAutocomplete.py index 901a01a6b..eb2d5c4e5 100644 --- a/hydrus/client/search/ClientSearchAutocomplete.py +++ b/hydrus/client/search/ClientSearchAutocomplete.py @@ -483,7 +483,7 @@ class PredicateResultsCacheInit( PredicateResultsCache ): def __init__( self ): - PredicateResultsCache.__init__( self, [] ) + super().__init__( [] ) class PredicateResultsCacheSystem( PredicateResultsCache ): @@ -499,7 +499,7 @@ class PredicateResultsCacheTag( PredicateResultsCache ): def __init__( self, predicates: typing.Iterable[ ClientSearchPredicate.Predicate ], strict_search_text: str, exact_match: bool ): - PredicateResultsCache.__init__( self, predicates ) + super().__init__( predicates ) self._strict_search_text = strict_search_text diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 31dba4c55..d98f77e92 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -105,8 +105,8 @@ # Misc NETWORK_VERSION = 20 -SOFTWARE_VERSION = 591 -CLIENT_API_VERSION = 71 +SOFTWARE_VERSION = 592 +CLIENT_API_VERSION = 72 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 ) @@ -1467,10 +1467,11 @@ DOCUMENTATION_DOWNLOADER_SHARING = 'downloader_sharing.html' DOCUMENTATION_DOWNLOADER_PARSERS_PAGE_PARSERS_PAGE_PARSERS = 'downloader_parsers_page_parsers.html#page_parsers' DOCUMENTATION_DOWNLOADER_PARSERS_CONTENT_PARSERS_CONTENT_PARSERS = 'downloader_parsers_content_parsers.html#content_parsers' -DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_COMPOUND_FORMULA = 'downloader_parsers_formulae.html#compound_formula' +DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_ZIPPER_FORMULA = 'downloader_parsers_formulae.html#zipper_formula' DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_CONTEXT_VARIABLE_FORMULA = 'downloader_parsers_formulae.html#context_variable_formula' DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_HTML_FORMULA = 'downloader_parsers_formulae.html#html_formula' DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_JSON_FORMULA = 'downloader_parsers_formulae.html#json_formula' +DOCUMENTATION_DOWNLOADER_PARSERS_FORMULAE_NESTED_FORMULA = 'downloader_parsers_formulae.html#nested_formula' DOCUMENTATION_SIDECARS = 'advanced_sidecars.html' DOCUMENTATION_ABOUT_DOCS = "about_docs.html" diff --git a/hydrus/core/HydrusDB.py b/hydrus/core/HydrusDB.py index ee5cd7c18..2a7ffa07d 100644 --- a/hydrus/core/HydrusDB.py +++ b/hydrus/core/HydrusDB.py @@ -135,7 +135,7 @@ class HydrusDB( HydrusDBBase.DBBase ): def __init__( self, controller: HydrusControllerInterface.HydrusControllerInterface, db_dir, db_name ): - HydrusDBBase.DBBase.__init__( self ) + super().__init__() self._controller = controller self._db_dir = db_dir @@ -590,7 +590,7 @@ def _ManageDBError( self, job, e ): raise NotImplementedError() - def _ProcessJob( self, job ): + def _ProcessJob( self, job: HydrusDBBase.JobDatabase ): job_type = job.GetType() @@ -611,6 +611,10 @@ def _ProcessJob( self, job ): self.publish_status_update() + time_job_started = HydrusTime.GetNowPrecise() + + result = None + if job_type in ( 'read', 'read_write' ): result = self._Read( action, *args, **kwargs ) @@ -620,6 +624,15 @@ def _ProcessJob( self, job ): result = self._Write( action, *args, **kwargs ) + time_job_finished = HydrusTime.GetNowPrecise() + + time_job_took = time_job_finished - time_job_started + + if time_job_took > 15 and not self._controller.CurrentlyIdle(): + + HydrusData.Print( f'The database job "{job.ToString()}" took {HydrusTime.TimeDeltaToPrettyTimeDelta( time_job_took )}.' ) + + if job.IsSynchronous(): job.PutResult( result ) diff --git a/hydrus/core/HydrusDBBase.py b/hydrus/core/HydrusDBBase.py index 539e5496e..6f439e7cb 100644 --- a/hydrus/core/HydrusDBBase.py +++ b/hydrus/core/HydrusDBBase.py @@ -606,7 +606,7 @@ class DBCursorTransactionWrapper( DBBase ): def __init__( self, c: sqlite3.Cursor, transaction_commit_period: int ): - DBBase.__init__( self ) + super().__init__() self._SetCursor( c ) diff --git a/hydrus/core/HydrusDBModule.py b/hydrus/core/HydrusDBModule.py index d4029a04e..f7b6f3905 100644 --- a/hydrus/core/HydrusDBModule.py +++ b/hydrus/core/HydrusDBModule.py @@ -10,7 +10,7 @@ class HydrusDBModule( HydrusDBBase.DBBase ): def __init__( self, name, cursor: sqlite3.Cursor ): - HydrusDBBase.DBBase.__init__( self ) + super().__init__() self.name = name diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py index c8d1343aa..15dbd382d 100644 --- a/hydrus/core/HydrusSerialisable.py +++ b/hydrus/core/HydrusSerialisable.py @@ -73,7 +73,7 @@ SERIALISABLE_TYPE_FILENAME_TAGGING_OPTIONS = 56 SERIALISABLE_TYPE_FILE_SEED = 57 SERIALISABLE_TYPE_PAGE_PARSER = 58 -SERIALISABLE_TYPE_PARSE_FORMULA_COMPOUND = 59 +SERIALISABLE_TYPE_PARSE_FORMULA_ZIPPER = 59 SERIALISABLE_TYPE_PARSE_FORMULA_CONTEXT_VARIABLE = 60 SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR = 61 SERIALISABLE_TYPE_PARSE_RULE_HTML = 62 @@ -147,6 +147,7 @@ SERIALISABLE_TYPE_AUTO_DUPLICATES_PAIR_COMPARATOR_ONE_FILE = 130 SERIALISABLE_TYPE_AUTO_DUPLICATES_PAIR_COMPARATOR_TWO_FILES_RELATIVE = 131 SERIALISABLE_TYPE_METADATA_CONDITIONAL = 132 +SERIALISABLE_TYPE_PARSE_FORMULA_NESTED = 133 SERIALISABLE_TYPES_TO_OBJECT_TYPES = {} @@ -399,7 +400,7 @@ class SerialisableBaseNamed( SerialisableBase ): def __init__( self, name ): - SerialisableBase.__init__( self ) + super().__init__() self._name = name diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py index 3895471f3..3ef08675f 100644 --- a/hydrus/core/HydrusText.py +++ b/hydrus/core/HydrusText.py @@ -28,8 +28,36 @@ re_leading_colons = re.compile( '^:+' ) re_leading_byte_order_mark = re.compile( '^' + HC.UNICODE_BYTE_ORDER_MARK ) # unicode .txt files prepend with this, wew +re_has_surrogate_garbage = re.compile( r'[\ud800-\udfff]' ) + HYDRUS_NOTE_NEWLINE = '\n' +def CleanseImportText( text: str ): + + # the website has placed utf-16 characters here due to a failure to encode some emoji properly + # we try to fix it + if re_has_surrogate_garbage.search( text ) is not None: + + try: + + return text.encode( 'utf-16', 'surrogatepass' ).decode( 'utf-16' ) + + except: + + import HydrusData + + HydrusData.Print( f'Could not cleanse surrogates from this: {text}' ) + + + + return text + + +def CleanseImportTexts( texts: typing.Collection[ str ] ): + + return [ CleanseImportText( text ) for text in texts ] + + def CleanNoteText( t: str ): # trim leading and trailing whitespace diff --git a/hydrus/core/HydrusThreading.py b/hydrus/core/HydrusThreading.py index 11877ca15..e51369102 100644 --- a/hydrus/core/HydrusThreading.py +++ b/hydrus/core/HydrusThreading.py @@ -191,7 +191,7 @@ class DAEMON( threading.Thread ): def __init__( self, controller, name ): - threading.Thread.__init__( self, name = name ) + super().__init__( name = name ) self._controller = controller self._name = name @@ -241,7 +241,7 @@ def __init__( self, controller, name, callable, topics = None, period = 3600, in topics = [] - DAEMON.__init__( self, controller, name ) + super().__init__( controller, name ) self._callable = callable self._topics = topics @@ -377,7 +377,7 @@ class THREADCallToThread( DAEMON ): def __init__( self, controller, name ): - DAEMON.__init__( self, controller, name ) + super().__init__( controller, name ) self._callable = None @@ -484,7 +484,7 @@ class JobScheduler( threading.Thread ): def __init__( self, controller ): - threading.Thread.__init__( self, name = 'Job Scheduler' ) + super().__init__( name = 'Job Scheduler' ) self._controller = controller @@ -748,7 +748,7 @@ class SchedulableJob( HydrusThreadingInterface.SchedulableJobInterface ): def __init__( self, controller, scheduler: JobScheduler, initial_delay, work_callable ): - HydrusThreadingInterface.SchedulableJobInterface.__init__( self ) + super().__init__() self._controller = controller self._scheduler = scheduler @@ -973,7 +973,7 @@ class SingleJob( SchedulableJob ): def __init__( self, controller, scheduler: JobScheduler, initial_delay, work_callable ): - SchedulableJob.__init__( self, controller, scheduler, initial_delay, work_callable ) + super().__init__( controller, scheduler, initial_delay, work_callable ) self._work_complete = threading.Event() @@ -997,7 +997,7 @@ class RepeatingJob( SchedulableJob ): def __init__( self, controller, scheduler: JobScheduler, initial_delay, period, work_callable ): - SchedulableJob.__init__( self, controller, scheduler, initial_delay, work_callable ) + super().__init__( controller, scheduler, initial_delay, work_callable ) self._period = period diff --git a/hydrus/core/networking/HydrusServer.py b/hydrus/core/networking/HydrusServer.py index 7547f6519..94fb6b83e 100644 --- a/hydrus/core/networking/HydrusServer.py +++ b/hydrus/core/networking/HydrusServer.py @@ -15,6 +15,7 @@ class FatHTTPChannel( HTTPChannel ): MAX_LENGTH = 2 * 1048576 totalHeadersSize = 2 * 1048576 # :^) + class HydrusService( Site ): def __init__( self, service ): @@ -34,7 +35,7 @@ def __init__( self, service ): root = self._InitRoot() - Site.__init__( self, root ) + super().__init__( root ) self.protocol = self._ProtocolFactory diff --git a/hydrus/core/networking/HydrusServerAMP.py b/hydrus/core/networking/HydrusServerAMP.py index 143a32bba..cdb555574 100644 --- a/hydrus/core/networking/HydrusServerAMP.py +++ b/hydrus/core/networking/HydrusServerAMP.py @@ -86,7 +86,7 @@ class MessagingServiceProtocol( HydrusAMP ): def __init__( self ): - amp.AMP.__init__( self ) + super().__init__() self._identifier = None self._name = None diff --git a/hydrus/core/networking/HydrusServerResources.py b/hydrus/core/networking/HydrusServerResources.py index c447143c2..6ab12e066 100644 --- a/hydrus/core/networking/HydrusServerResources.py +++ b/hydrus/core/networking/HydrusServerResources.py @@ -429,7 +429,7 @@ class HydrusResource( Resource ): def __init__( self, service, domain ): - Resource.__init__( self ) + super().__init__() self._service = service self._service_key = self._service.GetServiceKey() diff --git a/hydrus/server/ServerController.py b/hydrus/server/ServerController.py index c541403df..72c7e9a9e 100644 --- a/hydrus/server/ServerController.py +++ b/hydrus/server/ServerController.py @@ -169,7 +169,7 @@ class Controller( HydrusController.HydrusController ): def __init__( self, db_dir ): - HydrusController.HydrusController.__init__( self, db_dir ) + super().__init__( db_dir ) self._name = 'server' diff --git a/hydrus/server/ServerDB.py b/hydrus/server/ServerDB.py index 3233cd240..eb558d372 100644 --- a/hydrus/server/ServerDB.py +++ b/hydrus/server/ServerDB.py @@ -155,7 +155,7 @@ def __init__( self, controller, db_dir, db_name ): self._account_type_ids_to_account_types = {} self._service_ids_to_account_type_keys_to_account_type_ids = collections.defaultdict( dict ) - HydrusDB.HydrusDB.__init__( self, controller, db_dir, db_name ) + super().__init__( controller, db_dir, db_name ) def _AddAccountType( self, service_id, account_type: HydrusNetwork.AccountType ): diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py index f32724847..1c0fdee7e 100644 --- a/hydrus/test/TestClientAPI.py +++ b/hydrus/test/TestClientAPI.py @@ -7072,6 +7072,31 @@ def _test_get_files( self, connection, set_up_permissions ): os.unlink( file_path ) os.unlink( thumb_path ) + # local paths + + path = '/get_files/local_file_storage_locations' + + connection.request( 'GET', path, headers = headers ) + + response = connection.getresponse() + + data = response.read() + + text = str( data, 'utf-8' ) + + d = json.loads( text ) + + self.assertEqual( response.status, 200 ) + + locations = d[ 'locations' ] + + self.assertEqual( len( locations ), 1 ) + + self.assertEqual( locations[0][ 'ideal_weight' ], 1 ) + self.assertEqual( locations[0][ 'max_num_bytes' ], None ) + self.assertEqual( locations[0][ 'path' ], os.path.join( HG.test_controller.db_dir, 'client_files' ) ) + self.assertEqual( set( locations[0][ 'prefixes' ] ), { f'f{p}' for p in HydrusData.IterateHexPrefixes() }.union( { f't{p}' for p in HydrusData.IterateHexPrefixes() } ) ) + def _test_permission_failures( self, connection, set_up_permissions ): diff --git a/hydrus/test/TestClientMetadataMigration.py b/hydrus/test/TestClientMetadataMigration.py index 01fb5f590..bd1be5edb 100644 --- a/hydrus/test/TestClientMetadataMigration.py +++ b/hydrus/test/TestClientMetadataMigration.py @@ -138,8 +138,6 @@ def test_router( self ): string_processor.SetProcessingSteps( processing_steps ) - exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT() - router = ClientMetadataMigration.SingleFileMetadataRouter( importers = [ importer_1, importer_2 ], string_processor = string_processor, exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT() ) router.Work( media_result, actual_file_path ) diff --git a/hydrus/test/TestClientMigration.py b/hydrus/test/TestClientMigration.py index 9e8484333..d52bde3b0 100644 --- a/hydrus/test/TestClientMigration.py +++ b/hydrus/test/TestClientMigration.py @@ -849,11 +849,12 @@ def run_test( source, expected_data ): left_side_needs_count = False right_side_needs_count = False + either_side_needs_count = False needs_count_service_key = CC.DEFAULT_LOCAL_TAG_SERVICE_KEY for ( left_tag_filter, right_tag_filter ) in test_filters: - source = ClientMigration.MigrationSourceHTPA( self, htpa_path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceHTPA( self, htpa_path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) expected_data = [ ( left_tag, right_tag ) for ( left_tag, right_tag ) in current if left_tag_filter.TagOK( left_tag ) and right_tag_filter.TagOK( right_tag ) ] @@ -942,13 +943,14 @@ def run_test( source, expected_data ): left_side_needs_count = False right_side_needs_count = False + either_side_needs_count = False needs_count_service_key = CC.DEFAULT_LOCAL_TAG_SERVICE_KEY for ( left_tag_filter, right_tag_filter ) in test_filters: for ( service_key, content_lists, content_statuses ) in content_source_tests: - source = ClientMigration.MigrationSourceTagServicePairs( self, service_key, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceTagServicePairs( self, service_key, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) expected_data = set() @@ -1111,13 +1113,15 @@ def run_test( source, expected_data ): left_tag_filter = HydrusTags.TagFilter() right_tag_filter = HydrusTags.TagFilter() + either_side_needs_count = False + for left_side_needs_count in ( False, True ): for right_side_needs_count in ( False, True ): for needs_count_service_key in ( repo_service_key, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY ): - source = ClientMigration.MigrationSourceHTPA( self, htpa_path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceHTPA( self, htpa_path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) if needs_count_service_key == repo_service_key: @@ -1140,6 +1144,35 @@ def run_test( source, expected_data ): + left_side_needs_count = False + right_side_needs_count = False + + for either_side_needs_count in ( False, True ): + + for needs_count_service_key in ( repo_service_key, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY ): + + source = ClientMigration.MigrationSourceHTPA( self, htpa_path, content_type, left_tag_filter, right_tag_filter, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) + + if needs_count_service_key == repo_service_key: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count ] + + else: + + if content_type == HC.CONTENT_TYPE_TAG_SIBLINGS: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count or 'has_count' in a or 'ideal_yes' in a ] + + else: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count or 'has_count' in a or 'has_count' in b ] + + + + run_test( source, expected_data ) + + + # os.remove( htpa_path ) @@ -1169,13 +1202,15 @@ def run_test( source, expected_data ): left_tag_filter = HydrusTags.TagFilter() right_tag_filter = HydrusTags.TagFilter() + either_side_needs_count = False + for left_side_needs_count in ( False, True ): for right_side_needs_count in ( False, True ): for needs_count_service_key in ( repo_service_key, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY ): - source = ClientMigration.MigrationSourceTagServicePairs( self, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, needs_count_service_key ) + source = ClientMigration.MigrationSourceTagServicePairs( self, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) if needs_count_service_key == repo_service_key: @@ -1198,6 +1233,35 @@ def run_test( source, expected_data ): + left_side_needs_count = False + right_side_needs_count = False + + for either_side_needs_count in ( False, True ): + + for needs_count_service_key in ( repo_service_key, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY ): + + source = ClientMigration.MigrationSourceTagServicePairs( self, CC.DEFAULT_LOCAL_TAG_SERVICE_KEY, content_type, left_tag_filter, right_tag_filter, content_statuses, left_side_needs_count, right_side_needs_count, either_side_needs_count, needs_count_service_key ) + + if needs_count_service_key == repo_service_key: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count ] + + else: + + if content_type == HC.CONTENT_TYPE_TAG_SIBLINGS: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count or 'has_count' in a or 'ideal_yes' in a ] + + else: + + expected_data = [ ( a, b ) for ( a, b ) in count_filter_pairs[ content_type ] if not either_side_needs_count or 'has_count' in a or 'has_count' in b ] + + + + run_test( source, expected_data ) + + + def test_migration_mappings( self ): diff --git a/hydrus/test/TestClientParsing.py b/hydrus/test/TestClientParsing.py index 5cfd8bffb..554da97a0 100644 --- a/hydrus/test/TestClientParsing.py +++ b/hydrus/test/TestClientParsing.py @@ -14,7 +14,7 @@ class DummyFormula( ClientParsing.ParseFormula ): def __init__( self, result: typing.List[ str ] ): - ClientParsing.ParseFormula.__init__( self ) + super().__init__() self._result = result @@ -45,7 +45,7 @@ def ToPrettyMultilineString( self ): -class TestParseFormulaCompound( unittest.TestCase ): +class TestParseFormulaZipper( unittest.TestCase ): def test_complex_unicode( self ): @@ -57,7 +57,7 @@ def test_complex_unicode( self ): DummyFormula( [ b ] ) ] - pfc = ClientParsing.ParseFormulaCompound( formulae = formulae, sub_phrase = '\\1 \\2' ) + pfc = ClientParsing.ParseFormulaZipper( formulae = formulae, sub_phrase = '\\1 \\2' ) result = pfc.Parse( {}, 'gumpf', False ) @@ -65,6 +65,75 @@ def test_complex_unicode( self ): +class TestParseFormulaNested( unittest.TestCase ): + + def test_complex_unicode( self ): + + import html + import json + + payload = { 'test' : [1,'"yo"',3] } + + json_payload = json.dumps( payload ) + + parsing_text = f'
      hello!
      ' + + main_formula = ClientParsing.ParseFormulaHTML( + tag_rules = [ ClientParsing.ParseRuleHTML( rule_type = ClientParsing.HTML_RULE_TYPE_DESCENDING, tag_name = 'muh_tag' ) ], + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE, + attribute_to_fetch = 'buried-json-data' + ) + + sub_formula = ClientParsing.ParseFormulaJSON( + parse_rules = [ + ( ClientParsing.JSON_PARSE_RULE_TYPE_DICT_KEY, ClientStrings.StringMatch( match_type = ClientStrings.STRING_MATCH_FIXED, match_value = 'test', example_string = 'test' ) ), + ( ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM, 1 ) + ], + content_to_fetch = ClientParsing.JSON_CONTENT_STRING + ) + + pfn = ClientParsing.ParseFormulaNested( main_formula, sub_formula ) + + result = pfn.Parse( {}, parsing_text, True ) + + self.assertEqual( result, [ '"yo"' ] ) + + # + + payload = '
      hello
      ' + + data = { + 'test' : [ + 'hello', + payload, + 'some_other_thing' + ] + } + + parsing_text = json.dumps( data ) + + main_formula = ClientParsing.ParseFormulaJSON( + parse_rules = [ + ( ClientParsing.JSON_PARSE_RULE_TYPE_DICT_KEY, ClientStrings.StringMatch( match_type = ClientStrings.STRING_MATCH_FIXED, match_value = 'test', example_string = 'test' ) ), + ( ClientParsing.JSON_PARSE_RULE_TYPE_INDEXED_ITEM, 1 ) + ], + content_to_fetch = ClientParsing.JSON_CONTENT_STRING + ) + + sub_formula = ClientParsing.ParseFormulaHTML( + tag_rules = [ ClientParsing.ParseRuleHTML( rule_type = ClientParsing.HTML_RULE_TYPE_DESCENDING, tag_name = 'muh_tag' ) ], + content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE, + attribute_to_fetch = 'quantity' + ) + + pfn = ClientParsing.ParseFormulaNested( main_formula, sub_formula ) + + result = pfn.Parse( {}, parsing_text, True ) + + self.assertEqual( result, [ '>implying' ] ) + + + class TestContentParser( unittest.TestCase ): def test_mappings( self ): @@ -326,6 +395,7 @@ def test_compound( self ): self.assertEqual( string_converter.Convert( '0123456789' ), 'z xddddddcba' ) + class TestStringJoiner( unittest.TestCase ): def test_basics( self ): diff --git a/hydrus/test/TestController.py b/hydrus/test/TestController.py index a76febb68..4ddeda532 100644 --- a/hydrus/test/TestController.py +++ b/hydrus/test/TestController.py @@ -147,7 +147,7 @@ class TestFrame( QW.QWidget ): def __init__( self ): - QW.QWidget.__init__( self, None ) + super().__init__( None ) def SetPanel( self, panel ): diff --git a/hydrus/test/TestHydrusTags.py b/hydrus/test/TestHydrusTags.py index 6ed667d26..44a3b9f98 100644 --- a/hydrus/test/TestHydrusTags.py +++ b/hydrus/test/TestHydrusTags.py @@ -1,8 +1,7 @@ import unittest -from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusTags -from hydrus.core import HydrusGlobals as HG +from hydrus.core import HydrusText class TestHydrusTags( unittest.TestCase ): @@ -19,4 +18,7 @@ def test_cleaning_and_combining( self ): self.assertEqual( HydrusTags.CombineTag( '', 'unnamespace:withcolon' ), ':unnamespace:withcolon' ) + # note this is a dangerous string bro and the debugger will freak out if you inspect it + self.assertEqual( HydrusText.CleanseImportText( 'test \ud83d\ude1c' ), 'test \U0001f61c' ) + diff --git a/requirements.txt b/requirements.txt index 36e1d3472..65760a046 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,9 +26,9 @@ Twisted>=20.3.0 opencv-python-headless==4.8.1.78 mpv==1.0.6 -requests==2.31.0 QtPy==2.4.1 PySide6==6.6.3.1 +requests==2.31.0 setuptools==69.1.1 diff --git a/setup_venv.bat b/setup_venv.bat index a3fcab90e..f93b7b0f2 100644 --- a/setup_venv.bat +++ b/setup_venv.bat @@ -112,8 +112,8 @@ ECHO Most people want "6". ECHO If you are on Windows ^<=8.1, choose "5". If you want a specific version, choose "a". SET /P qt="Do you want Qt(5), Qt(6), or (a)dvanced? " -IF "%qt%" == "5" goto :question_mpv -IF "%qt%" == "6" goto :question_mpv +IF "%qt%" == "5" goto :question_qt_done +IF "%qt%" == "6" goto :question_qt_done IF "%qt%" == "a" goto :question_qt_advanced goto :parse_fail @@ -123,9 +123,9 @@ ECHO: ECHO If you have multi-monitor menu position bugs with the normal Qt6, try "o" on Python 3.9 or "m" on Python 3.10. SET /P qt="Do you want Qt6 (o)lder, Qt6 (m)iddle, Qt6 (t)est, or (w)rite your own? " -IF "%qt%" == "o" goto :question_mpv -IF "%qt%" == "m" goto :question_mpv -IF "%qt%" == "t" goto :question_mpv +IF "%qt%" == "o" goto :question_qt_advanced_done +IF "%qt%" == "m" goto :question_qt_advanced_done +IF "%qt%" == "t" goto :question_qt_advanced_done IF "%qt%" == "w" goto :question_qt_custom goto :parse_fail @@ -139,7 +139,9 @@ ECHO - For Python 3.12, your earliest available version is 6.6.0 SET /P qt_custom_pyside6="Version: " SET /P qt_custom_qtpy="Enter the exact qtpy version you want (probably '2.4.1'; if older try '2.3.1'): " -goto :question_mpv +:question_qt_custom_done +:question_qt_advanced_done +:question_qt_done :question_mpv @@ -151,38 +153,45 @@ ECHO Most people want "n". ECHO If it doesn't work, fall back to "o". SET /P mpv="Do you want (o)ld mpv, (n)ew mpv, or (t)est mpv? " -IF "%mpv%" == "o" goto :question_pillow -IF "%mpv%" == "n" goto :question_pillow -IF "%mpv%" == "t" goto :question_pillow +IF "%mpv%" == "o" goto :question_mpv_done +IF "%mpv%" == "n" goto :question_mpv_done +IF "%mpv%" == "t" goto :question_mpv_done goto :parse_fail -:question_pillow +:question_mpv_done +:question_opencv ECHO -------- -ECHO Pillow - Images +ECHO OpenCV - Images ECHO: ECHO Most people want "n". -ECHO If you are Python 3.7 or earlier, choose "o". -SET /P pillow="Do you want (o)ld pillow or (n)ew pillow? " +ECHO Python ^>=3.11 might need "t". +SET /P opencv="Do you want (n)ew OpenCV or (t)est OpenCV? " -IF "%pillow%" == "o" goto :question_opencv -IF "%pillow%" == "n" goto :question_opencv +IF "%opencv%" == "o" goto :question_opencv_done +IF "%opencv%" == "n" goto :question_opencv_done +IF "%opencv%" == "t" goto :question_opencv_done goto :parse_fail -:question_opencv +:question_opencv_done +:question_future + +set future=n + +REM comment this guy out if no special stuff going on ECHO -------- -ECHO OpenCV - Images +ECHO Future Libraries ECHO: -ECHO Most people want "n". -ECHO If it doesn't work, fall back to "o". Python ^>=3.11 might need "t". -SET /P opencv="Do you want (o)ld OpenCV, (n)ew OpenCV, or (t)est OpenCV? " +ECHO There is a future test of requests and setuptools. Want to try it? +SET /P future="(y)es/(n)o? " -IF "%opencv%" == "o" goto :create -IF "%opencv%" == "n" goto :create -IF "%opencv%" == "t" goto :create +IF "%future%" == "y" goto :question_future_done +IF "%future%" == "n" goto :question_future_done goto :parse_fail +:question_future_done + :create ECHO -------- @@ -221,9 +230,9 @@ IF "%install_type%" == "d" ( python -m pip install pyside2 python -m pip install PyQtChart PyQt5 python -m pip install PyQt6-Charts PyQt6 - python -m pip install -r static\requirements\advanced\requirements_pillow_new.txt python -m pip install -r static\requirements\advanced\requirements_mpv_test.txt python -m pip install -r static\requirements\advanced\requirements_opencv_test.txt + python -m pip install -r static\requirements\advanced\requirements_other_future.txt python -m pip install -r static\requirements\hydev\requirements_windows_build.txt ) @@ -266,9 +275,6 @@ IF "%install_type%" == "a" ( IF "%qt%" == "m" python -m pip install -r static\requirements\advanced\requirements_qt6_middle.txt IF "%qt%" == "t" python -m pip install -r static\requirements\advanced\requirements_qt6_test.txt - IF "%pillow%" == "o" python -m pip install -r static\requirements\advanced\requirements_pillow_old.txt - IF "%pillow%" == "n" python -m pip install -r static\requirements\advanced\requirements_pillow_new.txt - IF "%mpv%" == "o" python -m pip install -r static\requirements\advanced\requirements_mpv_old.txt IF "%mpv%" == "n" python -m pip install -r static\requirements\advanced\requirements_mpv_new.txt IF "%mpv%" == "t" python -m pip install -r static\requirements\advanced\requirements_mpv_test.txt @@ -277,6 +283,10 @@ IF "%install_type%" == "a" ( IF "%opencv%" == "n" python -m pip install -r static\requirements\advanced\requirements_opencv_new.txt IF "%opencv%" == "t" python -m pip install -r static\requirements\advanced\requirements_opencv_test.txt + IF "%future%" == "n" python -m pip install -r static\requirements\advanced\requirements_other_normal.txt + IF "%future%" == "y" python -m pip install -r static\requirements\advanced\requirements_other_future.txt + + ) CALL %venv_location%\Scripts\deactivate.bat diff --git a/setup_venv.command b/setup_venv.command index caa43bb61..b8b159c22 100755 --- a/setup_venv.command +++ b/setup_venv.command @@ -131,15 +131,17 @@ elif [ "$install_type" = "a" ]; then fi echo "--------" - echo "Pillow - Images" + echo "OpenCV - Images" echo echo "Most people want \"n\"." - echo "If you are Python 3.7 or earlier, choose \"o\"" - echo "Do you want (o)ld pillow or (n)ew pillow? " - read -r pillow - if [ "$pillow" = "o" ]; then + echo "Python >=3.11 might need \"t\"." + echo "Do you want (n)ew OpenCV or (t)est OpenCV? " + read -r opencv + if [ "$opencv" = "o" ]; then + : + elif [ "$opencv" = "n" ]; then : - elif [ "$pillow" = "n" ]; then + elif [ "$opencv" = "t" ]; then : else echo "Sorry, did not understand that input!" @@ -147,18 +149,18 @@ elif [ "$install_type" = "a" ]; then exit 1 fi + future=n + + # comment this guy out if no special stuff going on echo "--------" - echo "OpenCV - Images" + echo "Future Libraries" echo - echo "Most people want \"n\"." - echo "If it doesn't work, fall back to \"o\". Python >=3.11 might need \"t\"." - echo "Do you want (o)ld OpenCV, (n)ew OpenCV, or (t)est OpenCV? " - read -r opencv - if [ "$opencv" = "o" ]; then + echo "There is a test of new requests and setuptools. Want to try it?" + echo "(y)es/(n)o? " + read -r future + if [ "$future" = "y" ]; then : - elif [ "$opencv" = "n" ]; then - : - elif [ "$opencv" = "t" ]; then + elif [ "$future" = "n" ]; then : else echo "Sorry, did not understand that input!" @@ -234,12 +236,6 @@ elif [ "$install_type" = "a" ]; then python -m pip install -r static/requirements/advanced/requirements_mpv_test.txt fi - if [ "$pillow" = "o" ]; then - python -m pip install -r static/requirements/advanced/requirements_pillow_old.txt - elif [ "$pillow" = "n" ]; then - python -m pip install -r static/requirements/advanced/requirements_pillow_new.txt - fi - if [ "$opencv" = "o" ]; then python -m pip install -r static/requirements/advanced/requirements_opencv_old.txt elif [ "$opencv" = "n" ]; then @@ -247,6 +243,13 @@ elif [ "$install_type" = "a" ]; then elif [ "$opencv" = "t" ]; then python -m pip install -r static/requirements/advanced/requirements_opencv_test.txt fi + + if [ "$future" = "n" ]; then + python -m pip install -r static/requirements/advanced/requirements_other_normal.txt + elif [ "$future" = "y" ]; then + python -m pip install -r static/requirements/advanced/requirements_other_future.txt + fi + fi python -m pip install -r static/requirements/advanced/requirements_macos.txt diff --git a/setup_venv.sh b/setup_venv.sh index 721921df9..0d8bafb89 100755 --- a/setup_venv.sh +++ b/setup_venv.sh @@ -133,15 +133,17 @@ elif [ "$install_type" = "a" ]; then fi echo "--------" - echo "Pillow - Images" + echo "OpenCV - Images" echo echo "Most people want \"n\"." - echo "If you are Python 3.7 or earlier, choose \"o\"" - echo "Do you want (o)ld pillow or (n)ew pillow? " - read -r pillow - if [ "$pillow" = "o" ]; then + echo "Python >=3.11 might need \"t\"." + echo "Do you want (n)ew OpenCV or (t)est OpenCV? " + read -r opencv + if [ "$opencv" = "o" ]; then + : + elif [ "$opencv" = "n" ]; then : - elif [ "$pillow" = "n" ]; then + elif [ "$opencv" = "t" ]; then : else echo "Sorry, did not understand that input!" @@ -149,18 +151,18 @@ elif [ "$install_type" = "a" ]; then exit 1 fi + future=n + + # comment this guy out if no special stuff going on echo "--------" - echo "OpenCV - Images" + echo "Future Libraries" echo - echo "Most people want \"n\"." - echo "If it doesn't work, fall back to \"o\". Python >=3.11 might need \"t\"." - echo "Do you want (o)ld OpenCV, (n)ew OpenCV, or (t)est OpenCV? " - read -r opencv - if [ "$opencv" = "o" ]; then + echo "There is a test of new requests and setuptools. Want to try it?" + echo "(y)es/(n)o? " + read -r future + if [ "$future" = "y" ]; then : - elif [ "$opencv" = "n" ]; then - : - elif [ "$opencv" = "t" ]; then + elif [ "$future" = "n" ]; then : else echo "Sorry, did not understand that input!" @@ -234,12 +236,6 @@ elif [ "$install_type" = "a" ]; then python -m pip install -r static/requirements/advanced/requirements_mpv_test.txt fi - if [ "$pillow" = "o" ]; then - python -m pip install -r static/requirements/advanced/requirements_pillow_old.txt - elif [ "$pillow" = "n" ]; then - python -m pip install -r static/requirements/advanced/requirements_pillow_new.txt - fi - if [ "$opencv" = "o" ]; then python -m pip install -r static/requirements/advanced/requirements_opencv_old.txt elif [ "$opencv" = "n" ]; then @@ -247,6 +243,13 @@ elif [ "$install_type" = "a" ]; then elif [ "$opencv" = "t" ]; then python -m pip install -r static/requirements/advanced/requirements_opencv_test.txt fi + + if [ "$future" = "n" ]; then + python -m pip install -r static/requirements/advanced/requirements_other_normal.txt + elif [ "$future" = "y" ]; then + python -m pip install -r static/requirements/advanced/requirements_other_future.txt + fi + fi deactivate diff --git a/static/build_files/linux/requirements.txt b/static/build_files/linux/requirements.txt index 7bd813e94..2345d839d 100644 --- a/static/build_files/linux/requirements.txt +++ b/static/build_files/linux/requirements.txt @@ -26,11 +26,11 @@ Twisted>=20.3.0 opencv-python-headless==4.8.1.78 mpv==1.0.6 -requests==2.31.0 QtPy==2.4.1 PySide6==6.6.3.1 +requests==2.31.0 setuptools==69.1.1 pyinstaller==6.2 diff --git a/static/build_files/macos/requirements.txt b/static/build_files/macos/requirements.txt index 80d46cddf..e402ed58d 100644 --- a/static/build_files/macos/requirements.txt +++ b/static/build_files/macos/requirements.txt @@ -26,12 +26,12 @@ Twisted>=20.3.0 opencv-python-headless==4.8.1.78 mpv==1.0.6 -requests==2.31.0 QtPy==2.4.1 PyQt6==6.6.0 PyQt6-Qt6==6.6.0 +requests==2.31.0 setuptools==69.1.1 pyobjc-core>=10.1 diff --git a/static/build_files/windows/requirements.txt b/static/build_files/windows/requirements.txt index 8854e8056..d023f5702 100644 --- a/static/build_files/windows/requirements.txt +++ b/static/build_files/windows/requirements.txt @@ -26,11 +26,11 @@ Twisted>=20.3.0 opencv-python-headless==4.8.1.78 mpv==1.0.6 -requests==2.31.0 QtPy==2.4.1 PySide6==6.6.3.1 +requests==2.31.0 setuptools==69.1.1 pyinstaller==6.2 diff --git a/static/default/parsers/derpibooru.org file page parser.png b/static/default/parsers/derpibooru.org file page parser.png index 13c48ae2c75dfa205edd46895618455d3690317d..4dbf99d67c615ea0dc0a386d17ecfd109b6b51c0 100644 GIT binary patch delta 3093 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 delta 2949 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.5.0 lz4>=3.0.0 numpy>=1.16.0,<2.0.0 olefile>=0.47 +Pillow>=10.0.1 +pillow-heif>=0.12.0 psd-tools>=1.9.28 psutil>=5.0.0 pyOpenSSL>=19.1.0 @@ -21,6 +23,3 @@ Send2Trash>=1.5.0 service-identity>=18.1.0 show-in-file-manager>=1.1.5 Twisted>=20.3.0 - -requests==2.31.0 -setuptools==69.1.1 diff --git a/static/requirements/advanced/requirements_opencv_old.txt b/static/requirements/advanced/requirements_opencv_old.txt index dab81343b..e2fc3d4d7 100644 --- a/static/requirements/advanced/requirements_opencv_old.txt +++ b/static/requirements/advanced/requirements_opencv_old.txt @@ -1 +1 @@ -opencv-python-headless==4.5.3.56 +opencv-python-headless==4.8.1.78 diff --git a/static/requirements/advanced/requirements_other_future.txt b/static/requirements/advanced/requirements_other_future.txt new file mode 100644 index 000000000..029c2ca0d --- /dev/null +++ b/static/requirements/advanced/requirements_other_future.txt @@ -0,0 +1,2 @@ +requests==2.32.3 +setuptools==70.3.0 diff --git a/static/requirements/advanced/requirements_other_normal.txt b/static/requirements/advanced/requirements_other_normal.txt new file mode 100644 index 000000000..a77cc3beb --- /dev/null +++ b/static/requirements/advanced/requirements_other_normal.txt @@ -0,0 +1,2 @@ +requests==2.31.0 +setuptools==69.1.1 diff --git a/static/requirements/advanced/requirements_pillow_new.txt b/static/requirements/advanced/requirements_pillow_new.txt deleted file mode 100644 index e68991c0a..000000000 --- a/static/requirements/advanced/requirements_pillow_new.txt +++ /dev/null @@ -1,2 +0,0 @@ -Pillow>=10.0.1 -pillow-heif>=0.12.0 diff --git a/static/requirements/advanced/requirements_pillow_old.txt b/static/requirements/advanced/requirements_pillow_old.txt deleted file mode 100644 index 5f6fd0abb..000000000 --- a/static/requirements/advanced/requirements_pillow_old.txt +++ /dev/null @@ -1,2 +0,0 @@ -Pillow>=9.5.0 -pillow-heif>=0.12.0 diff --git a/static/requirements/advanced/requirements_server.txt b/static/requirements/advanced/requirements_server.txt index 66efa44fd..435388a3d 100644 --- a/static/requirements/advanced/requirements_server.txt +++ b/static/requirements/advanced/requirements_server.txt @@ -14,6 +14,6 @@ service-identity>=18.1.0 Twisted>=20.3.0 opencv-python-headless==4.8.1.78 -requests==2.31.0 +requests==2.31.0 setuptools==69.1.1