From 272485f6a8a9bf6a463a5911c6d616151958a5ec Mon Sep 17 00:00:00 2001 From: Hydrus Network Developer Date: Wed, 11 Sep 2024 15:43:52 -0500 Subject: [PATCH] Version 589 --- docs/changelog.md | 85 +- docs/developer_api.md | 7 +- docs/old_changelog.html | 36 + hydrus/client/ClientAPI.py | 4 +- hydrus/client/ClientApplicationCommand.py | 2 +- hydrus/client/ClientController.py | 15 +- hydrus/client/ClientData.py | 2 +- hydrus/client/ClientDownloading.py | 2 +- hydrus/client/ClientMigration.py | 22 +- hydrus/client/ClientOptions.py | 6 +- hydrus/client/ClientParsing.py | 16 +- hydrus/client/ClientRendering.py | 8 +- hydrus/client/ClientStrings.py | 16 +- hydrus/client/ClientTime.py | 8 +- hydrus/client/db/ClientDB.py | 70 + hydrus/client/db/ClientDBDefinitionsCache.py | 4 +- hydrus/client/db/ClientDBFileDeleteLock.py | 2 +- hydrus/client/db/ClientDBFilesDuplicates.py | 2 +- hydrus/client/db/ClientDBFilesInbox.py | 2 +- hydrus/client/db/ClientDBFilesMaintenance.py | 2 +- .../db/ClientDBFilesMaintenanceQueue.py | 2 +- .../client/db/ClientDBFilesMetadataBasic.py | 2 +- hydrus/client/db/ClientDBFilesMetadataRich.py | 2 +- .../client/db/ClientDBFilesPhysicalStorage.py | 2 +- hydrus/client/db/ClientDBFilesSearch.py | 4 +- hydrus/client/db/ClientDBFilesStorage.py | 9 +- hydrus/client/db/ClientDBFilesTimestamps.py | 2 +- hydrus/client/db/ClientDBFilesViewingStats.py | 2 +- hydrus/client/db/ClientDBMaintenance.py | 2 +- ...ientDBMappingsCacheCombinedFilesDisplay.py | 2 +- ...ientDBMappingsCacheCombinedFilesStorage.py | 2 +- .../ClientDBMappingsCacheSpecificDisplay.py | 2 +- .../ClientDBMappingsCacheSpecificStorage.py | 2 +- hydrus/client/db/ClientDBMappingsCounts.py | 2 +- .../client/db/ClientDBMappingsCountsUpdate.py | 2 +- hydrus/client/db/ClientDBMappingsStorage.py | 2 +- hydrus/client/db/ClientDBMaster.py | 8 +- hydrus/client/db/ClientDBNotesMap.py | 2 +- hydrus/client/db/ClientDBRatings.py | 2 +- hydrus/client/db/ClientDBRepositories.py | 2 +- hydrus/client/db/ClientDBSerialisable.py | 2 +- hydrus/client/db/ClientDBServicePaths.py | 2 +- hydrus/client/db/ClientDBServices.py | 2 +- hydrus/client/db/ClientDBSimilarFiles.py | 82 +- hydrus/client/db/ClientDBTagDisplay.py | 10 +- hydrus/client/db/ClientDBTagParents.py | 58 +- hydrus/client/db/ClientDBTagSearch.py | 2 +- hydrus/client/db/ClientDBTagSiblings.py | 56 +- hydrus/client/db/ClientDBTagSuggestions.py | 2 +- hydrus/client/db/ClientDBURLMap.py | 2 +- .../client/duplicates/ClientAutoDuplicates.py | 2 +- hydrus/client/duplicates/ClientDuplicates.py | 2 +- hydrus/client/gui/ClientGUI.py | 32 +- hydrus/client/gui/ClientGUIDialogs.py | 16 +- hydrus/client/gui/ClientGUIDialogsManage.py | 21 +- hydrus/client/gui/ClientGUIDownloaders.py | 159 ++- hydrus/client/gui/ClientGUIPopupMessages.py | 9 +- hydrus/client/gui/ClientGUIRatings.py | 6 +- hydrus/client/gui/ClientGUISerialisable.py | 4 +- .../client/gui/ClientGUIShortcutControls.py | 55 +- hydrus/client/gui/ClientGUIShortcuts.py | 6 +- hydrus/client/gui/ClientGUISplash.py | 2 +- hydrus/client/gui/ClientGUIStringControls.py | 47 +- hydrus/client/gui/ClientGUIStringPanels.py | 36 +- hydrus/client/gui/ClientGUISubscriptions.py | 164 ++- hydrus/client/gui/ClientGUITagSorting.py | 2 +- hydrus/client/gui/ClientGUITagSuggestions.py | 10 +- hydrus/client/gui/ClientGUITags.py | 161 ++- hydrus/client/gui/ClientGUITopLevelWindows.py | 2 +- hydrus/client/gui/QtPorting.py | 59 +- hydrus/client/gui/canvas/ClientGUICanvas.py | 8 +- .../client/gui/canvas/ClientGUICanvasFrame.py | 3 +- .../gui/canvas/ClientGUICanvasHoverFrames.py | 12 +- .../client/gui/canvas/ClientGUICanvasMedia.py | 15 +- hydrus/client/gui/canvas/ClientGUIMPV.py | 11 +- .../client/gui/exporting/ClientGUIExport.py | 42 +- .../gui/importing/ClientGUIFileSeedCache.py | 43 +- .../gui/importing/ClientGUIGallerySeedLog.py | 29 +- .../client/gui/importing/ClientGUIImport.py | 48 +- hydrus/client/gui/lists/ClientGUIListBoxes.py | 23 +- .../gui/lists/ClientGUIListBoxesData.py | 15 +- .../gui/lists/ClientGUIListConstants.py | 3 + hydrus/client/gui/lists/ClientGUIListCtrl.py | 1266 +---------------- .../client/gui/lists/ClientGUIListManager.py | 2 +- .../client/gui/lists/ClientGUIListStatus.py | 2 +- .../gui/media/ClientGUIMediaControls.py | 2 +- .../gui/metadata/ClientGUIEditTimestamps.py | 3 +- .../gui/metadata/ClientGUITagActions.py | 5 +- hydrus/client/gui/metadata/ClientGUITime.py | 8 +- .../gui/networking/ClientGUIHydrusNetwork.py | 2 +- .../client/gui/networking/ClientGUILogin.py | 2 +- .../networking/ClientGUINetworkJobControl.py | 3 +- .../pages/ClientGUIManagementController.py | 2 +- .../gui/pages/ClientGUIManagementPanels.py | 124 +- .../gui/pages/ClientGUINewPageChooser.py | 394 +++++ hydrus/client/gui/pages/ClientGUIPages.py | 383 +---- hydrus/client/gui/pages/ClientGUIResults.py | 43 +- .../gui/pages/ClientGUIResultsSortCollect.py | 6 +- hydrus/client/gui/pages/ClientGUISession.py | 2 +- ...ment.py => ClientGUIManageOptionsPanel.py} | 313 +--- .../panels/ClientGUIRepairFileSystemPanel.py | 258 ++++ .../gui/panels/ClientGUIScrolledPanels.py | 3 +- .../gui/panels/ClientGUIScrolledPanelsEdit.py | 76 +- .../panels/ClientGUIScrolledPanelsReview.py | 4 +- hydrus/client/gui/parsing/ClientGUIParsing.py | 39 +- .../gui/parsing/ClientGUIParsingLegacy.py | 4 +- .../gui/parsing/ClientGUIParsingTest.py | 2 +- .../client/gui/search/ClientGUIACDropdown.py | 260 ++-- .../gui/search/ClientGUIPredicatesMultiple.py | 6 +- .../gui/search/ClientGUIPredicatesOR.py | 2 +- .../gui/search/ClientGUIPredicatesSingle.py | 6 +- hydrus/client/gui/search/ClientGUISearch.py | 2 +- .../gui/search/ClientGUISearchPanels.py | 11 +- .../services/ClientGUIClientsideServices.py | 4 +- .../services/ClientGUIServersideServices.py | 2 +- .../widgets/ClientGUIApplicationCommand.py | 12 +- hydrus/client/gui/widgets/ClientGUIBytes.py | 4 +- .../gui/widgets/ClientGUIColourPicker.py | 2 +- hydrus/client/gui/widgets/ClientGUICommon.py | 103 +- .../client/gui/widgets/ClientGUIMenuButton.py | 46 +- .../client/gui/widgets/ClientGUINumberTest.py | 2 +- hydrus/client/gui/widgets/ClientGUIRegex.py | 2 +- .../client/gui/widgets/ClientGUITextInput.py | 2 +- .../client/importing/ClientImportFileSeeds.py | 4 +- .../client/importing/ClientImportGallery.py | 4 +- .../importing/ClientImportGallerySeeds.py | 4 +- hydrus/client/importing/ClientImportLocal.py | 2 +- .../importing/ClientImportSimpleURLs.py | 4 +- .../ClientImportSubscriptionLegacy.py | 2 +- .../ClientImportSubscriptionQuery.py | 2 +- .../importing/ClientImportSubscriptions.py | 2 +- .../client/importing/ClientImportWatchers.py | 4 +- .../importing/options/ClientImportOptions.py | 2 +- .../importing/options/FileImportOptions.py | 2 +- .../importing/options/NoteImportOptions.py | 2 +- .../options/PresentationImportOptions.py | 2 +- .../importing/options/TagImportOptions.py | 6 +- hydrus/client/media/ClientMedia.py | 13 +- hydrus/client/media/ClientMediaFileFilter.py | 2 +- .../metadata/ClientMetadataMigration.py | 2 +- .../ClientMetadataMigrationExporters.py | 21 +- .../ClientMetadataMigrationImporters.py | 27 +- hydrus/client/metadata/ClientTags.py | 3 +- hydrus/client/metadata/ClientTagsHandling.py | 31 +- .../networking/ClientLocalServerResources.py | 16 + .../networking/ClientNetworkingBandwidth.py | 2 +- .../ClientNetworkingBandwidthLegacy.py | 2 +- .../networking/ClientNetworkingContexts.py | 2 +- .../networking/ClientNetworkingDomain.py | 4 +- .../networking/ClientNetworkingLogin.py | 16 +- .../networking/ClientNetworkingSessions.py | 2 +- .../ClientNetworkingSessionsLegacy.py | 2 +- .../networking/ClientNetworkingURLClass.py | 2 +- hydrus/client/search/ClientSearch.py | 4 +- hydrus/core/HydrusConstants.py | 4 +- hydrus/core/HydrusController.py | 2 +- hydrus/core/HydrusData.py | 23 +- hydrus/core/HydrusExceptions.py | 3 +- hydrus/core/HydrusSerialisable.py | 9 +- hydrus/core/HydrusTags.py | 2 +- hydrus/core/networking/HydrusNetwork.py | 22 +- hydrus/core/networking/HydrusNetworking.py | 4 +- hydrus/test/TestClientAPI.py | 43 + .../parsers/catbox collection parser.png | Bin 0 -> 2320 bytes .../parsers/safebooru file page parser.png | Bin 2925 -> 2974 bytes .../default/url_classes/catbox collection.png | Bin 0 -> 2070 bytes 166 files changed, 2410 insertions(+), 2940 deletions(-) create mode 100644 hydrus/client/gui/pages/ClientGUINewPageChooser.py rename hydrus/client/gui/panels/{ClientGUIScrolledPanelsManagement.py => ClientGUIManageOptionsPanel.py} (96%) create mode 100644 hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py create mode 100644 static/default/parsers/catbox collection parser.png create mode 100644 static/default/url_classes/catbox collection.png diff --git a/docs/changelog.md b/docs/changelog.md index 53fe035a5..08acf14dc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,49 @@ title: Changelog !!! note This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html). +## [Version 589](https://github.com/hydrusnetwork/hydrus/releases/tag/v589) + +### misc + +* the similar-files search maintenance code has an important update that corrects tree rebalancing for a variety of clients that initialised with an unlucky first import file. in the database update, I will check if you were affected here and immediately optimise your tree if so. it might take a couple minutes if you have millions of files +* tag parent and sibling changes now calculate faster at the database level. a cache that maintains the structure of which pairs need to be synced is now adjusted with every parent/sibling content change, rather than regenerated. for the PTR, I believe this will save about a second of deferred CPU time on an arbitrary parent/sibling change for the price of about 2MB of memory, hooray. fingers crossed, looking at the _tags->sibling/parent sync->review_ panel while repository processing is going on will now be a smooth-updating affair, rather than repeated 'refreshing...' wait-flicker +* the 'the pairs you mean to add seem to connect to some loops' auto-loop-resolution popup in the manage siblings/parents dialogs will now only show when it is relevent to pairs to be added. previously, this thing was spamming during the pre-check of the process of the user actually breaking up loops by removing pairs +* added an item, 'sync now', to the _tags->sibling/parent sync_ menu. this is a nice easy way to force 'work hard' on all services that need work. it tells you if there was no work to do +* reworked the 'new page chooser' mini-dialog and better fixed-in-place the intended static 3x3 button layout +* showing 'all my files' and 'local files' in the 'new page chooser' mini-dialog is now configurable in _options->pages_. previously 'local files' was hidden behind advanced mode. 'all my files' will only ever show if you have more than one local files domain +* when a login script fails with 401 or 403, or indeed any other network error, it now presents a simpler error in UI (previously it could spam the raw html of the response up to UI) +* generally speaking, the network job status widget will now only show the first line of any status text it is given. if some crazy html document or other long error ends up spammed to this thing, it should now show a better summary +* the 'filename' and 'first/second/etc.. directory' checkbox-and-text-input controls in the filename tagging panel now auto-check when you type something in +* the 'review sibling/parent sync' and 'manage where tag siblings and parents apply' dialogs are now plugged into the 'default tag service' system. they open to this tab, and if you are set to update it to the last seen, they save over the value on changes + +### default downloaders + +* fixed the default safebooru file page parser to stop reading undesired '?' tags for every namespace (they changed their html recently I think) +* catbox 'collection' pages are now parseable by default + +### boring list stuff + +* fixed an issue with showing the 'manage export folders' dialog. sorry for the trouble--in my list rewrite, I didn't account for one thing that is special for this list and it somehow slipped through testing. as a side benefit, we are better prepped for a future update that will support column hiding and rearranging +* optimised about half of the new multi-column lists, as discussed last week. particularly included are file log, gallery log, watcher page, gallery page, and filename tagging panel, which all see a bunch of regular display/sort updates. the calls to get display data or sort data for a row are now separate, so if the display code is CPU expensive, it won't slow a sort +* in a couple places, url type column is now sorted by actual text, i.e. file url-gallery url-post url-watchable url, rather than the previous conveniently ordered enum. not sure if this is going to be annoying, so we'll see +* the filename tagging list no longer sorts the tag column by tag contents, instead it just does '#''. this makes this list sort superfast, so let's see if it is super annoying, but since this guy often has 10,000+ items, we may prefer the fast sort/updates for now + +### client api + +* the `/add_files/add_file` command now has a `delete_after_successful_import` parameter, default false, that does the same as the manual file import's similar checkbox. it only works on commands with a `path` parameter, obviously +* updated client api help and unit tests to test this +* client api version is now 70 + +### more boring cleanup + +* I cleaned up a mash of ancient shortcut-processing jank in the tag autocomplete input and fixed some logic. everything is now processed through one event filter, the result flags are no longer topsy-turvy, and the question of which key events are passed from the text input to the results list is now a simple strict whitelist--basically now only up/down/page up/page down/home/end/enter (sometimes)/escape (sometimes) and ctrl+p/n (for legacy reasons) are passed to the results list. this fixes some unhelpful behaviour where you couldn't select text and ctrl+c it _unless_ the results list was empty (since the list was jumping in, after recent updates, and saying 'hey, I can do ctrl+c, bro' and copying the currently selected results) +* the key event processing in multi-column lists is also cleaned up from the old wx bridge to native Qt handling +* and some crazy delete handling in the manage urls dialog is cleaned up too +* the old `EVT_KEY_DOWN` wx bridge is finally cleared out of the program. I also cleared out some other old wx event definitions that have long been replaced. mostly we just have some mouse handling and window state changes to deal with now +* replaced many of my ancient static inheritance references with python's `super()` gubbins. I disentangled all the program's multiple inheritance into super() and did I think about half of the rest. still like 360 `__init__` lines to do in future +* a couple of the 'noneable text' widgets that I recently set to have a default text, in the subscription dialogs, now use that text as placeholder rather than actual default. having 'my subscription' or whatever is ok as a guide, but when the user actually wants to edit, having it be real text is IRL a pain +* refactored the repair file locations dialog and manage options dialog and new page picker mini-dialog to their own python files + ## [Version 588](https://github.com/hydrusnetwork/hydrus/releases/tag/v588) ### fast new lists @@ -336,45 +379,3 @@ title: Changelog * updated the Client API help to talk about these * added some unit tests to test these * the client api is now version 65 - -## [Version 579](https://github.com/hydrusnetwork/hydrus/releases/tag/v579) - -### some url-checking logic - -* the 'during URL check, check for neighbour-spam?' checkbox in _file import options_ has some sophisticated new logic. check the issue for a longer explanation, but long story short is if you have two different booru URLs that share the same source URL (with one or both simply being incorrect e.g. both point to the same 'clean' source, even though one is 'messy'), then that bad source URL will no longer cause the second booru import job to get 'already in db'. it now recognises this is an untrustworthy mapping and goes ahead with the download, just as you actually want. once the file is imported, it is still able, as normal, to quickly recognise the true positive 'already in db' result, so I believe have successfully plugged a logical hole here without affecting normal good operation! (issue #1563) -* the 'associate source urls' option in file import options is more careful about the above logic. source urls are now definitely not included in the pre-import file url checks if this option is off - -### some regex quality of life - -* regex input text boxes have been given a pass. the regex 'help' links are folded into the button, the links are updated to something newer (one of the older ones seems to have died), the button is now put aside the input and labelled `.*`, the menu is a little neater, and the input has placeholder text and now shows green/red (valid/invalid in the stylesheet) depending on whether the current regex text compiles ok. just a nicer widget overall -* this widget is now in filename tagging, the String Match panel regex match, the String Converter panel regex step, and the 'regex favourites' options panel, which I was surprised to learn the existence of -* the regex menu for the String Converter regex step also now shows how to do grouping in python regex. I hadn't experimented with this properly in python, but I learned this past week that this thing can handle `(...) -> \1` group-replace fine and can do named groups with `(?P...) -> \g` too! -* for convenience, when editing a String Match, if you flick from 'any' to 'fixed' or 'regex', it now puts whatever was in your example text beforehand as the new value for the fixed text or regex - -### list selecting and scrolling - -* I added some new scroll-to tech to my multi-column lists -* pasting a URL into the 'edit URL Classes' dialog's test input now selects and scrolls to the matching URL Class -* the following lists should all have better list sort/select preservation, and will now scroll to and maintain visibility, on various edit/add events: edit url classes, edit gugs, edit parsers, edit shortcut sets, edit shortcut set, the options dialog frame locations, the options dialog media viewer options, manage services, manage account types, manage logins, manage login scripts, edit login script, and some weird legacy stuff. lots more to do in future -* when you 'add from defaults' for many lists, it will now try and scroll to what was just added. may not be perfect! -* same deal with 'import' buttons. it will now try and scroll to what you import! -* I am also moving to 'when you edit, you only edit one row at a time'. in general, when I have written list edit functions, I write them to edit each row of a multi-selection in turn with a new dialog, but: this is not used very much, can be confusing/annoying to the user, and increases code complexity, so I am undoing it. as I continue to work here, if you have a multi-selection, an edit call will increasingly just edit the top selected row. maybe in this case I'll reduce the selection, maybe I'll add some different way to do multi-edit again, let me know what you think - -### misc - -* import folders now work in a far more efficient way. previously, the client loaded import folders every three minutes to see which were ready to run; now, it loads them once on startup or change and then consults each folder to determine how long to wait until loading it again. it isn't perfect yet, but this ancient, terrible code from back when 100 files was a lot is now far more efficient. users with large import folders may notice less background lag, let me know how you get on. thanks to the users who spotted this--there's doubtless more out there -* to help muscle memory, the 'undo' menu is now disabled when there is nothing for it to hold, not invisible. same deal for the 'pending' menu, although this will still hide if you have no services to pend to (ipfs, hydrus repositories). see how this feels, maybe I'll add options for it -* the new 'is this webp animated?' check is now a little faster -* if your similar file search tree is missing a branch (this can happen after db damage or crash desync during a file import) and a new file import (wanting to add a new leaf) runs into this gap, the database now imports the file successfully and the user gets a popup message telling them to regen their similar files search tree when convenient (rather than raising an error and failing the import) -* added a FAQ question 'I just imported files from my hard drive collection. How can I get their tags from the boorus?', to talk about my feelings on this technical question and to link to the user guide here: https://wiki.hydrus.network/books/hydrus-manual/page/file-look-up -* the default bandwidth rules for a hydrus repository are boosted from 512MB a day to 2GB. my worries about a database syncing 'too fast' for maintenance timers to kick in are less critical these days - -### build and cleanup - -* since the recent test 'future build' went without any problems, I am folding its library updates into the normal build. Qt (PySide6) goes from 6.6.0 to 6.6.3.1 for Linux and Windows, there's a newer SQLite dll on Windows, and there's a newer mpv dll on Windows -* updated all the requirements.txts to specify to not use the brand new numpy 2.0.0, which it seems just released this week and breaks anything that was compiled to work with 1.x.x. if you tried to set up a new venv in the past few days and got weird numpy errors, please rebuild your venv in v579, it should work again -* thanks to a user, the Docker build's `requests` 'no_proxy' patch is fixed for python >3.10 -* cleaned up a ton of `SyntaxWarnings` boot logspam on python >=3.12 due to un-`r`-texted escape sequences like `\s`. thanks to the user who submitted all this, let me know if I missed any -* cleaned up some regex ui code -* cleaned up some garbage in the string panel ui code -* fixed some weird vertical stretch in some single-control dialogs diff --git a/docs/developer_api.md b/docs/developer_api.md index 47a283d8b..c7611ce87 100644 --- a/docs/developer_api.md +++ b/docs/developer_api.md @@ -494,7 +494,8 @@ Required Headers: Arguments (in JSON): : * `path`: (the path you want to import) - * [file domain](#parameters_file_domain) (optional, defaults to your "quiet" file import options's destination) + * `delete_after_successful_import`: (optional, defaults to `false`, sets to delete the source file on a 'successful' or 'already in db' result) + * [file domain](#parameters_file_domain) (optional, local file domain(s) only, defaults to your "quiet" file import options's destination) ```json title="Example request body" { @@ -502,11 +503,11 @@ Arguments (in JSON): } ``` -If you include a [file domain](#parameters_file_domain), it can only include 'local' file domains (by default this would be "my files"), but you can send multiple to import to more than one location at once. Sending 'all local files', 'all my files', 'trash', 'repository updates', or a file repository/ipfs will give you 400. +If you include a [file domain](#parameters_file_domain), it can only include 'local' file domains (by default on a new client this would just be "my files"), but you can send multiple to import to more than one location at once. Asking to import to 'all local files', 'all my files', 'trash', 'repository updates', or a file repository/ipfs will give you 400. Arguments (as bytes): : - You can alternately just send the file's bytes as the POST body. + You can alternately just send the file's raw bytes as the entire POST body. In this case, you cannot send any other parameters, so you will be left with the default import file domain. Response: : Some JSON with the import result. Please note that file imports for large files may take several seconds, and longer if the client is busy doing other db work, so make sure your request is willing to wait that long for the response. diff --git a/docs/old_changelog.html b/docs/old_changelog.html index fbb885cb9..ddef5c73a 100644 --- a/docs/old_changelog.html +++ b/docs/old_changelog.html @@ -34,6 +34,42 @@

changelog

    +
  • +

    version 589

    +
      +
    • misc

    • +
    • the similar-files search maintenance code has an important update that corrects tree rebalancing for a variety of clients that initialised with an unlucky first import file. in the database update, I will check if you were affected here and immediately optimise your tree if so. it might take a couple minutes if you have millions of files
    • +
    • tag parent and sibling changes now calculate faster at the database level. a cache that maintains the structure of which pairs need to be synced is now adjusted with every parent/sibling content change, rather than regenerated. for the PTR, I believe this will save about a second of deferred CPU time on an arbitrary parent/sibling change for the price of about 2MB of memory, hooray. fingers crossed, looking at the _tags->sibling/parent sync->review_ panel while repository processing is going on will now be a smooth-updating affair, rather than repeated 'refreshing...' wait-flicker
    • +
    • the 'the pairs you mean to add seem to connect to some loops' auto-loop-resolution popup in the manage siblings/parents dialogs will now only show when it is relevent to pairs to be added. previously, this thing was spamming during the pre-check of the process of the user actually breaking up loops by removing pairs
    • +
    • added an item, 'sync now', to the _tags->sibling/parent sync_ menu. this is a nice easy way to force 'work hard' on all services that need work. it tells you if there was no work to do
    • +
    • reworked the 'new page chooser' mini-dialog and better fixed-in-place the intended static 3x3 button layout
    • +
    • showing 'all my files' and 'local files' in the 'new page chooser' mini-dialog is now configurable in _options->pages_. previously 'local files' was hidden behind advanced mode. 'all my files' will only ever show if you have more than one local files domain
    • +
    • when a login script fails with 401 or 403, or indeed any other network error, it now presents a simpler error in UI (previously it could spam the raw html of the response up to UI)
    • +
    • generally speaking, the network job status widget will now only show the first line of any status text it is given. if some crazy html document or other long error ends up spammed to this thing, it should now show a better summary
    • +
    • the 'filename' and 'first/second/etc.. directory' checkbox-and-text-input controls in the filename tagging panel now auto-check when you type something in
    • +
    • the 'review sibling/parent sync' and 'manage where tag siblings and parents apply' dialogs are now plugged into the 'default tag service' system. they open to this tab, and if you are set to update it to the last seen, they save over the value on changes
    • +
    • default downloaders

    • +
    • fixed the default safebooru file page parser to stop reading undesired '?' tags for every namespace (they changed their html recently I think)
    • +
    • catbox 'collection' pages are now parseable by default
    • +
    • boring list stuff

    • +
    • fixed an issue with showing the 'manage export folders' dialog. sorry for the trouble--in my list rewrite, I didn't account for one thing that is special for this list and it somehow slipped through testing. as a side benefit, we are better prepped for a future update that will support column hiding and rearranging
    • +
    • optimised about half of the new multi-column lists, as discussed last week. particularly included are file log, gallery log, watcher page, gallery page, and filename tagging panel, which all see a bunch of regular display/sort updates. the calls to get display data or sort data for a row are now separate, so if the display code is CPU expensive, it won't slow a sort
    • +
    • in a couple places, url type column is now sorted by actual text, i.e. file url-gallery url-post url-watchable url, rather than the previous conveniently ordered enum. not sure if this is going to be annoying, so we'll see
    • +
    • the filename tagging list no longer sorts the tag column by tag contents, instead it just does '#''. this makes this list sort superfast, so let's see if it is super annoying, but since this guy often has 10,000+ items, we may prefer the fast sort/updates for now
    • +
    • client api

    • +
    • the `/add_files/add_file` command now has a `delete_after_successful_import` parameter, default false, that does the same as the manual file import's similar checkbox. it only works on commands with a `path` parameter, obviously
    • +
    • updated client api help and unit tests to test this
    • +
    • client api version is now 70
    • +
    • more boring cleanup

    • +
    • I cleaned up a mash of ancient shortcut-processing jank in the tag autocomplete input and fixed some logic. everything is now processed through one event filter, the result flags are no longer topsy-turvy, and the question of which key events are passed from the text input to the results list is now a simple strict whitelist--basically now only up/down/page up/page down/home/end/enter (sometimes)/escape (sometimes) and ctrl+p/n (for legacy reasons) are passed to the results list. this fixes some unhelpful behaviour where you couldn't select text and ctrl+c it _unless_ the results list was empty (since the list was jumping in, after recent updates, and saying 'hey, I can do ctrl+c, bro' and copying the currently selected results)
    • +
    • the key event processing in multi-column lists is also cleaned up from the old wx bridge to native Qt handling
    • +
    • and some crazy delete handling in the manage urls dialog is cleaned up too
    • +
    • the old `EVT_KEY_DOWN` wx bridge is finally cleared out of the program. I also cleared out some other old wx event definitions that have long been replaced. mostly we just have some mouse handling and window state changes to deal with now
    • +
    • replaced many of my ancient static inheritance references with python's `super()` gubbins. I disentangled all the program's multiple inheritance into super() and did I think about half of the rest. still like 360 `__init__` lines to do in future
    • +
    • a couple of the 'noneable text' widgets that I recently set to have a default text, in the subscription dialogs, now use that text as placeholder rather than actual default. having 'my subscription' or whatever is ok as a guide, but when the user actually wants to edit, having it be real text is IRL a pain
    • +
    • refactored the repair file locations dialog and manage options dialog and new page picker mini-dialog to their own python files
    • +
    +
  • version 588

      diff --git a/hydrus/client/ClientAPI.py b/hydrus/client/ClientAPI.py index 126507d99..f95ad2549 100644 --- a/hydrus/client/ClientAPI.py +++ b/hydrus/client/ClientAPI.py @@ -72,7 +72,7 @@ class APIManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._dirty = False @@ -253,7 +253,7 @@ def __init__( self, name = 'new api permissions', access_key = None, basic_permi search_tag_filter = HydrusTags.TagFilter() - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._access_key = access_key diff --git a/hydrus/client/ClientApplicationCommand.py b/hydrus/client/ClientApplicationCommand.py index 47361b7cf..aef7095a0 100644 --- a/hydrus/client/ClientApplicationCommand.py +++ b/hydrus/client/ClientApplicationCommand.py @@ -520,7 +520,7 @@ def __init__( self, command_type = None, data = None ): data = ( SIMPLE_ARCHIVE_FILE, None ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._command_type = command_type self._data = data diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py index 78543fc9e..2ef3c0d5c 100644 --- a/hydrus/client/ClientController.py +++ b/hydrus/client/ClientController.py @@ -49,14 +49,15 @@ class PubSubEvent( QC.QEvent ): def __init__( self ): - QC.QEvent.__init__( self, PubSubEventType ) + super().__init__( PubSubEventType ) + class PubSubEventCatcher( QC.QObject ): def __init__( self, parent, pubsub ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._pubsub = pubsub @@ -89,6 +90,7 @@ def eventFilter( self, watched, event ): return False + def MessageHandler( msg_type, context, text ): if msg_type not in ( QC.QtMsgType.QtDebugMsg, QC.QtMsgType.QtInfoMsg ): @@ -101,7 +103,7 @@ class App( QW.QApplication ): def __init__( self, pubsub, *args, **kwargs ): - QW.QApplication.__init__( self, *args, **kwargs ) + super().__init__( *args, **kwargs ) self._pubsub = pubsub @@ -178,8 +180,7 @@ def __init__( self, db_dir ): self.gui = None - HydrusController.HydrusController.__init__( self, db_dir ) - ClientControllerInterface.ClientControllerInterface.__init__( self ) + super().__init__( db_dir ) self._name = 'client' @@ -985,9 +986,9 @@ def qt_code( missing_subfolders ): with ClientGUITopLevelWindowsPanels.DialogManage( None, 'repair file system' ) as dlg: - from hydrus.client.gui.panels import ClientGUIScrolledPanelsManagement + from hydrus.client.gui.panels import ClientGUIRepairFileSystemPanel - panel = ClientGUIScrolledPanelsManagement.RepairFileSystemPanel( dlg, missing_subfolders ) + panel = ClientGUIRepairFileSystemPanel.RepairFileSystemPanel( dlg, missing_subfolders ) dlg.SetPanel( panel ) diff --git a/hydrus/client/ClientData.py b/hydrus/client/ClientData.py index 680210c02..b3c4139fd 100644 --- a/hydrus/client/ClientData.py +++ b/hydrus/client/ClientData.py @@ -263,7 +263,7 @@ class Credentials( HydrusData.HydrusYAMLBase ): def __init__( self, host, port, access_key = None ): - HydrusData.HydrusYAMLBase.__init__( self ) + super().__init__() if host == 'localhost': diff --git a/hydrus/client/ClientDownloading.py b/hydrus/client/ClientDownloading.py index acf9562a7..62d10b855 100644 --- a/hydrus/client/ClientDownloading.py +++ b/hydrus/client/ClientDownloading.py @@ -108,7 +108,7 @@ class GalleryIdentifier( HydrusSerialisable.SerialisableBase ): def __init__( self, site_type = None, additional_info = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._site_type = site_type self._additional_info = additional_info diff --git a/hydrus/client/ClientMigration.py b/hydrus/client/ClientMigration.py index 281f34640..f3a72efd9 100644 --- a/hydrus/client/ClientMigration.py +++ b/hydrus/client/ClientMigration.py @@ -69,7 +69,7 @@ def __init__( self, controller, path, desired_hash_type ): name = os.path.basename( path ) - MigrationDestination.__init__( self, controller, name ) + super().__init__( controller, name ) self._path = path self._desired_hash_type = desired_hash_type @@ -130,7 +130,7 @@ def __init__( self, controller, path, content_type ): name = os.path.basename( path ) - MigrationDestination.__init__( self, controller, name ) + super().__init__( controller, name ) self._path = path self._content_type = content_type @@ -186,7 +186,7 @@ def __init__( self, controller ): name = 'simple list destination' - MigrationDestination.__init__( self, controller, name ) + super().__init__( controller, name ) self._data_received = [] @@ -244,7 +244,7 @@ def __init__( self, controller, tag_service_key, content_action ): name = controller.services_manager.GetName( tag_service_key ) - MigrationDestination.__init__( self, controller, name ) + super().__init__( controller, name ) self._tag_service_key = tag_service_key @@ -263,7 +263,7 @@ class MigrationDestinationTagServiceMappings( MigrationDestinationTagService ): def __init__( self, controller, tag_service_key, content_action ): - MigrationDestinationTagService.__init__( self, controller, tag_service_key, content_action ) + super().__init__( controller, tag_service_key, content_action ) self._reason = 'Mass Migration Job' @@ -318,7 +318,7 @@ class MigrationDestinationTagServicePairs( MigrationDestinationTagService ): def __init__( self, controller, tag_service_key, content_action, content_type ): - MigrationDestinationTagService.__init__( self, controller, tag_service_key, content_action ) + super().__init__( controller, tag_service_key, content_action ) self._content_type = content_type @@ -452,7 +452,7 @@ def __init__( self, controller, path, location_context: ClientLocation.LocationC name = os.path.basename( path ) - MigrationSource.__init__( self, controller, name ) + super().__init__( controller, name ) self._path = path self._location_context = location_context @@ -618,7 +618,7 @@ def __init__( self, controller, path, left_tag_filter, right_tag_filter ): name = os.path.basename( path ) - MigrationSource.__init__( self, controller, name ) + super().__init__( controller, name ) self._path = path self._left_tag_filter = left_tag_filter @@ -672,7 +672,7 @@ def __init__( self, controller, data ): name = 'simple list source' - MigrationSource.__init__( self, controller, name ) + super().__init__( controller, name ) self._data = data self._iterator = None @@ -701,7 +701,7 @@ def __init__( self, controller, tag_service_key, location_context, desired_hash_ name = controller.services_manager.GetName( tag_service_key ) - MigrationSource.__init__( self, controller, name ) + super().__init__( controller, name ) self._location_context = location_context self._tag_service_key = tag_service_key @@ -743,7 +743,7 @@ def __init__( self, controller, tag_service_key, content_type, left_tag_filter, name = controller.services_manager.GetName( tag_service_key ) - MigrationSource.__init__( self, controller, name ) + super().__init__( controller, name ) self._tag_service_key = tag_service_key self._content_type = content_type diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py index 09b19153b..c4888b384 100644 --- a/hydrus/client/ClientOptions.py +++ b/hydrus/client/ClientOptions.py @@ -23,7 +23,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._dictionary = HydrusSerialisable.SerialisableDictionary() @@ -260,7 +260,9 @@ def _InitialiseDefaults( self ): 'command_palette_show_page_of_pages' : False, 'command_palette_show_main_menu' : False, 'command_palette_show_media_menu' : False, - 'disallow_media_drags_on_duration_media' : False + 'disallow_media_drags_on_duration_media' : False, + 'show_all_my_files_on_page_chooser' : True, + 'show_local_files_on_page_chooser' : False } # diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py index 853848158..7f3ebcf10 100644 --- a/hydrus/client/ClientParsing.py +++ b/hydrus/client/ClientParsing.py @@ -883,7 +883,7 @@ class ParseFormulaCompound( ParseFormula ): def __init__( self, formulae = None, sub_phrase = None, string_processor = None ): - ParseFormula.__init__( self, string_processor ) + super().__init__( string_processor ) if formulae is None: @@ -1040,7 +1040,7 @@ class ParseFormulaContextVariable( ParseFormula ): def __init__( self, variable_name = None, string_processor = None ): - ParseFormula.__init__( self, string_processor ) + super().__init__( string_processor ) if variable_name is None: @@ -1136,7 +1136,7 @@ class ParseFormulaHTML( ParseFormula ): def __init__( self, tag_rules = None, content_to_fetch = None, attribute_to_fetch = None, string_processor = None ): - ParseFormula.__init__( self, string_processor ) + super().__init__( string_processor ) if tag_rules is None: @@ -1491,7 +1491,7 @@ class ParseRuleHTML( HydrusSerialisable.SerialisableBase ): def __init__( self, rule_type = None, tag_name = None, tag_attributes = None, tag_index = None, tag_depth = None, should_test_tag_string = False, tag_string_string_match = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if rule_type is None: @@ -1757,7 +1757,7 @@ class ParseFormulaJSON( ParseFormula ): def __init__( self, parse_rules = None, content_to_fetch = None, string_processor = None ): - ParseFormula.__init__( self, string_processor ) + super().__init__( string_processor ) if parse_rules is None: @@ -2086,7 +2086,7 @@ def __init__( self, name = None, formula = None ): formula = ParseFormulaHTML() - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._formula = formula @@ -2555,7 +2555,7 @@ def __init__( self, name, parser_key = None, string_converter = None, sub_page_p example_parsing_context[ 'url' ] = 'https://example.com/posts/index.php?id=123456' - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._parser_key = parser_key self._string_converter = string_converter @@ -3059,7 +3059,7 @@ class ParseRootFileLookup( HydrusSerialisable.SerialisableBaseNamed ): def __init__( self, name, url = None, query_type = None, file_identifier_type = None, file_identifier_string_converter = None, file_identifier_arg_name = None, static_args = None, children = None ): - HydrusSerialisable.SerialisableBaseNamed.__init__( self, name ) + super().__init__( name ) self._url = url self._query_type = query_type diff --git a/hydrus/client/ClientRendering.py b/hydrus/client/ClientRendering.py index f3e83f81d..f41d897c1 100644 --- a/hydrus/client/ClientRendering.py +++ b/hydrus/client/ClientRendering.py @@ -86,7 +86,7 @@ class ImageRenderer( ClientCachesBase.CacheableObject ): def __init__( self, media, this_is_for_metadata_alone = False ): - ClientCachesBase.CacheableObject.__init__( self ) + super().__init__() self._numpy_image = None self._render_failed = False @@ -488,7 +488,7 @@ class ImageTile( ClientCachesBase.CacheableObject ): def __init__( self, hash: bytes, clip_rect: QC.QRect, qt_pixmap: QG.QPixmap ): - ClientCachesBase.CacheableObject.__init__( self ) + super().__init__() self.hash = hash self.clip_rect = clip_rect @@ -551,7 +551,7 @@ class RasterContainerVideo( RasterContainer ): def __init__( self, media, target_resolution = None, init_position = 0 ): - RasterContainer.__init__( self, media, target_resolution ) + super().__init__( media, target_resolution ) self._init_position = init_position @@ -1067,7 +1067,7 @@ class HydrusBitmap( ClientCachesBase.CacheableObject ): def __init__( self, data, size, depth, compressed = True ): - ClientCachesBase.CacheableObject.__init__( self ) + super().__init__() self._compressed = compressed diff --git a/hydrus/client/ClientStrings.py b/hydrus/client/ClientStrings.py index 2174d9f73..d5818a73d 100644 --- a/hydrus/client/ClientStrings.py +++ b/hydrus/client/ClientStrings.py @@ -94,7 +94,7 @@ def __init__( self, conversions = None, example_string = None ): example_string = 'example string' - StringProcessingStep.__init__( self ) + super().__init__() self.conversions = conversions @@ -491,7 +491,7 @@ class StringJoiner( StringProcessingStep ): def __init__( self, joiner: str = '', join_tuple_size: typing.Optional[ int ] = None ): - StringProcessingStep.__init__( self ) + super().__init__() self._joiner = joiner self._join_tuple_size = join_tuple_size @@ -627,7 +627,7 @@ class StringMatch( StringProcessingStep ): def __init__( self, match_type = STRING_MATCH_ANY, match_value = '', min_chars = None, max_chars = None, example_string = 'example string' ): - StringProcessingStep.__init__( self ) + super().__init__() self._match_type = match_type self._match_value = match_value @@ -863,7 +863,7 @@ class StringSlicer( StringProcessingStep ): def __init__( self, index_start: typing.Optional[ int ] = None, index_end: typing.Optional[ int ] = None ): - StringProcessingStep.__init__( self ) + super().__init__() self._index_start = index_start self._index_end = index_end @@ -1034,7 +1034,7 @@ class StringSorter( StringProcessingStep ): def __init__( self, sort_type: int = CONTENT_PARSER_SORT_TYPE_HUMAN_SORT, asc: bool = False, regex: typing.Optional[ str ] = None ): - StringProcessingStep.__init__( self ) + super().__init__() self._sort_type = sort_type self._asc = asc @@ -1168,7 +1168,7 @@ class StringSplitter( StringProcessingStep ): def __init__( self, separator: str = ',', max_splits: typing.Optional[ int ] = None ): - StringProcessingStep.__init__( self ) + super().__init__() self._separator = separator self._max_splits = max_splits @@ -1281,7 +1281,7 @@ class StringTagFilter( StringProcessingStep ): def __init__( self, tag_filter = None, example_string = 'blue eyes' ): - StringProcessingStep.__init__( self ) + super().__init__() if tag_filter is None: @@ -1409,7 +1409,7 @@ class StringProcessor( StringProcessingStep ): def __init__( self ): - StringProcessingStep.__init__( self ) + super().__init__() self._processing_steps = [] diff --git a/hydrus/client/ClientTime.py b/hydrus/client/ClientTime.py index b0cf2e5de..35858fe47 100644 --- a/hydrus/client/ClientTime.py +++ b/hydrus/client/ClientTime.py @@ -55,6 +55,12 @@ def ParseDate( date_string: str ): raise Exception( 'Sorry, you need the dateparse library for this, please try reinstalling your venv!' ) + # as a weird note, this function appears, in one case, to raise the sorry Exception, after several seconds of delay, if the boot locale is ru_RU + # it works on the same machine if locale is set to en_US. this suggests some environment variable locale-forcing or similar, or a bug in dateparser, that is causing the delay (and then?) failing the 'en' fallback + + # dateparser does not have ru-RU in its internal locale mappings, nor en-US. it has a whole bunch like 'en-PH', which will parse "19 October 2024, 3:45 PM", but not the more common ones + # it'll raise an error if you ask for en-US but not if you ask for locales = [ 'en' ], where 'en' _seems_ to be a proxy for 'en-US', so there's some weird locale init going on, idk + dt = dateparser.parse( date_string ) if dt is None: @@ -160,7 +166,7 @@ class TimestampData( HydrusSerialisable.SerialisableBase ): def __init__( self, timestamp_type = None, location = None, timestamp_ms: typing.Optional[ typing.Union[ int, float ] ] = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.timestamp_type = timestamp_type self.location = location diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py index b1f2a681c..5f5141bec 100644 --- a/hydrus/client/db/ClientDB.py +++ b/hydrus/client/db/ClientDB.py @@ -10699,6 +10699,76 @@ def ask_what_to_do_zip_docx_scan(): + if version == 588: + + try: + + domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER ) + + domain_manager.Initialise() + + # + + domain_manager.OverwriteDefaultParsers( [ + 'catbox collection parser', + 'safebooru file page parser' + ] ) + + domain_manager.OverwriteDefaultURLClasses( [ + 'catbox collection' + ] ) + + # + + 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 ) + + + try: + + result = self._Execute( 'SELECT phash_id, inner_population, outer_population FROM shape_vptree WHERE parent_id IS NULL;' ).fetchone() + + if result is not None: # if none, this client has no files + + ( root_phash_id, inner_population, outer_population ) = result + + is_decent_sized_branch = inner_population + outer_population > 16 + + if is_decent_sized_branch: + + larger = max( inner_population, outer_population ) + smaller = min( inner_population, outer_population ) + + if smaller / larger < 0.33: + + self._controller.frame_splash_status.SetSubtext( f'optimising similar files search' ) + + self.modules_similar_files.RegenerateTree() + + + + + except Exception as e: + + HydrusData.PrintException( e ) + + message = 'Trying to check/regenerate your similar file search tree 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/ClientDBDefinitionsCache.py b/hydrus/client/db/ClientDBDefinitionsCache.py index c23c608ec..1e802a7b3 100644 --- a/hydrus/client/db/ClientDBDefinitionsCache.py +++ b/hydrus/client/db/ClientDBDefinitionsCache.py @@ -31,7 +31,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_hashes: ClientDBMaster.Clien self._hash_ids_to_hashes_cache = {} - ClientDBModule.ClientDBModule.__init__( self, 'client hashes local cache', cursor ) + super().__init__( 'client hashes local cache', cursor ) def _DoLastShutdownWasBadWork( self ): @@ -387,7 +387,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_tags: ClientDBMaster.ClientD self._tag_ids_to_tags_cache = {} - ClientDBModule.ClientDBModule.__init__( self, 'client tags local cache', cursor ) + super().__init__( 'client tags local cache', cursor ) def _GetInitialTableGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBFileDeleteLock.py b/hydrus/client/db/ClientDBFileDeleteLock.py index e7266aac5..3e11d736f 100644 --- a/hydrus/client/db/ClientDBFileDeleteLock.py +++ b/hydrus/client/db/ClientDBFileDeleteLock.py @@ -15,7 +15,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self.modules_services = modules_services self.modules_files_inbox = modules_files_inbox - ClientDBModule.ClientDBModule.__init__( self, 'client file delete lock', cursor ) + super().__init__( 'client file delete lock', cursor ) def FilterForFileDeleteLock( self, service_id, hash_ids ): diff --git a/hydrus/client/db/ClientDBFilesDuplicates.py b/hydrus/client/db/ClientDBFilesDuplicates.py index 79e314067..c6ca47495 100644 --- a/hydrus/client/db/ClientDBFilesDuplicates.py +++ b/hydrus/client/db/ClientDBFilesDuplicates.py @@ -24,7 +24,7 @@ def __init__( modules_similar_files: ClientDBSimilarFiles.ClientDBSimilarFiles ): - ClientDBModule.ClientDBModule.__init__( self, 'client file duplicates', cursor ) + super().__init__( 'client file duplicates', cursor ) self.modules_files_storage = modules_files_storage self.modules_hashes_local_cache = modules_hashes_local_cache diff --git a/hydrus/client/db/ClientDBFilesInbox.py b/hydrus/client/db/ClientDBFilesInbox.py index 46277e25d..c44b42924 100644 --- a/hydrus/client/db/ClientDBFilesInbox.py +++ b/hydrus/client/db/ClientDBFilesInbox.py @@ -25,7 +25,7 @@ def __init__( self.inbox_hash_ids = set() - ClientDBModule.ClientDBModule.__init__( self, 'client files inbox', cursor ) + super().__init__( 'client files inbox', cursor ) self._InitCaches() diff --git a/hydrus/client/db/ClientDBFilesMaintenance.py b/hydrus/client/db/ClientDBFilesMaintenance.py index 96735afe3..3a8b59a70 100644 --- a/hydrus/client/db/ClientDBFilesMaintenance.py +++ b/hydrus/client/db/ClientDBFilesMaintenance.py @@ -34,7 +34,7 @@ def __init__( weakref_media_result_cache: ClientMediaResultCache.MediaResultCache ): - ClientDBModule.ClientDBModule.__init__( self, 'client files maintenance', cursor ) + super().__init__( 'client files maintenance', cursor ) self.modules_files_maintenance_queue = modules_files_maintenance_queue self.modules_hashes = modules_hashes diff --git a/hydrus/client/db/ClientDBFilesMaintenanceQueue.py b/hydrus/client/db/ClientDBFilesMaintenanceQueue.py index b5f0ac1b9..eff8268b3 100644 --- a/hydrus/client/db/ClientDBFilesMaintenanceQueue.py +++ b/hydrus/client/db/ClientDBFilesMaintenanceQueue.py @@ -20,7 +20,7 @@ def __init__( modules_hashes_local_cache: ClientDBDefinitionsCache.ClientDBCacheLocalHashes, ): - ClientDBModule.ClientDBModule.__init__( self, 'client files maintenance queue', cursor ) + super().__init__( 'client files maintenance queue', cursor ) self.modules_hashes_local_cache = modules_hashes_local_cache diff --git a/hydrus/client/db/ClientDBFilesMetadataBasic.py b/hydrus/client/db/ClientDBFilesMetadataBasic.py index 9d0241e1c..729023ab1 100644 --- a/hydrus/client/db/ClientDBFilesMetadataBasic.py +++ b/hydrus/client/db/ClientDBFilesMetadataBasic.py @@ -10,7 +10,7 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client files simple metadata', cursor ) + super().__init__( 'client files simple metadata', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBFilesMetadataRich.py b/hydrus/client/db/ClientDBFilesMetadataRich.py index 52fcdbb65..f50ea2751 100644 --- a/hydrus/client/db/ClientDBFilesMetadataRich.py +++ b/hydrus/client/db/ClientDBFilesMetadataRich.py @@ -42,7 +42,7 @@ def __init__( self.modules_hashes_local_cache = modules_hashes_local_cache self.modules_url_map = modules_url_map - ClientDBModule.ClientDBModule.__init__( self, 'client files rich metadata', cursor ) + super().__init__( 'client files rich metadata', cursor ) def FilterHashesByService( self, location_context: ClientLocation.LocationContext, hashes: typing.Sequence[ bytes ] ) -> typing.List[ bytes ]: diff --git a/hydrus/client/db/ClientDBFilesPhysicalStorage.py b/hydrus/client/db/ClientDBFilesPhysicalStorage.py index 0d390e673..1c57358aa 100644 --- a/hydrus/client/db/ClientDBFilesPhysicalStorage.py +++ b/hydrus/client/db/ClientDBFilesPhysicalStorage.py @@ -18,7 +18,7 @@ def __init__( db_dir: str ): - ClientDBModule.ClientDBModule.__init__( self, 'client files physical storage', cursor ) + super().__init__( 'client files physical storage', cursor ) self._db_dir = db_dir diff --git a/hydrus/client/db/ClientDBFilesSearch.py b/hydrus/client/db/ClientDBFilesSearch.py index 818bf54af..162def9fd 100644 --- a/hydrus/client/db/ClientDBFilesSearch.py +++ b/hydrus/client/db/ClientDBFilesSearch.py @@ -237,7 +237,7 @@ def __init__( self.modules_mappings_counts = modules_mappings_counts self.modules_tag_search = modules_tag_search - ClientDBModule.ClientDBModule.__init__( self, 'client file search using tags', cursor ) + super().__init__( 'client file search using tags', cursor ) def GetHashIdsAndNonZeroTagCounts( self, tag_display_type: int, location_context: ClientLocation.LocationContext, tag_context: ClientSearch.TagContext, hash_ids, namespace_wildcard = '*', job_status = None ): @@ -899,7 +899,7 @@ def __init__( self.modules_files_duplicates = modules_files_duplicates self.modules_files_search_tags = modules_files_search_tags - ClientDBModule.ClientDBModule.__init__( self, 'client file query', cursor ) + super().__init__( 'client file query', cursor ) def _DoNotePreds( self, system_predicates: ClientSearch.FileSystemPredicates, query_hash_ids: typing.Optional[ typing.Set[ int ] ], job_status: typing.Optional[ ClientThreading.JobStatus ] = None ) -> typing.Optional[ typing.Set[ int ] ]: diff --git a/hydrus/client/db/ClientDBFilesStorage.py b/hydrus/client/db/ClientDBFilesStorage.py index 45d77a47e..7e8a53317 100644 --- a/hydrus/client/db/ClientDBFilesStorage.py +++ b/hydrus/client/db/ClientDBFilesStorage.py @@ -61,10 +61,12 @@ def GenerateFilesTableName( service_id: int, status: int ) -> str: class DBLocationContext( object ): - def __init__( self, location_context: ClientLocation.LocationContext ): + def __init__( self, location_context: ClientLocation.LocationContext, *args, **kwargs ): self.location_context = location_context + super().__init__( *args, **kwargs ) + def GetLocationContext( self ) -> ClientLocation.LocationContext: @@ -152,8 +154,7 @@ class DBLocationContextBranch( DBLocationContext, ClientDBModule.ClientDBModule def __init__( self, cursor: sqlite3.Cursor, location_context: ClientLocation.LocationContext, files_table_names: typing.Collection[ str ] ): - ClientDBModule.ClientDBModule.__init__( self, 'db location (branch)', cursor ) - DBLocationContext.__init__( self, location_context ) + super().__init__( location_context, 'db location (branch)', cursor ) self._files_table_names = files_table_names self._single_table_initialised = False @@ -251,7 +252,7 @@ def __init__( self, cursor: sqlite3.Cursor, cursor_transaction_wrapper: HydrusDB self.modules_hashes = modules_hashes self.modules_texts = modules_texts - ClientDBModule.ClientDBModule.__init__( self, 'client file locations', cursor ) + super().__init__( 'client file locations', cursor ) def _GetInitialTableGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBFilesTimestamps.py b/hydrus/client/db/ClientDBFilesTimestamps.py index 09b5c2c11..5e77cb4ce 100644 --- a/hydrus/client/db/ClientDBFilesTimestamps.py +++ b/hydrus/client/db/ClientDBFilesTimestamps.py @@ -36,7 +36,7 @@ class ClientDBFilesTimestamps( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor, modules_urls: ClientDBMaster.ClientDBMasterURLs, modules_files_viewing_stats: ClientDBFilesViewingStats.ClientDBFilesViewingStats, modules_files_storage: ClientDBFilesStorage.ClientDBFilesStorage ): - ClientDBModule.ClientDBModule.__init__( self, 'client files timestamps', cursor ) + super().__init__( 'client files timestamps', cursor ) self.modules_urls = modules_urls self.modules_files_viewing_stats = modules_files_viewing_stats diff --git a/hydrus/client/db/ClientDBFilesViewingStats.py b/hydrus/client/db/ClientDBFilesViewingStats.py index 9fbb162b9..929dbe66d 100644 --- a/hydrus/client/db/ClientDBFilesViewingStats.py +++ b/hydrus/client/db/ClientDBFilesViewingStats.py @@ -19,7 +19,7 @@ def __init__( cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client files viewing stats', cursor ) + super().__init__( 'client files viewing stats', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBMaintenance.py b/hydrus/client/db/ClientDBMaintenance.py index e0530a071..e8f0850c4 100644 --- a/hydrus/client/db/ClientDBMaintenance.py +++ b/hydrus/client/db/ClientDBMaintenance.py @@ -20,7 +20,7 @@ class ClientDBMaintenance( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor, db_dir: str, db_filenames: typing.Collection[ str ], cursor_transaction_wrapper: HydrusDBBase.DBCursorTransactionWrapper, modules: typing.List[ HydrusDBModule.HydrusDBModule ] ): - ClientDBModule.ClientDBModule.__init__( self, 'client db maintenance', cursor ) + super().__init__( 'client db maintenance', cursor ) self._db_dir = db_dir self._db_filenames = db_filenames diff --git a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py index 91d8d047f..17eff8510 100644 --- a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py +++ b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py @@ -27,7 +27,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self.modules_tag_display = modules_tag_display self.modules_files_storage = modules_files_storage - ClientDBModule.ClientDBModule.__init__( self, 'client combined files display mappings cache', cursor ) + super().__init__( 'client combined files display mappings cache', cursor ) def AddImplications( self, tag_service_id, implication_tag_ids, tag_id, status_hook = None ): diff --git a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesStorage.py b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesStorage.py index dade31fb3..5827f4342 100644 --- a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesStorage.py +++ b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesStorage.py @@ -22,7 +22,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self.modules_mappings_counts_update = modules_mappings_counts_update self.modules_mappings_cache_combined_files_display = modules_mappings_cache_combined_files_display - ClientDBModule.ClientDBModule.__init__( self, 'client combined files storage mappings cache', cursor ) + super().__init__( 'client combined files storage mappings cache', cursor ) def Clear( self, tag_service_id, keep_pending = False ): diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py index 720a02e05..9f3b6eb13 100644 --- a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py +++ b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py @@ -32,7 +32,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self._missing_tag_service_pairs = set() - ClientDBModule.ClientDBModule.__init__( self, 'client specific display mappings cache', cursor ) + super().__init__( 'client specific display mappings cache', cursor ) def _GetServiceIndexGenerationDictSingle( self, file_service_id, tag_service_id ): diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py index 8eb502808..eec352d96 100644 --- a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py +++ b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py @@ -103,7 +103,7 @@ def __init__( self._missing_tag_service_pairs = set() - ClientDBModule.ClientDBModule.__init__( self, 'client specific display mappings cache', cursor ) + super().__init__( 'client specific display mappings cache', cursor ) def _GetServiceIndexGenerationDictSingle( self, file_service_id, tag_service_id ): diff --git a/hydrus/client/db/ClientDBMappingsCounts.py b/hydrus/client/db/ClientDBMappingsCounts.py index ec01a602e..261e9d841 100644 --- a/hydrus/client/db/ClientDBMappingsCounts.py +++ b/hydrus/client/db/ClientDBMappingsCounts.py @@ -61,7 +61,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self.modules_db_maintenance = modules_db_maintenance self.modules_services = modules_services - ClientDBModule.ClientDBModule.__init__( self, 'client mappings counts', cursor ) + super().__init__( 'client mappings counts', cursor ) self._missing_storage_tag_service_pairs = set() self._missing_display_tag_service_pairs = set() diff --git a/hydrus/client/db/ClientDBMappingsCountsUpdate.py b/hydrus/client/db/ClientDBMappingsCountsUpdate.py index 165d58414..ae2a16fc4 100644 --- a/hydrus/client/db/ClientDBMappingsCountsUpdate.py +++ b/hydrus/client/db/ClientDBMappingsCountsUpdate.py @@ -21,7 +21,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self.modules_tag_display = modules_tag_display self.modules_tag_search = modules_tag_search - ClientDBModule.ClientDBModule.__init__( self, 'client mappings counts update', cursor ) + super().__init__( 'client mappings counts update', cursor ) def AddCounts( self, tag_display_type, file_service_id, tag_service_id, ac_cache_changes ): diff --git a/hydrus/client/db/ClientDBMappingsStorage.py b/hydrus/client/db/ClientDBMappingsStorage.py index df60ea6d2..a4e757c26 100644 --- a/hydrus/client/db/ClientDBMappingsStorage.py +++ b/hydrus/client/db/ClientDBMappingsStorage.py @@ -83,7 +83,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self.modules_db_maintenance = modules_db_maintenance self.modules_services = modules_services - ClientDBModule.ClientDBModule.__init__( self, 'client mappings storage', cursor ) + super().__init__( 'client mappings storage', cursor ) def _GetServiceIndexGenerationDict( self, service_id ) -> dict: diff --git a/hydrus/client/db/ClientDBMaster.py b/hydrus/client/db/ClientDBMaster.py index cac90c887..1ecb73ee6 100644 --- a/hydrus/client/db/ClientDBMaster.py +++ b/hydrus/client/db/ClientDBMaster.py @@ -16,7 +16,7 @@ class ClientDBMasterHashes( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client hashes master', cursor ) + super().__init__( 'client hashes master', cursor ) self._hash_ids_to_hashes_cache = {} @@ -351,7 +351,7 @@ class ClientDBMasterTexts( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client texts master', cursor ) + super().__init__( 'client texts master', cursor ) def _GetInitialTableGenerationDict( self ) -> dict: @@ -451,7 +451,7 @@ class ClientDBMasterTags( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client tags master', cursor ) + super().__init__( 'client tags master', cursor ) self.null_namespace_id = None @@ -765,7 +765,7 @@ class ClientDBMasterURLs( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client urls master', cursor ) + super().__init__( 'client urls master', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBNotesMap.py b/hydrus/client/db/ClientDBNotesMap.py index 23290b968..fec06d044 100644 --- a/hydrus/client/db/ClientDBNotesMap.py +++ b/hydrus/client/db/ClientDBNotesMap.py @@ -16,7 +16,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_texts: ClientDBMaster.Client self.modules_texts = modules_texts - ClientDBModule.ClientDBModule.__init__( self, 'client notes mapping', cursor ) + super().__init__( 'client notes mapping', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBRatings.py b/hydrus/client/db/ClientDBRatings.py index 1fccb90c8..8614e22b5 100644 --- a/hydrus/client/db/ClientDBRatings.py +++ b/hydrus/client/db/ClientDBRatings.py @@ -18,7 +18,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self.modules_services = modules_services - ClientDBModule.ClientDBModule.__init__( self, 'client ratings', cursor ) + super().__init__( 'client ratings', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBRepositories.py b/hydrus/client/db/ClientDBRepositories.py index ab789489a..b7a848953 100644 --- a/hydrus/client/db/ClientDBRepositories.py +++ b/hydrus/client/db/ClientDBRepositories.py @@ -78,7 +78,7 @@ def __init__( # since we'll mostly be talking about hashes and tags we don't have locally, I think we shouldn't use the local caches - ClientDBModule.ClientDBModule.__init__( self, 'client repositories', cursor ) + super().__init__( 'client repositories', cursor ) self._cursor_transaction_wrapper = cursor_transaction_wrapper self.modules_db_maintenance = modules_db_maintenance diff --git a/hydrus/client/db/ClientDBSerialisable.py b/hydrus/client/db/ClientDBSerialisable.py index 39659cd99..a7e6810aa 100644 --- a/hydrus/client/db/ClientDBSerialisable.py +++ b/hydrus/client/db/ClientDBSerialisable.py @@ -150,7 +150,7 @@ class ClientDBSerialisable( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor, db_dir, cursor_transaction_wrapper: HydrusDBBase.DBCursorTransactionWrapper, modules_services: ClientDBServices.ClientDBMasterServices ): - ClientDBModule.ClientDBModule.__init__( self, 'client serialisable', cursor ) + super().__init__( 'client serialisable', cursor ) self._db_dir = db_dir self._cursor_transaction_wrapper = cursor_transaction_wrapper diff --git a/hydrus/client/db/ClientDBServicePaths.py b/hydrus/client/db/ClientDBServicePaths.py index 2275c7efb..758c5ddbe 100644 --- a/hydrus/client/db/ClientDBServicePaths.py +++ b/hydrus/client/db/ClientDBServicePaths.py @@ -20,7 +20,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self.modules_texts = modules_texts self.modules_hashes_local_cache = modules_hashes_local_cache - ClientDBModule.ClientDBModule.__init__( self, 'client service paths', cursor ) + super().__init__( 'client service paths', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBServices.py b/hydrus/client/db/ClientDBServices.py index 1c31f349c..3d3fc6c69 100644 --- a/hydrus/client/db/ClientDBServices.py +++ b/hydrus/client/db/ClientDBServices.py @@ -68,7 +68,7 @@ class ClientDBMasterServices( ClientDBModule.ClientDBModule ): def __init__( self, cursor: sqlite3.Cursor ): - ClientDBModule.ClientDBModule.__init__( self, 'client services master', cursor ) + super().__init__( 'client services master', cursor ) self._service_ids_to_services = {} self._service_keys_to_service_ids = {} diff --git a/hydrus/client/db/ClientDBSimilarFiles.py b/hydrus/client/db/ClientDBSimilarFiles.py index cdbc63a9d..6d39a8e09 100644 --- a/hydrus/client/db/ClientDBSimilarFiles.py +++ b/hydrus/client/db/ClientDBSimilarFiles.py @@ -28,7 +28,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C self._reported_on_a_broken_branch = False - ClientDBModule.ClientDBModule.__init__( self, 'client similar files', cursor ) + super().__init__( 'client similar files', cursor ) self._perceptual_hash_id_to_vp_tree_node_cache = {} self._non_vp_treed_perceptual_hash_ids = set() @@ -116,12 +116,14 @@ def _AddLeaf( self, perceptual_hash_id, perceptual_hash ): - if not an_ancestor_is_unbalanced and ancestor_inner_population + ancestor_outer_population > 16: + is_decent_sized_branch = ancestor_inner_population + ancestor_outer_population > 16 + + if not an_ancestor_is_unbalanced and is_decent_sized_branch: larger = max( ancestor_inner_population, ancestor_outer_population ) smaller = min( ancestor_inner_population, ancestor_outer_population ) - if smaller / larger < 0.5: + if smaller / larger < 0.33: self._Execute( 'INSERT OR IGNORE INTO shape_maintenance_branch_regen ( phash_id ) VALUES ( ? );', ( ancestor_id, ) ) @@ -435,14 +437,7 @@ def _RegenerateBranch( self, job_status, perceptual_hash_id ): ( parent_id, ) = self._Execute( 'SELECT parent_id FROM shape_vptree WHERE phash_id = ?;', ( perceptual_hash_id, ) ).fetchone() - if parent_id is None: - - # this is the root node! we can't rebalance since there is no parent to spread across! - - self._Execute( 'DELETE FROM shape_maintenance_branch_regen WHERE phash_id = ?;', ( perceptual_hash_id, ) ) - - return - + # if parent_id here is None, we've got the root node and we are doing the whole tree cte_table_name = 'branch ( branch_phash_id )' initial_select = 'SELECT ?' @@ -450,11 +445,11 @@ def _RegenerateBranch( self, job_status, perceptual_hash_id ): query_on_cte_table_name = 'SELECT branch_phash_id, phash FROM branch, shape_perceptual_hashes ON phash_id = branch_phash_id' # use UNION (large memory, set), not UNION ALL (small memory, inifinite loop on damaged cyclic graph causing 200GB journal file and disk full error, jesus) - query = 'WITH RECURSIVE {} AS ( {} UNION {} ) {};'.format( cte_table_name, initial_select, recursive_select, query_on_cte_table_name ) + query = f'WITH RECURSIVE {cte_table_name} AS ( {initial_select} UNION {recursive_select} ) {query_on_cte_table_name};' unbalanced_nodes = self._Execute( query, ( perceptual_hash_id, ) ).fetchall() - # removal of old branch, maintenance schedule, and orphan perceptual_hashes + # removal of old branch and maintenance schedule job_status.SetStatusText( HydrusNumbers.ToHumanInt( len( unbalanced_nodes ) ) + ' leaves found--now clearing out old branch', 2 ) @@ -466,6 +461,8 @@ def _RegenerateBranch( self, job_status, perceptual_hash_id ): self._ExecuteMany( 'DELETE FROM shape_maintenance_branch_regen WHERE phash_id = ?;', ( ( p_id, ) for p_id in unbalanced_perceptual_hash_ids ) ) + # let's take this chance to clear out any nodes that don't actually map to any files + with self._MakeTemporaryIntegerTable( unbalanced_perceptual_hash_ids, 'phash_id' ) as temp_perceptual_hash_ids_table_name: useful_perceptual_hash_ids = self._STS( self._Execute( 'SELECT phash_id FROM {} CROSS JOIN shape_perceptual_hash_map USING ( phash_id );'.format( temp_perceptual_hash_ids_table_name ) ) ) @@ -473,54 +470,67 @@ def _RegenerateBranch( self, job_status, perceptual_hash_id ): orphan_perceptual_hash_ids = unbalanced_perceptual_hash_ids.difference( useful_perceptual_hash_ids ) - self._ExecuteMany( 'DELETE FROM shape_perceptual_hashes WHERE phash_id = ?;', ( ( p_id, ) for p_id in orphan_perceptual_hash_ids ) ) + if len( orphan_perceptual_hash_ids ) > 0: + + self._ExecuteMany( 'DELETE FROM shape_perceptual_hashes WHERE phash_id = ?;', ( ( p_id, ) for p_id in orphan_perceptual_hash_ids ) ) + useful_nodes = [ row for row in unbalanced_nodes if row[0] in useful_perceptual_hash_ids ] - useful_population = len( useful_nodes ) + num_useful_population = len( useful_nodes ) # now create the new branch, starting by choosing a new root and updating the parent's left/right reference to that - if useful_population > 0: + if num_useful_population > 0: ( new_perceptual_hash_id, new_perceptual_hash ) = self._PopBestRootNode( useful_nodes ) else: + # the correct regen in this case is to cut the stem here. reset to None/0 + new_perceptual_hash_id = None new_perceptual_hash = None - result = self._Execute( 'SELECT inner_id FROM shape_vptree WHERE phash_id = ?;', ( parent_id, ) ).fetchone() + # ok we have removed the old branch and now picked a better root node for the new one + # now we need to construct and insert this branch back into the database - if result is None: + if parent_id is not None: - # expected parent is not in the tree! - # somehow some stuff got borked + # let's first update the pre-existing parent with its new child and perhaps let it know it has lost some orphans - self._Execute( 'DELETE FROM shape_maintenance_branch_regen;' ) + result = self._Execute( 'SELECT inner_id FROM shape_vptree WHERE phash_id = ?;', ( parent_id, ) ).fetchone() - HydrusData.ShowText( 'Your similar files search tree seemed to be damaged. Please regenerate it under the _database_ menu!' ) - - return + if result is None: + + # expected parent is not in the tree! + # somehow some stuff got borked + + self._Execute( 'DELETE FROM shape_maintenance_branch_regen;' ) + + HydrusData.ShowText( 'Your similar files search tree seemed to be damaged. Please regenerate it under the _database_ menu!' ) + + return + - - ( parent_inner_id, ) = result - - if parent_inner_id == perceptual_hash_id: + ( parent_inner_id, ) = result - query = 'UPDATE shape_vptree SET inner_id = ?, inner_population = ? WHERE phash_id = ?;' + if parent_inner_id == perceptual_hash_id: + + query = 'UPDATE shape_vptree SET inner_id = ?, inner_population = ? WHERE phash_id = ?;' + + else: + + query = 'UPDATE shape_vptree SET outer_id = ?, outer_population = ? WHERE phash_id = ?;' + - else: + self._Execute( query, ( new_perceptual_hash_id, num_useful_population, parent_id ) ) - query = 'UPDATE shape_vptree SET outer_id = ?, outer_population = ? WHERE phash_id = ?;' + self._ClearPerceptualHashesFromVPTreeNodeCache( ( parent_id, ) ) - self._Execute( query, ( new_perceptual_hash_id, useful_population, parent_id ) ) - - self._ClearPerceptualHashesFromVPTreeNodeCache( ( parent_id, ) ) - - if useful_population > 0: + if num_useful_population > 0: self._GenerateBranch( job_status, parent_id, new_perceptual_hash_id, new_perceptual_hash, useful_nodes ) diff --git a/hydrus/client/db/ClientDBTagDisplay.py b/hydrus/client/db/ClientDBTagDisplay.py index c8faaaf6e..7d728ad05 100644 --- a/hydrus/client/db/ClientDBTagDisplay.py +++ b/hydrus/client/db/ClientDBTagDisplay.py @@ -41,7 +41,7 @@ def __init__( self.modules_tag_parents = modules_tag_parents self.modules_tag_siblings = modules_tag_siblings - ClientDBModule.ClientDBModule.__init__( self, 'client tag display', cursor ) + super().__init__( 'client tag display', cursor ) def FilterChained( self, display_type, tag_service_id, tag_ids ) -> typing.Set[ int ]: @@ -224,11 +224,11 @@ def GetApplication( self ): def GetApplicationStatus( self, service_id ): - ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_sibling_rows, num_ideal_sibling_rows ) = self.modules_tag_siblings.GetApplicationStatus( service_id ) - ( parent_rows_to_add, parent_rows_to_remove, num_actual_parent_rows, num_ideal_parent_rows ) = self.modules_tag_parents.GetApplicationStatus( service_id ) + ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) = self.modules_tag_siblings.GetApplicationStatus( service_id ) + ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) = self.modules_tag_parents.GetApplicationStatus( service_id ) - num_actual_rows = num_actual_sibling_rows + num_actual_parent_rows - num_ideal_rows = num_ideal_sibling_rows + num_ideal_parent_rows + num_actual_rows = len( actual_sibling_rows ) + len( actual_parent_rows ) + num_ideal_rows = len( ideal_sibling_rows ) + len( ideal_parent_rows ) return ( sibling_rows_to_add, sibling_rows_to_remove, parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) diff --git a/hydrus/client/db/ClientDBTagParents.py b/hydrus/client/db/ClientDBTagParents.py index 02dbb8d02..35280d1b9 100644 --- a/hydrus/client/db/ClientDBTagParents.py +++ b/hydrus/client/db/ClientDBTagParents.py @@ -82,7 +82,7 @@ def __init__( self._service_ids_to_applicable_service_ids = None self._service_ids_to_interested_service_ids = None - ClientDBModule.ClientDBModule.__init__( self, 'client tag parents', cursor ) + super().__init__( 'client tag parents', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: @@ -404,15 +404,12 @@ def GetApplicationStatus( self, service_id ): parent_rows_to_remove = actual_parent_rows.difference( ideal_parent_rows ) parent_rows_to_add = ideal_parent_rows.difference( actual_parent_rows ) - num_actual_rows = len( actual_parent_rows ) - num_ideal_rows = len( ideal_parent_rows ) - - self._service_ids_to_display_application_status[ service_id ] = ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ service_id ] = ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) - ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ service_id ] + ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) = self._service_ids_to_display_application_status[ service_id ] - return ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) + return ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) def GetChainsMembers( self, display_type: int, tag_service_id: int, ideal_tag_ids: typing.Collection[ int ] ): @@ -790,13 +787,12 @@ def NotifyParentAddRowSynced( self, tag_service_id, row ): if tag_service_id in self._service_ids_to_display_application_status: - ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ tag_service_id ] + ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + actual_parent_rows.add( row ) parent_rows_to_add.discard( row ) - num_actual_rows += 1 - - self._service_ids_to_display_application_status[ tag_service_id ] = ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ tag_service_id ] = ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) @@ -804,13 +800,12 @@ def NotifyParentDeleteRowSynced( self, tag_service_id, row ): if tag_service_id in self._service_ids_to_display_application_status: - ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ tag_service_id ] + ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + actual_parent_rows.discard( row ) parent_rows_to_remove.discard( row ) - num_actual_rows -= 1 - - self._service_ids_to_display_application_status[ tag_service_id ] = ( parent_rows_to_add, parent_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ tag_service_id ] = ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) @@ -917,6 +912,19 @@ def RegenChains( self, tag_service_ids, tag_ids ): # this should now contain all possible tag_ids that could be in tag parents right now related to what we were given + if tag_service_id in self._service_ids_to_applicable_service_ids: + + with self._MakeTemporaryIntegerTable( tag_ids_to_clear_and_regen, 'tag_id' ) as temp_tag_ids_table_name: + + stuff_deleted = set( self._Execute( f'SELECT child_tag_id, ancestor_tag_id FROM {temp_tag_ids_table_name} CROSS JOIN {cache_tag_parents_lookup_table_name} ON ( child_tag_id = tag_id );' ) ) + stuff_deleted.update( self._Execute( f'SELECT child_tag_id, ancestor_tag_id FROM {temp_tag_ids_table_name} CROSS JOIN {cache_tag_parents_lookup_table_name} ON ( ancestor_tag_id = tag_id );' ) ) + + + else: + + stuff_deleted = set() + + self._ExecuteMany( 'DELETE FROM {} WHERE child_tag_id = ? OR ancestor_tag_id = ?;'.format( cache_tag_parents_lookup_table_name ), ( ( tag_id, tag_id ) for tag_id in tag_ids_to_clear_and_regen ) ) # we wipe them @@ -927,8 +935,6 @@ def RegenChains( self, tag_service_ids, tag_ids ): for applicable_tag_service_id in applicable_tag_service_ids: - service_key = self.modules_services.GetService( applicable_tag_service_id ).GetServiceKey() - unideal_statuses_to_pair_ids = self.GetTagParentsIdsChains( applicable_tag_service_id, tag_ids_to_clear_and_regen ) ideal_statuses_to_pair_ids = self.IdealiseStatusesToPairIds( tag_service_id, unideal_statuses_to_pair_ids ) @@ -953,11 +959,25 @@ def RegenChains( self, tag_service_ids, tag_ids ): - self._ExecuteMany( 'INSERT OR IGNORE INTO {} ( child_tag_id, ancestor_tag_id ) VALUES ( ?, ? );'.format( cache_tag_parents_lookup_table_name ), tps.IterateDescendantAncestorPairs() ) + stuff_added = set( tps.IterateDescendantAncestorPairs() ) + + self._ExecuteMany( 'INSERT OR IGNORE INTO {} ( child_tag_id, ancestor_tag_id ) VALUES ( ?, ? );'.format( cache_tag_parents_lookup_table_name ), stuff_added ) if tag_service_id in self._service_ids_to_display_application_status: - del self._service_ids_to_display_application_status[ tag_service_id ] + stuff_no_changes = stuff_deleted.intersection( stuff_added ) + stuff_deleted.difference_update( stuff_no_changes ) + stuff_added.difference_update( stuff_no_changes ) + + ( actual_parent_rows, ideal_parent_rows, parent_rows_to_add, parent_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + + ideal_parent_rows.difference_update( stuff_deleted ) + parent_rows_to_add.difference_update( stuff_deleted ) + parent_rows_to_remove.update( actual_parent_rows.intersection( stuff_deleted ) ) + + ideal_parent_rows.update( stuff_added ) + parent_rows_to_add.update( stuff_added.difference( actual_parent_rows ) ) + parent_rows_to_remove.difference_update( stuff_added ) diff --git a/hydrus/client/db/ClientDBTagSearch.py b/hydrus/client/db/ClientDBTagSearch.py index f12c2201a..647b0561f 100644 --- a/hydrus/client/db/ClientDBTagSearch.py +++ b/hydrus/client/db/ClientDBTagSearch.py @@ -157,7 +157,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self.modules_tag_siblings = modules_tag_siblings self.modules_mappings_counts = modules_mappings_counts - ClientDBModule.ClientDBModule.__init__( self, 'client tag search', cursor ) + super().__init__( 'client tag search', cursor ) self._missing_tag_search_service_pairs = set() diff --git a/hydrus/client/db/ClientDBTagSiblings.py b/hydrus/client/db/ClientDBTagSiblings.py index 686edf73f..010d06010 100644 --- a/hydrus/client/db/ClientDBTagSiblings.py +++ b/hydrus/client/db/ClientDBTagSiblings.py @@ -77,7 +77,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_db_maintenance: ClientDBMain self._service_ids_to_applicable_service_ids = None self._service_ids_to_interested_service_ids = None - ClientDBModule.ClientDBModule.__init__( self, 'client tag siblings', cursor ) + super().__init__( 'client tag siblings', cursor ) def _GenerateApplicationDicts( self ): @@ -416,15 +416,12 @@ def GetApplicationStatus( self, service_id ): sibling_rows_to_remove = actual_sibling_rows.difference( ideal_sibling_rows ) sibling_rows_to_add = ideal_sibling_rows.difference( actual_sibling_rows ) - num_actual_rows = len( actual_sibling_rows ) - num_ideal_rows = len( ideal_sibling_rows ) - - self._service_ids_to_display_application_status[ service_id ] = ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ service_id ] = ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) - ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ service_id ] + ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) = self._service_ids_to_display_application_status[ service_id ] - return ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) + return ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) def GetChainMembersFromIdeal( self, display_type, tag_service_id, ideal_tag_id ) -> typing.Set[ int ]: @@ -928,13 +925,12 @@ def NotifySiblingAddRowSynced( self, tag_service_id, row ): if tag_service_id in self._service_ids_to_display_application_status: - ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ tag_service_id ] + ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + actual_sibling_rows.add( row ) sibling_rows_to_add.discard( row ) - num_actual_rows += 1 - - self._service_ids_to_display_application_status[ tag_service_id ] = ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ tag_service_id ] = ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) @@ -942,13 +938,12 @@ def NotifySiblingDeleteRowSynced( self, tag_service_id, row ): if tag_service_id in self._service_ids_to_display_application_status: - ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) = self._service_ids_to_display_application_status[ tag_service_id ] + ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + actual_sibling_rows.discard( row ) sibling_rows_to_remove.discard( row ) - num_actual_rows -= 1 - - self._service_ids_to_display_application_status[ tag_service_id ] = ( sibling_rows_to_add, sibling_rows_to_remove, num_actual_rows, num_ideal_rows ) + self._service_ids_to_display_application_status[ tag_service_id ] = ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) @@ -1042,6 +1037,19 @@ def RegenChains( self, tag_service_ids, tag_ids ): tag_ids_to_clear_and_regen.update( self.GetChainsMembersFromIdeals( ClientTags.TAG_DISPLAY_DISPLAY_IDEAL, tag_service_id, ideal_tag_ids ) ) + if tag_service_id in self._service_ids_to_applicable_service_ids: + + with self._MakeTemporaryIntegerTable( tag_ids_to_clear_and_regen, 'tag_id' ) as temp_tag_ids_table_name: + + stuff_deleted = set( self._Execute( f'SELECT bad_tag_id, ideal_tag_id FROM {temp_tag_ids_table_name} CROSS JOIN {cache_tag_siblings_lookup_table_name} ON ( bad_tag_id = tag_id );' ) ) + stuff_deleted.update( self._Execute( f'SELECT bad_tag_id, ideal_tag_id FROM {temp_tag_ids_table_name} CROSS JOIN {cache_tag_siblings_lookup_table_name} ON ( ideal_tag_id = tag_id );' ) ) + + + else: + + stuff_deleted = set() + + self._ExecuteMany( 'DELETE FROM {} WHERE bad_tag_id = ? OR ideal_tag_id = ?;'.format( cache_tag_siblings_lookup_table_name ), ( ( tag_id, tag_id ) for tag_id in tag_ids_to_clear_and_regen ) ) applicable_tag_service_ids = self.GetApplicableServiceIds( tag_service_id ) @@ -1070,11 +1078,25 @@ def RegenChains( self, tag_service_ids, tag_ids ): - self._ExecuteMany( 'INSERT OR IGNORE INTO {} ( bad_tag_id, ideal_tag_id ) VALUES ( ?, ? );'.format( cache_tag_siblings_lookup_table_name ), tss.GetBadTagsToIdealTags().items() ) + stuff_added = set( tss.GetBadTagsToIdealTags().items() ) + + self._ExecuteMany( 'INSERT OR IGNORE INTO {} ( bad_tag_id, ideal_tag_id ) VALUES ( ?, ? );'.format( cache_tag_siblings_lookup_table_name ), stuff_added ) if tag_service_id in self._service_ids_to_display_application_status: - del self._service_ids_to_display_application_status[ tag_service_id ] + stuff_no_changes = stuff_deleted.intersection( stuff_added ) + stuff_deleted.difference_update( stuff_no_changes ) + stuff_added.difference_update( stuff_no_changes ) + + ( actual_sibling_rows, ideal_sibling_rows, sibling_rows_to_add, sibling_rows_to_remove ) = self._service_ids_to_display_application_status[ tag_service_id ] + + ideal_sibling_rows.difference_update( stuff_deleted ) + sibling_rows_to_add.difference_update( stuff_deleted ) + sibling_rows_to_remove.update( actual_sibling_rows.intersection( stuff_deleted ) ) + + ideal_sibling_rows.update( stuff_added ) + sibling_rows_to_add.update( stuff_added.difference( actual_sibling_rows ) ) + sibling_rows_to_remove.difference_update( stuff_added ) diff --git a/hydrus/client/db/ClientDBTagSuggestions.py b/hydrus/client/db/ClientDBTagSuggestions.py index 4e2daa2ad..c744be59c 100644 --- a/hydrus/client/db/ClientDBTagSuggestions.py +++ b/hydrus/client/db/ClientDBTagSuggestions.py @@ -18,7 +18,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_tags: ClientDBMaster.ClientD self.modules_services = modules_services self.modules_tags_local_cache = modules_tags_local_cache - ClientDBModule.ClientDBModule.__init__( self, 'client recent tags', cursor ) + super().__init__( 'client recent tags', cursor ) def _GetInitialTableGenerationDict( self ) -> dict: diff --git a/hydrus/client/db/ClientDBURLMap.py b/hydrus/client/db/ClientDBURLMap.py index b4e7f7b65..1ba536a7e 100644 --- a/hydrus/client/db/ClientDBURLMap.py +++ b/hydrus/client/db/ClientDBURLMap.py @@ -15,7 +15,7 @@ def __init__( self, cursor: sqlite3.Cursor, modules_urls: ClientDBMaster.ClientD self.modules_urls = modules_urls - ClientDBModule.ClientDBModule.__init__( self, 'client urls mapping', cursor ) + super().__init__( 'client urls mapping', cursor ) def _GetInitialIndexGenerationDict( self ) -> dict: diff --git a/hydrus/client/duplicates/ClientAutoDuplicates.py b/hydrus/client/duplicates/ClientAutoDuplicates.py index 83d2353d5..afc05dc79 100644 --- a/hydrus/client/duplicates/ClientAutoDuplicates.py +++ b/hydrus/client/duplicates/ClientAutoDuplicates.py @@ -104,7 +104,7 @@ class PairSelectorAndComparator( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._rules = HydrusSerialisable.SerialisableList() diff --git a/hydrus/client/duplicates/ClientDuplicates.py b/hydrus/client/duplicates/ClientDuplicates.py index 44a651f7a..3a5d9dbc9 100644 --- a/hydrus/client/duplicates/ClientDuplicates.py +++ b/hydrus/client/duplicates/ClientDuplicates.py @@ -836,7 +836,7 @@ class DuplicateContentMergeOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._tag_service_actions = [] self._rating_service_actions = [] diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py index 914a80f8c..e8fb38c32 100644 --- a/hydrus/client/gui/ClientGUI.py +++ b/hydrus/client/gui/ClientGUI.py @@ -87,9 +87,9 @@ from hydrus.client.gui.pages import ClientGUIManagementController from hydrus.client.gui.pages import ClientGUIPages from hydrus.client.gui.pages import ClientGUISession +from hydrus.client.gui.panels import ClientGUIManageOptionsPanel from hydrus.client.gui.panels import ClientGUIScrolledPanels from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit -from hydrus.client.gui.panels import ClientGUIScrolledPanelsManagement from hydrus.client.gui.panels import ClientGUIScrolledPanelsReview from hydrus.client.gui.parsing import ClientGUIParsing from hydrus.client.gui.parsing import ClientGUIParsingLegacy @@ -472,8 +472,7 @@ def __init__( self, controller: ClientControllerInterface.ClientControllerInterf self._controller = controller - ClientGUITopLevelWindows.MainFrameThatResizes.__init__( self, None, 'main', 'main_gui' ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( None, 'main', 'main_gui' ) self._currently_minimised_to_system_tray = False @@ -526,8 +525,6 @@ def __init__( self, controller: ClientControllerInterface.ClientControllerInterf self._pending_modal_job_statuses = set() - self._widget_event_filter = QP.WidgetEventFilter( self ) - self._controller.sub( self, 'AddModalMessage', 'modal_message' ) self._controller.sub( self, 'CreateNewSubscriptionGapDownloader', 'make_new_subscription_gap_downloader' ) self._controller.sub( self, 'DeleteOldClosedPages', 'delete_old_closed_pages' ) @@ -1546,7 +1543,7 @@ def _DebugMakeSomePopups( self ): HydrusData.ShowText( 'This is a test popup message -- ' + str( i ) ) - brother_classem_pinniped = '''++++What the fuck did you just fucking say about me, you worthless heretic? I'll have you know I graduated top of my aspirant tournament in the Heralds of Ultramar, and I've led an endless crusade of secret raids against the forces of The Great Enemy, and I have over 30 million confirmed purgings. I am trained in armored warfare and I'm the top brother in all the thousand Divine Chapters of the Adeptus Astartes. You are nothing to me but just another heretic. I will wipe you the fuck out with precision the likes of which has never been seen before in this universe, mark my fucking words. You think you can get away with saying that shit to me over the Warp? Think again, traitor. As we speak I am contacting my secret network of inquisitors across the galaxy and your malign powers are being traced right now so you better prepare for the holy storm, maggot. The storm that wipes out the pathetic little thing you call your soul. You're fucking dead, kid. I can warp anywhere, anytime, and I can kill you in over seven hundred ways, and that's just with my bolter. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the Departmento Munitorum and I will use it to its full extent to wipe your miserable ass off the face of the galaxy, you little shit. If only you could have known what holy retribution your little "clever" comment was about to bring down upon you, maybe you would have held your fucking impure tongue. But you couldn't, you didn't, and now you're paying the price, you Emperor-damned heretic.++++\n\n++++Better crippled in body than corrupt in mind++++\n\n++++The Emperor Protects++++''' + brother_classem_pinniped = '''++++What the fuck did you just fucking say about me, you worthless heretic? I'll have you know I graduated top of my aspirant tournament in the Heralds of Ultramar, and I've led an endless crusade of secret raids against the forces of The Great Enemy, and I have over 30 million confirmed purgings. I am trained in armored warfare and I'm the top brother in all the 8th Company. You are nothing to me but just another heretic. I will wipe you the fuck out with precision the likes of which has never been seen before in this Galaxy, mark my fucking words. You think you can get away with saying that shit to me over the Divine Astropathic Network? Think again, traitor. As we speak I am contacting my secret network of inquisitors across the galaxy and your malign powers are being traced right now so you better prepare for the holy storm, maggot. The storm that wipes out the pathetic little thing you call your soul. You're fucking dead, kid. I can transit the immaterium to anywhere, anytime, and I can kill you in over seven hundred ways, and that's just with my purity seals. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the Departmento Munitorum and I will use it to its full extent to wipe your miserable ass off the face of the galaxy, you little shit. If only you could have known what holy retribution your little "clever" comment was about to bring down upon you, maybe you would have held your fucking impure mutant tongue. But you couldn't, you didn't, and now you're paying the price, you Emperor-damned heretic.++++\n\n++++Better crippled in body than corrupt in mind++++\n\n++++The Emperor Protects++++''' HydrusData.ShowText( 'This is a very long message: \n\n' + brother_classem_pinniped ) @@ -3837,6 +3834,11 @@ def _InitialiseMenuInfoTags( self ): tag_display_maintenance_menu = ClientGUIMenus.GenerateMenu( menu ) ClientGUIMenus.AppendMenuItem( tag_display_maintenance_menu, 'review current sync', 'See how siblings and parents are currently applied.', self._ReviewTagDisplayMaintenance ) + + ClientGUIMenus.AppendSeparator( tag_display_maintenance_menu ) + + ClientGUIMenus.AppendMenuItem( tag_display_maintenance_menu, 'sync now', 'Start up any outstanding work now.', self._SyncTagDisplayMaintenanceNow ) + ClientGUIMenus.AppendSeparator( tag_display_maintenance_menu ) check_manager = ClientGUICommon.CheckboxManagerOptions( 'tag_display_maintenance_during_idle' ) @@ -4425,7 +4427,7 @@ def _ManageOptions( self ): with ClientGUITopLevelWindowsPanels.DialogManage( self, title, frame_key ) as dlg: - panel = ClientGUIScrolledPanelsManagement.ManageOptionsPanel( dlg ) + panel = ClientGUIManageOptionsPanel.ManageOptionsPanel( dlg ) dlg.SetPanel( panel ) @@ -6880,6 +6882,22 @@ def _SwitchBoolean( self, name ): + def _SyncTagDisplayMaintenanceNow( self ): + + def do_it(): + + # this guy can block for db access, so do it off Qt + there_was_work_to_do = CG.client_controller.tag_display_maintenance_manager.SyncFasterNow() + + if not there_was_work_to_do: + + HydrusData.ShowText( 'Seems like we are all synced already!' ) + + + + self._controller.CallToThread( do_it ) + + def _TestServerBusy( self, service_key ): def do_it( service ): diff --git a/hydrus/client/gui/ClientGUIDialogs.py b/hydrus/client/gui/ClientGUIDialogs.py index ba93ec9e7..f9ccb7c68 100644 --- a/hydrus/client/gui/ClientGUIDialogs.py +++ b/hydrus/client/gui/ClientGUIDialogs.py @@ -26,7 +26,7 @@ class Dialog( QP.Dialog ): def __init__( self, parent, title, style = QC.Qt.Dialog, position = 'topleft' ): - QP.Dialog.__init__( self, parent ) + super().__init__( parent ) self.setWindowFlags( style ) @@ -93,7 +93,7 @@ class DialogChooseNewServiceMethod( Dialog ): def __init__( self, parent ): - Dialog.__init__( self, parent, 'how to set up the account?', position = 'center' ) + super().__init__( parent, 'how to set up the account?', position = 'center' ) register_message = 'I want to initialise a new account with the server. I have a registration token (a hexadecimal key starting with \'r\').' @@ -143,7 +143,7 @@ class DialogGenerateNewAccounts( Dialog ): def __init__( self, parent, service_key ): - Dialog.__init__( self, parent, 'configure new accounts' ) + super().__init__( parent, 'configure new accounts' ) self._service_key = service_key @@ -260,7 +260,7 @@ class DialogInputNamespaceRegex( Dialog ): def __init__( self, parent, namespace = '', regex = '' ): - Dialog.__init__( self, parent, 'configure quick namespace' ) + super().__init__( parent, 'configure quick namespace' ) self._namespace = QW.QLineEdit( self ) @@ -351,7 +351,7 @@ class DialogInputTags( Dialog ): def __init__( self, parent, service_key, tag_display_type, tags, message = '' ): - Dialog.__init__( self, parent, 'input tags' ) + super().__init__( parent, 'input tags' ) self._service_key = service_key @@ -445,7 +445,7 @@ class DialogInputUPnPMapping( Dialog ): def __init__( self, parent, external_port, protocol_type, internal_port, description, duration ): - Dialog.__init__( self, parent, 'configure upnp mapping' ) + super().__init__( parent, 'configure upnp mapping' ) self._external_port = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 ) @@ -520,7 +520,7 @@ class DialogSelectFromURLTree( Dialog ): def __init__( self, parent, url_tree ): - Dialog.__init__( self, parent, 'select items' ) + super().__init__( parent, 'select items' ) self._tree = QP.TreeWidgetWithInheritedCheckState( self ) @@ -641,7 +641,7 @@ def __init__( self, parent, message, default = '', placeholder = None, allow_bla suggestions = [] - Dialog.__init__( self, parent, 'enter text', position = 'center' ) + super().__init__( parent, 'enter text', position = 'center' ) self._chosen_suggestion = None self._allow_blank = allow_blank diff --git a/hydrus/client/gui/ClientGUIDialogsManage.py b/hydrus/client/gui/ClientGUIDialogsManage.py index 5fea2121b..fb2983ad2 100644 --- a/hydrus/client/gui/ClientGUIDialogsManage.py +++ b/hydrus/client/gui/ClientGUIDialogsManage.py @@ -42,8 +42,7 @@ def __init__( self, parent, media ): self._hashes.update( m.GetHashes() ) - ClientGUIDialogs.Dialog.__init__( self, parent, 'manage ratings for ' + HydrusNumbers.ToHumanInt( len( self._hashes ) ) + ' files', position = 'topleft' ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent, 'manage ratings for ' + HydrusNumbers.ToHumanInt( len( self._hashes ) ) + ' files', position = 'topleft' ) # @@ -213,7 +212,7 @@ class _IncDecPanel( QW.QWidget ): def __init__( self, parent, services, media ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._services = services @@ -330,7 +329,7 @@ class _LikePanel( QW.QWidget ): def __init__( self, parent, services, media ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._services = services @@ -470,7 +469,7 @@ class _NumericalPanel( QW.QWidget ): def __init__( self, parent, services, media ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._services = services @@ -607,7 +606,7 @@ def __init__( self, parent ): self._mappings_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._mappings_list = ClientGUIListCtrl.BetterListCtrlTreeView( self._mappings_listctrl_panel, CGLC.COLUMN_LIST_MANAGE_UPNP_MAPPINGS.ID, 12, model, delete_key_callback = self._Remove, activation_callback = self._Edit ) @@ -710,7 +709,7 @@ def publish_callable( result ): - def _ConvertDataToListCtrlTuples( self, mapping ): + def _ConvertDataToDisplayTuple( self, mapping ): ( description, internal_ip, internal_port, external_port, protocol, duration ) = mapping @@ -723,10 +722,12 @@ def _ConvertDataToListCtrlTuples( self, mapping ): pretty_duration = HydrusTime.TimeDeltaToPrettyTimeDelta( duration ) - display_tuple = ( description, internal_ip, str( internal_port ), str( external_port ), protocol, pretty_duration ) - sort_tuple = mapping + return ( description, internal_ip, str( internal_port ), str( external_port ), protocol, pretty_duration ) - return ( display_tuple, sort_tuple ) + + def _ConvertDataToSortTuple( self, mapping ): + + return mapping def _Edit( self ): diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py index 3c7487e78..b797d2fd3 100644 --- a/hydrus/client/gui/ClientGUIDownloaders.py +++ b/hydrus/client/gui/ClientGUIDownloaders.py @@ -40,7 +40,7 @@ class EditDownloaderDisplayPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_display, url_classes, url_class_keys_to_display, show_unmatched_urls_in_media_viewer ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._gugs = gugs self._gug_keys_to_gugs: typing.Dict[ bytes, ClientNetworkingGUG.GalleryURLGenerator ] = { gug.GetGUGKey() : gug for gug in self._gugs } @@ -58,7 +58,7 @@ def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_displa self._gug_display_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, self._ConvertGUGDisplayDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, self._ConvertGUGDisplayDataToDisplayTuple, self._ConvertGUGDisplayDataToSortTuple ) self._gug_display_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_display_list_ctrl_panel, CGLC.COLUMN_LIST_GUG_KEYS_TO_DISPLAY.ID, 15, model, activation_callback = self._EditGUGDisplay ) @@ -72,7 +72,7 @@ def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_displa self._url_display_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( media_viewer_urls_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, self._ConvertURLDisplayDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, self._ConvertURLDisplayDataToDisplayTuple, self._ConvertURLDisplayDataToSortTuple ) self._url_display_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._url_display_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_DISPLAY.ID, 15, model, activation_callback = self._EditURLDisplay ) @@ -144,7 +144,7 @@ def __init__( self, parent: QW.QWidget, network_engine, gugs, gug_keys_to_displa self.widget().setLayout( vbox ) - def _ConvertGUGDisplayDataToListCtrlTuples( self, data ): + def _ConvertGUGDisplayDataToDisplayTuple( self, data ): ( gug_key, display ) = data @@ -163,13 +163,21 @@ def _ConvertGUGDisplayDataToListCtrlTuples( self, data ): pretty_display = 'no' - display_tuple = ( pretty_name, pretty_display ) - sort_tuple = ( name, display ) + return ( pretty_name, pretty_display ) - return ( display_tuple, sort_tuple ) + + def _ConvertGUGDisplayDataToSortTuple( self, data ): + + ( gug_key, display ) = data + + gug = self._gug_keys_to_gugs[ gug_key ] + + name = gug.GetName() + + return ( name, display ) - def _ConvertURLDisplayDataToListCtrlTuples( self, data ): + def _ConvertURLDisplayDataToDisplayTuple( self, data ): ( url_class_key, display ) = data @@ -190,10 +198,21 @@ def _ConvertURLDisplayDataToListCtrlTuples( self, data ): pretty_display = 'no' - display_tuple = ( pretty_name, pretty_url_type, pretty_display ) - sort_tuple = ( url_class_name, pretty_url_type, display ) + return ( pretty_name, pretty_url_type, pretty_display ) + + + def _ConvertURLDisplayDataToSortTuple( self, data ): + + ( url_class_key, display ) = data - return ( display_tuple, sort_tuple ) + url_class = self._url_class_keys_to_url_classes[ url_class_key ] + + url_class_name = url_class.GetName() + url_type = url_class.GetURLType() + + pretty_url_type = HC.url_type_string_lookup[ url_type ] + + return ( url_class_name, pretty_url_type, display ) def _EditGUGDisplay( self ): @@ -278,7 +297,7 @@ class EditGUGPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, gug: ClientNetworkingGUG.GalleryURLGenerator ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_gug = gug @@ -432,7 +451,7 @@ class EditNGUGPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, ngug: ClientNetworkingGUG.NestedGalleryURLGenerator, available_gugs: typing.Iterable[ ClientNetworkingGUG.GalleryURLGenerator ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_ngug = ngug self._available_gugs = list( available_gugs ) @@ -445,7 +464,7 @@ def __init__( self, parent: QW.QWidget, ngug: ClientNetworkingGUG.NestedGalleryU self._gug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NGUG_GUGS.ID, self._ConvertGUGDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_NGUG_GUGS.ID, self._ConvertGUGDataToDisplayTuple, self._ConvertGUGDataToSortTuple ) self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUG_GUGS.ID, 30, model, use_simple_delete = True ) @@ -524,7 +543,7 @@ def _AddGUGButtonClick( self ): - def _ConvertGUGDataToListCtrlTuples( self, gug_key_and_name ): + def _ConvertGUGDataToDisplayTuple( self, gug_key_and_name ): ( gug_key, gug_name ) = gug_key_and_name @@ -542,10 +561,18 @@ def _ConvertGUGDataToListCtrlTuples( self, gug_key_and_name ): pretty_available = 'no' - display_tuple = ( pretty_name, pretty_available ) - sort_tuple = ( name, available ) + return ( pretty_name, pretty_available ) + + + def _ConvertGUGDataToSortTuple( self, gug_key_and_name ): + + ( gug_key, gug_name ) = gug_key_and_name + + name = gug_name + + available = gug_key in ( gug.GetGUGKey() for gug in self._available_gugs ) or gug_name in ( gug.GetName() for gug in self._available_gugs ) - return ( display_tuple, sort_tuple ) + return ( name, available ) def GetValue( self ) -> ClientNetworkingGUG.NestedGalleryURLGenerator: @@ -567,7 +594,7 @@ class EditGUGsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, gugs ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) menu_items = [] @@ -587,7 +614,7 @@ def __init__( self, parent: QW.QWidget, gugs ): self._gug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GUGS.ID, self._ConvertGUGToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_GUGS.ID, self._ConvertGUGToDisplayTuple, self._ConvertGUGToSortTuple ) self._gug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gug_list_ctrl_panel, CGLC.COLUMN_LIST_GUGS.ID, 30, model, delete_key_callback = self._DeleteGUG, activation_callback = self._EditGUG ) @@ -605,7 +632,7 @@ def __init__( self, parent: QW.QWidget, gugs ): self._ngug_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_NGUGS.ID, self._ConvertNGUGToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_NGUGS.ID, self._ConvertNGUGToDisplayTuple, self._ConvertNGUGToSortTuple ) self._ngug_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._ngug_list_ctrl_panel, CGLC.COLUMN_LIST_NGUGS.ID, 20, model, use_simple_delete = True, activation_callback = self._EditNGUG ) @@ -707,7 +734,7 @@ def _AddNGUG( self, ngug, select_sort_and_scroll = False ): self._ngug_list_ctrl.AddDatas( ( ngug, ), select_sort_and_scroll = select_sort_and_scroll ) - def _ConvertGUGToListCtrlTuples( self, gug ): + def _ConvertGUGToDisplayTuple( self, gug ): name = gug.GetName() example_url = gug.GetExampleURL() @@ -726,25 +753,22 @@ def _ConvertGUGToListCtrlTuples( self, gug ): if url_class is None: - gallery_url_class = False pretty_gallery_url_class = '' else: - gallery_url_class = True pretty_gallery_url_class = url_class.GetName() pretty_name = name pretty_example_url = example_url - display_tuple = ( pretty_name, pretty_example_url, pretty_gallery_url_class ) - sort_tuple = ( name, example_url, gallery_url_class ) - - return ( display_tuple, sort_tuple ) + return ( pretty_name, pretty_example_url, pretty_gallery_url_class ) - def _ConvertNGUGToListCtrlTuples( self, ngug ): + _ConvertGUGToSortTuple = _ConvertGUGToDisplayTuple + + def _ConvertNGUGToDisplayTuple( self, ngug ): existing_names = { gug.GetName() for gug in self._gug_list_ctrl.GetData() } @@ -764,12 +788,20 @@ def _ConvertNGUGToListCtrlTuples( self, ngug ): pretty_missing = '' - sort_gugs = len( gugs ) + return ( pretty_name, pretty_gugs, pretty_missing ) - display_tuple = ( pretty_name, pretty_gugs, pretty_missing ) - sort_tuple = ( name, sort_gugs, missing ) + + def _ConvertNGUGToSortTuple( self, ngug ): - return ( display_tuple, sort_tuple ) + existing_names = { gug.GetName() for gug in self._gug_list_ctrl.GetData() } + + name = ngug.GetName() + gugs = ngug.GetGUGNames() + missing = len( set( gugs ).difference( existing_names ) ) > 0 + + sort_gugs = len( gugs ) + + return ( name, sort_gugs, missing ) def _DeleteGUG( self ): @@ -916,7 +948,7 @@ class EditURLClassComponentPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, string_match: ClientStrings.StringMatch, default_value: typing.Optional[ str ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) from hydrus.client.gui import ClientGUIStringPanels @@ -1041,7 +1073,7 @@ def __init__( self, parent: QW.QWidget, parameter: ClientNetworkingURLClass.URLC # maybe graduate this guy to a 'any type of parameter' panel and have a dropdown and show/hide fixed name etc.. - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._dupe_names = dupe_names @@ -1287,7 +1319,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLClass ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._update_already_in_progress = False # Used to avoid infinite recursion on control updates. @@ -1350,7 +1382,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC parameters_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( parameters_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_PARAMETERS.ID, self._ConvertParameterToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_URL_CLASS_PARAMETERS.ID, self._ConvertParameterToDisplayTuple, self._ConvertParameterToSortTuple ) self._parameters = ClientGUIListCtrl.BetterListCtrlTreeView( parameters_listctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_PARAMETERS.ID, 5, model, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters ) @@ -1761,7 +1793,7 @@ def _AddPathComponent( self ): return self._EditPathComponent( ( string_match, default ) ) - def _ConvertParameterToListCtrlTuples( self, parameter: ClientNetworkingURLClass.URLClassParameterFixedName ): + def _ConvertParameterToDisplayTuple( self, parameter: ClientNetworkingURLClass.URLClassParameterFixedName ): name = parameter.GetName() value_string_match = parameter.GetValueStringMatch() @@ -1779,15 +1811,11 @@ def _ConvertParameterToListCtrlTuples( self, parameter: ClientNetworkingURLClass pretty_value_string_match += ' (is ephemeral)' - sort_name = pretty_name - sort_string_match = pretty_value_string_match - - display_tuple = ( pretty_name, pretty_value_string_match ) - sort_tuple = ( sort_name, sort_string_match ) - - return ( display_tuple, sort_tuple ) + return ( pretty_name, pretty_value_string_match ) + _ConvertParameterToSortTuple = _ConvertParameterToDisplayTuple + def _ConvertPathComponentRowToString( self, row ): ( string_match, default ) = row @@ -2276,11 +2304,12 @@ def UserIsOKToOK( self ): return True + class EditURLClassesPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, url_classes: typing.Iterable[ ClientNetworkingURLClass.URLClass ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) menu_items = [] @@ -2299,7 +2328,7 @@ def __init__( self, parent: QW.QWidget, url_classes: typing.Iterable[ ClientNetw self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASSES.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_URL_CLASSES.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASSES.ID, 15, model, use_simple_delete = True, activation_callback = self._Edit ) @@ -2371,7 +2400,7 @@ def _AddURLClass( self, url_class, select_sort_and_scroll = False ): self._changes_made = True - def _ConvertDataToListCtrlTuples( self, url_class ): + def _ConvertDataToDisplayTuple( self, url_class ): name = url_class.GetName() url_type = url_class.GetURLType() @@ -2389,12 +2418,11 @@ def _ConvertDataToListCtrlTuples( self, url_class ): pretty_url_type = HC.url_type_string_lookup[ url_type ] pretty_example_url = example_url - display_tuple = ( pretty_name, pretty_url_type, pretty_example_url ) - sort_tuple = ( name, url_type, example_url ) - - return ( display_tuple, sort_tuple ) + return ( pretty_name, pretty_url_type, pretty_example_url ) + _ConvertDataToSortTuple = _ConvertDataToDisplayTuple + def _Edit( self ): data = self._list_ctrl.GetTopSelectedData() @@ -2516,7 +2544,7 @@ class EditURLClassLinksPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, network_engine, url_classes, parsers, url_class_keys_to_parser_keys ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._url_classes = url_classes self._url_class_keys_to_url_classes = { url_class.GetClassKey() : url_class for url_class in self._url_classes } @@ -2532,7 +2560,7 @@ def __init__( self, parent: QW.QWidget, network_engine, url_classes, parsers, ur # - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, self._ConvertAPIPairDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, self._ConvertAPIPairDataToDisplayTuple, self._ConvertAPIPairDataToSortTuple ) self._api_pairs_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._notebook, CGLC.COLUMN_LIST_URL_CLASS_API_PAIRS.ID, 10, model ) @@ -2540,7 +2568,7 @@ def __init__( self, parent: QW.QWidget, network_engine, url_classes, parsers, ur self._parser_list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._notebook ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, self._ConvertParserDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, self._ConvertParserDataToDisplayTuple, self._ConvertParserDataToSortTuple ) self._parser_list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._parser_list_ctrl_panel, CGLC.COLUMN_LIST_URL_CLASS_KEYS_TO_PARSER_KEYS.ID, 24, model, activation_callback = self._EditParser ) @@ -2630,23 +2658,19 @@ def _ClearParser( self ): - def _ConvertAPIPairDataToListCtrlTuples( self, data ): + def _ConvertAPIPairDataToDisplayTuple( self, data ): ( a, b ) = data a_name = a.GetName() b_name = b.GetName() - pretty_a_name = a_name - pretty_b_name = b_name - - display_tuple = ( pretty_a_name, pretty_b_name ) - sort_tuple = ( a_name, b_name ) - - return ( display_tuple, sort_tuple ) + return ( a_name, b_name ) - def _ConvertParserDataToListCtrlTuples( self, data ): + _ConvertAPIPairDataToSortTuple = _ConvertAPIPairDataToDisplayTuple + + def _ConvertParserDataToDisplayTuple( self, data ): ( url_class_key, parser_key ) = data @@ -2673,12 +2697,11 @@ def _ConvertParserDataToListCtrlTuples( self, data ): pretty_parser_name = parser_name - display_tuple = ( pretty_url_class_name, pretty_url_type, pretty_parser_name ) - sort_tuple = ( url_class_name, pretty_url_type, parser_name ) - - return ( display_tuple, sort_tuple ) + return ( pretty_url_class_name, pretty_url_type, pretty_parser_name ) + _ConvertParserDataToSortTuple = _ConvertParserDataToDisplayTuple + def _EditParser( self ): if len( self._parsers ) == 0: diff --git a/hydrus/client/gui/ClientGUIPopupMessages.py b/hydrus/client/gui/ClientGUIPopupMessages.py index 8b2dad319..f6f4aec9d 100644 --- a/hydrus/client/gui/ClientGUIPopupMessages.py +++ b/hydrus/client/gui/ClientGUIPopupMessages.py @@ -35,7 +35,7 @@ class PopupWindow( QW.QFrame ): def __init__( self, parent ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Plain ) @@ -53,6 +53,7 @@ def EventDismiss( self, event ): self.TryToDismiss() + class PopupMessage( PopupWindow ): TEXT_CUTOFF = 1024 @@ -806,7 +807,7 @@ class PopupMessageManager( QW.QFrame ): def __init__( self, parent, job_status_queue: JobStatusPopupQueue ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised ) self.setLineWidth( 1 ) @@ -1281,7 +1282,7 @@ class PopupMessageDialogPanel( QW.QWidget ): def __init__( self, parent, job_status, hide_main_gui = False ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._yesno_open = False @@ -1478,7 +1479,7 @@ class PopupMessageSummaryBar( QW.QFrame ): def __init__( self, parent ): - QW.QFrame.__init__( self, parent ) + super().__init__( parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Plain ) diff --git a/hydrus/client/gui/ClientGUIRatings.py b/hydrus/client/gui/ClientGUIRatings.py index a17079500..895ba3fa1 100644 --- a/hydrus/client/gui/ClientGUIRatings.py +++ b/hydrus/client/gui/ClientGUIRatings.py @@ -263,7 +263,7 @@ class RatingIncDec( QW.QWidget ): def __init__( self, parent, service_key ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -455,7 +455,7 @@ class RatingLike( QW.QWidget ): def __init__( self, parent, service_key ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -606,7 +606,7 @@ class RatingNumerical( QW.QWidget ): def __init__( self, parent, service_key ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key diff --git a/hydrus/client/gui/ClientGUISerialisable.py b/hydrus/client/gui/ClientGUISerialisable.py index 5e0c45980..1c5c5b88e 100644 --- a/hydrus/client/gui/ClientGUISerialisable.py +++ b/hydrus/client/gui/ClientGUISerialisable.py @@ -17,7 +17,7 @@ class PNGExportPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, payload_obj, title = None, description = None, payload_description = None ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._payload_obj = payload_obj @@ -180,7 +180,7 @@ class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ): def __init__( self, parent, payload_objs ): - ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent ) + super().__init__( parent ) self._payload_objs = payload_objs diff --git a/hydrus/client/gui/ClientGUIShortcutControls.py b/hydrus/client/gui/ClientGUIShortcutControls.py index 046d24c5b..cf67721b0 100644 --- a/hydrus/client/gui/ClientGUIShortcutControls.py +++ b/hydrus/client/gui/ClientGUIShortcutControls.py @@ -62,7 +62,7 @@ class EditShortcutAndCommandPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, shortcut, command, shortcuts_name ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -105,13 +105,13 @@ class EditShortcutSetPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, shortcuts: ClientGUIShortcuts.ShortcutSet ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._name = QW.QLineEdit( self ) self._shortcuts_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUTS.ID, self._ConvertSortTupleToPrettyTuple ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SHORTCUTS.ID, self._ConvertShortcutTupleToDisplayTuple, self._ConvertShortcutTupleToSortTuple ) self._shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( self._shortcuts_panel, CGLC.COLUMN_LIST_SHORTCUTS.ID, 20, model, delete_key_callback = self.RemoveShortcuts, activation_callback = self.EditShortcuts ) @@ -190,16 +190,15 @@ def __init__( self, parent, shortcuts: ClientGUIShortcuts.ShortcutSet ): self.widget().setLayout( vbox ) - def _ConvertSortTupleToPrettyTuple( self, shortcut_tuple ): + def _ConvertShortcutTupleToDisplayTuple( self, shortcut_tuple ): ( shortcut, command ) = shortcut_tuple - display_tuple = ( shortcut.ToString(), command.ToString() ) - sort_tuple = display_tuple - - return ( display_tuple, sort_tuple ) + return ( shortcut.ToString(), command.ToString() ) + _ConvertShortcutTupleToSortTuple = _ConvertShortcutTupleToDisplayTuple + def _AddShortcutSet( self, shortcut_set: ClientGUIShortcuts.ShortcutSet ): self._shortcuts.AddDatas( shortcut_set.GetShortcutsAndCommands() ) @@ -373,7 +372,7 @@ class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merge_non_number_numpad, all_shortcuts ): - 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 editing shortcuts.' ) ) @@ -386,7 +385,7 @@ def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merg reserved_panel = ClientGUICommon.StaticBox( self, 'built-in hydrus shortcut sets' ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetDisplayTuple, self._GetSortTuple ) self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( reserved_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, model, activation_callback = self._EditReserved ) @@ -399,7 +398,7 @@ def __init__( self, parent, call_mouse_buttons_primary_secondary, shortcuts_merg custom_panel = ClientGUICommon.StaticBox( self, 'custom user sets' ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, self._GetDisplayTuple, self._GetSortTuple ) self._custom_shortcuts = ClientGUIListCtrl.BetterListCtrlTreeView( custom_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, model, delete_key_callback = self._Delete, activation_callback = self._EditCustom ) @@ -567,28 +566,40 @@ def _EditReserved( self ): - - def _GetTuples( self, shortcuts ): + def _GetDisplayTuple( self, shortcuts ): name = shortcuts.GetName() if name in ClientGUIShortcuts.shortcut_names_to_descriptions: pretty_name = ClientGUIShortcuts.shortcut_names_to_pretty_names[ name ] - sort_name = ClientGUIShortcuts.shortcut_names_sorted.index( name ) else: pretty_name = name - sort_name = name size = len( shortcuts ) - display_tuple = ( pretty_name, HydrusNumbers.ToHumanInt( size ) ) - sort_tuple = ( sort_name, size ) + return ( pretty_name, HydrusNumbers.ToHumanInt( size ) ) + + + def _GetSortTuple( self, shortcuts ): + + name = shortcuts.GetName() + + if name in ClientGUIShortcuts.shortcut_names_to_descriptions: + + sort_name = ClientGUIShortcuts.shortcut_names_sorted.index( name ) + + else: + + sort_name = name + + + size = len( shortcuts ) - return ( display_tuple, sort_tuple ) + return ( sort_name, size ) def _RestoreDefaults( self ): @@ -695,7 +706,7 @@ class ShortcutWidget( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._mouse_radio = QW.QRadioButton( 'mouse', self ) self._mouse_shortcut = MouseShortcutWidget( self ) @@ -760,7 +771,7 @@ def __init__( self, parent ): self._shortcut = ClientGUIShortcuts.Shortcut() - QW.QLineEdit.__init__( self, parent ) + super().__init__( parent ) self._SetShortcutString() @@ -804,7 +815,7 @@ class MouseShortcutWidget( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._button = MouseShortcutButton( self ) @@ -868,7 +879,7 @@ def __init__( self, parent ): self._press_instead_of_release = True - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) self._SetShortcutString() diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py index 2b0176942..8002286b6 100644 --- a/hydrus/client/gui/ClientGUIShortcuts.py +++ b/hydrus/client/gui/ClientGUIShortcuts.py @@ -863,7 +863,7 @@ def __init__( self, shortcut_type = None, shortcut_key = None, shortcut_press_ty modifiers = sorted( modifiers ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.shortcut_type = shortcut_type self.shortcut_key = shortcut_key @@ -1363,7 +1363,7 @@ class ShortcutsHandler( QC.QObject ): def __init__( self, parent: QW.QWidget, initial_shortcuts_names: typing.Collection[ str ], alternate_filter_target = None, catch_mouse = False, ignore_activating_mouse_click = False ): - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._catch_mouse = catch_mouse @@ -1732,7 +1732,7 @@ def __init__( self, shortcut_sets = None ): parent = CGC.core() - QC.QObject.__init__( self, parent ) + super().__init__( parent ) self._names_to_shortcut_sets = {} diff --git a/hydrus/client/gui/ClientGUISplash.py b/hydrus/client/gui/ClientGUISplash.py index e9b4054ce..471a2f216 100644 --- a/hydrus/client/gui/ClientGUISplash.py +++ b/hydrus/client/gui/ClientGUISplash.py @@ -19,7 +19,7 @@ class FrameSplashPanel( QW.QWidget ): def __init__( self, parent, controller, frame_splash_status ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._controller = controller diff --git a/hydrus/client/gui/ClientGUIStringControls.py b/hydrus/client/gui/ClientGUIStringControls.py index e289d5784..1d70df921 100644 --- a/hydrus/client/gui/ClientGUIStringControls.py +++ b/hydrus/client/gui/ClientGUIStringControls.py @@ -26,7 +26,7 @@ class StringConverterButton( ClientGUICommon.BetterButton ): def __init__( self, parent, string_converter: ClientStrings.StringConverter ): - ClientGUICommon.BetterButton.__init__( self, parent, 'edit string converter', self._Edit ) + super().__init__( parent, 'edit string converter', self._Edit ) self._string_converter = string_converter @@ -90,7 +90,7 @@ class StringMatchButton( ClientGUICommon.BetterButton ): def __init__( self, parent, string_match: ClientStrings.StringMatch ): - ClientGUICommon.BetterButton.__init__( self, parent, 'edit string match', self._Edit ) + super().__init__( parent, 'edit string match', self._Edit ) self._string_match = string_match @@ -141,7 +141,7 @@ class StringProcessorButton( ClientGUICommon.BetterButton ): def __init__( self, parent, string_processor: ClientStrings.StringProcessor, test_data_callable: typing.Callable[ [], ClientParsing.ParsingTestData ] ): - ClientGUICommon.BetterButton.__init__( self, parent, 'edit string processor', self._Edit ) + super().__init__( parent, 'edit string processor', self._Edit ) self._string_processor = string_processor self._test_data_callable = test_data_callable @@ -204,7 +204,7 @@ class StringMatchToStringMatchDictControl( QW.QWidget ): def __init__( self, parent, initial_dict: typing.Dict[ ClientStrings.StringMatch, ClientStrings.StringMatch ], min_height = 10, key_name = 'key' ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._key_name = key_name @@ -212,7 +212,7 @@ def __init__( self, parent, initial_dict: typing.Dict[ ClientStrings.StringMatch column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.KEY : self._key_name } - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, model, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides ) @@ -237,19 +237,18 @@ def __init__( self, parent, initial_dict: typing.Dict[ ClientStrings.StringMatch self.setLayout( vbox ) - def _ConvertDataToListCtrlTuples( self, data ): + def _ConvertDataToDisplayTuple( self, data ): ( key_string_match, value_string_match ) = data pretty_key = key_string_match.ToString() pretty_value = value_string_match.ToString() - display_tuple = ( pretty_key, pretty_value ) - sort_tuple = ( pretty_key, pretty_value ) - - return ( display_tuple, sort_tuple ) + return ( pretty_key, pretty_value ) + _ConvertDataToSortTuple = _ConvertDataToDisplayTuple + def _Add( self ): with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit ' + self._key_name ) as dlg: @@ -348,7 +347,7 @@ class StringToStringDictButton( ClientGUICommon.BetterButton ): def __init__( self, parent, label ): - ClientGUICommon.BetterButton.__init__( self, parent, label, self._Edit ) + super().__init__( parent, label, self._Edit ) self._value = {} @@ -388,7 +387,7 @@ class StringToStringDictControl( QW.QWidget ): def __init__( self, parent, initial_dict: typing.Dict[ str, str ], min_height = 10, key_name = 'key', value_name = 'value', allow_add_delete = True, edit_keys = True ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._key_name = key_name self._value_name = value_name @@ -401,7 +400,7 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, str ], min_height = column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_VALUE.KEY : self._key_name, CGLC.COLUMN_LIST_KEY_TO_VALUE.VALUE : self._value_name } - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_VALUE.ID, min_height, model, use_simple_delete = use_simple_delete, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides ) self._listctrl.columnListContentsChanged.connect( self.columnListContentsChanged ) @@ -433,16 +432,15 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, str ], min_height = self.setLayout( vbox ) - def _ConvertDataToListCtrlTuples( self, data ): + def _ConvertDataToDisplayTuple( self, data ): ( key, value ) = data - display_tuple = ( key, value ) - sort_tuple = ( key, value ) - - return ( display_tuple, sort_tuple ) + return ( key, value ) + _ConvertDataToSortTuple = _ConvertDataToDisplayTuple + def _Add( self ): with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: @@ -561,7 +559,7 @@ class StringToStringMatchDictControl( QW.QWidget ): def __init__( self, parent, initial_dict: typing.Dict[ str, ClientStrings.StringMatch ], min_height = 10, key_name = 'key' ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._key_name = key_name @@ -569,7 +567,7 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, ClientStrings.String column_types_to_name_overrides = { CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.KEY : self._key_name } - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_KEY_TO_STRING_MATCH.ID, min_height, model, use_simple_delete = True, activation_callback = self._Edit, column_types_to_name_overrides = column_types_to_name_overrides ) @@ -594,18 +592,17 @@ def __init__( self, parent, initial_dict: typing.Dict[ str, ClientStrings.String self.setLayout( vbox ) - def _ConvertDataToListCtrlTuples( self, data ): + def _ConvertDataToDisplayTuple( self, data ): ( key, string_match ) = data pretty_string_match = string_match.ToString() - display_tuple = ( key, pretty_string_match ) - sort_tuple = ( key, pretty_string_match ) - - return ( display_tuple, sort_tuple ) + return ( key, pretty_string_match ) + _ConvertDataToSortTuple = _ConvertDataToDisplayTuple + def _Add( self ): with ClientGUIDialogs.DialogTextEntry( self, 'enter the ' + self._key_name, allow_blank = False ) as dlg: diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py index bd1f16418..6b6335dd8 100644 --- a/hydrus/client/gui/ClientGUIStringPanels.py +++ b/hydrus/client/gui/ClientGUIStringPanels.py @@ -33,7 +33,7 @@ class MultilineStringConversionTestPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, string_processor: ClientStrings.StringProcessor ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._string_processor = string_processor @@ -162,7 +162,7 @@ class SingleStringConversionTestPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, string_processor: ClientStrings.StringProcessor ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._string_processor = string_processor @@ -308,11 +308,11 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, string_converter: ClientStrings.StringConverter, example_string_override = None ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) conversions_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, self._ConvertConversionToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, self._ConvertConversionToDisplayTuple, self._ConvertConversionToSortTuple ) self._conversions = ClientGUIListCtrl.BetterListCtrlTreeView( conversions_panel, CGLC.COLUMN_LIST_STRING_CONVERTER_CONVERSIONS.ID, 7, model, delete_key_callback = self._DeleteConversion, activation_callback = self._EditConversion ) @@ -455,7 +455,7 @@ def _CanMoveUp( self ): return False - def _ConvertConversionToListCtrlTuples( self, conversion ): + def _ConvertConversionToDisplayTuple( self, conversion ): ( number, conversion_type, data ) = conversion @@ -473,10 +473,14 @@ def _ConvertConversionToListCtrlTuples( self, conversion ): pretty_result = str( e ) - display_tuple = ( pretty_number, pretty_conversion, pretty_result ) - sort_tuple = ( number, number, number ) + return ( pretty_number, pretty_conversion, pretty_result ) - return ( display_tuple, sort_tuple ) + + def _ConvertConversionToSortTuple( self, conversion ): + + ( number, conversion_type, data ) = conversion + + return ( number, number, number ) def _DeleteConversion( self ): @@ -666,7 +670,7 @@ class _ConversionPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, conversion_type, data, example_text ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._control_panel = ClientGUICommon.StaticBox( self, 'string conversion step' ) @@ -1185,7 +1189,7 @@ def __init__( self, parent, string_joiner: ClientStrings.StringJoiner, test_data test_data = [] - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1312,7 +1316,7 @@ class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, string_match: ClientStrings.StringMatch, test_data = typing.Optional[ ClientParsing.ParsingTestData ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._match_type = ClientGUICommon.BetterChoice( self ) @@ -1565,7 +1569,7 @@ def __init__( self, parent, string_slicer: ClientStrings.StringSlicer, test_data test_data = [] - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1761,7 +1765,7 @@ def __init__( self, parent, string_sorter: ClientStrings.StringSorter, test_data test_data = [] - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -1917,7 +1921,7 @@ class EditStringSplitterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, string_splitter: ClientStrings.StringSplitter, example_string: str = '' ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # @@ -2054,7 +2058,7 @@ class EditStringTagFilterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, string_tag_filter: ClientStrings.StringTagFilter, test_data = typing.Optional[ ClientParsing.ParsingTestData ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) message = 'This works the same as any tag filter elsewhere in the program. Note that it converts your texts to valid hydrus tags, so everything is coming out lowercase with trimmed whitespace, and invalid tags will never pass.' @@ -2154,7 +2158,7 @@ class EditStringProcessorPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, string_processor: ClientStrings.StringProcessor, test_data: ClientParsing.ParsingTestData ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) # diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py index 6b888e01c..2c5cdb0b3 100644 --- a/hydrus/client/gui/ClientGUISubscriptions.py +++ b/hydrus/client/gui/ClientGUISubscriptions.py @@ -187,7 +187,7 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions. queries_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._query_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, self._ConvertQueryHeaderToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, self._ConvertQueryHeaderToDisplayTuple, self._ConvertQueryHeaderToSortTuple ) self._query_headers = ClientGUIListCtrl.BetterListCtrlTreeView( queries_panel, CGLC.COLUMN_LIST_SUBSCRIPTION_QUERIES.ID, 10, model, use_simple_delete = True, activation_callback = self._EditQuery ) @@ -248,7 +248,7 @@ def __init__( self, parent: QW.QWidget, subscription: ClientImportSubscriptions. self._publish_files_to_popup_button = QW.QCheckBox( self._file_presentation_panel ) self._publish_files_to_page = QW.QCheckBox( self._file_presentation_panel ) - self._publish_label_override = ClientGUICommon.NoneableTextCtrl( self._file_presentation_panel, 'subscription files', none_phrase = 'no, use subscription name' ) + self._publish_label_override = ClientGUICommon.NoneableTextCtrl( self._file_presentation_panel, '', placeholder_text = 'subscription files', none_phrase = 'no, use subscription name' ) self._merge_query_publish_events = QW.QCheckBox( self._file_presentation_panel ) tt = 'This is great to merge multiple subs to a combined location!' @@ -475,10 +475,9 @@ def _CheckNow( self ): self._UpdateDelayText() - def _ConvertQueryHeaderToListCtrlTuples( self, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader ): + def _ConvertQueryHeaderToDisplayTuple( self, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader ): last_check_time = query_header.GetLastCheckTime() - next_check_time = query_header.GetNextCheckTime() paused = query_header.IsPaused() checker_status = query_header.GetCheckerStatus() @@ -529,6 +528,44 @@ def _ConvertQueryHeaderToListCtrlTuples( self, query_header: ClientImportSubscri ( file_velocity, pretty_file_velocity ) = query_header.GetFileVelocityInfo() + try: + + estimate = query_header.GetBandwidthWaitingEstimate( CG.client_controller.network_engine.bandwidth_manager, self._original_subscription.GetName() ) + + if estimate == 0: + + pretty_delay = '' + + else: + + pretty_delay = 'bandwidth: ' + HydrusTime.TimeDeltaToPrettyTimeDelta( estimate ) + + + except: + + pretty_delay = 'could not determine bandwidth--there may be a problem with some of the urls in this query' + + + pretty_items = file_seed_cache_status.GetStatusText( simple = True ) + + return ( pretty_name, pretty_paused, pretty_status, pretty_latest_new_file_time, pretty_last_check_time, pretty_next_check_time, pretty_file_velocity, pretty_delay, pretty_items ) + + + def _ConvertQueryHeaderToSortTuple( self, query_header: ClientImportSubscriptionQuery.SubscriptionQueryHeader ): + + last_check_time = query_header.GetLastCheckTime() + next_check_time = query_header.GetNextCheckTime() + paused = query_header.IsPaused() + checker_status = query_header.GetCheckerStatus() + + name = query_header.GetHumanName() + + file_seed_cache_status = query_header.GetFileSeedCacheStatus() + + latest_new_file_time = file_seed_cache_status.GetLatestAddedTime() + + ( file_velocity, pretty_file_velocity ) = query_header.GetFileVelocityInfo() + file_velocity = tuple( file_velocity ) # for sorting, list/tuple -> tuple try: @@ -537,18 +574,15 @@ def _ConvertQueryHeaderToListCtrlTuples( self, query_header: ClientImportSubscri if estimate == 0: - pretty_delay = '' delay = 0 else: - pretty_delay = 'bandwidth: ' + HydrusTime.TimeDeltaToPrettyTimeDelta( estimate ) delay = estimate except: - pretty_delay = 'could not determine bandwidth--there may be a problem with some of the urls in this query' delay = 0 @@ -556,16 +590,11 @@ def _ConvertQueryHeaderToListCtrlTuples( self, query_header: ClientImportSubscri items = ( num_total, num_done ) - pretty_items = file_seed_cache_status.GetStatusText( simple = True ) - sort_latest_new_file_time = ClientGUIListCtrl.SafeNoneInt( latest_new_file_time ) sort_last_check_time = ClientGUIListCtrl.SafeNoneInt( last_check_time ) sort_next_check_time = ClientGUIListCtrl.SafeNoneInt( next_check_time ) - display_tuple = ( pretty_name, pretty_paused, pretty_status, pretty_latest_new_file_time, pretty_last_check_time, pretty_next_check_time, pretty_file_velocity, pretty_delay, pretty_items ) - sort_tuple = ( name, paused, checker_status, sort_latest_new_file_time, sort_last_check_time, sort_next_check_time, file_velocity, delay, items ) - - return ( display_tuple, sort_tuple ) + return ( name, paused, checker_status, sort_latest_new_file_time, sort_last_check_time, sort_next_check_time, file_velocity, delay, items ) def _CopyQueries( self ): @@ -1170,7 +1199,7 @@ def __init__( self, parent: QW.QWidget, query_header: ClientImportSubscriptionQu self._status_st.setMinimumWidth( st_width ) - self._display_name = ClientGUICommon.NoneableTextCtrl( self, 'my subscription', none_phrase = 'use query text' ) + self._display_name = ClientGUICommon.NoneableTextCtrl( self, '', placeholder_text = 'my subscription', none_phrase = 'use query text' ) self._query_text = QW.QLineEdit( self ) self._check_now = QW.QCheckBox( self ) self._paused = QW.QCheckBox( self ) @@ -1324,7 +1353,7 @@ def __init__( self, parent: QW.QWidget, subscriptions: typing.Collection[ Client self._subscriptions_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, self._ConvertSubscriptionToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, self._ConvertSubscriptionToDisplayTuple, self._ConvertSubscriptionToSortTuple ) self._subscriptions = ClientGUIListCtrl.BetterListCtrlTreeView( self._subscriptions_panel, CGLC.COLUMN_LIST_SUBSCRIPTIONS.ID, 12, model, use_simple_delete = True, activation_callback = self.Edit ) @@ -1537,7 +1566,7 @@ def _CanSeparate( self ): return False - def _ConvertSubscriptionToListCtrlTuples( self, subscription ): + def _ConvertSubscriptionToDisplayTuple( self, subscription ): ( name, gug_key_and_name, query_headers, checker_options, initial_file_limit, periodic_file_limit, paused, file_import_options, tag_import_options, no_work_until, no_work_until_reason ) = subscription.ToTuple() @@ -1594,8 +1623,6 @@ def _ConvertSubscriptionToListCtrlTuples( self, subscription ): num_ok = num_queries - ( num_dead + num_paused ) - status = ( num_queries, num_paused, num_dead ) - if num_queries == 0: pretty_status = 'no queries' @@ -1628,45 +1655,35 @@ def _ConvertSubscriptionToListCtrlTuples( self, subscription ): if max_estimate == 0: # don't seem to be any delays of any kind pretty_delay = '' - delay = 0 elif min_estimate == 0: # some are good to go, but there are delays pretty_delay = 'bandwidth: some ok, some up to ' + HydrusTime.TimeDeltaToPrettyTimeDelta( max_estimate ) - delay = max_estimate else: if min_estimate == max_estimate: # probably just one query, and it is delayed pretty_delay = 'bandwidth: up to ' + HydrusTime.TimeDeltaToPrettyTimeDelta( max_estimate ) - delay = max_estimate else: pretty_delay = 'bandwidth: from ' + HydrusTime.TimeDeltaToPrettyTimeDelta( min_estimate ) + ' to ' + HydrusTime.TimeDeltaToPrettyTimeDelta( max_estimate ) - delay = max_estimate except: pretty_delay = 'could not determine bandwidth, there may be an error with the sub or its urls' - delay = 0 else: pretty_delay = 'delayed--retrying ' + ClientTime.TimestampToPrettyTimeDelta( no_work_until, just_now_threshold = 0 ) + ' - because: ' + no_work_until_reason - delay = HydrusTime.GetTimeDeltaUntilTime( no_work_until ) file_seed_cache_status = ClientImportSubscriptionQuery.GenerateQueryHeadersStatus( query_headers ) - ( num_done, num_total ) = file_seed_cache_status.GetValueRange() - - items = ( num_total, num_done ) - pretty_items = file_seed_cache_status.GetStatusText( simple = True ) if paused: @@ -1678,13 +1695,96 @@ def _ConvertSubscriptionToListCtrlTuples( self, subscription ): pretty_paused = '' + return ( name, pretty_site, pretty_status, pretty_latest_new_file_time, pretty_last_checked, pretty_delay, pretty_items, pretty_paused ) + + + def _ConvertSubscriptionToSortTuple( self, subscription ): + + ( name, gug_key_and_name, query_headers, checker_options, initial_file_limit, periodic_file_limit, paused, file_import_options, tag_import_options, no_work_until, no_work_until_reason ) = subscription.ToTuple() + + pretty_site = gug_key_and_name[1] + + if len( query_headers ) > 0: + + latest_new_file_time = max( ( query_header.GetLatestAddedTime() for query_header in query_headers ) ) + + last_checked = max( ( query_header.GetLastCheckTime() for query_header in query_headers ) ) + + else: + + latest_new_file_time = 0 + + last_checked = 0 + + + # + + num_queries = len( query_headers ) + num_dead = 0 + num_paused = 0 + + for query_header in query_headers: + + if query_header.IsDead(): + + num_dead += 1 + + elif query_header.IsPaused(): + + num_paused += 1 + + + + status = ( num_queries, num_paused, num_dead ) + + # + + if HydrusTime.TimeHasPassed( no_work_until ): + + try: + + ( min_estimate, max_estimate ) = subscription.GetBandwidthWaitingEstimateMinMax( CG.client_controller.network_engine.bandwidth_manager ) + + if max_estimate == 0: # don't seem to be any delays of any kind + + delay = 0 + + elif min_estimate == 0: # some are good to go, but there are delays + + delay = max_estimate + + else: + + if min_estimate == max_estimate: # probably just one query, and it is delayed + + delay = max_estimate + + else: + + delay = max_estimate + + + + except: + + delay = 0 + + + else: + + delay = HydrusTime.GetTimeDeltaUntilTime( no_work_until ) + + + file_seed_cache_status = ClientImportSubscriptionQuery.GenerateQueryHeadersStatus( query_headers ) + + ( num_done, num_total ) = file_seed_cache_status.GetValueRange() + + items = ( num_total, num_done ) + sort_latest_new_file_time = ClientGUIListCtrl.SafeNoneInt( latest_new_file_time ) sort_last_checked = ClientGUIListCtrl.SafeNoneInt( last_checked ) - display_tuple = ( name, pretty_site, pretty_status, pretty_latest_new_file_time, pretty_last_checked, pretty_delay, pretty_items, pretty_paused ) - sort_tuple = ( name, pretty_site, status, sort_latest_new_file_time, sort_last_checked, delay, items, paused ) - - return ( display_tuple, sort_tuple ) + return ( name, pretty_site, status, sort_latest_new_file_time, sort_last_checked, delay, items, paused ) def _DoAsyncGetQueryLogContainers( self, query_headers: typing.Collection[ ClientImportSubscriptionQuery.SubscriptionQueryHeader ], call: HydrusData.Call ): diff --git a/hydrus/client/gui/ClientGUITagSorting.py b/hydrus/client/gui/ClientGUITagSorting.py index fa1eb319b..f0117988b 100644 --- a/hydrus/client/gui/ClientGUITagSorting.py +++ b/hydrus/client/gui/ClientGUITagSorting.py @@ -14,7 +14,7 @@ class TagSortControl( QW.QWidget ): def __init__( self, parent: QW.QWidget, tag_sort: ClientTagSorting.TagSort, show_siblings = False ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) choice_tuples = [ ( ClientTagSorting.sort_type_str_lookup[ sort_type ], sort_type ) for sort_type in ( ClientTagSorting.SORT_BY_HUMAN_TAG, ClientTagSorting.SORT_BY_HUMAN_SUBTAG, ClientTagSorting.SORT_BY_COUNT ) ] diff --git a/hydrus/client/gui/ClientGUITagSuggestions.py b/hydrus/client/gui/ClientGUITagSuggestions.py index 0ec1239e9..831229c6e 100644 --- a/hydrus/client/gui/ClientGUITagSuggestions.py +++ b/hydrus/client/gui/ClientGUITagSuggestions.py @@ -175,7 +175,7 @@ class FavouritesTagsPanel( QW.QWidget ): def __init__( self, parent, service_key, tag_presentation_location: int, activate_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._tag_presentation_location = tag_presentation_location @@ -228,7 +228,7 @@ class RecentTagsPanel( QW.QWidget ): def __init__( self, parent, service_key, activate_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._media = [] @@ -336,7 +336,7 @@ class RelatedTagsPanel( QW.QWidget ): def __init__( self, parent, service_key, activate_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._media = [] @@ -589,7 +589,7 @@ class FileLookupScriptTagsPanel( QW.QWidget ): def __init__( self, parent, service_key, activate_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._media = [] @@ -785,7 +785,7 @@ class SuggestedTagsPanel( QW.QWidget ): def __init__( self, parent, service_key, tag_presentation_location, handling_one_media, activate_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._tag_presentation_location = tag_presentation_location diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py index efa72ab02..80d7ad79a 100644 --- a/hydrus/client/gui/ClientGUITags.py +++ b/hydrus/client/gui/ClientGUITags.py @@ -123,7 +123,7 @@ class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, tag_autocomplete_options: ClientTagsHandling.TagAutocompleteOptions ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_tag_autocomplete_options = tag_autocomplete_options services_manager = CG.client_controller.services_manager @@ -327,7 +327,7 @@ def __init__( self, parent, master_service_keys_to_sibling_applicable_service_ke master_service_keys_to_sibling_applicable_service_keys = collections.defaultdict( list, master_service_keys_to_sibling_applicable_service_keys ) master_service_keys_to_parent_applicable_service_keys = collections.defaultdict( list, master_service_keys_to_parent_applicable_service_keys ) - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._tag_services_notebook = ClientGUICommon.BetterNotebook( self ) @@ -339,7 +339,7 @@ def __init__( self, parent, master_service_keys_to_sibling_applicable_service_ke services = list( CG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES ) ) - select_service_key = services[0].GetServiceKey() + default_tag_service_key = CG.client_controller.new_options.GetKey( 'default_tag_service_tab' ) for service in services: @@ -353,7 +353,7 @@ def __init__( self, parent, master_service_keys_to_sibling_applicable_service_ke self._tag_services_notebook.addTab( page, name ) - if master_service_key == select_service_key: + if master_service_key == default_tag_service_key: # Py 3.11/PyQt6 6.5.0/two tabs/total tab characters > ~12/select second tab during init = first tab disappears bug QP.CallAfter( self._tag_services_notebook.setCurrentWidget, page ) @@ -411,6 +411,18 @@ def __init__( self, parent, master_service_keys_to_sibling_applicable_service_ke self.widget().setLayout( vbox ) + self._tag_services_notebook.currentChanged.connect( self._ServicePageChanged ) + + + def _ServicePageChanged( self ): + + if CG.client_controller.new_options.GetBoolean( 'save_default_tag_service_tab_on_change' ): + + current_page = self._tag_services_notebook.currentWidget() + + CG.client_controller.new_options.SetKey( 'default_tag_service_tab', current_page.GetServiceKey() ) + + def GetValue( self ): @@ -432,7 +444,7 @@ class _Panel( QW.QWidget ): def __init__( self, parent: QW.QWidget, master_service_key: bytes, sibling_applicable_service_keys: typing.Sequence[ bytes ], parent_applicable_service_keys: typing.Sequence[ bytes ] ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._master_service_key = master_service_key @@ -517,6 +529,11 @@ def _AddSibling( self ): return self._AddService( current_service_keys ) + def GetServiceKey( self ): + + return self._master_service_key + + def GetValue( self ): return ( self._master_service_key, self._sibling_service_keys_listbox.GetData(), self._parent_service_keys_listbox.GetData() ) @@ -527,7 +544,7 @@ class EditTagDisplayManagerPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, tag_display_manager: ClientTagsHandling.TagDisplayManager ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._original_tag_display_manager = tag_display_manager @@ -590,7 +607,7 @@ class _Panel( QW.QWidget ): def __init__( self, parent: QW.QWidget, tag_display_manager: ClientTagsHandling.TagDisplayManager, service_key: bytes ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) single_tag_filter = tag_display_manager.GetTagFilter( ClientTags.TAG_DISPLAY_SINGLE_MEDIA, service_key ) selection_tag_filter = tag_display_manager.GetTagFilter( ClientTags.TAG_DISPLAY_SELECTION_LIST, service_key ) @@ -681,7 +698,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces = None, message = None, read_only = False ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._only_show_blacklist = only_show_blacklist self._namespaces = namespaces @@ -1963,7 +1980,7 @@ class IncrementalTaggingPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, service_key: bytes, medias: typing.List[ ClientMedia.MediaSingleton ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._medias = medias @@ -2275,8 +2292,7 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa def __init__( self, parent, location_context: ClientLocation.LocationContext, tag_presentation_location: int, medias: typing.List[ ClientMedia.MediaSingleton ], immediate_commit = False, canvas_key = None ): - ClientGUIScrolledPanels.ManagePanel.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._location_context = location_context self._tag_presentation_location = tag_presentation_location @@ -2594,7 +2610,7 @@ 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 ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) CAC.ApplicationCommandProcessorMixin.__init__( self ) self._location_context = location_context @@ -3543,7 +3559,7 @@ class _Panel( QW.QWidget ): def __init__( self, parent, service_key, tags = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._current_pertinent_tags = set() @@ -3573,7 +3589,7 @@ def __init__( self, parent, service_key, tags = None ): self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_PARENTS.ID, self._ConvertPairToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_TAG_PARENTS.ID, self._ConvertPairToDisplayTuple, self._ConvertPairToSortTuple ) self._tag_parents = ClientGUIListCtrl.BetterListCtrlTreeView( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 6, model, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows ) @@ -3707,7 +3723,7 @@ def _CanAddFromCurrentInput( self ): return True - def _ConvertPairToListCtrlTuples( self, pair ): + def _ConvertPairToDisplayTuple( self, pair ): ( old, new ) = pair @@ -3733,14 +3749,38 @@ def _ConvertPairToListCtrlTuples( self, pair ): note = '' - sign = HydrusData.ConvertStatusToPrefix( status ) + pretty_status = HydrusData.status_to_prefix.get( status, '(?) ' ) + + return ( pretty_status, old, new, note ) - pretty_status = sign + + def _ConvertPairToSortTuple( self, pair ): + + ( old, new ) = pair - display_tuple = ( pretty_status, old, new, note ) - sort_tuple = ( status, old, new, note ) + ( in_pending, in_petitioned, reason ) = self._parent_action_context.GetPairListCtrlInfo( pair ) - return ( display_tuple, sort_tuple ) + note = reason + + if in_pending or in_petitioned: + + if in_pending: + + status = HC.CONTENT_STATUS_PENDING + + else: + + status = HC.CONTENT_STATUS_PETITIONED + + + else: + + status = HC.CONTENT_STATUS_CURRENT + + note = '' + + + return ( status, old, new, note ) def _DeleteSelectedRows( self ): @@ -4298,7 +4338,7 @@ class _Panel( QW.QWidget ): def __init__( self, parent, service_key, tags = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._current_pertinent_tags = set() @@ -4321,7 +4361,7 @@ def __init__( self, parent, service_key, tags = None ): self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, self._ConvertPairToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, self._ConvertPairToDisplayTuple, self._ConvertPairToSortTuple ) self._tag_siblings = ClientGUIListCtrl.BetterListCtrlTreeView( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 14, model, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows ) @@ -4450,7 +4490,7 @@ def _CanAddFromCurrentInput( self ): return True - def _ConvertPairToListCtrlTuples( self, pair ): + def _ConvertPairToDisplayTuple( self, pair ): ( old, new ) = pair @@ -4476,9 +4516,7 @@ def _ConvertPairToListCtrlTuples( self, pair ): note = '' - sign = HydrusData.ConvertStatusToPrefix( status ) - - pretty_status = sign + pretty_status = HydrusData.status_to_prefix.get( status, '(?) ' ) existing_olds = self._old_siblings.GetTags() @@ -4494,10 +4532,50 @@ def _ConvertPairToListCtrlTuples( self, pair ): - display_tuple = ( pretty_status, old, new, note ) - sort_tuple = ( status, old, new, note ) + return ( pretty_status, old, new, note ) + + + def _ConvertPairToSortTuple( self, pair ): + + ( old, new ) = pair + + ( in_pending, in_petitioned, reason ) = self._sibling_action_context.GetPairListCtrlInfo( pair ) - return ( display_tuple, sort_tuple ) + note = reason + + if in_pending or in_petitioned: + + if in_pending: + + status = HC.CONTENT_STATUS_PENDING + + else: + + status = HC.CONTENT_STATUS_PETITIONED + + + else: + + status = HC.CONTENT_STATUS_CURRENT + + note = '' + + + existing_olds = self._old_siblings.GetTags() + + if old in existing_olds: + + if status == HC.CONTENT_STATUS_PENDING: + + note = 'CONFLICT: Will be rescinded on add.' + + elif status == HC.CONTENT_STATUS_CURRENT: + + note = 'CONFLICT: Will be petitioned/deleted on add.' + + + + return ( status, old, new, note ) def _DeleteSelectedRows( self ): @@ -4968,7 +5046,7 @@ def __init__( self, parent ): services = list( CG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES ) ) - select_service_key = services[0].GetServiceKey() + default_tag_service_key = CG.client_controller.new_options.GetKey( 'default_tag_service_tab' ) for service in services: @@ -4979,7 +5057,7 @@ def __init__( self, parent ): self._tag_services_notebook.addTab( page, name ) - if service_key == select_service_key: + if service_key == default_tag_service_key: QP.CallAfter( self._tag_services_notebook.setCurrentWidget, page ) @@ -5009,6 +5087,18 @@ def __init__( self, parent ): CG.client_controller.sub( self, '_UpdateStatusText', 'notify_new_menu_option' ) + self._tag_services_notebook.currentChanged.connect( self._ServicePageChanged ) + + + def _ServicePageChanged( self ): + + if CG.client_controller.new_options.GetBoolean( 'save_default_tag_service_tab_on_change' ): + + current_page = self._tag_services_notebook.currentWidget() + + CG.client_controller.new_options.SetKey( 'default_tag_service_tab', current_page.GetServiceKey() ) + + def _UpdateStatusText( self ): @@ -5039,7 +5129,7 @@ class _Panel( QW.QWidget ): def __init__( self, parent, service_key ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -5222,6 +5312,11 @@ def _SyncFaster( self ): self._StartRefresh() + def GetServiceKey( self ): + + return self._service_key + + def NotifyRefresh( self, service_key ): if service_key == self._service_key: @@ -5497,7 +5592,7 @@ class EditTagSummaryGeneratorPanel( ClientGUIScrolledPanels.EditPanel ): def __init__( self, parent: QW.QWidget, tag_summary_generator: TagSummaryGenerator ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) + super().__init__( parent ) show_panel = ClientGUICommon.StaticBox( self, 'shows' ) diff --git a/hydrus/client/gui/ClientGUITopLevelWindows.py b/hydrus/client/gui/ClientGUITopLevelWindows.py index cbda741a1..1fe8f3d9d 100644 --- a/hydrus/client/gui/ClientGUITopLevelWindows.py +++ b/hydrus/client/gui/ClientGUITopLevelWindows.py @@ -639,7 +639,7 @@ class Frame( QW.QWidget ): def __init__( self, parent, title ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self.setWindowTitle( title ) diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py index b05b84d64..8b13273f3 100644 --- a/hydrus/client/gui/QtPorting.py +++ b/hydrus/client/gui/QtPorting.py @@ -76,7 +76,7 @@ class LabelledSlider( QW.QWidget ): def __init__( self, parent = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self.setLayout( VBoxLayout( spacing = 2 ) ) @@ -143,7 +143,7 @@ class DirPickerCtrl( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) layout = HBoxLayout( spacing = 2 ) @@ -214,7 +214,7 @@ class FilePickerCtrl( QW.QWidget ): def __init__( self, parent = None, wildcard = None, starting_directory = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) layout = HBoxLayout( spacing = 2 ) @@ -1012,8 +1012,9 @@ def setMargin( self, val ): self.setContentsMargins( val, val, val, val ) -def AddToLayout( layout, item, flag = None, alignment = None ): +def AddToLayout( layout, item, flag = None, alignment = None ): + if isinstance( layout, GridLayout ): row = layout.next_row @@ -2006,8 +2007,6 @@ def ListsToTuples( potentially_nested_lists ): class WidgetEventFilter ( QC.QObject ): - _mouse_tracking_required = { 'EVT_MOUSE_EVENTS' } - _strong_focus_required = { 'EVT_KEY_DOWN' } def __init__( self, parent_widget ): @@ -2047,19 +2046,11 @@ def eventFilter( self, watched, event ): event_killed = False - if type == QC.QEvent.KeyPress: - - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KEY_DOWN', event ) - - elif type == QC.QEvent.WindowStateChange: + if type == QC.QEvent.WindowStateChange: if isValid( self._parent_widget ): if self._parent_widget.isMaximized() or (event.oldState() & QC.Qt.WindowMaximized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MAXIMIZE', event ) - - elif type == QC.QEvent.MouseMove: - - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event ) elif type == QC.QEvent.MouseButtonDblClick: @@ -2072,8 +2063,6 @@ def eventFilter( self, watched, event ): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DCLICK', event ) - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event ) - elif type == QC.QEvent.MouseButtonPress: if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DOWN', event ) @@ -2082,24 +2071,10 @@ def eventFilter( self, watched, event ): if event.buttons() & QC.Qt.RightButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DOWN', event ) - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event ) - elif type == QC.QEvent.MouseButtonRelease: if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_UP', event ) - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event ) - - elif type == QC.QEvent.Wheel: - - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSEWHEEL', event ) - - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event ) - - elif type == QC.QEvent.Scroll: - - event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SCROLLWIN', event ) - elif type == QC.QEvent.Move: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE', event ) @@ -2145,20 +2120,14 @@ def eventFilter( self, watched, event ): def _AddCallback( self, evt_name, callback ): - if evt_name in self._mouse_tracking_required: - - self._parent_widget.setMouseTracking( True ) - if evt_name in self._strong_focus_required: self._parent_widget.setFocusPolicy( QC.Qt.StrongFocus ) + self._callback_map[ evt_name ].append( callback ) - - def EVT_KEY_DOWN( self, callback ): - self._AddCallback( 'EVT_KEY_DOWN', callback ) - + def EVT_LEFT_DCLICK( self, callback ): self._AddCallback( 'EVT_LEFT_DCLICK', callback ) @@ -2183,14 +2152,6 @@ def EVT_MIDDLE_DOWN( self, callback ): self._AddCallback( 'EVT_MIDDLE_DOWN', callback ) - def EVT_MOUSE_EVENTS( self, callback ): - - self._AddCallback( 'EVT_MOUSE_EVENTS', callback ) - - def EVT_MOUSEWHEEL( self, callback ): - - self._AddCallback( 'EVT_MOUSEWHEEL', callback ) - def EVT_MOVE( self, callback ): self._AddCallback( 'EVT_MOVE', callback ) @@ -2203,10 +2164,6 @@ def EVT_RIGHT_DOWN( self, callback ): self._AddCallback( 'EVT_RIGHT_DOWN', callback ) - def EVT_SCROLLWIN( self, callback ): - - self._AddCallback( 'EVT_SCROLLWIN', callback ) - def EVT_SIZE( self, callback ): self._AddCallback( 'EVT_SIZE', callback ) diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py index 422c11e68..a293caf61 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvas.py +++ b/hydrus/client/gui/canvas/ClientGUICanvas.py @@ -323,8 +323,7 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext ): CC.COLOUR_MEDIA_TEXT : QG.QColor( 0, 0, 0 ) } - QW.QWidget.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self.setObjectName( 'HydrusMediaViewer' ) @@ -366,8 +365,6 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext ): self._current_drag_is_touch = False self._last_motion_pos = QC.QPoint( 0, 0 ) - self._widget_event_filter = QP.WidgetEventFilter( self ) - self._media_container.readyForNeighbourPrefetch.connect( self._PrefetchNeighbours ) self._media_container.zoomChanged.connect( self.ZoomChanged ) @@ -3464,8 +3461,7 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ): def __init__( self, parent, page_key, location_context: ClientLocation.LocationContext, media_results ): - CanvasWithHovers.__init__( self, parent, location_context ) - ClientMedia.ListeningMediaList.__init__( self, location_context, media_results ) + super().__init__( location_context, media_results, parent, location_context ) self._page_key = page_key diff --git a/hydrus/client/gui/canvas/ClientGUICanvasFrame.py b/hydrus/client/gui/canvas/ClientGUICanvasFrame.py index 5ce37ee51..99bc20776 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvasFrame.py +++ b/hydrus/client/gui/canvas/ClientGUICanvasFrame.py @@ -16,8 +16,7 @@ class CanvasFrame( CAC.ApplicationCommandProcessorMixin, ClientGUITopLevelWindow def __init__( self, parent ): # Parent is set to None here so that this window shows up as a separate entry on the taskbar - ClientGUITopLevelWindows.FrameThatResizesWithHovers.__init__( self, None, 'hydrus client media viewer', 'media_viewer' ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( None, 'hydrus client media viewer', 'media_viewer' ) self._canvas_window = None diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py index ad7a69058..f2025fb0b 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py +++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py @@ -667,7 +667,7 @@ class CanvasHoverFrameTop( CanvasHoverFrame ): def __init__( self, parent, my_canvas, canvas_key ): - CanvasHoverFrame.__init__( self, parent, my_canvas, canvas_key ) + super().__init__( parent, my_canvas, canvas_key ) self._current_zoom = 1.0 self._current_index_string = '' @@ -1238,7 +1238,7 @@ class CanvasHoverFrameTopRight( CanvasHoverFrame ): def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_key ): - CanvasHoverFrame.__init__( self, parent, my_canvas, canvas_key ) + super().__init__( parent, my_canvas, canvas_key ) self._top_hover = top_hover @@ -1527,7 +1527,7 @@ class NotePanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, name: str, note: str, note_visible: bool ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._name = name self._note_visible = note_visible @@ -1647,7 +1647,7 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ): def __init__( self, parent, my_canvas, top_right_hover: CanvasHoverFrameTopRight, canvas_key ): - CanvasHoverFrame.__init__( self, parent, my_canvas, canvas_key ) + super().__init__( parent, my_canvas, canvas_key ) self._top_right_hover = top_right_hover @@ -1846,7 +1846,7 @@ class CanvasHoverFrameRightDuplicates( CanvasHoverFrame ): def __init__( self, parent: QW.QWidget, my_canvas: QW.QWidget, canvas_key: bytes ): - CanvasHoverFrame.__init__( self, parent, my_canvas, canvas_key ) + super().__init__( parent, my_canvas, canvas_key ) self._always_on_top = True @@ -2164,7 +2164,7 @@ class CanvasHoverFrameTags( CanvasHoverFrame ): def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_key, location_context: ClientLocation.LocationContext ): - CanvasHoverFrame.__init__( self, parent, my_canvas, canvas_key ) + super().__init__( parent, my_canvas, canvas_key ) self._top_hover = top_hover diff --git a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py index bb8922b47..d095072e8 100644 --- a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py +++ b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py @@ -319,7 +319,7 @@ class Animation( QW.QWidget ): def __init__( self, parent, canvas_type, background_colour_generator ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._canvas_type = canvas_type self._background_colour_generator = background_colour_generator @@ -878,7 +878,7 @@ class AnimationBar( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._qss_colours = { 'hab_border' : QG.QColor( 0, 0, 0 ), @@ -1341,7 +1341,7 @@ class MediaContainer( QW.QWidget ): def __init__( self, parent, canvas_type, background_colour_generator, additional_event_filter: QC.QObject ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._canvas_type = canvas_type @@ -2668,7 +2668,7 @@ class EmbedButton( QW.QWidget ): def __init__( self, parent, background_colour_generator ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._background_colour_generator = background_colour_generator @@ -2796,7 +2796,7 @@ class OpenExternallyPanel( QW.QWidget ): def __init__( self, parent, media ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -2873,7 +2873,7 @@ class QtMediaPlayer( QW.QWidget ): def __init__( self, parent: QW.QWidget, canvas_type, background_colour_generator ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._canvas_type = canvas_type self._background_colour_generator = background_colour_generator @@ -3179,8 +3179,7 @@ class StaticImage( CAC.ApplicationCommandProcessorMixin, QW.QWidget ): def __init__( self, parent, canvas_type, background_colour_generator ): - QW.QWidget.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._canvas_type = canvas_type self._background_colour_generator = background_colour_generator diff --git a/hydrus/client/gui/canvas/ClientGUIMPV.py b/hydrus/client/gui/canvas/ClientGUIMPV.py index 4584e02f1..a251a82f1 100644 --- a/hydrus/client/gui/canvas/ClientGUIMPV.py +++ b/hydrus/client/gui/canvas/ClientGUIMPV.py @@ -116,7 +116,7 @@ class MPVShutdownEvent( QC.QEvent ): def __init__( self ): - QC.QEvent.__init__( self, MPVShutdownEventType ) + super().__init__( MPVShutdownEventType ) @@ -126,7 +126,7 @@ class MPVFileLoadedEvent( QC.QEvent ): def __init__( self ): - QC.QEvent.__init__( self, MPVFileLoadedEventType ) + super().__init__( MPVFileLoadedEventType ) ''' @@ -136,7 +136,7 @@ class MPVLogEvent( QC.QEvent ): def __init__( self, player, event ): - QC.QEvent.__init__( self, MPVLogEventType ) + super().__init__( MPVLogEventType ) self.player = player self.event = event @@ -149,7 +149,7 @@ class MPVFileSeekedEvent( QC.QEvent ): def __init__( self ): - QC.QEvent.__init__( self, MPVFileSeekedEventType ) + super().__init__( MPVFileSeekedEventType ) @@ -187,8 +187,7 @@ class MPVWidget( CAC.ApplicationCommandProcessorMixin, QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._canvas_type = CC.CANVAS_PREVIEW diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py index 272047b3e..1a7824268 100644 --- a/hydrus/client/gui/exporting/ClientGUIExport.py +++ b/hydrus/client/gui/exporting/ClientGUIExport.py @@ -51,7 +51,7 @@ def __init__( self, parent, export_folders ): self._export_folders_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, self._ConvertExportFolderToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, self._ConvertExportFolderToDisplayTuple, self._ConvertExportFolderToSortTuple ) self._export_folders = ClientGUIListCtrl.BetterListCtrlTreeView( self._export_folders_panel, CGLC.COLUMN_LIST_EXPORT_FOLDERS.ID, 6, model, use_simple_delete = True, activation_callback = self._Edit ) @@ -145,7 +145,7 @@ def _AddFolder( self ): - def _ConvertExportFolderToListCtrlTuples( self, export_folder: ClientExportingFiles.ExportFolder ): + def _ConvertExportFolderToDisplayTuple( self, export_folder: ClientExportingFiles.ExportFolder ): ( name, path, export_type, delete_from_client_after_export, export_symlinks, file_search_context, run_regularly, period, phrase, last_checked, run_now ) = export_folder.ToTuple() @@ -177,17 +177,13 @@ def _ConvertExportFolderToListCtrlTuples( self, export_folder: ClientExportingFi pretty_period += ' (running after dialog ok)' - pretty_phrase = phrase - last_error = export_folder.GetLastError() - display_tuple = ( name, path, pretty_export_type, pretty_file_search_context, pretty_period, pretty_phrase, last_error ) - - sort_tuple = ( name, path, pretty_export_type, pretty_file_search_context, period, phrase, last_error ) - - return ( display_tuple, sort_tuple ) + return ( name, path, pretty_export_type, pretty_file_search_context, pretty_period, phrase, last_error ) + _ConvertExportFolderToSortTuple = _ConvertExportFolderToDisplayTuple + def _Edit( self ): export_folder: typing.Optional[ ClientExportingFiles.ExportFolder ] = self._export_folders.GetTopSelectedData() @@ -589,7 +585,7 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ): self._tags_box.setMinimumSize( QC.QSize( 220, 300 ) ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._paths = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_EXPORT_FILES.ID, 24, model, delete_key_callback = self._DeletePaths ) @@ -701,7 +697,7 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ): - def _ConvertDataToListCtrlTuples( self, media ): + def _ConvertDataToDisplayTuple( self, media ): directory = self._directory_picker.GetPath() @@ -727,10 +723,28 @@ def _ConvertDataToListCtrlTuples( self, media ): pretty_path = 'INVALID, above destination directory: ' + path - display_tuple = ( pretty_number, pretty_mime, pretty_path ) - sort_tuple = ( number, pretty_mime, path ) + return ( pretty_number, pretty_mime, pretty_path ) + + + def _ConvertDataToSortTuple( self, media ): + + directory = self._directory_picker.GetPath() + + number = self._media_to_number_indices[ media ] + mime = media.GetMime() + + try: + + path = self._GetPath( media ) + + except Exception as e: + + path = str( e ) + + + pretty_mime = HC.mime_string_lookup[ mime ] - return ( display_tuple, sort_tuple ) + return ( number, pretty_mime, path ) def _DeletePaths( self ): diff --git a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py index daf836a12..7c885366e 100644 --- a/hydrus/client/gui/importing/ClientGUIFileSeedCache.py +++ b/hydrus/client/gui/importing/ClientGUIFileSeedCache.py @@ -336,7 +336,7 @@ def __init__( self, parent, controller, file_seed_cache: ClientImportFileSeeds.F # add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on file_seed add/remove - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, self._ConvertFileSeedToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, self._ConvertFileSeedToDisplayTuple, self._ConvertFileSeedToSortTuple ) self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_FILE_SEED_CACHE.ID, 30, model, activation_callback = self._ShowSelectionInNewPage, delete_key_callback = self._DeleteSelected ) @@ -363,7 +363,7 @@ def __init__( self, parent, controller, file_seed_cache: ClientImportFileSeeds.F QP.CallAfter( self._UpdateText ) - def _ConvertFileSeedToListCtrlTuples( self, file_seed: ClientImportFileSeeds.FileSeed ): + def _ConvertFileSeedToDisplayTuple( self, file_seed: ClientImportFileSeeds.FileSeed ): try: @@ -373,8 +373,6 @@ def _ConvertFileSeedToListCtrlTuples( self, file_seed: ClientImportFileSeeds.Fil except: - file_seed_index = '--' - pretty_file_seed_index = '--' @@ -407,14 +405,41 @@ def _ConvertFileSeedToListCtrlTuples( self, file_seed: ClientImportFileSeeds.Fil pretty_source_time = ClientTime.TimestampToPrettyTimeDelta( source_time ) - sort_source_time = ClientGUIListCtrl.SafeNoneInt( source_time ) - pretty_note = HydrusText.GetFirstLine( note ) - display_tuple = ( pretty_file_seed_index, pretty_file_seed_data, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note ) - sort_tuple = ( file_seed_index, pretty_file_seed_data, status, added, modified, sort_source_time, note ) + return ( pretty_file_seed_index, pretty_file_seed_data, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note ) + + + def _ConvertFileSeedToSortTuple( self, file_seed: ClientImportFileSeeds.FileSeed ): + + try: + + file_seed_index = self._file_seed_cache.GetFileSeedIndex( file_seed ) + + except: + + file_seed_index = -1 + + + file_seed_data = file_seed.file_seed_data_for_comparison + status = file_seed.status + added = file_seed.created + modified = file_seed.modified + source_time = file_seed.source_time + note = file_seed.note + + if file_seed.file_seed_type == ClientImportFileSeeds.FILE_SEED_TYPE_URL: + + pretty_file_seed_data = ClientNetworkingFunctions.ConvertURLToHumanString( file_seed_data ) + + else: + + pretty_file_seed_data = file_seed_data + + + sort_source_time = ClientGUIListCtrl.SafeNoneInt( source_time ) - return ( display_tuple, sort_tuple ) + return ( file_seed_index, pretty_file_seed_data, status, added, modified, sort_source_time, note ) def _CopySelectedNotes( self ): diff --git a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py index cf3dcc480..d8607393f 100644 --- a/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py +++ b/hydrus/client/gui/importing/ClientGUIGallerySeedLog.py @@ -258,7 +258,7 @@ def __init__( self, parent, controller, read_only: bool, can_generate_more_pages # add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on gallery_seed add/remove - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, self._ConvertGallerySeedToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, self._ConvertGallerySeedToDisplayTuple, self._ConvertGallerySeedToSortTuple ) self._list_ctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_GALLERY_SEED_LOG.ID, 30, model, delete_key_callback = self._DeleteSelected ) @@ -285,7 +285,7 @@ def __init__( self, parent, controller, read_only: bool, can_generate_more_pages QP.CallAfter( self._UpdateText ) - def _ConvertGallerySeedToListCtrlTuples( self, gallery_seed ): + def _ConvertGallerySeedToDisplayTuple( self, gallery_seed ): try: @@ -310,10 +310,29 @@ def _ConvertGallerySeedToListCtrlTuples( self, gallery_seed ): pretty_note = HydrusText.GetFirstLine( note ) - display_tuple = ( pretty_gallery_seed_index, pretty_url, pretty_status, pretty_added, pretty_modified, pretty_note ) - sort_tuple = ( gallery_seed_index, pretty_url, status, added, modified, note ) + return ( pretty_gallery_seed_index, pretty_url, pretty_status, pretty_added, pretty_modified, pretty_note ) - return ( display_tuple, sort_tuple ) + + def _ConvertGallerySeedToSortTuple( self, gallery_seed ): + + try: + + gallery_seed_index = self._gallery_seed_log.GetGallerySeedIndex( gallery_seed ) + + except: + + gallery_seed_index = '--' + + + url = gallery_seed.url + status = gallery_seed.status + added = gallery_seed.created + modified = gallery_seed.modified + note = gallery_seed.note + + pretty_url = ClientNetworkingFunctions.ConvertURLToHumanString( url ) + + return ( gallery_seed_index, pretty_url, status, added, modified, note ) def _CopySelectedGalleryURLs( self ): diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py index c6f59348c..243fcfc6f 100644 --- a/hydrus/client/gui/importing/ClientGUIImport.py +++ b/hydrus/client/gui/importing/ClientGUIImport.py @@ -106,7 +106,7 @@ class CheckBoxLineEdit( QW.QWidget ): def __init__( self, parent, placeholder_text ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._checkbox = QW.QCheckBox( self ) @@ -128,11 +128,14 @@ def __init__( self, parent, placeholder_text ): def _LineEditChanged( self ): - if self.IsChecked(): + # if the user unchecks and then clears the text, we won't re-check, but otherwise check on text change + if self._lineedit.text() != '' and not self._checkbox.isChecked(): - self.valueChanged.emit() + self._checkbox.setChecked( True ) + self.valueChanged.emit() + def GetValue( self ): @@ -170,7 +173,7 @@ def __init__( self, parent, service_key, filename_tagging_options = None, presen filename_tagging_options = TagImportOptions.FilenameTaggingOptions() - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -240,7 +243,7 @@ class _AdvancedPanel( QW.QWidget ): def __init__( self, parent, service_key, filename_tagging_options, present_for_accompanying_file_list ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._present_for_accompanying_file_list = present_for_accompanying_file_list @@ -487,7 +490,7 @@ class _SimplePanel( QW.QWidget ): def __init__( self, parent, service_key, filename_tagging_options, present_for_accompanying_file_list ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._present_for_accompanying_file_list = present_for_accompanying_file_list @@ -956,12 +959,12 @@ class _FilenameTaggingOptionsPanel( QW.QWidget ): def __init__( self, parent, service_key, paths ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._paths = paths - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._paths_list = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, model ) @@ -993,7 +996,7 @@ def __init__( self, parent, service_key, paths ): self._filename_tagging_panel.tagsChanged.connect( self.ScheduleRefreshFileList ) - def _ConvertDataToListCtrlTuples( self, data ): + def _ConvertDataToDisplayTuple( self, data ): ( index, path ) = data @@ -1001,13 +1004,16 @@ def _ConvertDataToListCtrlTuples( self, data ): pretty_index = HydrusNumbers.ToHumanInt( index + 1 ) - pretty_path = path pretty_tags = ', '.join( tags ) - display_tuple = ( pretty_index, pretty_path, pretty_tags ) - sort_tuple = ( index, path, tags ) + return ( pretty_index, path, pretty_tags ) - return ( display_tuple, sort_tuple ) + + def _ConvertDataToSortTuple( self, data ): + + ( index, path ) = data + + return ( index, path, index ) def _GetTags( self, index, path ): @@ -1069,11 +1075,11 @@ class _MetadataRoutersPanel( QW.QWidget ): def __init__( self, parent, metadata_routers, paths ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._paths = paths - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._paths_list = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_PATHS_TO_TAGS.ID, 10, model ) @@ -1114,7 +1120,7 @@ def __init__( self, parent, metadata_routers, paths ): self._metadata_routers_control.listBoxChanged.connect( self.ScheduleRefreshFileList ) - def _ConvertDataToListCtrlTuples( self, data ): + def _ConvertDataToDisplayTuple( self, data ): ( index, path ) = data @@ -1125,10 +1131,14 @@ def _ConvertDataToListCtrlTuples( self, data ): pretty_path = path pretty_strings = ', '.join( strings ) - display_tuple = ( pretty_index, pretty_path, pretty_strings ) - sort_tuple = ( index, path, strings ) + return ( pretty_index, pretty_path, pretty_strings ) - return ( display_tuple, sort_tuple ) + + def _ConvertDataToSortTuple( self, data ): + + ( index, path ) = data + + return ( index, path, index ) def _GetStrings( self, path ): diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py index e6329002c..e6e75e9db 100644 --- a/hydrus/client/gui/lists/ClientGUIListBoxes.py +++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py @@ -44,6 +44,13 @@ class BetterQListWidget( QW.QListWidget ): + def __init__( self, parent, delete_callable = None ): + + self._delete_callable = delete_callable + + super().__init__( parent ) + + def _DeleteIndices( self, indices: typing.Iterable[ int ] ): indices = sorted( indices, reverse = True ) @@ -191,7 +198,15 @@ def HasData( self, obj ): def keyPressEvent( self, event: QG.QKeyEvent ): - if event.modifiers() & QC.Qt.ControlModifier and event.key() in ( QC.Qt.Key_C, QC.Qt.Key_Insert ): + ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) + + if key in ClientGUIShortcuts.DELETE_KEYS_QT and self._delete_callable is not None: + + event.accept() + + self._delete_callable() + + elif event.modifiers() & QC.Qt.ControlModifier and event.key() in ( QC.Qt.Key_C, QC.Qt.Key_Insert ): event.accept() @@ -295,7 +310,7 @@ def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_calla self._add_callable = add_callable self._edit_callable = edit_callable - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._listbox = BetterQListWidget( self ) self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection ) @@ -845,7 +860,7 @@ def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_calla self._add_callable = add_callable self._edit_callable = edit_callable - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._listbox = BetterQListWidget( self ) self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection ) @@ -2294,7 +2309,7 @@ class _InnerWidget( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._parent = parent diff --git a/hydrus/client/gui/lists/ClientGUIListBoxesData.py b/hydrus/client/gui/lists/ClientGUIListBoxesData.py index 4d33abd48..4687eab0d 100644 --- a/hydrus/client/gui/lists/ClientGUIListBoxesData.py +++ b/hydrus/client/gui/lists/ClientGUIListBoxesData.py @@ -10,11 +10,6 @@ class ListBoxItem( object ): - def __init__( self ): - - pass - - def __eq__( self, other ): if isinstance( other, ListBoxItem ): @@ -79,7 +74,7 @@ class ListBoxItemTagSlice( ListBoxItem ): def __init__( self, tag_slice: str ): - ListBoxItem.__init__( self ) + super().__init__() self._tag_slice = tag_slice @@ -130,7 +125,7 @@ class ListBoxItemNamespaceColour( ListBoxItem ): def __init__( self, namespace: str, colour: typing.Tuple[ int, int, int ] ): - ListBoxItem.__init__( self ) + super().__init__() self._namespace = namespace self._colour = colour @@ -186,7 +181,7 @@ class ListBoxItemTextTag( ListBoxItem ): def __init__( self, tag: str ): - ListBoxItem.__init__( self ) + super().__init__() self._tag = tag self._ideal_tag = None @@ -339,7 +334,7 @@ def __init__( self, tag: str, current_count: int, deleted_count: int, pending_co # some have deleted and petitioned as well, so think about this - ListBoxItemTextTag.__init__( self, tag ) + super().__init__( tag ) self._current_count = current_count self._deleted_count = deleted_count @@ -470,7 +465,7 @@ class ListBoxItemPredicate( ListBoxItem ): def __init__( self, predicate: ClientSearch.Predicate ): - ListBoxItem.__init__( self ) + super().__init__() self._predicate = predicate self._i_am_an_or_under_construction = False diff --git a/hydrus/client/gui/lists/ClientGUIListConstants.py b/hydrus/client/gui/lists/ClientGUIListConstants.py index fbdf3164c..48925e01f 100644 --- a/hydrus/client/gui/lists/ClientGUIListConstants.py +++ b/hydrus/client/gui/lists/ClientGUIListConstants.py @@ -93,11 +93,14 @@ default_column_list_sort_lookup = {} +column_list_column_type_logical_position_lookup = collections.defaultdict( dict ) + def register_column_type( column_list_type: int, column_type: int, name: str, hideable: bool, default_width: int, initially_shown: bool ): column_list_column_name_lookup[ column_list_type ][ column_type ] = name column_list_column_hideable_lookup[ column_list_type ][ column_type ] = hideable default_column_list_columns_lookup[ column_list_type ].append( ( column_type, default_width, initially_shown ) ) + column_list_column_type_logical_position_lookup[ column_list_type ][ column_type ] = len( column_list_column_type_logical_position_lookup[ column_list_type ] ) class COLUMN_LIST_DEFINITION( object ): diff --git a/hydrus/client/gui/lists/ClientGUIListCtrl.py b/hydrus/client/gui/lists/ClientGUIListCtrl.py index dee2797d0..3e6a5769f 100644 --- a/hydrus/client/gui/lists/ClientGUIListCtrl.py +++ b/hydrus/client/gui/lists/ClientGUIListCtrl.py @@ -43,7 +43,7 @@ class HydrusListItemModel( QC.QAbstractItemModel ): def __init__( self, parent: QW.QWidget, column_list_type: int, data_to_display_tuple_func: typing.Callable, data_to_sort_tuple_func: typing.Callable, column_types_to_name_overrides = None ): - QC.QAbstractItemModel.__init__( self, parent ) + super().__init__( parent ) if column_types_to_name_overrides is None: @@ -139,7 +139,9 @@ def data( self, index: QC.QModelIndex, role = QC.Qt.DisplayRole ): self._data_to_display_tuples[ data ] = display_tuple - text = self._data_to_display_tuples[ data ][ column_type ] + column_logical_position = CGLC.column_list_column_type_logical_position_lookup[ self._column_list_type ][ column_type ] + + text = self._data_to_display_tuples[ data ][ column_logical_position ] if role == QC.Qt.ToolTipRole: @@ -341,7 +343,7 @@ def SetData( self, datas ): - def sort( self, column: int, order: QC.Qt.SortOrder = QC.Qt.AscendingOrder ): + def sort( self, column_logical_position: int, order: QC.Qt.SortOrder = QC.Qt.AscendingOrder ): self.layoutAboutToBeChanged.emit() @@ -350,7 +352,7 @@ def sort( self, column: int, order: QC.Qt.SortOrder = QC.Qt.AscendingOrder ): # it would also allow quick filtering # note, important, however, that you need to be careful in the view or whatever to do mapFromSource and mapToSource when handling indices since they'll jump around via the proxy's sort/filtering - self._sort_column_type = self._column_list_status.GetColumnTypeFromIndex( column ) + self._sort_column_type = self.headerData( column_logical_position, QC.Qt.Orientation.Horizontal, QC.Qt.UserRole ) asc = order == QC.Qt.AscendingOrder @@ -375,8 +377,8 @@ def master_sort_key( data ): else: # TODO: when we do hidden/rearranged columns, there will be a question on how to arrange the fallback here. I guess a frozen tuple according to the current order, if that isn't too CPU crazy - # or just the first column or two! - return ( 0, sort_tuple[ column ], sort_tuple ) + # or just the first column_logical_position or two! + return ( 0, sort_tuple[ column_logical_position ], sort_tuple ) @@ -422,11 +424,11 @@ def UpdateDatas( self, datas, check_for_changed_sort_data = False ): try: - existing_sort_index = self._column_list_status.GetColumnIndexFromType( self._sort_column_type ) + existing_sort_logical_index = CGLC.column_list_column_type_logical_position_lookup[ self._column_list_type ][ self._sort_column_type ] except: - existing_sort_index = 0 + existing_sort_logical_index = 0 for data in datas: @@ -457,7 +459,7 @@ def UpdateDatas( self, datas, check_for_changed_sort_data = False ): new_sort_tuple = self._data_to_sort_tuple_func( data ) - if existing_sort_tuple[ existing_sort_index ] != new_sort_tuple[ existing_sort_index ]: + if existing_sort_tuple[ existing_sort_logical_index ] != new_sort_tuple[ existing_sort_logical_index ]: sort_data_has_changed = True @@ -510,7 +512,7 @@ def data_to_sort_tuple_func( data ): return sort_tuple - HydrusListItemModel.__init__( self, parent, column_list_type, data_to_display_tuple_func, data_to_sort_tuple_func, column_types_to_name_overrides = column_types_to_name_overrides ) + super().__init__( parent, column_list_type, data_to_display_tuple_func, data_to_sort_tuple_func, column_types_to_name_overrides = column_types_to_name_overrides ) @@ -520,7 +522,7 @@ class BetterListCtrlTreeView( QW.QTreeView ): def __init__( self, parent, column_list_type, height_num_chars, model: HydrusListItemModel, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, column_types_to_name_overrides = None ): - QW.QTreeView.__init__( self, parent ) + super().__init__( parent ) self._have_shown_a_column_data_error = False @@ -606,9 +608,6 @@ def __init__( self, parent, column_list_type, height_num_chars, model: HydrusLis self.Sort() - self._widget_event_filter = QP.WidgetEventFilter( self ) - self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown ) - self.header().setSectionsMovable( False ) # can only turn this on when we move from data/sort tuples # self.header().setFirstSectionMovable( True ) # same self.header().setSectionsClickable( True ) @@ -913,45 +912,52 @@ def DeleteSelected( self ): self.DeleteDatas( deletee_datas ) - def EventKeyDown( self, event ): + def keyPressEvent( self, event ): ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) + event_processed = False + if key in ClientGUIShortcuts.DELETE_KEYS_QT: self.ProcessDeleteAction() + event_processed = True + elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ): self.ProcessActivateAction() + event_processed = True + elif key in ( ord( 'A' ), ord( 'a' ) ) and modifier == QC.Qt.ControlModifier: self.selectAll() + event_processed = True + elif key in ( ord( 'C' ), ord( 'c' ) ) and modifier == QC.Qt.ControlModifier: - if self._copy_rows_callable is None: - - return True - - else: + if self._copy_rows_callable is not None: copyable_texts = self._copy_rows_callable() - if len( copyable_texts ) == 0: - - return True - - else: + if len( copyable_texts ) > 0: CG.client_controller.pub( 'clipboard', 'text', '\n'.join( copyable_texts ) ) + event_processed = True + + + if event_processed: + + event.accept() + else: - return True # was: event.ignore() + QW.QTreeView.keyPressEvent( self, event ) @@ -1375,6 +1381,8 @@ def Sort( self, sort_column_type = None, sort_asc = None ): sort_asc = default_sort_asc + # TODO: this may want to be column_list_column_type_logical_position_lookup rather than the status lookup, depending on how we implement column order memory + # or it may simply need to navigate that question carefully if we have multiple lists open with different orders or whatever column = self._column_list_status.GetColumnIndexFromType( sort_column_type ) ord = QC.Qt.AscendingOrder if sort_asc else QC.Qt.DescendingOrder @@ -1401,1219 +1409,17 @@ def UpdateDatas( self, datas: typing.Optional[ typing.Iterable[ object ] ] = Non -class BetterListCtrl( QW.QTreeWidget ): - - columnListContentsChanged = QC.Signal() - - def __init__( self, parent, column_list_type, height_num_chars, data_to_tuples_func, use_simple_delete = False, delete_key_callback = None, can_delete_callback = None, activation_callback = None, column_types_to_name_overrides = None ): - - QW.QTreeWidget.__init__( self, parent ) - - self._have_shown_a_column_data_error = False - - self._creation_time = HydrusTime.GetNow() - - self._column_list_type = column_list_type - - self._column_list_status: ClientGUIListStatus.ColumnListStatus = CG.client_controller.column_list_manager.GetStatus( self._column_list_type ) - self._original_column_list_status = self._column_list_status - - self.setAlternatingRowColors( True ) - self.setColumnCount( self._column_list_status.GetColumnCount() ) - self.setSortingEnabled( False ) # Keeping the custom sort implementation. It would be better to use Qt's native sorting in the future so sort indicators are displayed on the headers as expected. - self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection ) - self.setRootIsDecorated( False ) - - self._initial_height_num_chars = height_num_chars - self._forced_height_num_chars = None - - self._has_initialised_size = False - - self._data_to_tuples_func = data_to_tuples_func - - self._use_simple_delete = use_simple_delete - self._has_done_deletes = False - self._can_delete_callback = can_delete_callback - - self._copy_rows_callable = None - - self._rows_menu_callable = None - - ( self._sort_column_type, self._sort_asc ) = self._column_list_status.GetSort() - - self._indices_to_data_info = {} - self._data_to_indices = {} - - # old way - ''' - #sizing_column_initial_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width() - total_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width() - - resize_column = 1 - - for ( i, ( name, width_num_chars ) ) in enumerate( columns ): - - if width_num_chars == -1: - - width = -1 - - resize_column = i + 1 - - else: - - width = self.fontMetrics().boundingRect( 'x' * width_num_chars ).width() - - total_width += width - - - self.headerItem().setText( i, name ) - - self.setColumnWidth( i, width ) - - - # Technically this is the previous behavior, but the two commented lines might work better in some cases (?) - self.header().setStretchLastSection( False ) - self.header().setSectionResizeMode( resize_column - 1 , QW.QHeaderView.Stretch ) - #self.setColumnWidth( resize_column - 1, sizing_column_initial_width ) - #self.header().setStretchLastSection( True ) - - self.setMinimumWidth( total_width ) - ''' - - main_tlw = CG.client_controller.GetMainTLW() - - # if last section is set too low, for instance 3, the column seems unable to ever shrink from initial (expanded to fill space) size - # _ _ ___ _ _ __ __ ___ - # ( \/\/ )( _)( \/\/ ) ( ) ( ) ( \ - # \ / ) _) \ / )(__ /__\ ) ) ) - # \/\/ (___) \/\/ (____)(_)(_)(___/ - # - # I think this is because of mismatch between set size and min size! So ensuring we never set smaller than that initially should fix this???!? - - MIN_SECTION_SIZE_CHARS = 3 - - self._min_section_width = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, MIN_SECTION_SIZE_CHARS ) - - self.header().setMinimumSectionSize( self._min_section_width ) - - last_column_index = self._column_list_status.GetColumnCount() - 1 - - self.header().setStretchLastSection( True ) - - for ( i, column_type ) in enumerate( self._column_list_status.GetColumnTypes() ): - - self.headerItem().setData( i, QC.Qt.UserRole, column_type ) - - if column_types_to_name_overrides is not None and column_type in column_types_to_name_overrides: - - name = column_types_to_name_overrides[ column_type ] - - else: - - name = CGLC.column_list_column_name_lookup[ self._column_list_type ][ column_type ] - - - self.headerItem().setText( i, name ) - self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) ) - - if i == last_column_index: - - width_chars = MIN_SECTION_SIZE_CHARS - - else: - - width_chars = self._column_list_status.GetColumnWidth( column_type ) - - - width_chars = max( width_chars, MIN_SECTION_SIZE_CHARS ) - - # ok this is a pain in the neck issue, but fontmetrics changes afte widget init. I guess font gets styled on top afterwards - # this means that if I use this window's fontmetrics here, in init, then it is different later on, and we get creeping growing columns lmao - # several other places in the client are likely affected in different ways by this also! - width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars ) - - self.setColumnWidth( i, width_pixels ) - - - self._delete_key_callback = delete_key_callback - self._activation_callback = activation_callback - - self._widget_event_filter = QP.WidgetEventFilter( self ) - self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown ) - self.itemDoubleClicked.connect( self.ProcessActivateAction ) - - self.header().setSectionsMovable( False ) # can only turn this on when we move from data/sort tuples - # self.header().setFirstSectionMovable( True ) # same - self.header().setSectionsClickable( True ) - self.header().sectionClicked.connect( self.EventColumnClick ) - - #self.header().sectionMoved.connect( self._DoStatusChanged ) # same - self.header().sectionResized.connect( self._SectionsResized ) - - self.header().setContextMenuPolicy( QC.Qt.CustomContextMenu ) - self.header().customContextMenuRequested.connect( self._ShowHeaderMenu ) - - CG.client_controller.CallAfterQtSafe( self, 'initialising multi-column list widths', self._InitialiseColumnWidths ) - - CG.client_controller.sub( self, 'NotifySettingsUpdated', 'reset_all_listctrl_status' ) - CG.client_controller.sub( self, 'NotifySettingsUpdated', 'reset_listctrl_status' ) - - - def _InitialiseColumnWidths( self ): - - MIN_SECTION_SIZE_CHARS = 3 - - main_tlw = CG.client_controller.GetMainTLW() - - last_column_index = self._column_list_status.GetColumnCount() - 1 - - for ( i, column_type ) in enumerate( self._column_list_status.GetColumnTypes() ): - - if i == last_column_index: - - width_chars = MIN_SECTION_SIZE_CHARS - - else: - - width_chars = self._column_list_status.GetColumnWidth( column_type ) - - - width_chars = max( width_chars, MIN_SECTION_SIZE_CHARS ) - - # ok this is a pain in the neck issue, but fontmetrics changes afte widget init. I guess font gets styled on top afterwards - # this means that if I use this window's fontmetrics here, in init, then it is different later on, and we get creeping growing columns lmao - # several other places in the client are likely affected in different ways by this also! - width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars ) - - self.setColumnWidth( i, width_pixels ) - - - self._has_initialised_size = True - - - def _AddDataInfo( self, data_info ): - - ( data, display_tuple, sort_tuple ) = data_info - - if data in self._data_to_indices: - - return - - - append_item = QW.QTreeWidgetItem() - - for i in range( len( display_tuple ) ): - - text = display_tuple[i] - - append_item.setText( i, HydrusText.GetFirstLine( text ) ) - append_item.setToolTip( i, ClientGUIFunctions.WrapToolTip( text ) ) - - - self.addTopLevelItem( append_item ) - - index = self.topLevelItemCount() - 1 - - self._indices_to_data_info[ index ] = data_info - self._data_to_indices[ data ] = index - - - def _DoStatusChanged( self ): - - self._column_list_status = self._GenerateCurrentStatus() - - CG.client_controller.column_list_manager.SaveStatus( self._column_list_status ) - - - def _GenerateCurrentStatus( self ) -> ClientGUIListStatus.ColumnListStatus: - - status = ClientGUIListStatus.ColumnListStatus() - - status.SetColumnListType( self._column_list_type ) - - main_tlw = CG.client_controller.GetMainTLW() - - columns = [] - - header = self.header() - - num_columns = header.count() - - last_column_index = num_columns - 1 - - # ok, the big pain in the ass situation here is getting a precise last column size that is reproduced on next dialog launch - # ultimately, with fuzzy sizing, style padding, scrollbars appearing, and other weirdness, the more precisely we try to define it, the more we will get dialogs that grow/shrink by a pixel each time - # *therefore*, the actual solution here is to move to snapping with a decent snap distance. the user loses size setting precision, but we'll snap back to a decent size every time, compensating for fuzz - - LAST_COLUMN_SNAP_DISTANCE_CHARS = 5 - - total_fixed_columns_width = 0 - - for visual_index in range( num_columns ): - - logical_index = header.logicalIndex( visual_index ) - - column_type = self.headerItem().data( logical_index, QC.Qt.UserRole ) - width_pixels = header.sectionSize( logical_index ) - shown = not header.isSectionHidden( logical_index ) - - if visual_index == last_column_index: - - # testing if scrollbar is visible is unreliable, since we don't know if it is laid out correct yet (we could be doing that now!) - # so let's just hack it - - width_pixels = self.width() - ( self.frameWidth() * 2 ) - total_fixed_columns_width - - else: - - total_fixed_columns_width += width_pixels - - - width_chars = ClientGUIFunctions.ConvertPixelsToTextWidth( main_tlw, width_pixels ) - - if visual_index == last_column_index: - - # here's the snap magic. final width_chars is always a multiple of 5 - width_chars = round( width_chars / LAST_COLUMN_SNAP_DISTANCE_CHARS ) * LAST_COLUMN_SNAP_DISTANCE_CHARS - - - columns.append( ( column_type, width_chars, shown ) ) - - - status.SetColumns( columns ) - - status.SetSort( self._sort_column_type, self._sort_asc ) - - return status - - - def _GetDisplayAndSortTuples( self, data ): - - try: - - ( display_tuple, sort_tuple ) = self._data_to_tuples_func( data ) - - except Exception as e: - - if not self._have_shown_a_column_data_error: - - HydrusData.ShowText( 'A multi-column list was unable to generate text or sort data for one or more rows! Please send hydrus dev the traceback!' ) - HydrusData.ShowException( e ) - - self._have_shown_a_column_data_error = True - - - error_display_tuple = [ 'unable to display' for i in range( self._column_list_status.GetColumnCount() ) ] - - return ( error_display_tuple, None ) - - - better_sort = [] - - for item in sort_tuple: - - if isinstance( item, str ): - - item = HydrusData.HumanTextSortKey( item ) - - - better_sort.append( item ) - - - sort_tuple = tuple( better_sort ) - - return ( display_tuple, sort_tuple ) - - - def _GetSelectedIndices( self ) -> typing.List[ int ]: - - indices = [] - - for i in range( self.topLevelItemCount() ): - - if self.topLevelItem( i ).isSelected(): - - indices.append( i ) - - - - return indices - - - def _IterateTopLevelItems( self ) -> typing.Iterator[ QW.QTreeWidgetItem ]: - - for i in range( self.topLevelItemCount() ): - - yield self.topLevelItem( i ) - - - - def _RecalculateIndicesAfterDelete( self ): - - indices_and_data_info = sorted( self._indices_to_data_info.items() ) - - self._indices_to_data_info = {} - self._data_to_indices = {} - - for ( index, ( old_index, data_info ) ) in enumerate( indices_and_data_info ): - - ( data, display_tuple, sort_tuple ) = data_info - - self._data_to_indices[ data ] = index - self._indices_to_data_info[ index ] = data_info - - - - def _RefreshHeaderNames( self ): - - for i in range( self.header().count() ): - - column_type = self.headerItem().data( i, QC.Qt.UserRole ) - - name = CGLC.column_list_column_name_lookup[ self._column_list_type ][ column_type ] - - if column_type == self._sort_column_type: - - char = '\u25B2' if self._sort_asc else '\u25BC' - - name_for_title = '{} {}'.format( name, char ) - - else: - - name_for_title = name - - - self.headerItem().setText( i, name_for_title ) - self.headerItem().setToolTip( i, ClientGUIFunctions.WrapToolTip( name ) ) - - - - def _SectionsResized( self, logical_index, old_size, new_size ): - - if self._has_initialised_size: - - self._DoStatusChanged() - - self.updateGeometry() - - - - def _ShowHeaderMenu( self ): - - menu = ClientGUIMenus.GenerateMenu( self ) - - name = CGLC.column_list_type_name_lookup[ self._column_list_type ] - - ClientGUIMenus.AppendMenuItem( menu, f'reset default column widths for "{name}" lists', 'Reset the column widths and other display settings for all lists of this type', CG.client_controller.column_list_manager.ResetToDefaults, self._column_list_type ) - - CGC.core().PopupMenu( self, menu ) - - - def _ShowRowsMenu( self ): - - if self._rows_menu_callable is None: - - return - - - try: - - menu = self._rows_menu_callable() - - except HydrusExceptions.DataMissing: - - return - - - CGC.core().PopupMenu( self, menu ) - - - def _SortDataInfo( self ): - - sort_column_index = self._column_list_status.GetColumnIndexFromType( self._sort_column_type ) - - data_infos = list( self._indices_to_data_info.values() ) - - data_infos_good = [ ( data, display_tuple, sort_tuple ) for ( data, display_tuple, sort_tuple ) in data_infos if sort_tuple is not None ] - data_infos_bad = [ ( data, display_tuple, sort_tuple ) for ( data, display_tuple, sort_tuple ) in data_infos if sort_tuple is None ] - - def sort_key( data_info ): - - ( data, display_tuple, sort_tuple ) = data_info - - return ( sort_tuple[ sort_column_index ], sort_tuple ) # add the sort tuple to get secondary sorting - - - try: - - data_infos_good.sort( key = sort_key, reverse = not self._sort_asc ) - - except Exception as e: - - HydrusData.ShowText( 'A multi-column list failed to sort! Please send hydrus dev the traceback!' ) - HydrusData.ShowException( e ) - - - data_infos_bad.extend( data_infos_good ) - - data_infos = data_infos_bad - - return data_infos - - - def _SortAndRefreshRows( self ): - - selected_data_quick = set( self.GetData( only_selected = True ) ) - - self.clearSelection() - - sorted_data_info = self._SortDataInfo() - - self._indices_to_data_info = {} - self._data_to_indices = {} - - for ( index, data_info ) in enumerate( sorted_data_info ): - - self._indices_to_data_info[ index ] = data_info - - ( data, display_tuple, sort_tuple ) = data_info - - self._data_to_indices[ data ] = index - - self._UpdateRow( index, display_tuple ) - - if data in selected_data_quick: - - self.topLevelItem( index ).setSelected( True ) - - - - self._RefreshHeaderNames() - - - def _UpdateRow( self, index, display_tuple ): - - for ( column_index, value ) in enumerate( display_tuple ): - - tree_widget_item = self.topLevelItem( index ) - - first_line = HydrusText.GetFirstLine( value ) - existing_value = tree_widget_item.text( column_index ) - - if existing_value != first_line: - - tree_widget_item.setText( column_index, first_line ) - tree_widget_item.setToolTip( column_index, ClientGUIFunctions.WrapToolTip( value ) ) - - - - - def AddDatas( self, datas: typing.Iterable[ object ], select_sort_and_scroll = False ): - - datas = list( datas ) - - if len( datas ) == 0: - - return - - - for data in datas: - - data = QP.ListsToTuples( data ) - - ( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data ) - - self._AddDataInfo( ( data, display_tuple, sort_tuple ) ) - - - if select_sort_and_scroll: - - self.clearSelection() - - self.SelectDatas( datas ) - - self.Sort() - - first_data = sorted( ( ( self._data_to_indices[ data ], data ) for data in datas ) )[0][1] - - self.ScrollToData( first_data ) - - - self.columnListContentsChanged.emit() - - - def AddRowsMenuCallable( self, menu_callable ): - - self._rows_menu_callable = menu_callable - - self.setContextMenuPolicy( QC.Qt.CustomContextMenu ) - self.customContextMenuRequested.connect( self.EventShowMenu ) - - - def DeleteDatas( self, datas: typing.Iterable[ object ] ): - - datas = [ QP.ListsToTuples( data ) for data in datas ] - - deletees = [ ( self._data_to_indices[ data ], data ) for data in datas if data in self._data_to_indices ] - - if len( deletees ) == 0: - - return - - - deletees.sort( reverse = True ) - - # The below comment is most probably obsolote (from before the Qt port), but keeping it just in case it is not and also as an explanation. - # - # I am not sure, but I think if subsequent deleteitems occur in the same event, the event processing of the first is forced!! - # this means that button checking and so on occurs for n-1 times on an invalid indices structure in this thing before correcting itself in the last one - # if a button update then tests selected data against the invalid index and a selection is on the i+1 or whatever but just got bumped up into invalid area, we are exception city - # this doesn't normally affect us because mostly we _are_ deleting selections when we do deletes, but 'try to link url stuff' auto thing hit this - # I obviously don't want to recalc all indices for every delete - # so I wrote a catch in getdata to skip the missing error, and now I'm moving the data deletion to a second loop, which seems to help - - for ( index, data ) in deletees: - - self.takeTopLevelItem( index ) - - - for ( index, data ) in deletees: - - del self._data_to_indices[ data ] - - del self._indices_to_data_info[ index ] - - - self._RecalculateIndicesAfterDelete() - - self.columnListContentsChanged.emit() - - self._has_done_deletes = True - - - def DeleteSelected( self ): - - indices = self._GetSelectedIndices() - - indices.sort( reverse = True ) - - for index in indices: - - ( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ index ] - - item = self.takeTopLevelItem( index ) - - del item - - del self._data_to_indices[ data ] - - del self._indices_to_data_info[ index ] - - - self._RecalculateIndicesAfterDelete() - - self.columnListContentsChanged.emit() - - self._has_done_deletes = True - - - def EventColumnClick( self, col ): - - sort_column_type = self._column_list_status.GetColumnTypeFromIndex( col ) - - if sort_column_type == self._sort_column_type: - - self._sort_asc = not self._sort_asc - - else: - - self._sort_column_type = sort_column_type - - self._sort_asc = True - - - self._SortAndRefreshRows() - - self._DoStatusChanged() - - - def EventKeyDown( self, event ): - - ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) - - if key in ClientGUIShortcuts.DELETE_KEYS_QT: - - self.ProcessDeleteAction() - - elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ): - - self.ProcessActivateAction() - - elif key in ( ord( 'A' ), ord( 'a' ) ) and modifier == QC.Qt.ControlModifier: - - self.selectAll() - - elif key in ( ord( 'C' ), ord( 'c' ) ) and modifier == QC.Qt.ControlModifier: - - if self._copy_rows_callable is None: - - return True - - else: - - copyable_texts = self._copy_rows_callable() - - if len( copyable_texts ) == 0: - - return True - - else: - - CG.client_controller.pub( 'clipboard', 'text', '\n'.join( copyable_texts ) ) - - - - else: - - return True # was: event.ignore() - - - - def EventShowMenu( self ): - - QP.CallAfter( self._ShowRowsMenu ) - - - def ForceHeight( self, rows ): - - self._forced_height_num_chars = rows - - self.updateGeometry() - - # +2 for the header row and * 1.25 for magic rough text-to-rowheight conversion - - #existing_min_width = self.minimumWidth() - - #( width_gumpf, ideal_client_height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 20, int( ( ideal_rows + 2 ) * 1.25 ) ) ) - - #QP.SetMinClientSize( self, ( existing_min_width, ideal_client_height ) ) - - - def GetData( self, only_selected = False ) -> list: - - if only_selected: - - indices = self._GetSelectedIndices() - - else: - - indices = list( self._indices_to_data_info.keys() ) - - - indices.sort() - - result = [] - - for index in indices: - - # this can get fired while indices are invalid, wew - if index not in self._indices_to_data_info: - - continue - - - ( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ index ] - - result.append( data ) - - - return result - - - def GetTopSelectedData( self ) -> typing.Optional[ object ]: - - indices = self._GetSelectedIndices() - - if len( indices ) > 0: - - top_index = min( indices ) - - ( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ top_index ] - - return data - - else: - - return None - - - - def HasData( self, data: object ): - - data = QP.ListsToTuples( data ) - - return data in self._data_to_indices - - - def HasDoneDeletes( self ): - - return self._has_done_deletes - - - def HasOneSelected( self ): - - return len( self.selectedItems() ) == 1 - - - def HasSelected( self ): - - return len( self.selectedItems() ) > 0 - - - def NotifySettingsUpdated( self, column_list_type = None ): - - if column_list_type is not None and column_list_type != self._column_list_type: - - return - - - self.blockSignals( True ) - self.header().blockSignals( True ) - - self._column_list_status: ClientGUIListStatus.ColumnListStatus = CG.client_controller.column_list_manager.GetStatus( self._column_list_type ) - self._original_column_list_status = self._column_list_status - - # - - ( self._sort_column_type, self._sort_asc ) = self._column_list_status.GetSort() - - # - - main_tlw = CG.client_controller.GetMainTLW() - - MIN_SECTION_SIZE_CHARS = 3 - - last_column_index = self._column_list_status.GetColumnCount() - 1 - - for ( i, column_type ) in enumerate( self._column_list_status.GetColumnTypes() ): - - if i == last_column_index: - - width_chars = MIN_SECTION_SIZE_CHARS - - else: - - width_chars = self._column_list_status.GetColumnWidth( column_type ) - - - width_chars = max( width_chars, MIN_SECTION_SIZE_CHARS ) - - width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars ) - - self.setColumnWidth( i, width_pixels ) - - - self.header().blockSignals( False ) - self.blockSignals( False ) - - # - - self.Sort() # note this saves the current status, so don't do it until we resize stuff - - - def ProcessActivateAction( self ): - - if self._activation_callback is not None: - - try: - - self._activation_callback() - - except Exception as e: - - HydrusData.ShowException( e ) - - - - - def ProcessDeleteAction( self ): - - if self._can_delete_callback is not None: - - if not self._can_delete_callback(): - - return - - - - if self._use_simple_delete: - - self.ShowDeleteSelectedDialog() - - elif self._delete_key_callback is not None: - - self._delete_key_callback() - - - - def ScrollToData( self, data: object ): - - data = QP.ListsToTuples( data ) - - if data in self._data_to_indices: - - index = self._data_to_indices[ data ] - - item = self.topLevelItem( index ) - - self.scrollToItem( item, hint = QW.QAbstractItemView.ScrollHint.PositionAtCenter ) - - self.setFocus( QC.Qt.OtherFocusReason ) - - - - def SelectDatas( self, datas: typing.Iterable[ object ], deselect_others = False ): - - datas = [ QP.ListsToTuples( data ) for data in datas ] - - selectee_indices = { self._data_to_indices[ data ] for data in datas if data in self._data_to_indices } - - if deselect_others: - - for ( index, item ) in enumerate( self._IterateTopLevelItems() ): - - item.setSelected( index in selectee_indices ) - - - else: - - for index in selectee_indices: - - item = self.topLevelItem( index ) - - item.setSelected( True ) - - - - - def SetCopyRowsCallable( self, copy_rows_callable ): - - self._copy_rows_callable = copy_rows_callable - - - def SetData( self, datas: typing.Iterable[ object ] ): - - datas = [ QP.ListsToTuples( data ) for data in datas ] - - existing_datas = set( self._data_to_indices.keys() ) - - # useful to preserve order here sometimes (e.g. export file path generation order) - datas_to_add = [ data for data in datas if data not in existing_datas ] - datas_to_update = [ data for data in datas if data in existing_datas ] - datas_to_delete = existing_datas.difference( datas ) - - if len( datas_to_delete ) > 0: - - self.DeleteDatas( datas_to_delete ) - - - if len( datas_to_update ) > 0: - - self.UpdateDatas( datas_to_update ) - - - if len( datas_to_add ) > 0: - - self.AddDatas( datas_to_add ) - - - self._SortAndRefreshRows() - - self.columnListContentsChanged.emit() - - - def ShowDeleteSelectedDialog( self ): - - from hydrus.client.gui import ClientGUIDialogsQuick - - result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' ) - - if result == QW.QDialog.Accepted: - - self.DeleteSelected() - - - - def _GetRowHeightEstimate( self ): - - if self.topLevelItemCount() > 0: - - height = self.rowHeight( self.indexFromItem( self.topLevelItem( 0 ) ) ) - - else: - - ( width_gumpf, height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 20, 1 ) ) - - - return height - - - def minimumSizeHint( self ): - - width = 0 - - for i in range( self.columnCount() - 1 ): - - width += self.columnWidth( i ) - - - width += self._min_section_width # the last column - - width += self.frameWidth() * 2 - - if self._forced_height_num_chars is None: - - min_num_rows = 4 - - else: - - min_num_rows = self._forced_height_num_chars - - - header_size = self.header().sizeHint() # this is better than min size hint for some reason ?( 69, 69 )? - - data_area_height = self._GetRowHeightEstimate() * min_num_rows - - PADDING = 10 - - min_size_hint = QC.QSize( width, header_size.height() + data_area_height + PADDING ) - - return min_size_hint - - - def resizeEvent( self, event ): - - result = QW.QTreeWidget.resizeEvent( self, event ) - - # do not touch this! weird hack that fixed a new bug in 6.6.1 where all columns would reset on load to 100px wide! - if self._has_initialised_size: - - self._DoStatusChanged() - - - return result - - - def sizeHint( self ): - - width = 0 - - width += self.frameWidth() * 2 - - # all but last column - - for i in range( self.columnCount() - 1 ): - - width += self.columnWidth( i ) - - - # - - # ok, we are going full slippery dippery doo now - # the issue is: when we first boot up, we want to give a 'hey, it would be nice' size of the last actual recorded final column - # HOWEVER, after that: we want to use the current size of the last column - # so, if it is the first couple of seconds, lmao. after that, oaml - # I later updated this to use the columnWidth, rather than hickery dickery text-to-pixel-width, since it was juddering resize around text width phase - - last_column_type = self._column_list_status.GetColumnTypes()[-1] - - if HydrusTime.TimeHasPassed( self._creation_time + 2 ): - - width += self.columnWidth( self.columnCount() - 1 ) - - # this is a hack to stop the thing suddenly growing to screen width in a weird resize loop - # I couldn't reproduce this error, so I assume it is a QSS or whatever font/style/scrollbar on some systems that caused inaccurate columnWidth result - width = min( width, self.width() ) - - else: - - last_column_chars = self._original_column_list_status.GetColumnWidth( last_column_type ) - - main_tlw = CG.client_controller.GetMainTLW() - - width += ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, last_column_chars ) - - - # - - if self._forced_height_num_chars is None: - - num_rows = self._initial_height_num_chars - - else: - - num_rows = self._forced_height_num_chars - - - header_size = self.header().sizeHint() - - data_area_height = self._GetRowHeightEstimate() * num_rows - - PADDING = 10 - - size_hint = QC.QSize( width, header_size.height() + data_area_height + PADDING ) - - return size_hint - - - def Sort( self, sort_column_type = None, sort_asc = None ): - - if sort_column_type is not None: - - self._sort_column_type = sort_column_type - - - if sort_asc is not None: - - self._sort_asc = sort_asc - - - self._SortAndRefreshRows() - - self.columnListContentsChanged.emit() - - self._DoStatusChanged() - - - def UpdateDatas( self, datas: typing.Optional[ typing.Iterable[ object ] ] = None, check_for_changed_sort_data = False ): - - if datas is None: - - # keep it sorted here, which is sometimes useful - - indices_and_datas = sorted( ( ( index, data ) for ( data, index ) in self._data_to_indices.items() ) ) - - datas = [ data for ( index, data ) in indices_and_datas ] - - else: - - datas = [ QP.ListsToTuples( data ) for data in datas ] - - - sort_data_has_changed = False - sort_index = self._column_list_status.GetColumnIndexFromType( self._sort_column_type ) - - for data in datas: - - ( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data ) - - data_info = ( data, display_tuple, sort_tuple ) - - index = self._data_to_indices[ data ] - - existing_data_info = self._indices_to_data_info[ index ] - - # catching an object that __eq__ with another but is actually a different lad--we want to swap the new one in - the_data_is_actually_a_different_object = data is not existing_data_info[0] - - if the_data_is_actually_a_different_object: - - self._data_to_indices[ data ] = index - - - if data_info != existing_data_info or the_data_is_actually_a_different_object: - - if check_for_changed_sort_data and not sort_data_has_changed: - - existing_sort_tuple = existing_data_info[2] - - if existing_sort_tuple is not None and sort_tuple is not None: - - # this does not govern secondary sorts, but let's not spam sorts m8 - if sort_tuple[ sort_index ] != existing_sort_tuple[ sort_index ]: - - sort_data_has_changed = True - - - - - self._indices_to_data_info[ index ] = data_info - - self._UpdateRow( index, display_tuple ) - - - - self.columnListContentsChanged.emit() - - return sort_data_has_changed - - - def SetNonDupeName( self, obj: object ): - - current_names = { o.GetName() for o in self.GetData() if o is not obj } - - HydrusSerialisable.SetNonDupeName( obj, current_names ) - - - def ReplaceData( self, old_data: object, new_data: object, sort_and_scroll = False ): - - self.ReplaceDatas( [ ( old_data, new_data ) ], sort_and_scroll = sort_and_scroll ) - - - def ReplaceDatas( self, replacement_tuples, sort_and_scroll = False ): - - if len( replacement_tuples ) == 0: - - return - - - first_new_data = None - - for ( old_data, new_data ) in replacement_tuples: - - old_data = QP.ListsToTuples( old_data ) - new_data = QP.ListsToTuples( new_data ) - - if first_new_data is None: - - first_new_data = new_data - - - data_index = self._data_to_indices[ old_data ] - - ( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( new_data ) - - data_info = ( new_data, display_tuple, sort_tuple ) - - self._indices_to_data_info[ data_index ] = data_info - - del self._data_to_indices[ old_data ] - - self._data_to_indices[ new_data ] = data_index - - self._UpdateRow( data_index, display_tuple ) - - - if sort_and_scroll and first_new_data is not None: - - self.Sort() - - self.ScrollToData( first_new_data ) - - - - class BetterListCtrlPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._vbox = QP.VBoxLayout() self._buttonbox = QP.HBoxLayout() - self._listctrl: typing.Optional[ BetterListCtrl ] = None + self._listctrl: typing.Optional[ BetterListCtrlTreeView ] = None self._permitted_object_types = [] self._import_add_callable = lambda x: None diff --git a/hydrus/client/gui/lists/ClientGUIListManager.py b/hydrus/client/gui/lists/ClientGUIListManager.py index 25405e764..88e5dbe50 100644 --- a/hydrus/client/gui/lists/ClientGUIListManager.py +++ b/hydrus/client/gui/lists/ClientGUIListManager.py @@ -11,7 +11,7 @@ class ColumnListManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._column_list_types_to_statuses = HydrusSerialisable.SerialisableDictionary() diff --git a/hydrus/client/gui/lists/ClientGUIListStatus.py b/hydrus/client/gui/lists/ClientGUIListStatus.py index d137758e9..b2f6a2822 100644 --- a/hydrus/client/gui/lists/ClientGUIListStatus.py +++ b/hydrus/client/gui/lists/ClientGUIListStatus.py @@ -12,7 +12,7 @@ class ColumnListStatus( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._column_list_type = 0 self._columns = [] diff --git a/hydrus/client/gui/media/ClientGUIMediaControls.py b/hydrus/client/gui/media/ClientGUIMediaControls.py index 02e7f0dd7..622edb50e 100644 --- a/hydrus/client/gui/media/ClientGUIMediaControls.py +++ b/hydrus/client/gui/media/ClientGUIMediaControls.py @@ -87,7 +87,7 @@ class VolumeControl( QW.QWidget ): def __init__( self, parent, canvas_type, direction = 'down' ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._canvas_type = canvas_type diff --git a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py index 165d3e5a1..4a892aa91 100644 --- a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py +++ b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py @@ -35,8 +35,7 @@ class EditFileTimestampsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUISc def __init__( self, parent: QW.QWidget, ordered_medias: typing.List[ ClientMedia.MediaSingleton ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._ordered_medias = ordered_medias diff --git a/hydrus/client/gui/metadata/ClientGUITagActions.py b/hydrus/client/gui/metadata/ClientGUITagActions.py index fbfee8b54..c05b3ab79 100644 --- a/hydrus/client/gui/metadata/ClientGUITagActions.py +++ b/hydrus/client/gui/metadata/ClientGUITagActions.py @@ -110,7 +110,10 @@ def AutoPetitionLoops( self, widget, pairs ): pre_existing_loop_strings = [] - for ( potential_new_a, potential_new_b ) in pairs: + # we only want to auto-petition stuff (and give the user dialog warnings) if they are _adding_. if they are removing an existing pair from a loop, great! + addee_pairs = set( pairs ).difference( current_pairs ) + + for ( potential_new_a, potential_new_b ) in addee_pairs: tags_to_check = [ ( potential_new_b, set(), [] ) ] diff --git a/hydrus/client/gui/metadata/ClientGUITime.py b/hydrus/client/gui/metadata/ClientGUITime.py index bf1a2c9e5..6c8f5dd62 100644 --- a/hydrus/client/gui/metadata/ClientGUITime.py +++ b/hydrus/client/gui/metadata/ClientGUITime.py @@ -678,7 +678,7 @@ class DateTimesCtrl( QW.QWidget ): def __init__( self, parent, time_allowed = True, seconds_allowed = False, milliseconds_allowed = False, none_allowed = False, only_past_dates = False ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._time_allowed = time_allowed self._seconds_allowed = seconds_allowed @@ -1090,7 +1090,7 @@ class TimeDeltaCtrl( QW.QWidget ): def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, milliseconds = False, monthly_allowed = False, monthly_label = 'monthly', negative_allowed = False ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._min = min self._show_days = days @@ -1373,7 +1373,7 @@ class TimestampDataStubCtrl( QW.QWidget ): def __init__( self, parent, timestamp_data_stub = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) if timestamp_data_stub is None: @@ -1602,7 +1602,7 @@ class VelocityCtrl( QW.QWidget ): def __init__( self, parent, min_unit_value, max_unit_value, min_time_delta, days = False, hours = False, minutes = False, seconds = False, per_phrase = 'per', unit = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._num = ClientGUICommon.BetterSpinBox( self, min=min_unit_value, max=max_unit_value, width = 60 ) diff --git a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py index 5209b3ee4..5b9b0bfb7 100644 --- a/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py +++ b/hydrus/client/gui/networking/ClientGUIHydrusNetwork.py @@ -494,7 +494,7 @@ class ReviewAccountsPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, service_key: bytes, account_identifiers: typing.Collection[ HydrusNetwork.AccountIdentifier ] ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key self._service = CG.client_controller.services_manager.GetService( self._service_key ) diff --git a/hydrus/client/gui/networking/ClientGUILogin.py b/hydrus/client/gui/networking/ClientGUILogin.py index fa89b397c..552750f4b 100644 --- a/hydrus/client/gui/networking/ClientGUILogin.py +++ b/hydrus/client/gui/networking/ClientGUILogin.py @@ -303,7 +303,7 @@ def __init__( self, parent, engine, login_scripts, domains_to_login_info ): warning = 'WARNING: Your credentials are stored in plaintext! For this and other reasons, I recommend you use throwaway accounts with hydrus!' warning += '\n' * 2 - warning += 'If a login script does not work for you, or the site you want has a complicated captcha, check out the Hydrus Companion web browser add-on--it can copy login cookies to hydrus! Pixiv recently changed their login system and now require this!' + warning += 'If a login script does not work for you, or the site you want has a complicated captcha, check out the Hydrus Companion web browser add-on--it can copy login cookies to hydrus! Pixiv now requires this! If you do set up HC for an external login, I recommend you set the respective domain(s) you are logging into to "not active" here (hit "flip active" on them), so hydrus knows it is not supposed to be taking responsibility.' warning_st = ClientGUICommon.BetterStaticText( self, warning ) warning_st.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter ) diff --git a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py index 0995518a4..66d265e62 100644 --- a/hydrus/client/gui/networking/ClientGUINetworkJobControl.py +++ b/hydrus/client/gui/networking/ClientGUINetworkJobControl.py @@ -5,6 +5,7 @@ from hydrus.core import HydrusData from hydrus.core import HydrusGlobals as HG +from hydrus.core import HydrusText from hydrus.core import HydrusTime from hydrus.client import ClientConstants as CC @@ -254,7 +255,7 @@ def _Update( self ): ( status_text, current_speed, bytes_read, bytes_to_read ) = self._network_job.GetStatus() - self._left_text.setText( status_text ) + self._left_text.setText( HydrusText.GetFirstLine( status_text ) ) speed_text = '' diff --git a/hydrus/client/gui/pages/ClientGUIManagementController.py b/hydrus/client/gui/pages/ClientGUIManagementController.py index 11cdc85c2..951c2239e 100644 --- a/hydrus/client/gui/pages/ClientGUIManagementController.py +++ b/hydrus/client/gui/pages/ClientGUIManagementController.py @@ -199,7 +199,7 @@ class ManagementController( HydrusSerialisable.SerialisableBase ): def __init__( self, page_name = 'page' ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._page_name = page_name diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py index 0ed03f076..1f96457e5 100644 --- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py +++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py @@ -1289,7 +1289,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa self._gallery_importers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._gallery_downloader_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._gallery_importers_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._gallery_importers_listctrl_panel, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, 4, model, delete_key_callback = self._RemoveGalleryImports, activation_callback = self._HighlightSelectedGalleryImport ) @@ -1486,7 +1486,7 @@ def _ClearExistingHighlightAndPanel( self ): self._gallery_importers_listctrl.UpdateDatas() - def _ConvertDataToListCtrlTuples( self, gallery_import ): + def _ConvertDataToDisplayTuple( self, gallery_import ): query_text = gallery_import.GetQueryText() @@ -1517,20 +1517,14 @@ def _ConvertDataToListCtrlTuples( self, gallery_import ): pretty_files_paused = CG.client_controller.new_options.GetString( 'stop_character' ) - sort_files_paused = -1 - elif files_paused: pretty_files_paused = CG.client_controller.new_options.GetString( 'pause_character' ) - sort_files_paused = 0 - else: pretty_files_paused = '' - sort_files_paused = 1 - gallery_finished = gallery_import.GalleryFinished() gallery_paused = gallery_import.GalleryPaused() @@ -1539,18 +1533,65 @@ def _ConvertDataToListCtrlTuples( self, gallery_import ): pretty_gallery_paused = CG.client_controller.new_options.GetString( 'stop_character' ) - sort_gallery_paused = -1 - elif gallery_paused: pretty_gallery_paused = CG.client_controller.new_options.GetString( 'pause_character' ) - sort_gallery_paused = 0 - else: pretty_gallery_paused = '' + + ( status_enum, pretty_status ) = gallery_import.GetSimpleStatus() + + file_seed_cache_status = gallery_import.GetFileSeedCache().GetStatus() + + pretty_progress = file_seed_cache_status.GetStatusText( simple = True ) + + added = gallery_import.GetCreationTime() + + pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False ) + + return ( pretty_query_text, pretty_source, pretty_files_paused, pretty_gallery_paused, pretty_status, pretty_progress, pretty_added ) + + + def _ConvertDataToSortTuple( self, gallery_import ): + + query_text = gallery_import.GetQueryText() + + source = gallery_import.GetSourceName() + + pretty_source = source + + files_finished = gallery_import.FilesFinished() + files_paused = gallery_import.FilesPaused() + + if files_finished: + + sort_files_paused = -1 + + elif files_paused: + + sort_files_paused = 0 + + else: + + sort_files_paused = 1 + + + gallery_finished = gallery_import.GalleryFinished() + gallery_paused = gallery_import.GalleryPaused() + + if gallery_finished: + + sort_gallery_paused = -1 + + elif gallery_paused: + + sort_gallery_paused = 0 + + else: + sort_gallery_paused = 1 @@ -1564,16 +1605,9 @@ def _ConvertDataToListCtrlTuples( self, gallery_import ): progress = ( num_total, num_done ) - pretty_progress = file_seed_cache_status.GetStatusText( simple = True ) - added = gallery_import.GetCreationTime() - pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False ) - - display_tuple = ( pretty_query_text, pretty_source, pretty_files_paused, pretty_gallery_paused, pretty_status, pretty_progress, pretty_added ) - sort_tuple = ( query_text, pretty_source, sort_files_paused, sort_gallery_paused, sort_status, progress, added ) - - return ( display_tuple, sort_tuple ) + return ( query_text, pretty_source, sort_files_paused, sort_gallery_paused, sort_status, progress, added ) def _CopySelectedQueries( self ): @@ -2346,7 +2380,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa self._watchers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._watchers_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_WATCHERS.ID, self._ConvertDataToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_WATCHERS.ID, self._ConvertDataToDisplayTuple, self._ConvertDataToSortTuple ) self._watchers_listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( self._watchers_listctrl_panel, CGLC.COLUMN_LIST_WATCHERS.ID, 4, model, delete_key_callback = self._RemoveWatchers, activation_callback = self._HighlightSelectedWatcher ) @@ -2567,7 +2601,7 @@ def _ClearExistingHighlightAndPanel( self ): self._watchers_listctrl.UpdateDatas() - def _ConvertDataToListCtrlTuples( self, watcher: ClientImportWatchers.WatcherImport ): + def _ConvertDataToDisplayTuple( self, watcher: ClientImportWatchers.WatcherImport ): subject = watcher.GetSubject() @@ -2605,18 +2639,47 @@ def _ConvertDataToListCtrlTuples( self, watcher: ClientImportWatchers.WatcherImp pretty_checking_paused = CG.client_controller.new_options.GetString( 'stop_character' ) - sort_checking_paused = -1 - elif checking_paused: pretty_checking_paused = CG.client_controller.new_options.GetString( 'pause_character' ) - sort_checking_paused = 0 - else: pretty_checking_paused = '' + + file_seed_cache_status = watcher.GetFileSeedCache().GetStatus() + + pretty_progress = file_seed_cache_status.GetStatusText( simple = True ) + + added = watcher.GetCreationTime() + + pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False ) + + ( status_enum, pretty_watcher_status ) = self._multiple_watcher_import.GetWatcherSimpleStatus( watcher ) + + return ( pretty_subject, pretty_files_paused, pretty_checking_paused, pretty_watcher_status, pretty_progress, pretty_added ) + + + def _ConvertDataToSortTuple( self, watcher: ClientImportWatchers.WatcherImport ): + + subject = watcher.GetSubject() + + files_paused = watcher.FilesPaused() + + checking_dead = watcher.IsDead() + checking_paused = watcher.CheckingPaused() + + if checking_dead: + + sort_checking_paused = -1 + + elif checking_paused: + + sort_checking_paused = 0 + + else: + sort_checking_paused = 1 @@ -2626,12 +2689,8 @@ def _ConvertDataToListCtrlTuples( self, watcher: ClientImportWatchers.WatcherImp progress = ( num_total, num_done ) - pretty_progress = file_seed_cache_status.GetStatusText( simple = True ) - added = watcher.GetCreationTime() - pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False ) - ( status_enum, pretty_watcher_status ) = self._multiple_watcher_import.GetWatcherSimpleStatus( watcher ) checking_status = watcher.GetCheckingStatus() @@ -2654,10 +2713,7 @@ def _ConvertDataToListCtrlTuples( self, watcher: ClientImportWatchers.WatcherImp sort_watcher_status = ( ClientImporting.downloader_enum_sort_lookup[ status_enum ], checking_status ) - display_tuple = ( pretty_subject, pretty_files_paused, pretty_checking_paused, pretty_watcher_status, pretty_progress, pretty_added ) - sort_tuple = ( subject, files_paused, sort_checking_paused, sort_watcher_status, progress, added ) - - return ( display_tuple, sort_tuple ) + return ( subject, files_paused, sort_checking_paused, sort_watcher_status, progress, added ) def _CopySelectedSubjects( self ): diff --git a/hydrus/client/gui/pages/ClientGUINewPageChooser.py b/hydrus/client/gui/pages/ClientGUINewPageChooser.py new file mode 100644 index 000000000..a38d69bd7 --- /dev/null +++ b/hydrus/client/gui/pages/ClientGUINewPageChooser.py @@ -0,0 +1,394 @@ +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW + +from hydrus.core import HydrusConstants as HC + +from hydrus.client import ClientConstants as CC +from hydrus.client import ClientGlobals as CG +from hydrus.client import ClientLocation +from hydrus.client.gui import ClientGUIDialogs +from hydrus.client.gui import ClientGUIFunctions +from hydrus.client.gui import ClientGUIShortcuts +from hydrus.client.gui import QtPorting as QP +from hydrus.client.gui.pages import ClientGUIManagementController +from hydrus.client.search import ClientSearch + +class DialogPageChooser( ClientGUIDialogs.Dialog ): + + def __init__( self, parent, controller ): + + ClientGUIDialogs.Dialog.__init__( self, parent, 'new page', position = 'center' ) + + self._controller = controller + + self._action_picked = False + + self._result = None + + # spawn and add to layout in this order, so focus precipitates from the graphical top + + self._button_7 = QW.QPushButton( '', self ) + self._button_8 = QW.QPushButton( '', self ) + self._button_9 = QW.QPushButton( '', self ) + self._button_4 = QW.QPushButton( '', self ) + self._button_5 = QW.QPushButton( '', self ) + self._button_6 = QW.QPushButton( '', self ) + self._button_1 = QW.QPushButton( '', self ) + self._button_2 = QW.QPushButton( '', self ) + self._button_3 = QW.QPushButton( '', self ) + + size_policy = self._button_1.sizePolicy() + size_policy.setVerticalPolicy( QW.QSizePolicy.Expanding ) + size_policy.setHorizontalPolicy( QW.QSizePolicy.Expanding ) + size_policy.setRetainSizeWhenHidden( True ) + + self._button_7.setSizePolicy( size_policy ) + self._button_8.setSizePolicy( size_policy ) + self._button_9.setSizePolicy( size_policy ) + self._button_4.setSizePolicy( size_policy ) + self._button_5.setSizePolicy( size_policy ) + self._button_6.setSizePolicy( size_policy ) + self._button_1.setSizePolicy( size_policy ) + self._button_2.setSizePolicy( size_policy ) + self._button_3.setSizePolicy( size_policy ) + + self._button_7.setObjectName('7') + self._button_8.setObjectName('8') + self._button_9.setObjectName('9') + self._button_4.setObjectName('4') + self._button_5.setObjectName('5') + self._button_6.setObjectName('6') + self._button_1.setObjectName('1') + self._button_2.setObjectName('2') + self._button_3.setObjectName('3') + + # this ensures these buttons won't get focus and receive key events, letting dialog handle arrow/number shortcuts + self._button_7.setFocusPolicy( QC.Qt.NoFocus ) + self._button_8.setFocusPolicy( QC.Qt.NoFocus ) + self._button_9.setFocusPolicy( QC.Qt.NoFocus ) + self._button_4.setFocusPolicy( QC.Qt.NoFocus ) + self._button_5.setFocusPolicy( QC.Qt.NoFocus ) + self._button_6.setFocusPolicy( QC.Qt.NoFocus ) + self._button_1.setFocusPolicy( QC.Qt.NoFocus ) + self._button_2.setFocusPolicy( QC.Qt.NoFocus ) + self._button_3.setFocusPolicy( QC.Qt.NoFocus ) + + gridbox = QP.GridLayout( cols = 3 ) + + # do not add a flag here! we need the above size policy + QP.AddToLayout( gridbox, self._button_7 ) + QP.AddToLayout( gridbox, self._button_8 ) + QP.AddToLayout( gridbox, self._button_9 ) + QP.AddToLayout( gridbox, self._button_4 ) + QP.AddToLayout( gridbox, self._button_5 ) + QP.AddToLayout( gridbox, self._button_6 ) + QP.AddToLayout( gridbox, self._button_1 ) + QP.AddToLayout( gridbox, self._button_2 ) + QP.AddToLayout( gridbox, self._button_3 ) + + self.setLayout( gridbox ) + + ( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 64, 14 ) ) + + self.setMinimumWidth( width ) + self.setMinimumHeight( height ) + + self._petition_service_keys = [ service.GetServiceKey() for service in CG.client_controller.services_manager.GetServices( HC.REPOSITORIES ) if True in ( service.HasPermission( content_type, HC.PERMISSION_ACTION_MODERATE ) for content_type in HC.SERVICE_TYPES_TO_CONTENT_TYPES[ service.GetServiceType() ] ) ] + + self._InitButtons( 'home' ) + + self._button_7.clicked.connect( lambda: self._HitButton( 7 ) ) + self._button_8.clicked.connect( lambda: self._HitButton( 8 ) ) + self._button_9.clicked.connect( lambda: self._HitButton( 9 ) ) + self._button_4.clicked.connect( lambda: self._HitButton( 4 ) ) + self._button_5.clicked.connect( lambda: self._HitButton( 5 ) ) + self._button_6.clicked.connect( lambda: self._HitButton( 6 ) ) + self._button_1.clicked.connect( lambda: self._HitButton( 1 ) ) + self._button_2.clicked.connect( lambda: self._HitButton( 2 ) ) + self._button_3.clicked.connect( lambda: self._HitButton( 3 ) ) + + + def _AddEntry( self, button, entry ): + + button_id = int( button.objectName() ) + + self._command_dict[ button_id ] = entry + + ( entry_type, obj ) = entry + + if entry_type == 'menu': + + button.setText( obj ) + + elif entry_type == 'page_duplicate_filter': + + button.setText( 'duplicates processing' ) + + elif entry_type == 'pages_notebook': + + button.setText( 'page of pages' ) + + elif entry_type in ( 'page_query', 'page_petitions' ): + + name = CG.client_controller.services_manager.GetService( obj ).GetName() + + button.setText( name ) + + elif entry_type == 'page_import_gallery': + + button.setText( 'gallery' ) + + elif entry_type == 'page_import_simple_downloader': + + button.setText( 'simple downloader' ) + + elif entry_type == 'page_import_watcher': + + button.setText( 'watcher' ) + + elif entry_type == 'page_import_urls': + + button.setText( 'urls' ) + + + button.show() + + + def _HitButton( self, button_id ): + + if button_id in self._command_dict: + + ( entry_type, obj ) = self._command_dict[ button_id ] + + if entry_type == 'menu': + + self._InitButtons( obj ) + + else: + + if entry_type == 'page_query': + + file_service_key = obj + + page_name = 'files' + + search_enabled = True + + new_options = self._controller.new_options + + tag_service_key = new_options.GetKey( 'default_tag_service_search_page' ) + + if not self._controller.services_manager.ServiceExists( tag_service_key ): + + tag_service_key = CC.COMBINED_TAG_SERVICE_KEY + + + location_context = ClientLocation.LocationContext.STATICCreateSimple( file_service_key ) + + tag_context = ClientSearch.TagContext( service_key = tag_service_key ) + + file_search_context = ClientSearch.FileSearchContext( location_context = location_context, tag_context = tag_context ) + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerQuery( page_name, file_search_context, search_enabled ) ) + + elif entry_type == 'page_duplicate_filter': + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerDuplicateFilter() ) + + elif entry_type == 'pages_notebook': + + self._result = ( 'pages', None ) + + elif entry_type == 'page_import_gallery': + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportGallery() ) + + elif entry_type == 'page_import_simple_downloader': + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportSimpleDownloader() ) + + elif entry_type == 'page_import_watcher': + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportMultipleWatcher() ) + + elif entry_type == 'page_import_urls': + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportURLs() ) + + elif entry_type == 'page_petitions': + + petition_service_key = obj + + self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerPetitions( petition_service_key ) ) + + + self._action_picked = True + + self.done( QW.QDialog.Accepted ) + + + + + def _InitButtons( self, menu_keyword ): + + self._command_dict = {} + + entries = [] + + if menu_keyword == 'home': + + entries.append( ( 'menu', 'file search' ) ) + entries.append( ( 'menu', 'download' ) ) + + if len( self._petition_service_keys ) > 0: + + entries.append( ( 'menu', 'petitions' ) ) + + + entries.append( ( 'menu', 'special' ) ) + + elif menu_keyword == 'file search': + + for service_key in self._controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ): + + entries.append( ( 'page_query', service_key ) ) + + + if len( entries ) > 1 and self._controller.new_options.GetBoolean( 'show_all_my_files_on_page_chooser' ): + + entries.append( ( 'page_query', CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) + + + entries.append( ( 'page_query', CC.TRASH_SERVICE_KEY ) ) + + if self._controller.new_options.GetBoolean( 'show_local_files_on_page_chooser' ): + + entries.append( ( 'page_query', CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) ) + + + for service_key in self._controller.services_manager.GetServiceKeys( ( HC.FILE_REPOSITORY, ) ): + + entries.append( ( 'page_query', service_key ) ) + + + elif menu_keyword == 'download': + + entries.append( ( 'page_import_urls', None ) ) + entries.append( ( 'page_import_watcher', None ) ) + entries.append( ( 'page_import_gallery', None ) ) + entries.append( ( 'page_import_simple_downloader', None ) ) + + elif menu_keyword == 'petitions': + + entries = [ ( 'page_petitions', service_key ) for service_key in self._petition_service_keys ] + + elif menu_keyword == 'special': + + entries.append( ( 'pages_notebook', None ) ) + entries.append( ( 'page_duplicate_filter', None ) ) + + + if len( entries ) <= 4: + + self._button_1.setVisible( False ) + self._button_3.setVisible( False ) + self._button_5.setVisible( False ) + self._button_7.setVisible( False ) + self._button_9.setVisible( False ) + + potential_buttons = [ self._button_8, self._button_4, self._button_6, self._button_2 ] + + elif len( entries ) <= 9: + + potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ] + + else: + + # sort out a multi-page solution? maybe only if this becomes a big thing; the person can always select from the menus, yeah? + + potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ] + entries = entries[:9] + + + for entry in entries: + + self._AddEntry( potential_buttons.pop( 0 ), entry ) + + + unused_buttons = potential_buttons + + for button in unused_buttons: + + button.setVisible( False ) + + + + def event( self, event ): + + if event.type() == QC.QEvent.WindowDeactivate and not self._action_picked: + + self.done( QW.QDialog.Rejected ) + + return True + + else: + + return ClientGUIDialogs.Dialog.event( self, event ) + + + + def keyPressEvent( self, event ): + + button_id = None + + ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) + + if key == QC.Qt.Key_Up: button_id = 8 + elif key == QC.Qt.Key_Left: button_id = 4 + elif key == QC.Qt.Key_Right: button_id = 6 + elif key == QC.Qt.Key_Down: button_id = 2 + elif key == QC.Qt.Key_1 and modifier == QC.Qt.KeypadModifier: button_id = 1 + elif key == QC.Qt.Key_2 and modifier == QC.Qt.KeypadModifier: button_id = 2 + elif key == QC.Qt.Key_3 and modifier == QC.Qt.KeypadModifier: button_id = 3 + elif key == QC.Qt.Key_4 and modifier == QC.Qt.KeypadModifier: button_id = 4 + elif key == QC.Qt.Key_5 and modifier == QC.Qt.KeypadModifier: button_id = 5 + elif key == QC.Qt.Key_6 and modifier == QC.Qt.KeypadModifier: button_id = 6 + elif key == QC.Qt.Key_7 and modifier == QC.Qt.KeypadModifier: button_id = 7 + elif key == QC.Qt.Key_8 and modifier == QC.Qt.KeypadModifier: button_id = 8 + elif key == QC.Qt.Key_9 and modifier == QC.Qt.KeypadModifier: button_id = 9 + elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ): + + # get the 'first', scanning from top-left + + for possible_id in ( 7, 8, 9, 4, 5, 6, 1, 2, 3 ): + + if possible_id in self._command_dict: + + button_id = possible_id + + break + + + + elif key == QC.Qt.Key_Escape: + + self.done( QW.QDialog.Rejected ) + + return + + else: + + event.ignore() + + + if button_id is not None: + + self._HitButton( button_id ) + + + + def GetValue( self ): + + return self._result + + diff --git a/hydrus/client/gui/pages/ClientGUIPages.py b/hydrus/client/gui/pages/ClientGUIPages.py index 45444d1fc..b18c97b37 100644 --- a/hydrus/client/gui/pages/ClientGUIPages.py +++ b/hydrus/client/gui/pages/ClientGUIPages.py @@ -32,6 +32,7 @@ from hydrus.client.gui.canvas import ClientGUICanvas 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 ClientGUISession from hydrus.client.gui.pages import ClientGUISessionLegacy # to get serialisable data types loaded @@ -49,388 +50,12 @@ def ConvertNumSeedsToWeight( num_seeds: int ) -> int: return num_seeds * 20 -class DialogPageChooser( ClientGUIDialogs.Dialog ): - - def __init__( self, parent, controller ): - - ClientGUIDialogs.Dialog.__init__( self, parent, 'new page', position = 'center' ) - - self._controller = controller - - self._action_picked = False - - self._result = None - - # spawn and add to layout in this order, so focus precipitates from the graphical top - - self._button_7 = QW.QPushButton( '', self ) - self._button_8 = QW.QPushButton( '', self ) - self._button_9 = QW.QPushButton( '', self ) - self._button_4 = QW.QPushButton( '', self ) - self._button_5 = QW.QPushButton( '', self ) - self._button_6 = QW.QPushButton( '', self ) - self._button_1 = QW.QPushButton( '', self ) - self._button_2 = QW.QPushButton( '', self ) - self._button_3 = QW.QPushButton( '', self ) - - size_policy = self._button_1.sizePolicy() - size_policy.setVerticalPolicy( QW.QSizePolicy.Expanding ) - size_policy.setRetainSizeWhenHidden( True ) - - self._button_7.setSizePolicy( size_policy ) - self._button_8.setSizePolicy( size_policy ) - self._button_9.setSizePolicy( size_policy ) - self._button_4.setSizePolicy( size_policy ) - self._button_5.setSizePolicy( size_policy ) - self._button_6.setSizePolicy( size_policy ) - self._button_1.setSizePolicy( size_policy ) - self._button_2.setSizePolicy( size_policy ) - self._button_3.setSizePolicy( size_policy ) - - self._button_7.setObjectName('7') - self._button_8.setObjectName('8') - self._button_9.setObjectName('9') - self._button_4.setObjectName('4') - self._button_5.setObjectName('5') - self._button_6.setObjectName('6') - self._button_1.setObjectName('1') - self._button_2.setObjectName('2') - self._button_3.setObjectName('3') - - # this ensures these buttons won't get focus and receive key events, letting dialog handle arrow/number shortcuts - self._button_7.setFocusPolicy( QC.Qt.NoFocus ) - self._button_8.setFocusPolicy( QC.Qt.NoFocus ) - self._button_9.setFocusPolicy( QC.Qt.NoFocus ) - self._button_4.setFocusPolicy( QC.Qt.NoFocus ) - self._button_5.setFocusPolicy( QC.Qt.NoFocus ) - self._button_6.setFocusPolicy( QC.Qt.NoFocus ) - self._button_1.setFocusPolicy( QC.Qt.NoFocus ) - self._button_2.setFocusPolicy( QC.Qt.NoFocus ) - self._button_3.setFocusPolicy( QC.Qt.NoFocus ) - - gridbox = QP.GridLayout( cols = 3 ) - - QP.AddToLayout( gridbox, self._button_7, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_8, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_9, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_4, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_5, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_6, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_1, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_2, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( gridbox, self._button_3, CC.FLAGS_EXPAND_BOTH_WAYS ) - - self.setLayout( gridbox ) - - ( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 64, 14 ) ) - - self.setMinimumWidth( width ) - self.setMinimumHeight( height ) - - self._petition_service_keys = [ service.GetServiceKey() for service in CG.client_controller.services_manager.GetServices( HC.REPOSITORIES ) if True in ( service.HasPermission( content_type, HC.PERMISSION_ACTION_MODERATE ) for content_type in HC.SERVICE_TYPES_TO_CONTENT_TYPES[ service.GetServiceType() ] ) ] - - self._InitButtons( 'home' ) - - self._button_7.clicked.connect( lambda: self._HitButton( 7 ) ) - self._button_8.clicked.connect( lambda: self._HitButton( 8 ) ) - self._button_9.clicked.connect( lambda: self._HitButton( 9 ) ) - self._button_4.clicked.connect( lambda: self._HitButton( 4 ) ) - self._button_5.clicked.connect( lambda: self._HitButton( 5 ) ) - self._button_6.clicked.connect( lambda: self._HitButton( 6 ) ) - self._button_1.clicked.connect( lambda: self._HitButton( 1 ) ) - self._button_2.clicked.connect( lambda: self._HitButton( 2 ) ) - self._button_3.clicked.connect( lambda: self._HitButton( 3 ) ) - - - def _AddEntry( self, button, entry ): - - button_id = int( button.objectName() ) - - self._command_dict[ button_id ] = entry - - ( entry_type, obj ) = entry - - if entry_type == 'menu': - - button.setText( obj ) - - elif entry_type == 'page_duplicate_filter': - - button.setText( 'duplicates processing' ) - - elif entry_type == 'pages_notebook': - - button.setText( 'page of pages' ) - - elif entry_type in ( 'page_query', 'page_petitions' ): - - name = CG.client_controller.services_manager.GetService( obj ).GetName() - - button.setText( name ) - - elif entry_type == 'page_import_gallery': - - button.setText( 'gallery' ) - - elif entry_type == 'page_import_simple_downloader': - - button.setText( 'simple downloader' ) - - elif entry_type == 'page_import_watcher': - - button.setText( 'watcher' ) - - elif entry_type == 'page_import_urls': - - button.setText( 'urls' ) - - - button.show() - - - def _HitButton( self, button_id ): - - if button_id in self._command_dict: - - ( entry_type, obj ) = self._command_dict[ button_id ] - - if entry_type == 'menu': - - self._InitButtons( obj ) - - else: - - if entry_type == 'page_query': - - file_service_key = obj - - page_name = 'files' - - search_enabled = True - - new_options = self._controller.new_options - - tag_service_key = new_options.GetKey( 'default_tag_service_search_page' ) - - if not self._controller.services_manager.ServiceExists( tag_service_key ): - - tag_service_key = CC.COMBINED_TAG_SERVICE_KEY - - - location_context = ClientLocation.LocationContext.STATICCreateSimple( file_service_key ) - - tag_context = ClientSearch.TagContext( service_key = tag_service_key ) - - file_search_context = ClientSearch.FileSearchContext( location_context = location_context, tag_context = tag_context ) - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerQuery( page_name, file_search_context, search_enabled ) ) - - elif entry_type == 'page_duplicate_filter': - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerDuplicateFilter() ) - - elif entry_type == 'pages_notebook': - - self._result = ( 'pages', None ) - - elif entry_type == 'page_import_gallery': - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportGallery() ) - - elif entry_type == 'page_import_simple_downloader': - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportSimpleDownloader() ) - - elif entry_type == 'page_import_watcher': - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportMultipleWatcher() ) - - elif entry_type == 'page_import_urls': - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerImportURLs() ) - - elif entry_type == 'page_petitions': - - petition_service_key = obj - - self._result = ( 'page', ClientGUIManagementController.CreateManagementControllerPetitions( petition_service_key ) ) - - - self._action_picked = True - - self.done( QW.QDialog.Accepted ) - - - - - def _InitButtons( self, menu_keyword ): - - self._command_dict = {} - - entries = [] - - if menu_keyword == 'home': - - entries.append( ( 'menu', 'file search' ) ) - entries.append( ( 'menu', 'download' ) ) - - if len( self._petition_service_keys ) > 0: - - entries.append( ( 'menu', 'petitions' ) ) - - - entries.append( ( 'menu', 'special' ) ) - - elif menu_keyword == 'file search': - - for service_key in self._controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ): - - entries.append( ( 'page_query', service_key ) ) - - - if len( entries ) > 1: - - entries.append( ( 'page_query', CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) ) - - - entries.append( ( 'page_query', CC.TRASH_SERVICE_KEY ) ) - - if self._controller.new_options.GetBoolean( 'advanced_mode' ): - - entries.append( ( 'page_query', CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) ) - - - for service_key in self._controller.services_manager.GetServiceKeys( ( HC.FILE_REPOSITORY, ) ): - - entries.append( ( 'page_query', service_key ) ) - - - elif menu_keyword == 'download': - - entries.append( ( 'page_import_urls', None ) ) - entries.append( ( 'page_import_watcher', None ) ) - entries.append( ( 'page_import_gallery', None ) ) - entries.append( ( 'page_import_simple_downloader', None ) ) - - elif menu_keyword == 'petitions': - - entries = [ ( 'page_petitions', service_key ) for service_key in self._petition_service_keys ] - - elif menu_keyword == 'special': - - entries.append( ( 'pages_notebook', None ) ) - entries.append( ( 'page_duplicate_filter', None ) ) - - - if len( entries ) <= 4: - - self._button_1.setVisible( False ) - self._button_3.setVisible( False ) - self._button_5.setVisible( False ) - self._button_7.setVisible( False ) - self._button_9.setVisible( False ) - - potential_buttons = [ self._button_8, self._button_4, self._button_6, self._button_2 ] - - elif len( entries ) <= 9: - - potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ] - - else: - - # sort out a multi-page solution? maybe only if this becomes a big thing; the person can always select from the menus, yeah? - - potential_buttons = [ self._button_7, self._button_8, self._button_9, self._button_4, self._button_5, self._button_6, self._button_1, self._button_2, self._button_3 ] - entries = entries[:9] - - - for entry in entries: - - self._AddEntry( potential_buttons.pop( 0 ), entry ) - - - unused_buttons = potential_buttons - - for button in unused_buttons: - - button.setVisible( False ) - - - - def event( self, event ): - - if event.type() == QC.QEvent.WindowDeactivate and not self._action_picked: - - self.done( QW.QDialog.Rejected ) - - return True - - else: - - return ClientGUIDialogs.Dialog.event( self, event ) - - - - def keyPressEvent( self, event ): - - button_id = None - - ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) - - if key == QC.Qt.Key_Up: button_id = 8 - elif key == QC.Qt.Key_Left: button_id = 4 - elif key == QC.Qt.Key_Right: button_id = 6 - elif key == QC.Qt.Key_Down: button_id = 2 - elif key == QC.Qt.Key_1 and modifier == QC.Qt.KeypadModifier: button_id = 1 - elif key == QC.Qt.Key_2 and modifier == QC.Qt.KeypadModifier: button_id = 2 - elif key == QC.Qt.Key_3 and modifier == QC.Qt.KeypadModifier: button_id = 3 - elif key == QC.Qt.Key_4 and modifier == QC.Qt.KeypadModifier: button_id = 4 - elif key == QC.Qt.Key_5 and modifier == QC.Qt.KeypadModifier: button_id = 5 - elif key == QC.Qt.Key_6 and modifier == QC.Qt.KeypadModifier: button_id = 6 - elif key == QC.Qt.Key_7 and modifier == QC.Qt.KeypadModifier: button_id = 7 - elif key == QC.Qt.Key_8 and modifier == QC.Qt.KeypadModifier: button_id = 8 - elif key == QC.Qt.Key_9 and modifier == QC.Qt.KeypadModifier: button_id = 9 - elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ): - - # get the 'first', scanning from top-left - - for possible_id in ( 7, 8, 9, 4, 5, 6, 1, 2, 3 ): - - if possible_id in self._command_dict: - - button_id = possible_id - - break - - - - elif key == QC.Qt.Key_Escape: - - self.done( QW.QDialog.Rejected ) - - return - - else: - - event.ignore() - - - if button_id is not None: - - self._HitButton( button_id ) - - - - def GetValue( self ): - - return self._result - - + class Page( QW.QWidget ): def __init__( self, parent, controller, management_controller: ClientGUIManagementController.ManagementController, initial_hashes ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._parent_notebook = parent @@ -1330,7 +955,7 @@ def _ChooseNewPage( self, insertion_index = None ): self._next_new_page_index = insertion_index - with DialogPageChooser( self, self._controller ) as dlg: + with ClientGUINewPageChooser.DialogPageChooser( self, self._controller ) as dlg: if dlg.exec() == QW.QDialog.Accepted: diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py index c0d60b7b3..6cec13b2e 100644 --- a/hydrus/client/gui/pages/ClientGUIResults.py +++ b/hydrus/client/gui/pages/ClientGUIResults.py @@ -41,14 +41,12 @@ 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.exporting import ClientGUIExport 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.gui.panels import ClientGUIScrolledPanelsManagement from hydrus.client.media import ClientMedia from hydrus.client.media import ClientMediaFileFilter from hydrus.client.metadata import ClientContentUpdates @@ -170,8 +168,6 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed def __init__( self, parent, page_key, management_controller: ClientGUIManagementController.ManagementController, media_results ): - QW.QScrollArea.__init__( self, parent ) - self._qss_colours = { CC.COLOUR_THUMBGRID_BACKGROUND : QG.QColor( 255, 255, 255 ), CC.COLOUR_THUMB_BACKGROUND : QG.QColor( 255, 255, 255 ), @@ -184,6 +180,14 @@ def __init__( self, parent, page_key, management_controller: ClientGUIManagement 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 ) @@ -193,12 +197,6 @@ def __init__( self, parent, page_key, management_controller: ClientGUIManagement self.setWidget( QW.QWidget( self ) ) self.setWidgetResizable( True ) - self._page_key = page_key - self._management_controller = management_controller - - ClientMedia.ListeningMediaList.__init__( self, self._management_controller.GetLocationContext(), media_results ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) - self._UpdateBackgroundColour() self.verticalScrollBar().setSingleStep( 50 ) @@ -2655,7 +2653,7 @@ class _InnerWidget( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._parent = parent @@ -2747,7 +2745,7 @@ def __init__( self, parent, page_key, management_controller: ClientGUIManagement self._hashes_to_thumbnails_waiting_to_be_drawn: typing.Dict[ bytes, ThumbnailWaitingToBeDrawn ] = {} self._hashes_faded = set() - MediaPanel.__init__( self, parent, page_key, management_controller, media_results ) + super().__init__( parent, page_key, management_controller, media_results ) self._last_device_pixel_ratio = self.devicePixelRatio() @@ -4544,7 +4542,7 @@ class _InnerWidget( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._parent = parent @@ -4902,7 +4900,12 @@ def AddSelectMenu( win: MediaPanel, menu, filter_counts, all_specific_file_domai class Selectable( object ): - def __init__( self ): self._selected = False + def __init__( self, *args, **kwargs ): + + self._selected = False + + super().__init__( *args, **kwargs ) + def Deselect( self ): self._selected = False @@ -4912,9 +4915,9 @@ def Select( self ): self._selected = True class Thumbnail( Selectable ): - def __init__( self ): + def __init__( self, *args, **kwargs ): - Selectable.__init__( self ) + super().__init__( *args, **kwargs ) self._last_tags = None @@ -5338,19 +5341,19 @@ def GetQtImage( self, media_panel: MediaPanel, device_pixel_ratio ) -> QG.QImage 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 ): - ClientMedia.MediaCollection.__init__( self, location_context, media_results ) - Thumbnail.__init__( self ) + super().__init__( location_context, media_results ) class ThumbnailMediaSingleton( Thumbnail, ClientMedia.MediaSingleton ): def __init__( self, media_result ): - ClientMedia.MediaSingleton.__init__( self, media_result ) - Thumbnail.__init__( self ) + super().__init__( media_result ) diff --git a/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py b/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py index 1a7a1885e..04861650e 100644 --- a/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py +++ b/hydrus/client/gui/pages/ClientGUIResultsSortCollect.py @@ -29,7 +29,7 @@ class CheckBoxDelegate( QW.QStyledItemDelegate ): def __init__( self, parent = None ): - super( CheckBoxDelegate, self ).__init__( parent ) + super().__init__( parent ) def createEditor( self, parent, op, idx ): @@ -311,7 +311,7 @@ class MediaCollectControl( QW.QWidget ): def __init__( self, parent, media_collect = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) # this is trash, rewrite it to deal with the media_collect object, not the management controller @@ -475,7 +475,7 @@ class MediaSortControl( QW.QWidget ): def __init__( self, parent, media_sort = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) if media_sort is None: diff --git a/hydrus/client/gui/pages/ClientGUISession.py b/hydrus/client/gui/pages/ClientGUISession.py index 5121a34f8..55df34e50 100644 --- a/hydrus/client/gui/pages/ClientGUISession.py +++ b/hydrus/client/gui/pages/ClientGUISession.py @@ -215,7 +215,7 @@ class GUISessionPageData( HydrusSerialisable.SerialisableBase ): def __init__( self, management_controller = None, hashes = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if management_controller is None: diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py similarity index 96% rename from hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py rename to hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py index 937d44340..19c500cb9 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsManagement.py +++ b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py @@ -108,7 +108,7 @@ class _AdvancedPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -142,7 +142,7 @@ class _AudioPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -196,7 +196,7 @@ class _ColoursPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -352,7 +352,7 @@ class _ConnectionPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -547,7 +547,7 @@ class _DownloadingPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -757,7 +757,7 @@ class _DuplicatesPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -904,7 +904,7 @@ class _ExternalProgramsPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -1096,7 +1096,7 @@ class _FilesAndTrashPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -1340,7 +1340,7 @@ class _FileViewingStatisticsPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -1418,7 +1418,7 @@ class _GUIPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._main_gui_panel = ClientGUICommon.StaticBox( self, 'main window' ) @@ -1673,7 +1673,7 @@ class _GUIPagesPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -1699,6 +1699,11 @@ def __init__( self, parent, new_options ): self._pages_panel = ClientGUICommon.StaticBox( self, 'pages' ) + self._show_all_my_files_on_page_chooser = QW.QCheckBox( self._pages_panel ) + self._show_all_my_files_on_page_chooser.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will only show if you have more than one local file domain.' ) ) + self._show_local_files_on_page_chooser = QW.QCheckBox( self._pages_panel ) + self._show_local_files_on_page_chooser.setToolTip( ClientGUIFunctions.WrapToolTip( 'If you do not know what this is, you do not want it!' ) ) + self._default_new_page_goes = ClientGUICommon.BetterChoice( self._pages_panel ) for value in [ CC.NEW_PAGE_GOES_FAR_LEFT, CC.NEW_PAGE_GOES_LEFT_OF_CURRENT, CC.NEW_PAGE_GOES_RIGHT_OF_CURRENT, CC.NEW_PAGE_GOES_FAR_RIGHT ]: @@ -1784,6 +1789,9 @@ def __init__( self, parent, new_options ): self._show_session_size_warnings.setChecked( self._new_options.GetBoolean( 'show_session_size_warnings' ) ) + self._show_all_my_files_on_page_chooser.setChecked( self._new_options.GetBoolean( 'show_all_my_files_on_page_chooser' ) ) + self._show_local_files_on_page_chooser.setChecked( self._new_options.GetBoolean( 'show_local_files_on_page_chooser' ) ) + self._default_new_page_goes.SetValue( self._new_options.GetInteger( 'default_new_page_goes' ) ) self._notebook_tab_alignment.SetValue( self._new_options.GetInteger( 'notebook_tab_alignment' ) ) @@ -1827,6 +1835,8 @@ def __init__( self, parent, new_options ): rows = [] + rows.append( ( 'In new page chooser, show "all my files" if appropriate: ', self._show_all_my_files_on_page_chooser ) ) + rows.append( ( 'In new page chooser, show "local files": ', self._show_local_files_on_page_chooser ) ) rows.append( ( 'By default, put new page tabs on: ', self._default_new_page_goes ) ) rows.append( ( 'Notebook tab alignment: ', self._notebook_tab_alignment ) ) rows.append( ( 'Selection chases dropped page after drag and drop: ', self._page_drop_chase_normally ) ) @@ -1901,6 +1911,9 @@ def UpdateOptions( self ): self._new_options.SetBoolean( 'only_save_last_session_during_idle', self._only_save_last_session_during_idle.isChecked() ) + self._new_options.SetBoolean( 'show_all_my_files_on_page_chooser', self._show_all_my_files_on_page_chooser.isChecked() ) + self._new_options.SetBoolean( 'show_local_files_on_page_chooser', self._show_local_files_on_page_chooser.isChecked() ) + self._new_options.SetInteger( 'default_new_page_goes', self._default_new_page_goes.GetValue() ) self._new_options.SetInteger( 'max_page_name_chars', self._max_page_name_chars.value() ) @@ -1930,7 +1943,7 @@ class _ImportingPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -1989,7 +2002,7 @@ def __init__( self, parent, new_options ): self._new_options = new_options - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._command_palette_panel = ClientGUICommon.StaticBox( self, 'command palette' ) @@ -2047,7 +2060,7 @@ class _MaintenanceAndProcessingPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -2555,7 +2568,7 @@ class _MediaViewerPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -2775,7 +2788,7 @@ class _MediaPlaybackPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = CG.client_controller.new_options @@ -3183,7 +3196,7 @@ class _NotesPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -3215,7 +3228,7 @@ class _PopupPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -3286,7 +3299,7 @@ class _RegexPanel( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) regex_favourites = HC.options[ 'regex_favourites' ] @@ -3311,7 +3324,7 @@ class _SearchPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -3496,7 +3509,7 @@ class _SortCollectPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -3632,7 +3645,7 @@ class _SpeedAndMemoryPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4048,7 +4061,7 @@ class _StylePanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4184,7 +4197,7 @@ class _SystemPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4246,7 +4259,7 @@ class _SystemTrayPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4324,7 +4337,7 @@ class _TagsPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4490,7 +4503,7 @@ class _TagPresentationPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -4734,7 +4747,7 @@ class _TagSuggestionsPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -5232,7 +5245,7 @@ class _ThumbnailsPanel( QW.QWidget ): def __init__( self, parent, new_options ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._new_options = new_options @@ -5439,245 +5452,3 @@ def CommitChanges( self ): - -class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ): - - def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPhysical.FilesStorageSubfolder ] ): - - ClientGUIScrolledPanels.ManagePanel.__init__( self, 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 - - self._only_thumbs = True not in ( subfolder.IsForFiles() for subfolder in missing_subfolders ) - - self._missing_subfolders_to_new_subfolders = {} - - text = 'This dialog has launched because some expected file storage directories were not found. This is a serious error. You have two options:' - text += '\n' * 2 - text += '1) If you know what these should be (e.g. you recently remapped their external drive to another location), update the paths here manually. For most users, this will be clicking _add a possibly correct location_ and then select the new folder where the subdirectories all went. You can repeat this if your folders are missing in multiple locations. Check everything reports _ok!_' - text += '\n' * 2 - text += 'Although it is best if you can find everything, you only _have_ to fix the subdirectories starting with \'f\', which store your original files. Those starting \'t\' and \'r\' are for your thumbnails, which can be regenerated with a bit of work.' - text += '\n' * 2 - text += 'Then hit \'apply\', and the client will launch. You should double-check all your locations under \'database->move media files\' immediately.' - text += '\n' * 2 - text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help. Regardless of the situation, the document at "install_dir/db/help my media files are broke.txt" may be useful background reading.' - - if self._only_thumbs: - - text += '\n' * 2 - text += 'SPECIAL NOTE FOR YOUR SITUATION: The only paths missing are thumbnail paths. If you cannot recover these folders, you can hit apply to create empty paths at the original or corrected locations and then run a maintenance routine to regenerate the thumbnails from their originals.' - - - st = ClientGUICommon.BetterStaticText( self, text ) - st.setWordWrap( True ) - - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, self._ConvertPrefixToListCtrlTuples ) - - self._locations = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, 12, model, activation_callback = self._SetLocations ) - - self._set_button = ClientGUICommon.BetterButton( self, 'set correct location', self._SetLocations ) - self._add_button = ClientGUICommon.BetterButton( self, 'add a possibly correct location (let the client figure out what it contains)', self._AddLocation ) - - # add a button here for 'try to fill them in for me'. you give it a dir, and it tries to figure out and fill in the prefixes for you - - # - - self._locations.SetData( missing_subfolders ) - - # - - vbox = QP.VBoxLayout() - - QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR ) - QP.AddToLayout( vbox, self._locations, CC.FLAGS_EXPAND_BOTH_WAYS ) - QP.AddToLayout( vbox, self._set_button, CC.FLAGS_ON_RIGHT ) - QP.AddToLayout( vbox, self._add_button, CC.FLAGS_ON_RIGHT ) - - self.widget().setLayout( vbox ) - - - def _AddLocation( self ): - - with QP.DirDialog( self, 'Select the potential correct location.' ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - path = dlg.GetPath() - - potential_base_locations = [ - ClientFilesPhysical.FilesStorageBaseLocation( path, 0 ), - ClientFilesPhysical.FilesStorageBaseLocation( os.path.join( path, 'client_files' ), 0 ), - ClientFilesPhysical.FilesStorageBaseLocation( os.path.join( path, 'thumbnails' ), 0 ) - ] - - for subfolder in self._locations.GetData(): - - for potential_base_location in potential_base_locations: - - new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, potential_base_location ) - - ok = new_subfolder.PathExists() - - if ok: - - self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok ) - - break - - - - - self._locations.UpdateDatas() - - - - - def _ConvertPrefixToListCtrlTuples( self, subfolder ): - - prefix = subfolder.prefix - incorrect_base_location = subfolder.base_location - - if subfolder in self._missing_subfolders_to_new_subfolders: - - ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ] - - correct_base_location = new_subfolder.base_location - - if ok: - - pretty_ok = 'ok!' - - else: - - pretty_ok = 'not found' - - - pretty_correct_base_location = correct_base_location.path - - else: - - pretty_correct_base_location = '' - ok = None - pretty_ok = '' - - - pretty_incorrect_base_location = incorrect_base_location.path - pretty_prefix = prefix - - display_tuple = ( pretty_incorrect_base_location, pretty_prefix, pretty_correct_base_location, pretty_ok ) - sort_tuple = ( pretty_incorrect_base_location, prefix, pretty_correct_base_location, ok ) - - return ( display_tuple, sort_tuple ) - - - def _GetValue( self ): - - correct_rows = [] - - thumb_problems = False - - for subfolder in self._locations.GetData(): - - prefix = subfolder.prefix - - if subfolder not in self._missing_subfolders_to_new_subfolders: - - if prefix.startswith( 'f' ): - - raise HydrusExceptions.VetoException( 'You did not correct all the file locations!' ) - - else: - - thumb_problems = True - - new_subfolder = subfolder - - - else: - - ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ] - - if not ok: - - if prefix.startswith( 'f' ): - - raise HydrusExceptions.VetoException( 'You did not find all the correct file locations!' ) - - else: - - thumb_problems = True - - - - - correct_rows.append( ( subfolder, new_subfolder ) ) - - - return ( correct_rows, thumb_problems ) - - - def _SetLocations( self ): - - subfolders = self._locations.GetData( only_selected = True ) - - if len( subfolders ) > 0: - - with QP.DirDialog( self, 'Select correct location.' ) as dlg: - - if dlg.exec() == QW.QDialog.Accepted: - - path = dlg.GetPath() - - base_location = ClientFilesPhysical.FilesStorageBaseLocation( path, 0 ) - - for subfolder in subfolders: - - new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, base_location ) - - ok = new_subfolder.PathExists() - - self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok ) - - - self._locations.UpdateDatas() - - - - - - def CheckValid( self ): - - # raises veto if invalid - self._GetValue() - - - def CommitChanges( self ): - - ( correct_rows, thumb_problems ) = self._GetValue() - - CG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows ) - - - def UserIsOKToOK( self ): - - ( correct_rows, thumb_problems ) = self._GetValue() - - if thumb_problems: - - message = 'Some or all of your incorrect paths have not been corrected, but they are all thumbnail paths.' - message += '\n' * 2 - message += 'Would you like instead to create new empty subdirectories at the previous (or corrected, if you have entered them) locations?' - message += '\n' * 2 - message += 'You can run database->regenerate->thumbnails to fill them up again.' - - result = ClientGUIDialogsQuick.GetYesNo( self, message ) - - if result != QW.QDialog.Accepted: - - return False - - - - return True - diff --git a/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py b/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py new file mode 100644 index 000000000..0677a38b4 --- /dev/null +++ b/hydrus/client/gui/panels/ClientGUIRepairFileSystemPanel.py @@ -0,0 +1,258 @@ +import os +import typing + +from qtpy import QtWidgets as QW + +from hydrus.core import HydrusExceptions + +from hydrus.client import ClientConstants as CC +from hydrus.client import ClientFilesPhysical +from hydrus.client import ClientGlobals as CG +from hydrus.client.gui import ClientGUIDialogsQuick +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.panels import ClientGUIScrolledPanels +from hydrus.client.gui.widgets import ClientGUICommon + +class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ): + + def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPhysical.FilesStorageSubfolder ] ): + + ClientGUIScrolledPanels.ManagePanel.__init__( self, 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 + + self._only_thumbs = True not in ( subfolder.IsForFiles() for subfolder in missing_subfolders ) + + self._missing_subfolders_to_new_subfolders = {} + + text = 'This dialog has launched because some expected file storage directories were not found. This is a serious error. You have two options:' + text += '\n' * 2 + text += '1) If you know what these should be (e.g. you recently remapped their external drive to another location), update the paths here manually. For most users, this will be clicking _add a possibly correct location_ and then select the new folder where the subdirectories all went. You can repeat this if your folders are missing in multiple locations. Check everything reports _ok!_' + text += '\n' * 2 + text += 'Although it is best if you can find everything, you only _have_ to fix the subdirectories starting with \'f\', which store your original files. Those starting \'t\' and \'r\' are for your thumbnails, which can be regenerated with a bit of work.' + text += '\n' * 2 + text += 'Then hit \'apply\', and the client will launch. You should double-check all your locations under \'database->move media files\' immediately.' + text += '\n' * 2 + text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help. Regardless of the situation, the document at "install_dir/db/help my media files are broke.txt" may be useful background reading.' + + if self._only_thumbs: + + text += '\n' * 2 + text += 'SPECIAL NOTE FOR YOUR SITUATION: The only paths missing are thumbnail paths. If you cannot recover these folders, you can hit apply to create empty paths at the original or corrected locations and then run a maintenance routine to regenerate the thumbnails from their originals.' + + + st = ClientGUICommon.BetterStaticText( self, text ) + st.setWordWrap( True ) + + model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, self._ConvertPrefixToListCtrlTuples ) + + self._locations = ClientGUIListCtrl.BetterListCtrlTreeView( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, 12, model, activation_callback = self._SetLocations ) + + self._set_button = ClientGUICommon.BetterButton( self, 'set correct location', self._SetLocations ) + self._add_button = ClientGUICommon.BetterButton( self, 'add a possibly correct location (let the client figure out what it contains)', self._AddLocation ) + + # add a button here for 'try to fill them in for me'. you give it a dir, and it tries to figure out and fill in the prefixes for you + + # + + self._locations.SetData( missing_subfolders ) + + # + + vbox = QP.VBoxLayout() + + QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR ) + QP.AddToLayout( vbox, self._locations, CC.FLAGS_EXPAND_BOTH_WAYS ) + QP.AddToLayout( vbox, self._set_button, CC.FLAGS_ON_RIGHT ) + QP.AddToLayout( vbox, self._add_button, CC.FLAGS_ON_RIGHT ) + + self.widget().setLayout( vbox ) + + + def _AddLocation( self ): + + with QP.DirDialog( self, 'Select the potential correct location.' ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + path = dlg.GetPath() + + potential_base_locations = [ + ClientFilesPhysical.FilesStorageBaseLocation( path, 0 ), + ClientFilesPhysical.FilesStorageBaseLocation( os.path.join( path, 'client_files' ), 0 ), + ClientFilesPhysical.FilesStorageBaseLocation( os.path.join( path, 'thumbnails' ), 0 ) + ] + + for subfolder in self._locations.GetData(): + + for potential_base_location in potential_base_locations: + + new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, potential_base_location ) + + ok = new_subfolder.PathExists() + + if ok: + + self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok ) + + break + + + + + self._locations.UpdateDatas() + + + + + def _ConvertPrefixToListCtrlTuples( self, subfolder ): + + prefix = subfolder.prefix + incorrect_base_location = subfolder.base_location + + if subfolder in self._missing_subfolders_to_new_subfolders: + + ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ] + + correct_base_location = new_subfolder.base_location + + if ok: + + pretty_ok = 'ok!' + + else: + + pretty_ok = 'not found' + + + pretty_correct_base_location = correct_base_location.path + + else: + + pretty_correct_base_location = '' + ok = None + pretty_ok = '' + + + pretty_incorrect_base_location = incorrect_base_location.path + pretty_prefix = prefix + + display_tuple = ( pretty_incorrect_base_location, pretty_prefix, pretty_correct_base_location, pretty_ok ) + sort_tuple = ( pretty_incorrect_base_location, prefix, pretty_correct_base_location, ok ) + + return ( display_tuple, sort_tuple ) + + + def _GetValue( self ): + + correct_rows = [] + + thumb_problems = False + + for subfolder in self._locations.GetData(): + + prefix = subfolder.prefix + + if subfolder not in self._missing_subfolders_to_new_subfolders: + + if prefix.startswith( 'f' ): + + raise HydrusExceptions.VetoException( 'You did not correct all the file locations!' ) + + else: + + thumb_problems = True + + new_subfolder = subfolder + + + else: + + ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ] + + if not ok: + + if prefix.startswith( 'f' ): + + raise HydrusExceptions.VetoException( 'You did not find all the correct file locations!' ) + + else: + + thumb_problems = True + + + + + correct_rows.append( ( subfolder, new_subfolder ) ) + + + return ( correct_rows, thumb_problems ) + + + def _SetLocations( self ): + + subfolders = self._locations.GetData( only_selected = True ) + + if len( subfolders ) > 0: + + with QP.DirDialog( self, 'Select correct location.' ) as dlg: + + if dlg.exec() == QW.QDialog.Accepted: + + path = dlg.GetPath() + + base_location = ClientFilesPhysical.FilesStorageBaseLocation( path, 0 ) + + for subfolder in subfolders: + + new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, base_location ) + + ok = new_subfolder.PathExists() + + self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok ) + + + self._locations.UpdateDatas() + + + + + + def CheckValid( self ): + + # raises veto if invalid + self._GetValue() + + + def CommitChanges( self ): + + ( correct_rows, thumb_problems ) = self._GetValue() + + CG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows ) + + + def UserIsOKToOK( self ): + + ( correct_rows, thumb_problems ) = self._GetValue() + + if thumb_problems: + + message = 'Some or all of your incorrect paths have not been corrected, but they are all thumbnail paths.' + message += '\n' * 2 + message += 'Would you like instead to create new empty subdirectories at the previous (or corrected, if you have entered them) locations?' + message += '\n' * 2 + message += 'You can run database->regenerate->thumbnails to fill them up again.' + + result = ClientGUIDialogsQuick.GetYesNo( self, message ) + + if result != QW.QDialog.Accepted: + + return False + + + + return True + diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanels.py b/hydrus/client/gui/panels/ClientGUIScrolledPanels.py index 50305a4a9..6a7b3aa44 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanels.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanels.py @@ -206,8 +206,7 @@ class EditSingleCtrlPanel( CAC.ApplicationCommandProcessorMixin, EditPanel ): def __init__( self, parent, ok_on_these_commands = None, message = None ): - EditPanel.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._control = None diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py index e54ed471f..9f31695a1 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py @@ -1870,8 +1870,7 @@ class EditFileNotesPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolle def __init__( self, parent: QW.QWidget, names_to_notes: typing.Dict[ str, str ], name_to_start_on: typing.Optional[ str ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._original_names = set() @@ -2762,8 +2761,7 @@ class EditURLsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPane def __init__( self, parent, medias: typing.Collection[ ClientMedia.MediaSingleton ] ): - ClientGUIScrolledPanels.EditPanel.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self._current_media = [ m.Duplicate() for m in medias ] @@ -2775,13 +2773,10 @@ def __init__( self, parent, medias: typing.Collection[ ClientMedia.MediaSingleto self._multiple_files_warning.hide() - self._urls_listbox = ClientGUIListBoxes.BetterQListWidget( self ) + self._urls_listbox = ClientGUIListBoxes.BetterQListWidget( self, delete_callable = self.DeleteSelected ) self._urls_listbox.setSelectionMode( QW.QAbstractItemView.ExtendedSelection ) self._urls_listbox.setSortingEnabled( False ) - self._urls_listbox.itemDoubleClicked.connect( self.EventListDoubleClick ) - - self._listbox_event_filter = QP.WidgetEventFilter( self._urls_listbox ) - self._listbox_event_filter.EVT_KEY_DOWN( self.EventListKeyDown ) + self._urls_listbox.itemDoubleClicked.connect( self.ListDoubleClicked ) ( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._urls_listbox, ( 120, 10 ) ) @@ -3014,42 +3009,6 @@ def _UpdateList( self ): - def EventListDoubleClick( self, item ): - - urls = self._urls_listbox.GetData( only_selected = True ) - - for url in urls: - - self._RemoveURL( url ) - - - if len( urls ) == 1: - - url = urls[0] - - self._url_input.setText( url ) - - - - def EventListKeyDown( self, event ): - - ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) - - if key in ClientGUIShortcuts.DELETE_KEYS_QT: - - urls = self._urls_listbox.GetData( only_selected = True ) - - for url in urls: - - self._RemoveURL( url ) - - - else: - - return True # was: event.ignore() - - - def AddURL( self ): url = self._url_input.text() @@ -3073,11 +3032,38 @@ def AddURL( self ): + def DeleteSelected( self ): + + urls = self._urls_listbox.GetData( only_selected = True ) + + for url in urls: + + self._RemoveURL( url ) + + + def GetValue( self ): return list( self._pending_content_updates ) + def ListDoubleClicked( self, item ): + + urls = self._urls_listbox.GetData( only_selected = True ) + + for url in urls: + + self._RemoveURL( url ) + + + if len( urls ) == 1: + + url = urls[0] + + self._url_input.setText( url ) + + + def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ): command_processed = True diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py index 3fac9f642..e5958cd9b 100644 --- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py +++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsReview.py @@ -3413,7 +3413,7 @@ def __init__( self, parent, controller, scheduler_name ): self._controller = controller self._scheduler_name = scheduler_name - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) @@ -3471,7 +3471,7 @@ def __init__( self, parent, controller ): self._controller = controller - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._list_ctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) diff --git a/hydrus/client/gui/parsing/ClientGUIParsing.py b/hydrus/client/gui/parsing/ClientGUIParsing.py index 3d82a9127..c2a46c7dd 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsing.py +++ b/hydrus/client/gui/parsing/ClientGUIParsing.py @@ -62,7 +62,7 @@ def __init__( self, parent, network_engine ): listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, self._ConvertContentToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, self._ConvertContentToDisplayTuple, self._ConvertContentToSortTuple ) self._listctrl = ClientGUIListCtrl.BetterListCtrlTreeView( listctrl_panel, CGLC.COLUMN_LIST_DOWNLOADER_EXPORT.ID, 14, model, use_simple_delete = True ) @@ -222,7 +222,7 @@ def _CanExport( self ): return len( self._listctrl.GetData() ) > 0 - def _ConvertContentToListCtrlTuples( self, content ): + def _ConvertContentToDisplayTuple( self, content ): if isinstance( content, ClientNetworkingDomain.DomainMetadataPackage ): @@ -235,15 +235,11 @@ def _ConvertContentToListCtrlTuples( self, content ): t = content.SERIALISABLE_NAME - pretty_name = name - pretty_t = t - - display_tuple = ( pretty_name, pretty_t ) - sort_tuple = ( name, t ) - - return ( display_tuple, sort_tuple ) + return ( name, t ) + _ConvertContentToSortTuple = _ConvertContentToDisplayTuple + def _Export( self ): export_object = HydrusSerialisable.SerialisableList( self._listctrl.GetData() ) @@ -1034,7 +1030,7 @@ def __init__( self, parent: QW.QWidget, test_data_callable: typing.Callable[ [], content_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, self._ConvertContentParserToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, self._ConvertContentParserToDisplayTuple, self._ConvertContentParserToSortTuple ) self._content_parsers = ClientGUIListCtrl.BetterListCtrlTreeView( content_parsers_panel, CGLC.COLUMN_LIST_CONTENT_PARSERS.ID, 6, model, use_simple_delete = True, activation_callback = self._Edit ) @@ -1092,24 +1088,21 @@ def _AddContentParser( self, content_parser ): self._content_parsers.Sort() - def _ConvertContentParserToListCtrlTuples( self, content_parser ): + def _ConvertContentParserToDisplayTuple( self, content_parser ): name = content_parser.GetName() produces = list( content_parser.GetParsableContent() ) - pretty_name = name - pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces, include_veto = True ) # produces has some garbage stuff like StringMatch that doesn't sort nice, so sort on pretty produces - display_tuple = ( pretty_name, pretty_produces ) - sort_tuple = ( name, pretty_produces ) - - return ( display_tuple, sort_tuple ) + return ( name, pretty_produces ) + _ConvertContentParserToSortTuple = _ConvertContentParserToDisplayTuple + def _Edit( self ): edited_datas = [] @@ -1261,7 +1254,7 @@ def __init__( self, parent, parser: ClientParsing.PageParser, formula = None, te sub_page_parsers_panel = ClientGUIListCtrl.BetterListCtrlPanel( sub_page_parsers_notebook_panel ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, self._ConvertSubPageParserToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, self._ConvertSubPageParserToDisplayTuple, self._ConvertSubPageParserToSortTuple ) self._sub_page_parsers = ClientGUIListCtrl.BetterListCtrlTreeView( sub_page_parsers_panel, CGLC.COLUMN_LIST_SUB_PAGE_PARSERS.ID, 4, model, use_simple_delete = True, activation_callback = self._EditSubPageParser ) @@ -1441,7 +1434,7 @@ def _AddSubPageParser( self ): - def _ConvertSubPageParserToListCtrlTuples( self, sub_page_parser ): + def _ConvertSubPageParserToDisplayTuple( self, sub_page_parser ): ( formula, page_parser ) = sub_page_parser @@ -1451,16 +1444,14 @@ def _ConvertSubPageParserToListCtrlTuples( self, sub_page_parser ): produces = sorted( produces, key = lambda row: ( row[0], row[1] ) ) # ( name, content_type ), ignores potentially unsortable StringMatch etc.. in additional info in case of dupe - pretty_name = name pretty_formula = formula.ToPrettyString() pretty_produces = ClientParsing.ConvertParsableContentToPrettyString( produces ) - display_tuple = ( pretty_name, pretty_formula, pretty_produces ) - sort_tuple = ( name, pretty_formula, pretty_produces ) - - return ( display_tuple, sort_tuple ) + return ( name, pretty_formula, pretty_produces ) + _ConvertSubPageParserToSortTuple = _ConvertSubPageParserToDisplayTuple + def _EditExampleURL( self, example_url ): message = 'Enter example URL.' diff --git a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py index 3064af35e..25589bf64 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py +++ b/hydrus/client/gui/parsing/ClientGUIParsingLegacy.py @@ -39,7 +39,7 @@ class EditNodes( QW.QWidget ): def __init__( self, parent, nodes, referral_url_callable, example_data_callable ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._referral_url_callable = referral_url_callable self._example_data_callable = example_data_callable @@ -1200,7 +1200,7 @@ class ScriptManagementControl( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._job_status = None diff --git a/hydrus/client/gui/parsing/ClientGUIParsingTest.py b/hydrus/client/gui/parsing/ClientGUIParsingTest.py index f905d0e67..e4159979f 100644 --- a/hydrus/client/gui/parsing/ClientGUIParsingTest.py +++ b/hydrus/client/gui/parsing/ClientGUIParsingTest.py @@ -32,7 +32,7 @@ class TestPanel( QW.QWidget ): def __init__( self, parent, object_callable, test_data: typing.Optional[ ClientParsing.ParsingTestData ] = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) if test_data is None: diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py index 869fa8500..d5e4663df 100644 --- a/hydrus/client/gui/search/ClientGUIACDropdown.py +++ b/hydrus/client/gui/search/ClientGUIACDropdown.py @@ -804,8 +804,7 @@ def __init__( self, parent ): CC.COLOUR_AUTOCOMPLETE_BACKGROUND : QG.QColor( 235, 248, 255 ) } - QW.QWidget.__init__( self, parent ) - CAC.ApplicationCommandProcessorMixin.__init__( self ) + super().__init__( parent ) self.setObjectName( 'HydrusTagAutocomplete' ) @@ -832,12 +831,8 @@ def __init__( self, parent ): self._last_attempted_dropdown_width = 0 - self._text_ctrl_widget_event_filter = QP.WidgetEventFilter( self._text_ctrl ) - self._text_ctrl.textChanged.connect( self.EventText ) - self._text_ctrl_widget_event_filter.EVT_KEY_DOWN( self.keyPressFilter ) - self._text_ctrl.installEventFilter( self ) self._main_vbox = QP.VBoxLayout( margin = 0 ) @@ -962,6 +957,11 @@ def _BroadcastChoices( self, predicates, shift_down ): raise NotImplementedError() + def _BroadcastCurrentInputFromEnterKey( self, shift_down ): + + raise NotImplementedError() + + def _CancelSearchResultsFetchJob( self ): if self._current_fetch_job_status is not None: @@ -1102,6 +1102,11 @@ def _SetResultsToList( self, results, parsed_autocomplete_text ): self._time_results_last_set = HydrusTime.GetNow() + def _ShouldBroadcastCurrentInputOnEnterKey( self ): + + raise NotImplementedError() + + def _ShouldShow( self ): if self._force_dropdown_hide: @@ -1118,11 +1123,6 @@ def _ShouldShow( self ): return i_am_active_and_focused and visible - def _ShouldTakeResponsibilityForEnter( self ): - - raise NotImplementedError() - - def _ShowDropdown( self ): text_panel_size = self._text_input_panel.size() @@ -1165,11 +1165,6 @@ def _StartSearchResultsFetchJob( self, job_status ): raise NotImplementedError() - def _TakeResponsibilityForEnter( self, shift_down ): - - raise NotImplementedError() - - def _UpdateBackgroundColour( self ): bg_colour = self.GetColour( CC.COLOUR_AUTOCOMPLETE_BACKGROUND ) @@ -1212,55 +1207,6 @@ def DoDropdownHideShow( self ): self._DropdownHideShow() - def keyPressFilter( self, event ): - - CG.client_controller.ResetIdleTimer() - - ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event ) - - if self._can_intercept_unusual_key_events: - - send_input_to_current_list = False - - current_results_list = self._dropdown_notebook.currentWidget() - - if key in ( ord( 'A' ), ord( 'a' ) ) and modifier == QC.Qt.ControlModifier: - - return True # was: event.ignore() - - elif key in ( QC.Qt.Key_Return, QC.Qt.Key_Enter ) and self._ShouldTakeResponsibilityForEnter(): - - shift_down = modifier == QC.Qt.ShiftModifier - - self._TakeResponsibilityForEnter( shift_down ) - - elif key == QC.Qt.Key_Escape: - - escape_caught = self._HandleEscape() - - if not escape_caught: - - send_input_to_current_list = True - - - else: - - send_input_to_current_list = True - - - if send_input_to_current_list: - - current_results_list.keyPressEvent( event ) # ultimately, this typically ignores the event, letting the text ctrl take it - - return not event.isAccepted() - - - else: - - return True # was: event.ignore() - - - def EventCloseDropdown( self, event ): CG.client_controller.gui.close() @@ -1274,7 +1220,69 @@ def eventFilter( self, watched, event ): if watched == self._text_ctrl: - if event.type() == QC.QEvent.Wheel: + if event.type() == QC.QEvent.KeyPress and self._can_intercept_unusual_key_events: + + # ok for a while this thing was a mis-mash of logical tests and basically sending anything not explicitly caught to the list + # this resulted in annoying miss-cases where ctrl+c et al were being passed to the list and so you couldn't copy text from the text input + # THUS we are moving to a strict whitelist. a handful of events will pass down to the list, everything else we jealously keep + + CG.client_controller.ResetIdleTimer() + + ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( 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: + + send_input_to_current_list = True + + elif key in ( QC.Qt.Key_Return, QC.Qt.Key_Enter ): + + if self._ShouldBroadcastCurrentInputOnEnterKey(): + + shift_down = modifier == QC.Qt.ShiftModifier + + self._BroadcastCurrentInputFromEnterKey( shift_down ) + + event.accept() + + return True + + else: + + send_input_to_current_list = True + + + elif key == QC.Qt.Key_Escape: + + escape_caught = self._HandleEscape() + + if escape_caught: + + event.accept() + + return True + + else: + + send_input_to_current_list = True + + + + if send_input_to_current_list: + + current_results_list = self._dropdown_notebook.currentWidget() + + current_results_list.keyPressEvent( event ) + + return event.isAccepted() + + + elif event.type() == QC.QEvent.Wheel: current_results_list = self._dropdown_notebook.currentWidget() @@ -1776,6 +1784,11 @@ def _GetCurrentBroadcastTextPredicate( self ) -> typing.Optional[ ClientSearch.P raise NotImplementedError() + def _BroadcastCurrentInputFromEnterKey( self, shift_down ): + + raise NotImplementedError() + + def _GetParsedAutocompleteText( self ) -> ClientSearchAutocomplete.ParsedAutocompleteText: collapse_search_characters = True @@ -1882,7 +1895,7 @@ def _SetTagService( self, tag_service_key ): return True - def _ShouldTakeResponsibilityForEnter( self ): + def _ShouldBroadcastCurrentInputOnEnterKey( self ): raise NotImplementedError() @@ -1930,11 +1943,6 @@ def _TagContextJustChanged( self, tag_context: ClientSearch.TagContext ): return True - def _TakeResponsibilityForEnter( self, shift_down ): - - raise NotImplementedError() - - def _UpdateChildrenListIfNeeded( self ): if self._dropdown_notebook.currentWidget() == self._children_list: @@ -2228,6 +2236,16 @@ def _BroadcastChoices( self, predicates, shift_down ): self._ClearInput() + def _BroadcastCurrentInputFromEnterKey( self, shift_down ): + + current_broadcast_predicate = self._GetCurrentBroadcastTextPredicate() + + if current_broadcast_predicate is not None: + + self._BroadcastChoices( { current_broadcast_predicate }, shift_down ) + + + def _CancelORConstruction( self ): self._under_construction_or_predicate = None @@ -2601,24 +2619,28 @@ def _NotifyPredicatesBoxChanged( self ): self._SignalNewSearchState() - def _SignalNewSearchState( self ): + def _ShouldBroadcastCurrentInputOnEnterKey( self ): - file_search_context = self._file_search_context.Duplicate() + looking_at_search_results = self._dropdown_notebook.currentWidget() == self._search_results_list - self.searchChanged.emit( file_search_context ) + something_to_broadcast = self._GetCurrentBroadcastTextPredicate() is not None - - def _SynchronisedChanged( self, value ): + parsed_autocomplete_text = self._GetParsedAutocompleteText() - self._SignalNewSearchState() + # the list has results, but they are out of sync with what we have currently entered + # when the user has quickly typed something in and the results are not yet in + results_desynced_with_text = parsed_autocomplete_text != self._current_list_parsed_autocomplete_text - self._RestoreTextCtrlFocus() + p1 = looking_at_search_results and something_to_broadcast and results_desynced_with_text - if not self._search_pause_play.IsOn() and not self._file_search_context.GetSystemPredicates().HasSystemLimit(): - - # update if user goes from sync to non-sync - self._SetListDirty() - + return p1 + + + def _SignalNewSearchState( self ): + + file_search_context = self._file_search_context.Duplicate() + + self.searchChanged.emit( file_search_context ) def _StartSearchResultsFetchJob( self, job_status ): @@ -2639,21 +2661,17 @@ def _StartSearchResultsFetchJob( self, job_status ): CG.client_controller.CallToThread( ReadFetch, self, job_status, self.SetPrefetchResults, self.SetFetchedResults, parsed_autocomplete_text, self._media_callable, fsc, self._search_pause_play.IsOn(), self._include_unusual_predicate_types, self._results_cache, under_construction_or_predicate, self._force_system_everything ) - def _ShouldTakeResponsibilityForEnter( self ): - - looking_at_search_results = self._dropdown_notebook.currentWidget() == self._search_results_list - - something_to_broadcast = self._GetCurrentBroadcastTextPredicate() is not None - - parsed_autocomplete_text = self._GetParsedAutocompleteText() + def _SynchronisedChanged( self, value ): - # the list has results, but they are out of sync with what we have currently entered - # when the user has quickly typed something in and the results are not yet in - results_desynced_with_text = parsed_autocomplete_text != self._current_list_parsed_autocomplete_text + self._SignalNewSearchState() - p1 = looking_at_search_results and something_to_broadcast and results_desynced_with_text + self._RestoreTextCtrlFocus() - return p1 + if not self._search_pause_play.IsOn() and not self._file_search_context.GetSystemPredicates().HasSystemLimit(): + + # update if user goes from sync to non-sync + self._SetListDirty() + def _TagContextJustChanged( self, tag_context: ClientSearch.TagContext ): @@ -2668,16 +2686,6 @@ def _TagContextJustChanged( self, tag_context: ClientSearch.TagContext ): - def _TakeResponsibilityForEnter( self, shift_down ): - - current_broadcast_predicate = self._GetCurrentBroadcastTextPredicate() - - if current_broadcast_predicate is not None: - - self._BroadcastChoices( { current_broadcast_predicate }, shift_down ) - - - def _UpdateORButtons( self ): if self._under_construction_or_predicate is None: @@ -3146,6 +3154,25 @@ def _BroadcastChoices( self, predicates, shift_down ): self._ClearInput() + def _BroadcastCurrentInputFromEnterKey( self, shift_down ): + + parsed_autocomplete_text = self._GetParsedAutocompleteText() + + if parsed_autocomplete_text.IsEmpty() and self._dropdown_notebook.currentWidget() == self._search_results_list: + + self.nullEntered.emit() + + else: + + current_broadcast_predicate = self._GetCurrentBroadcastTextPredicate() + + if current_broadcast_predicate is not None: + + self._BroadcastChoices( { current_broadcast_predicate }, shift_down ) + + + + def _GetCurrentBroadcastTextPredicate( self ) -> typing.Optional[ ClientSearch.Predicate ]: parsed_autocomplete_text = self._GetParsedAutocompleteText() @@ -3228,7 +3255,7 @@ def _Paste( self ): - def _ShouldTakeResponsibilityForEnter( self ): + def _ShouldBroadcastCurrentInputOnEnterKey( self ): parsed_autocomplete_text = self._GetParsedAutocompleteText() @@ -3261,25 +3288,6 @@ def _StartSearchResultsFetchJob( self, job_status ): CG.client_controller.CallToThread( WriteFetch, self, job_status, self.SetPrefetchResults, self.SetFetchedResults, parsed_autocomplete_text, file_search_context, self._results_cache ) - def _TakeResponsibilityForEnter( self, shift_down ): - - parsed_autocomplete_text = self._GetParsedAutocompleteText() - - if parsed_autocomplete_text.IsEmpty() and self._dropdown_notebook.currentWidget() == self._search_results_list: - - self.nullEntered.emit() - - else: - - current_broadcast_predicate = self._GetCurrentBroadcastTextPredicate() - - if current_broadcast_predicate is not None: - - self._BroadcastChoices( { current_broadcast_predicate }, shift_down ) - - - - def RefreshFavouriteTags( self ): favourite_tags = sorted( CG.client_controller.new_options.GetStringList( 'favourite_tags' ) ) diff --git a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py index ef09d7c4b..dd1fe16e6 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesMultiple.py @@ -20,7 +20,7 @@ class PredicateSystemRatingIncDecControl( QW.QWidget ): def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Optional[ ClientSearch.Predicate ] ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_key = service_key @@ -108,7 +108,7 @@ class PredicateSystemRatingLikeControl( QW.QWidget ): def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Optional[ ClientSearch.Predicate ] ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set "is" and leave rating null to search for "unrated".' ) ) @@ -244,7 +244,7 @@ class PredicateSystemRatingNumericalControl( QW.QWidget ): def __init__( self, parent: QW.QWidget, service_key: bytes, predicate: typing.Optional[ ClientSearch.Predicate ] ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set "is" and leave rating null to search for "unrated".' ) ) diff --git a/hydrus/client/gui/search/ClientGUIPredicatesOR.py b/hydrus/client/gui/search/ClientGUIPredicatesOR.py index 6ff5a3916..ade0d428c 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesOR.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesOR.py @@ -22,7 +22,7 @@ class ORPredicateControl( QW.QWidget ): def __init__( self, parent: QW.QWidget, predicate: ClientSearch.Predicate, empty_file_search_context: typing.Optional[ ClientSearch.FileSearchContext ] = None ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) from hydrus.client.gui.search import ClientGUIACDropdown diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py index cbb3e124d..28f8d6de9 100644 --- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py +++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py @@ -33,7 +33,7 @@ class StaticSystemPredicateButton( QW.QWidget ): def __init__( self, parent, predicates, forced_label = None, show_remove_button = True ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._predicates = predicates self._forced_label = forced_label @@ -203,7 +203,7 @@ class PanelPredicateSimpleTagTypes( QW.QWidget ): def __init__( self, parent: QW.QWidget, predicate: ClientSearch.Predicate ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) if predicate.GetType() not in ( ClientSearch.PREDICATE_TYPE_TAG, ClientSearch.PREDICATE_TYPE_NAMESPACE, ClientSearch.PREDICATE_TYPE_WILDCARD ): @@ -265,7 +265,7 @@ class PanelPredicateSystem( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) def CheckValid( self ): diff --git a/hydrus/client/gui/search/ClientGUISearch.py b/hydrus/client/gui/search/ClientGUISearch.py index c5a267ab6..a1d5720f6 100644 --- a/hydrus/client/gui/search/ClientGUISearch.py +++ b/hydrus/client/gui/search/ClientGUISearch.py @@ -921,7 +921,7 @@ class _PredOKPanel( QW.QWidget ): def __init__( self, parent, predicate_panel_class, predicate ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._defaults_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().star, self._DefaultsMenu ) self._defaults_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Set a new default.' ) ) diff --git a/hydrus/client/gui/search/ClientGUISearchPanels.py b/hydrus/client/gui/search/ClientGUISearchPanels.py index 2a8488174..1fac59aa7 100644 --- a/hydrus/client/gui/search/ClientGUISearchPanels.py +++ b/hydrus/client/gui/search/ClientGUISearchPanels.py @@ -177,7 +177,7 @@ def __init__( self, parent, favourite_searches_rows, initial_search_row_to_edit self._favourite_searches_panel = ClientGUIListCtrl.BetterListCtrlPanel( self ) - model = ClientGUIListCtrl.HydrusListItemModelBridge( self, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, self._ConvertRowToListCtrlTuples ) + model = ClientGUIListCtrl.HydrusListItemModel( self, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, self._ConvertRowToDisplayTuple, self._ConvertRowToSortTuple ) self._favourite_searches = ClientGUIListCtrl.BetterListCtrlTreeView( self._favourite_searches_panel, CGLC.COLUMN_LIST_FAVOURITE_SEARCHES.ID, 20, model, use_simple_delete = True, activation_callback = self._EditFavouriteSearch ) @@ -252,7 +252,7 @@ def _AddNewFavouriteSearch( self, search_row = None ): - def _ConvertRowToListCtrlTuples( self, row ): + def _ConvertRowToDisplayTuple( self, row ): ( foldername, name, file_search_context, synchronised, media_sort, media_collect ) = row @@ -286,12 +286,11 @@ def _ConvertRowToListCtrlTuples( self, row ): pretty_media_collect = media_collect.ToString() - display_tuple = ( pretty_foldername, pretty_name, pretty_file_search_context, pretty_media_sort, pretty_media_collect ) - sort_tuple = ( pretty_foldername, pretty_name, pretty_file_search_context, pretty_media_sort, pretty_media_collect ) - - return ( display_tuple, sort_tuple ) + return ( pretty_foldername, pretty_name, pretty_file_search_context, pretty_media_sort, pretty_media_collect ) + _ConvertRowToSortTuple = _ConvertRowToDisplayTuple + def _DeleteRow( self, foldername, name ): for row in self._favourite_searches.GetData(): diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py index 7173c1788..a31ff3415 100644 --- a/hydrus/client/gui/services/ClientGUIClientsideServices.py +++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py @@ -1624,7 +1624,7 @@ class ReviewServicePanel( QW.QWidget ): def __init__( self, parent, service ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service = service @@ -2710,7 +2710,7 @@ class ReviewServiceRepositorySubPanel( QW.QWidget ): def __init__( self, parent, service ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service = service diff --git a/hydrus/client/gui/services/ClientGUIServersideServices.py b/hydrus/client/gui/services/ClientGUIServersideServices.py index 44f7166f0..1b4c7cf5d 100644 --- a/hydrus/client/gui/services/ClientGUIServersideServices.py +++ b/hydrus/client/gui/services/ClientGUIServersideServices.py @@ -142,7 +142,7 @@ class _ServiceRestrictedPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, dictionary ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) bandwidth_rules = dictionary[ 'bandwidth_rules' ] diff --git a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py index ac4dcda93..b7c2a4505 100644 --- a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py +++ b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py @@ -22,7 +22,7 @@ class LocalFilesSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._add_or_move_action = ClientGUICommon.BetterChoice( self ) @@ -84,7 +84,7 @@ class RatingLikeSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._flip_or_set_action = ClientGUICommon.BetterChoice( self ) @@ -193,7 +193,7 @@ class RatingNumericalSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._current_ratings_numerical_service = None @@ -338,7 +338,7 @@ class RatingIncDecSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._service_keys = ClientGUICommon.BetterChoice( self ) @@ -404,7 +404,7 @@ class SimpleSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget, shortcuts_name: str ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) if shortcuts_name in ClientGUIShortcuts.SHORTCUTS_RESERVED_NAMES: @@ -870,7 +870,7 @@ class TagSubPanel( QW.QWidget ): def __init__( self, parent: QW.QWidget ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._flip_or_set_action = ClientGUICommon.BetterChoice( self ) diff --git a/hydrus/client/gui/widgets/ClientGUIBytes.py b/hydrus/client/gui/widgets/ClientGUIBytes.py index 97521e731..9b7f108fe 100644 --- a/hydrus/client/gui/widgets/ClientGUIBytes.py +++ b/hydrus/client/gui/widgets/ClientGUIBytes.py @@ -14,7 +14,7 @@ class BytesControl( QW.QWidget ): def __init__( self, parent, initial_value = 65536 ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._spin = ClientGUICommon.BetterSpinBox( self, min=0, max=1048576 ) @@ -91,7 +91,7 @@ class NoneableBytesControl( QW.QWidget ): def __init__( self, parent, default_bytes: int, none_label = 'no limit' ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._bytes = BytesControl( self ) self._bytes.SetValue( default_bytes ) diff --git a/hydrus/client/gui/widgets/ClientGUIColourPicker.py b/hydrus/client/gui/widgets/ClientGUIColourPicker.py index ffa4f1e68..6ac6201d8 100644 --- a/hydrus/client/gui/widgets/ClientGUIColourPicker.py +++ b/hydrus/client/gui/widgets/ClientGUIColourPicker.py @@ -22,7 +22,7 @@ class AlphaColourControl( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._colour_picker = ColourPickerButton( self ) diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py index e7da46c9a..c5914ccbe 100644 --- a/hydrus/client/gui/widgets/ClientGUICommon.py +++ b/hydrus/client/gui/widgets/ClientGUICommon.py @@ -159,13 +159,15 @@ def WrapInText( control, parent, text, object_name = None ): class ShortcutAwareToolTipMixin( object ): - def __init__( self, tt_callable ): + def __init__( self, *args, **kwargs ): - self._tt_callable = tt_callable + self._tt_callable = None self._tt = '' self._simple_shortcut_command = None + super().__init__( *args, **kwargs ) + if ClientGUIShortcuts.shortcuts_manager_initialised(): ClientGUIShortcuts.shortcuts_manager().shortcutsChanged.connect( self.RefreshToolTip ) @@ -174,50 +176,52 @@ def __init__( self, tt_callable ): def _RefreshToolTip( self ): + if self._tt_callable is None or self._simple_shortcut_command is None: + + return + + tt = self._tt - if self._simple_shortcut_command is not None: - - tt += '\n' * 2 - tt += '----------' + tt += '\n' * 2 + tt += '----------' + + names_to_shortcuts = ClientGUIShortcuts.shortcuts_manager().GetNamesToShortcuts( self._simple_shortcut_command ) + + if len( names_to_shortcuts ) > 0: - names_to_shortcuts = ClientGUIShortcuts.shortcuts_manager().GetNamesToShortcuts( self._simple_shortcut_command ) + names = sorted( names_to_shortcuts.keys() ) - if len( names_to_shortcuts ) > 0: + for name in names: - names = sorted( names_to_shortcuts.keys() ) + shortcuts = names_to_shortcuts[ name ] - for name in names: - - shortcuts = names_to_shortcuts[ name ] - - shortcut_strings = sorted( ( shortcut.ToString() for shortcut in shortcuts ) ) + shortcut_strings = sorted( ( shortcut.ToString() for shortcut in shortcuts ) ) + + if name in ClientGUIShortcuts.shortcut_names_to_pretty_names: - if name in ClientGUIShortcuts.shortcut_names_to_pretty_names: - - pretty_name = ClientGUIShortcuts.shortcut_names_to_pretty_names[ name ] - - else: - - pretty_name = name - + pretty_name = ClientGUIShortcuts.shortcut_names_to_pretty_names[ name ] - tt += '\n' * 2 + else: - tt += ', '.join( shortcut_strings ) - tt += '\n' - tt += '({}->{})'.format( pretty_name, CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) + pretty_name = name - else: - tt += '\n' * 2 - tt += 'no shortcuts set' + tt += ', '.join( shortcut_strings ) tt += '\n' - tt += '({})'.format( CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) + tt += '({}->{})'.format( pretty_name, CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) + else: + + tt += '\n' * 2 + + tt += 'no shortcuts set' + tt += '\n' + tt += '({})'.format( CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) + self._tt_callable( tt ) @@ -230,6 +234,13 @@ def RefreshToolTip( self ): + def SetToolTipCallable( self, c ): + + self._tt_callable = c + + self._RefreshToolTip() + + def SetToolTipWithShortcuts( self, tt: str, simple_shortcut_command: int ): self._tt = tt @@ -242,11 +253,13 @@ class BetterBitmapButton( ShortcutAwareToolTipMixin, QW.QPushButton ): def __init__( self, parent, bitmap, func, *args, **kwargs ): - QW.QPushButton.__init__( self, parent ) + super().__init__( parent ) + + self.SetToolTipCallable( self.setToolTip ) + self.setIcon( QG.QIcon( bitmap ) ) self.setIconSize( bitmap.size() ) self.setSizePolicy( QW.QSizePolicy.Maximum, QW.QSizePolicy.Maximum ) - ShortcutAwareToolTipMixin.__init__( self, self.setToolTip ) self._func = func self._args = args @@ -264,8 +277,9 @@ class BetterButton( ShortcutAwareToolTipMixin, QW.QPushButton ): def __init__( self, parent, label, func, *args, **kwargs ): - QW.QPushButton.__init__( self, parent ) - ShortcutAwareToolTipMixin.__init__( self, self.setToolTip ) + super().__init__( parent ) + + self.SetToolTipCallable( self.setToolTip ) self.setText( label ) @@ -1159,7 +1173,7 @@ class NoneableSpinCtrl( QW.QWidget ): def __init__( self, parent, default_int: int, message = '', none_phrase = 'no limit', min = 0, max = 1000000, unit = None, multiplier = 1 ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._unit = unit self._multiplier = multiplier @@ -1266,7 +1280,7 @@ class NoneableDoubleSpinCtrl( QW.QWidget ): def __init__( self, parent, default_ints, message = '', none_phrase = 'no limit', min = 0, max = 1000000, unit = None, multiplier = 1 ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._unit = unit self._multiplier = multiplier @@ -1389,16 +1403,25 @@ class NoneableTextCtrl( QW.QWidget ): valueChanged = QC.Signal() - def __init__( self, parent, default_text, message = '', none_phrase = 'none' ): + def __init__( self, parent, default_text, message = '', placeholder_text = '', none_phrase = 'none' ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._checkbox = QW.QCheckBox( self ) self._checkbox.stateChanged.connect( self.EventCheckBox ) self._checkbox.setText( none_phrase ) self._text = QW.QLineEdit( self ) - self._text.setText( default_text ) + + if default_text != '': + + self._text.setText( default_text ) + + + if placeholder_text != '': + + self._text.setPlaceholderText( placeholder_text ) + hbox = QP.HBoxLayout( margin = 0 ) @@ -1632,7 +1655,7 @@ class TextAndGauge( QW.QWidget ): def __init__( self, parent ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._st = BetterStaticText( self ) self._gauge = Gauge( self ) diff --git a/hydrus/client/gui/widgets/ClientGUIMenuButton.py b/hydrus/client/gui/widgets/ClientGUIMenuButton.py index 5dfe5a8b7..178bbae19 100644 --- a/hydrus/client/gui/widgets/ClientGUIMenuButton.py +++ b/hydrus/client/gui/widgets/ClientGUIMenuButton.py @@ -7,11 +7,14 @@ from hydrus.client.gui import ClientGUIMenus from hydrus.client.gui.widgets import ClientGUICommon +# TODO: if I am feeling clever, I'm pretty sure this guy can inherit from QW.QWidget and do the DoMenu stuff itself, and super() now resolves the diamon inheritance problem class MenuMixin( object ): - def __init__( self, menu_items ): + def __init__( self, *args, **kwargs ): - self._menu_items = menu_items + self._menu_items = [] + + super().__init__( *args, **kwargs ) def _PopulateMenu( self, menu, menu_items ): @@ -60,16 +63,23 @@ def _SetMenuItems( self, menu_items ): self._menu_items = menu_items + class MenuBitmapButton( MenuMixin, ClientGUICommon.BetterBitmapButton ): def __init__( self, parent, bitmap, menu_items ): - ClientGUICommon.BetterBitmapButton.__init__( self, parent, bitmap, self.DoMenu ) - MenuMixin.__init__( self, menu_items ) + super().__init__( parent, bitmap, self.DoMenu ) + + self._SetMenuItems( menu_items ) def DoMenu( self ): + if len( self._menu_items ) == 0: + + return + + menu = ClientGUIMenus.GenerateMenu( self ) self._PopulateMenu( menu, self._menu_items ) @@ -79,19 +89,26 @@ def DoMenu( self ): def SetMenuItems( self, menu_items ): - MenuMixin._SetMenuItems( self, menu_items ) + self._SetMenuItems( menu_items ) + class MenuButton( MenuMixin, ClientGUICommon.BetterButton ): def __init__( self, parent, label, menu_items ): - ClientGUICommon.BetterButton.__init__( self, parent, label, self.DoMenu ) - MenuMixin.__init__( self, menu_items ) + super().__init__( parent, label, self.DoMenu ) + + self._SetMenuItems( menu_items ) def DoMenu( self ): + if len( self._menu_items ) == 0: + + return + + menu = ClientGUIMenus.GenerateMenu( self ) self._PopulateMenu( menu, self._menu_items ) @@ -101,9 +118,10 @@ def DoMenu( self ): def SetMenuItems( self, menu_items ): - MenuMixin._SetMenuItems( self, menu_items ) + self._SetMenuItems( menu_items ) + class MenuChoiceButton( MenuMixin, ClientGUICommon.BetterButton ): valueChanged = QC.Signal() @@ -115,10 +133,11 @@ def __init__( self, parent, choice_tuples ): label = self._GetCurrentDisplayString() + super().__init__( parent, label, self.DoMenu ) + menu_items = self._GenerateMenuItems() - ClientGUICommon.BetterButton.__init__( self, parent, label, self.DoMenu ) - MenuMixin.__init__( self, menu_items ) + self._SetMenuItems( menu_items ) def _GenerateMenuItems( self ): @@ -169,6 +188,11 @@ def _SetValueIndex( self, value_index ): def DoMenu( self ): + if len( self._menu_items ) == 0: + + return + + menu = ClientGUIMenus.GenerateMenu( self ) self._PopulateMenu( menu, self._menu_items ) @@ -197,7 +221,7 @@ def SetChoiceTuples( self, choice_tuples ): menu_items = self._GenerateMenuItems() - MenuMixin._SetMenuItems( self, menu_items ) + self._SetMenuItems( menu_items ) if len( menu_items ) == 0: diff --git a/hydrus/client/gui/widgets/ClientGUINumberTest.py b/hydrus/client/gui/widgets/ClientGUINumberTest.py index e3124d896..7450acc31 100644 --- a/hydrus/client/gui/widgets/ClientGUINumberTest.py +++ b/hydrus/client/gui/widgets/ClientGUINumberTest.py @@ -13,7 +13,7 @@ class NumberTestWidget( QW.QWidget ): def __init__( self, parent, allowed_operators = None, max = 200000, unit_string = None, appropriate_absolute_plus_or_minus_default = 1, appropriate_percentage_plus_or_minus_default = 15 ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) choice_tuples = [] diff --git a/hydrus/client/gui/widgets/ClientGUIRegex.py b/hydrus/client/gui/widgets/ClientGUIRegex.py index de058f0be..03ac5e5a2 100644 --- a/hydrus/client/gui/widgets/ClientGUIRegex.py +++ b/hydrus/client/gui/widgets/ClientGUIRegex.py @@ -170,7 +170,7 @@ class RegexInput( QW.QWidget ): def __init__( self, parent: QW.QWidget, show_group_menu = False ): - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._allow_enter_key_to_propagate_outside = True diff --git a/hydrus/client/gui/widgets/ClientGUITextInput.py b/hydrus/client/gui/widgets/ClientGUITextInput.py index d1d494575..183a7d09f 100644 --- a/hydrus/client/gui/widgets/ClientGUITextInput.py +++ b/hydrus/client/gui/widgets/ClientGUITextInput.py @@ -21,7 +21,7 @@ def __init__( self, parent, add_callable, allow_empty_input = False ): self._add_callable = add_callable self._allow_empty_input = allow_empty_input - QW.QWidget.__init__( self, parent ) + super().__init__( parent ) self._text_input = QW.QLineEdit( self ) self._text_input.installEventFilter( ClientGUICommon.TextCatchEnterEventFilter( self._text_input, self.EnterText ) ) diff --git a/hydrus/client/importing/ClientImportFileSeeds.py b/hydrus/client/importing/ClientImportFileSeeds.py index 1372e32d4..230338675 100644 --- a/hydrus/client/importing/ClientImportFileSeeds.py +++ b/hydrus/client/importing/ClientImportFileSeeds.py @@ -145,7 +145,7 @@ def __init__( self, file_seed_type: int = None, file_seed_data: str = None ): file_seed_data = 'https://big-guys.4u/monica_lewinsky_hott.tiff.exe.vbs' - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.file_seed_type = file_seed_type self.file_seed_data = file_seed_data @@ -2170,7 +2170,7 @@ class FileSeedCache( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._file_seeds = HydrusSerialisable.SerialisableList() diff --git a/hydrus/client/importing/ClientImportGallery.py b/hydrus/client/importing/ClientImportGallery.py index 212ad5ef8..da34d8217 100644 --- a/hydrus/client/importing/ClientImportGallery.py +++ b/hydrus/client/importing/ClientImportGallery.py @@ -48,7 +48,7 @@ def __init__( self, query = None, source_name = None, initial_search_urls = None initial_search_urls = HydrusData.DedupeList( initial_search_urls ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._creation_time = HydrusTime.GetNow() self._gallery_import_key = HydrusData.GenerateKey() @@ -1072,7 +1072,7 @@ def __init__( self, gug_key_and_name = None ): gug_key_and_name = ( HydrusData.GenerateKey(), 'unknown source' ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() diff --git a/hydrus/client/importing/ClientImportGallerySeeds.py b/hydrus/client/importing/ClientImportGallerySeeds.py index cf982232f..39ecefe9b 100644 --- a/hydrus/client/importing/ClientImportGallerySeeds.py +++ b/hydrus/client/importing/ClientImportGallerySeeds.py @@ -128,7 +128,7 @@ def __init__( self, url = None, can_generate_more_pages = True ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.url = url self._can_generate_more_pages = can_generate_more_pages @@ -752,7 +752,7 @@ class GallerySeedLog( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._gallery_seeds = HydrusSerialisable.SerialisableList() diff --git a/hydrus/client/importing/ClientImportLocal.py b/hydrus/client/importing/ClientImportLocal.py index 7012352dc..ed2b7be02 100644 --- a/hydrus/client/importing/ClientImportLocal.py +++ b/hydrus/client/importing/ClientImportLocal.py @@ -40,7 +40,7 @@ class HDDImport( HydrusSerialisable.SerialisableBase ): def __init__( self, paths = None, file_import_options = None, metadata_routers = None, paths_to_additional_service_keys_to_tags = None, delete_after_success = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if metadata_routers is None: diff --git a/hydrus/client/importing/ClientImportSimpleURLs.py b/hydrus/client/importing/ClientImportSimpleURLs.py index 8146b1868..33e857f1c 100644 --- a/hydrus/client/importing/ClientImportSimpleURLs.py +++ b/hydrus/client/importing/ClientImportSimpleURLs.py @@ -32,7 +32,7 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._pending_jobs = [] self._gallery_seed_log = ClientImportGallerySeeds.GallerySeedLog() @@ -839,7 +839,7 @@ class URLsImport( HydrusSerialisable.SerialisableBase ): def __init__( self, destination_location_context = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._gallery_seed_log = ClientImportGallerySeeds.GallerySeedLog() self._file_seed_cache = ClientImportFileSeeds.FileSeedCache() diff --git a/hydrus/client/importing/ClientImportSubscriptionLegacy.py b/hydrus/client/importing/ClientImportSubscriptionLegacy.py index 83d662917..298a181c5 100644 --- a/hydrus/client/importing/ClientImportSubscriptionLegacy.py +++ b/hydrus/client/importing/ClientImportSubscriptionLegacy.py @@ -33,7 +33,7 @@ class SubscriptionQueryLegacy( HydrusSerialisable.SerialisableBase ): def __init__( self, query = 'query text' ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._query = query self._display_name = None diff --git a/hydrus/client/importing/ClientImportSubscriptionQuery.py b/hydrus/client/importing/ClientImportSubscriptionQuery.py index d8b86a41a..ff4dce2cd 100644 --- a/hydrus/client/importing/ClientImportSubscriptionQuery.py +++ b/hydrus/client/importing/ClientImportSubscriptionQuery.py @@ -88,7 +88,7 @@ class SubscriptionQueryHeader( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._query_log_container_name = GenerateQueryLogContainerName() self._query_text = 'query' diff --git a/hydrus/client/importing/ClientImportSubscriptions.py b/hydrus/client/importing/ClientImportSubscriptions.py index 158842604..0c8c1b677 100644 --- a/hydrus/client/importing/ClientImportSubscriptions.py +++ b/hydrus/client/importing/ClientImportSubscriptions.py @@ -1731,7 +1731,7 @@ class SubscriptionContainer( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.subscription = Subscription( 'default' ) self.query_log_containers = HydrusSerialisable.SerialisableList() diff --git a/hydrus/client/importing/ClientImportWatchers.py b/hydrus/client/importing/ClientImportWatchers.py index 2ddd8d9d8..0c9da3f87 100644 --- a/hydrus/client/importing/ClientImportWatchers.py +++ b/hydrus/client/importing/ClientImportWatchers.py @@ -34,7 +34,7 @@ class MultipleWatcherImport( HydrusSerialisable.SerialisableBase ): def __init__( self, url = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() @@ -694,7 +694,7 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._page_key = b'initialising page key' self._publish_to_page = False diff --git a/hydrus/client/importing/options/ClientImportOptions.py b/hydrus/client/importing/options/ClientImportOptions.py index 829be6359..357ad15ea 100644 --- a/hydrus/client/importing/options/ClientImportOptions.py +++ b/hydrus/client/importing/options/ClientImportOptions.py @@ -40,7 +40,7 @@ class CheckerOptions( HydrusSerialisable.SerialisableBase ): def __init__( self, intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._intended_files_per_check = intended_files_per_check self._never_faster_than = never_faster_than diff --git a/hydrus/client/importing/options/FileImportOptions.py b/hydrus/client/importing/options/FileImportOptions.py index 7765332cd..e42e50995 100644 --- a/hydrus/client/importing/options/FileImportOptions.py +++ b/hydrus/client/importing/options/FileImportOptions.py @@ -48,7 +48,7 @@ class FileImportOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._exclude_deleted = True self._preimport_hash_check_type = DO_CHECK_AND_MATCHES_ARE_DISPOSITIVE diff --git a/hydrus/client/importing/options/NoteImportOptions.py b/hydrus/client/importing/options/NoteImportOptions.py index 64413cdfb..9f488eaf2 100644 --- a/hydrus/client/importing/options/NoteImportOptions.py +++ b/hydrus/client/importing/options/NoteImportOptions.py @@ -30,7 +30,7 @@ class NoteImportOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._get_notes = True self._extend_existing_note_if_possible = True diff --git a/hydrus/client/importing/options/PresentationImportOptions.py b/hydrus/client/importing/options/PresentationImportOptions.py index 715853803..6f3b64b35 100644 --- a/hydrus/client/importing/options/PresentationImportOptions.py +++ b/hydrus/client/importing/options/PresentationImportOptions.py @@ -33,7 +33,7 @@ class PresentationImportOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY ) self._presentation_status = PRESENTATION_STATUS_ANY_GOOD diff --git a/hydrus/client/importing/options/TagImportOptions.py b/hydrus/client/importing/options/TagImportOptions.py index e74f992dc..73f870228 100644 --- a/hydrus/client/importing/options/TagImportOptions.py +++ b/hydrus/client/importing/options/TagImportOptions.py @@ -25,7 +25,7 @@ class FilenameTaggingOptions( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._tags_for_all = set() @@ -252,7 +252,7 @@ class TagImportOptions( HydrusSerialisable.SerialisableBase ): def __init__( self, fetch_tags_even_if_url_recognised_and_file_already_in_db = False, fetch_tags_even_if_hash_recognised_and_file_already_in_db = False, tag_blacklist = None, tag_whitelist = None, service_keys_to_service_tag_import_options = None, is_default = False ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if tag_blacklist is None: @@ -701,7 +701,7 @@ def __init__( self, get_tags = False, get_tags_filter = None, additional_tags = only_add_existing_tags_filter = HydrusTags.TagFilter() - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._get_tags = get_tags self._get_tags_filter = get_tags_filter diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py index 86e3fb905..d8f324a74 100644 --- a/hydrus/client/media/ClientMedia.py +++ b/hydrus/client/media/ClientMedia.py @@ -541,7 +541,9 @@ def ToString( self ): class MediaList( object ): - def __init__( self, location_context: ClientLocation.LocationContext, media_results ): + def __init__( self, location_context: ClientLocation.LocationContext, media_results, *args, **kwargs ): + + super().__init__( *args, **kwargs ) hashes_seen = set() @@ -1352,9 +1354,9 @@ def Sort( self, media_sort = None ): class ListeningMediaList( MediaList ): - def __init__( self, location_context: ClientLocation.LocationContext, media_results ): + def __init__( self, location_context: ClientLocation.LocationContext, media_results, *args, **kwargs ): - MediaList.__init__( self, location_context, media_results ) + super().__init__( location_context, media_results, *args, **kwargs ) CG.client_controller.sub( self, 'ProcessContentUpdatePackage', 'content_updates_gui' ) CG.client_controller.sub( self, 'ProcessServiceUpdates', 'service_updates_gui' ) @@ -1367,8 +1369,7 @@ def __init__( self, location_context: ClientLocation.LocationContext, media_resu # note for later: ideal here is to stop this multiple inheritance mess and instead have this be a media that *has* a list, not *is* a list - Media.__init__( self ) - MediaList.__init__( self, location_context, media_results ) + super().__init__( location_context, media_results ) self._archive = True self._inbox = False @@ -1816,7 +1817,7 @@ class MediaSingleton( Media ): def __init__( self, media_result: ClientMediaResult.MediaResult ): - Media.__init__( self ) + super().__init__() self._media_result = media_result diff --git a/hydrus/client/media/ClientMediaFileFilter.py b/hydrus/client/media/ClientMediaFileFilter.py index f16becda2..da73a3129 100644 --- a/hydrus/client/media/ClientMediaFileFilter.py +++ b/hydrus/client/media/ClientMediaFileFilter.py @@ -52,7 +52,7 @@ class FileFilter( HydrusSerialisable.SerialisableBase ): def __init__( self, filter_type = None, filter_data = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.filter_type = filter_type self.filter_data = filter_data diff --git a/hydrus/client/metadata/ClientMetadataMigration.py b/hydrus/client/metadata/ClientMetadataMigration.py index 86e2066cc..b2af0f576 100644 --- a/hydrus/client/metadata/ClientMetadataMigration.py +++ b/hydrus/client/metadata/ClientMetadataMigration.py @@ -36,7 +36,7 @@ def __init__( exporter = ClientMetadataMigrationExporters.SingleFileMetadataExporterTXT() - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._importers = HydrusSerialisable.SerialisableList( importers ) self._string_processor = string_processor diff --git a/hydrus/client/metadata/ClientMetadataMigrationExporters.py b/hydrus/client/metadata/ClientMetadataMigrationExporters.py index 04312958f..77aa0e8a6 100644 --- a/hydrus/client/metadata/ClientMetadataMigrationExporters.py +++ b/hydrus/client/metadata/ClientMetadataMigrationExporters.py @@ -49,8 +49,7 @@ class SingleFileMetadataExporterSidecar( SingleFileMetadataExporter, ClientMetad def __init__( self, remove_actual_filename_ext: bool, suffix: str, filename_string_converter: ClientStrings.StringConverter ): - ClientMetadataMigrationCore.SidecarNode.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter ) - SingleFileMetadataExporter.__init__( self ) + super().__init__( remove_actual_filename_ext, suffix, filename_string_converter ) def Export( self, actual_file_path: str, rows: typing.Collection[ str ] ): @@ -72,8 +71,7 @@ class SingleFileMetadataExporterMediaNotes( SingleFileMetadataExporterMedia, Hyd def __init__( self, forced_name = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterMedia.__init__( self ) + super().__init__() self._forced_name = forced_name @@ -190,8 +188,7 @@ class SingleFileMetadataExporterMediaTags( SingleFileMetadataExporterMedia, Hydr def __init__( self, service_key = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterMedia.__init__( self ) + super().__init__() if service_key is None: @@ -290,8 +287,7 @@ class SingleFileMetadataExporterMediaTimestamps( SingleFileMetadataExporterMedia def __init__( self, timestamp_data_stub: typing.Optional[ ClientTime.TimestampData ] = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterMedia.__init__( self ) + super().__init__() if timestamp_data_stub is None: @@ -380,8 +376,7 @@ class SingleFileMetadataExporterMediaURLs( SingleFileMetadataExporterMedia, Hydr def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterMedia.__init__( self ) + super().__init__() def _GetSerialisableInfo( self ): @@ -471,8 +466,7 @@ def __init__( self, remove_actual_filename_ext = None, suffix = None, filename_s filename_string_converter = ClientStrings.StringConverter( example_string = '0123456789abcdef.jpg.json' ) - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterSidecar.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter ) + super().__init__( remove_actual_filename_ext, suffix, filename_string_converter ) if nested_object_names is None: @@ -635,8 +629,7 @@ def __init__( self, remove_actual_filename_ext = None, suffix = None, filename_s separator = '\n' - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataExporterSidecar.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter ) + super().__init__( remove_actual_filename_ext, suffix, filename_string_converter ) self._separator = separator diff --git a/hydrus/client/metadata/ClientMetadataMigrationImporters.py b/hydrus/client/metadata/ClientMetadataMigrationImporters.py index 5298030e1..e884bc88d 100644 --- a/hydrus/client/metadata/ClientMetadataMigrationImporters.py +++ b/hydrus/client/metadata/ClientMetadataMigrationImporters.py @@ -20,10 +20,12 @@ class SingleFileMetadataImporter( ClientMetadataMigrationCore.ImporterExporterNode ): - def __init__( self, string_processor: ClientStrings.StringProcessor ): + def __init__( self, string_processor: ClientStrings.StringProcessor, *args, **kwargs ): self._string_processor = string_processor + super().__init__( *args, **kwargs ) + def GetStringProcessor( self ) -> ClientStrings.StringProcessor: @@ -67,8 +69,7 @@ def __init__( self, string_processor: typing.Optional[ ClientStrings.StringProce string_processor = ClientStrings.StringProcessor() - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterMedia.__init__( self, string_processor ) + super().__init__( string_processor ) def _GetSerialisableInfo( self ): @@ -139,8 +140,7 @@ def __init__( self, string_processor = None, service_key = None, tag_display_typ string_processor = ClientStrings.StringProcessor() - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterMedia.__init__( self, string_processor ) + super().__init__( string_processor ) if service_key is None: @@ -286,8 +286,7 @@ def __init__( self, string_processor = None, timestamp_data_stub = None ): string_processor = ClientStrings.StringProcessor() - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterMedia.__init__( self, string_processor ) + super().__init__( string_processor ) if timestamp_data_stub is None: @@ -381,8 +380,7 @@ def __init__( self, string_processor = None ): string_processor = ClientStrings.StringProcessor() - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterMedia.__init__( self, string_processor ) + super().__init__( string_processor ) def _GetSerialisableInfo( self ): @@ -458,8 +456,7 @@ class SingleFileMetadataImporterSidecar( SingleFileMetadataImporter, ClientMetad def __init__( self, string_processor: ClientStrings.StringProcessor, remove_actual_filename_ext: bool, suffix: str, filename_string_converter: ClientStrings.StringConverter ): - ClientMetadataMigrationCore.SidecarNode.__init__( self, remove_actual_filename_ext, suffix, filename_string_converter ) - SingleFileMetadataImporter.__init__( self, string_processor ) + super().__init__( string_processor, remove_actual_filename_ext, suffix, filename_string_converter ) def GetPossibleSidecarPaths( self, path: str ) -> typing.Collection[ str ]: @@ -521,8 +518,7 @@ def __init__( self, string_processor = None, remove_actual_filename_ext = None, string_processor = ClientStrings.StringProcessor() - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterSidecar.__init__( self, string_processor, remove_actual_filename_ext, suffix, filename_string_converter ) + super().__init__( string_processor, remove_actual_filename_ext, suffix, filename_string_converter ) if json_parsing_formula is None: @@ -684,10 +680,9 @@ def __init__( self, string_processor = None, remove_actual_filename_ext = None, separator = '\n' - self._separator = separator + super().__init__( string_processor, remove_actual_filename_ext, suffix, filename_string_converter ) - HydrusSerialisable.SerialisableBase.__init__( self ) - SingleFileMetadataImporterSidecar.__init__( self, string_processor, remove_actual_filename_ext, suffix, filename_string_converter ) + self._separator = separator def _GetSerialisableInfo( self ): diff --git a/hydrus/client/metadata/ClientTags.py b/hydrus/client/metadata/ClientTags.py index c50eaf551..7e2911b36 100644 --- a/hydrus/client/metadata/ClientTags.py +++ b/hydrus/client/metadata/ClientTags.py @@ -112,8 +112,7 @@ class ServiceKeysToTags( HydrusSerialisable.SerialisableBase, collections.defaul def __init__( self, *args, **kwargs ): - collections.defaultdict.__init__( self, set, *args, **kwargs ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__( set, *args, **kwargs ) def _GetSerialisableInfo( self ): diff --git a/hydrus/client/metadata/ClientTagsHandling.py b/hydrus/client/metadata/ClientTagsHandling.py index 086c844d4..15c801c3f 100644 --- a/hydrus/client/metadata/ClientTagsHandling.py +++ b/hydrus/client/metadata/ClientTagsHandling.py @@ -28,7 +28,7 @@ def __init__( self, service_key: typing.Optional[ bytes ] = None ): service_key = CC.COMBINED_TAG_SERVICE_KEY - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._service_key = service_key @@ -650,6 +650,33 @@ def Start( self ): self._controller.CallToThreadLongRunning( self.MainLoop ) + def SyncFasterNow( self ) -> bool: + + with self._lock: + + # this actually initialises the 'needs_work' structure, so it is important + if self._WorkToDo(): + + outstanding = { service_key for ( service_key, needs_work ) in self._service_keys_to_needs_work.items() if needs_work } + + self._go_faster.update( outstanding ) + + else: + + return False + + + + for service_key in outstanding: + + self._controller.pub( 'notify_new_tag_display_sync_status', service_key ) + + + self.Wake() + + return True + + def Wake( self ): self._wake_event.set() @@ -663,7 +690,7 @@ class TagDisplayManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() service_keys_to_tag_filters_defaultdict = lambda: collections.defaultdict( HydrusTags.TagFilter ) diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py index d01d8acca..71c53d5fe 100644 --- a/hydrus/client/networking/ClientLocalServerResources.py +++ b/hydrus/client/networking/ClientLocalServerResources.py @@ -42,6 +42,7 @@ from hydrus.client import ClientConstants as CC from hydrus.client import ClientGlobals as CG from hydrus.client import ClientLocation +from hydrus.client import ClientPaths from hydrus.client import ClientThreading from hydrus.client import ClientTime from hydrus.client import ClientRendering @@ -1454,8 +1455,13 @@ class HydrusResourceClientAPIRestrictedAddFilesAddFile( HydrusResourceClientAPIR def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): + path = None + delete_after_successful_import = False + if not hasattr( request, 'temp_file_info' ): + # ok the caller has not sent us a file in the POST content, we have a 'path' + path = request.parsed_request_args.GetValue( 'path', str ) if not os.path.exists( path ): @@ -1468,6 +1474,8 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): raise HydrusExceptions.BadRequestException( 'Path "{}" is not a file!'.format( path ) ) + delete_after_successful_import = request.parsed_request_args.GetValue( 'delete_after_successful_import', bool, default_value = False ) + ( os_file_handle, temp_path ) = HydrusTemp.GetTempPath() request.temp_file_info = ( os_file_handle, temp_path ) @@ -1510,6 +1518,14 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ): body_dict[ 'traceback' ] = traceback.format_exc() + if path is not None: + + if delete_after_successful_import and file_import_status.status in CC.SUCCESSFUL_IMPORT_STATES: + + ClientPaths.DeletePath( path ) + + + body_dict[ 'status' ] = file_import_status.status body_dict[ 'hash' ] = HydrusData.BytesToNoneOrHex( file_import_status.hash ) body_dict[ 'note' ] = file_import_status.note diff --git a/hydrus/client/networking/ClientNetworkingBandwidth.py b/hydrus/client/networking/ClientNetworkingBandwidth.py index 50680687e..ba2eb6bf8 100644 --- a/hydrus/client/networking/ClientNetworkingBandwidth.py +++ b/hydrus/client/networking/ClientNetworkingBandwidth.py @@ -63,7 +63,7 @@ class NetworkBandwidthManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._dirty = False diff --git a/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py b/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py index da7c22335..a100c428f 100644 --- a/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py +++ b/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py @@ -16,7 +16,7 @@ class NetworkBandwidthManagerLegacy( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._network_contexts_to_bandwidth_trackers = collections.defaultdict( HydrusNetworking.BandwidthTracker ) self._network_contexts_to_bandwidth_rules = collections.defaultdict( HydrusNetworking.BandwidthRules ) diff --git a/hydrus/client/networking/ClientNetworkingContexts.py b/hydrus/client/networking/ClientNetworkingContexts.py index 012b9308e..bd6541d62 100644 --- a/hydrus/client/networking/ClientNetworkingContexts.py +++ b/hydrus/client/networking/ClientNetworkingContexts.py @@ -13,7 +13,7 @@ class NetworkContext( HydrusSerialisable.SerialisableBase ): def __init__( self, context_type = None, context_data = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.context_type = context_type self.context_data = context_data diff --git a/hydrus/client/networking/ClientNetworkingDomain.py b/hydrus/client/networking/ClientNetworkingDomain.py index 9bc216436..607eb318e 100644 --- a/hydrus/client/networking/ClientNetworkingDomain.py +++ b/hydrus/client/networking/ClientNetworkingDomain.py @@ -39,7 +39,7 @@ class NetworkDomainManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._gugs = HydrusSerialisable.SerialisableList() self._url_classes = HydrusSerialisable.SerialisableList() @@ -2243,7 +2243,7 @@ class DomainMetadataPackage( HydrusSerialisable.SerialisableBase ): def __init__( self, domain = None, headers_list = None, bandwidth_rules = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if domain is None: diff --git a/hydrus/client/networking/ClientNetworkingLogin.py b/hydrus/client/networking/ClientNetworkingLogin.py index 375803279..3e53c6209 100644 --- a/hydrus/client/networking/ClientNetworkingLogin.py +++ b/hydrus/client/networking/ClientNetworkingLogin.py @@ -10,6 +10,7 @@ from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusSerialisable +from hydrus.core import HydrusText from hydrus.core import HydrusTime from hydrus.client import ClientConstants as CC @@ -63,7 +64,7 @@ class NetworkLoginManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.engine = None @@ -1465,7 +1466,18 @@ def Start( self, engine, network_context, given_credentials, network_job_present HydrusData.ShowException( e ) - message = str( e ) + if isinstance( e, HydrusExceptions.InsufficientCredentialsException ): + + message = '403 - login script or credentials may be invalid' + + elif isinstance( e, HydrusExceptions.MissingCredentialsException ): + + message = '401 - login script or credentials may be invalid' + + else: + + message = HydrusText.GetFirstLine( str( e ) ) + engine.login_manager.DelayLoginScript( login_domain, self._login_script_key, message ) diff --git a/hydrus/client/networking/ClientNetworkingSessions.py b/hydrus/client/networking/ClientNetworkingSessions.py index 4acd73fbb..f8db73614 100644 --- a/hydrus/client/networking/ClientNetworkingSessions.py +++ b/hydrus/client/networking/ClientNetworkingSessions.py @@ -175,7 +175,7 @@ class NetworkSessionManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._dirty = False self._dirty_session_container_names = set() diff --git a/hydrus/client/networking/ClientNetworkingSessionsLegacy.py b/hydrus/client/networking/ClientNetworkingSessionsLegacy.py index 4eacef800..5b02903a3 100644 --- a/hydrus/client/networking/ClientNetworkingSessionsLegacy.py +++ b/hydrus/client/networking/ClientNetworkingSessionsLegacy.py @@ -16,7 +16,7 @@ class NetworkSessionManagerLegacy( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._network_contexts_to_sessions = {} diff --git a/hydrus/client/networking/ClientNetworkingURLClass.py b/hydrus/client/networking/ClientNetworkingURLClass.py index 48dd94485..c0cd0db59 100644 --- a/hydrus/client/networking/ClientNetworkingURLClass.py +++ b/hydrus/client/networking/ClientNetworkingURLClass.py @@ -97,7 +97,7 @@ def __init__( self, name = None, value_string_match = None ): value_string_match = ClientStrings.StringMatch( match_type = ClientStrings.STRING_MATCH_FIXED, match_value = 'value', example_string = 'value' ) - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._name = name self._value_string_match = value_string_match diff --git a/hydrus/client/search/ClientSearch.py b/hydrus/client/search/ClientSearch.py index d645d81a7..2bd2fa1f5 100644 --- a/hydrus/client/search/ClientSearch.py +++ b/hydrus/client/search/ClientSearch.py @@ -312,7 +312,7 @@ class NumberTest( HydrusSerialisable.SerialisableBase ): def __init__( self, operator = NUMBER_TEST_OPERATOR_EQUAL, value = 1, extra_value = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if operator == NUMBER_TEST_OPERATOR_APPROXIMATE_PERCENT and value == 0: @@ -1445,7 +1445,7 @@ class FavouriteSearchManager( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._favourite_search_rows = [] diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index 9d475a06f..f425a8f9e 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -105,8 +105,8 @@ # Misc NETWORK_VERSION = 20 -SOFTWARE_VERSION = 588 -CLIENT_API_VERSION = 69 +SOFTWARE_VERSION = 589 +CLIENT_API_VERSION = 70 SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 ) diff --git a/hydrus/core/HydrusController.py b/hydrus/core/HydrusController.py index 7c5a8ee9b..98a85021e 100644 --- a/hydrus/core/HydrusController.py +++ b/hydrus/core/HydrusController.py @@ -22,7 +22,7 @@ class HydrusController( HydrusControllerInterface.HydrusControllerInterface ): def __init__( self, db_dir ): - HydrusControllerInterface.HydrusControllerInterface.__init__( self ) + super().__init__() HG.controller = self diff --git a/hydrus/core/HydrusData.py b/hydrus/core/HydrusData.py index 48313836c..e729040d1 100644 --- a/hydrus/core/HydrusData.py +++ b/hydrus/core/HydrusData.py @@ -91,23 +91,12 @@ def CleanRunningFile( db_path, instance ): -def ConvertPrettyStringsToUglyNamespaces( pretty_strings ): - - result = { s for s in pretty_strings if s != 'no namespace' } - - if 'no namespace' in pretty_strings: result.add( '' ) - - return result - - -def ConvertStatusToPrefix( status ): - - if status == HC.CONTENT_STATUS_CURRENT: return '' - elif status == HC.CONTENT_STATUS_PENDING: return '(+) ' - elif status == HC.CONTENT_STATUS_PETITIONED: return '(-) ' - elif status == HC.CONTENT_STATUS_DELETED: return '(X) ' - else: return '(?)' - +status_to_prefix = { + HC.CONTENT_STATUS_CURRENT : '', + HC.CONTENT_STATUS_PENDING : '(+) ', + HC.CONTENT_STATUS_PETITIONED : '(-) ', + HC.CONTENT_STATUS_DELETED : '(X) ' +} def ConvertValueRangeToBytes( value, range ): diff --git a/hydrus/core/HydrusExceptions.py b/hydrus/core/HydrusExceptions.py index 09182d968..30e20f6a2 100644 --- a/hydrus/core/HydrusExceptions.py +++ b/hydrus/core/HydrusExceptions.py @@ -42,9 +42,10 @@ def __init__( self, e, first_line, db_traceback ): self.db_e = e - HydrusException.__init__( self, first_line, db_traceback ) + super().__init__( first_line, db_traceback ) + class DBAccessException( HydrusException ): pass class DBCredentialsException( HydrusException ): pass class DBVersionException( HydrusException ): pass diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py index d58703bb1..5cc3a0c0a 100644 --- a/hydrus/core/HydrusSerialisable.py +++ b/hydrus/core/HydrusSerialisable.py @@ -435,8 +435,7 @@ class SerialisableDictionary( SerialisableBase, dict ): def __init__( self, *args, **kwargs ): - dict.__init__( self, *args, **kwargs ) - SerialisableBase.__init__( self ) + super().__init__( *args, **kwargs ) def _GetSerialisableInfo( self ): @@ -563,8 +562,7 @@ class SerialisableBytesDictionary( SerialisableBase, dict ): def __init__( self, *args, **kwargs ): - dict.__init__( self, *args, **kwargs ) - SerialisableBase.__init__( self ) + super().__init__( *args, **kwargs ) def _GetSerialisableInfo( self ): @@ -641,8 +639,7 @@ class SerialisableList( SerialisableBase, list ): def __init__( self, *args, **kwargs ): - list.__init__( self, *args, **kwargs ) - SerialisableBase.__init__( self ) + super().__init__( *args, **kwargs ) def _GetSerialisableInfo( self ): diff --git a/hydrus/core/HydrusTags.py b/hydrus/core/HydrusTags.py index cf3572981..76996c7d3 100644 --- a/hydrus/core/HydrusTags.py +++ b/hydrus/core/HydrusTags.py @@ -349,7 +349,7 @@ class TagFilter( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() # TODO: update this guy to more carefully navigate how it does advanced filters # we want to support both: diff --git a/hydrus/core/networking/HydrusNetwork.py b/hydrus/core/networking/HydrusNetwork.py index 8ca8a47e7..801ca286f 100644 --- a/hydrus/core/networking/HydrusNetwork.py +++ b/hydrus/core/networking/HydrusNetwork.py @@ -149,7 +149,7 @@ class Account( object ): def __init__( self, account_key: bytes, account_type: "AccountType", created: int, expires: typing.Optional[ int ] ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() @@ -752,7 +752,7 @@ class AccountIdentifier( HydrusSerialisable.SerialisableBase ): def __init__( self, account_key = None, content = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if account_key is not None: @@ -861,7 +861,7 @@ def __init__( auto_creation_history = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() if account_type_key is None: @@ -1168,7 +1168,7 @@ class ClientToServerUpdate( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._actions_to_contents_and_reasons = collections.defaultdict( list ) @@ -1289,7 +1289,7 @@ class Content( HydrusSerialisable.SerialisableBase ): def __init__( self, content_type = None, content_data = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._content_type = content_type self._content_data = content_data @@ -1526,7 +1526,7 @@ class ContentUpdate( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._content_data = {} @@ -1666,7 +1666,7 @@ class Credentials( HydrusSerialisable.SerialisableBase ): def __init__( self, host = None, port = None, access_key = None ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._host = host self._port = port @@ -1876,7 +1876,7 @@ class DefinitionsUpdate( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._hash_ids_to_hashes = {} self._tag_ids_to_tags = {} @@ -1963,7 +1963,7 @@ def __init__( self, metadata = None, next_update_due = None ): next_update_due = 0 - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() @@ -2332,7 +2332,7 @@ def __init__( self, petitioner_account = None, petition_header = None, actions_a actions_and_contents = [] - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._petitioner_account = petitioner_account self._petition_header = petition_header @@ -2605,7 +2605,7 @@ def __init__( self, content_type = None, status = None, account_key = None, reas reason = '' - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self.content_type = content_type self.status = status diff --git a/hydrus/core/networking/HydrusNetworking.py b/hydrus/core/networking/HydrusNetworking.py index d20945d7f..3d32dfe60 100644 --- a/hydrus/core/networking/HydrusNetworking.py +++ b/hydrus/core/networking/HydrusNetworking.py @@ -90,7 +90,7 @@ class BandwidthRules( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() @@ -319,7 +319,7 @@ class BandwidthTracker( HydrusSerialisable.SerialisableBase ): def __init__( self ): - HydrusSerialisable.SerialisableBase.__init__( self ) + super().__init__() self._lock = threading.Lock() diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py index 90c13c277..75ae70264 100644 --- a/hydrus/test/TestClientAPI.py +++ b/hydrus/test/TestClientAPI.py @@ -1022,6 +1022,49 @@ def _test_add_files_add_file( self, connection, set_up_permissions ): self.assertEqual( response_json, expected_result ) + self.assertTrue( os.path.exists( hydrus_png_path ) ) + + # do hydrus png as path, and delete it + + f = ClientImportFiles.FileImportStatus.STATICGetUnknownStatus() + + f.hash = hash + f.note = 'test note' + + HG.test_controller.SetRead( 'hash_status', f ) + + headers = { 'Hydrus-Client-API-Access-Key' : access_key_hex, 'Content-Type' : HC.mime_mimetype_string_lookup[ HC.APPLICATION_JSON ] } + + path = '/add_files/add_file' + + temp_hydrus_png_path = os.path.join( HG.test_controller.db_dir, 'hydrus_png_client_api_import_test.wew' ) + + HydrusPaths.MirrorFile( hydrus_png_path, temp_hydrus_png_path ) + + body_dict = { 'path' : temp_hydrus_png_path, 'delete_after_successful_import' : True } + + body = json.dumps( body_dict ) + + connection.request( 'POST', path, body = body, headers = headers ) + + response = connection.getresponse() + + data = response.read() + + text = str( data, 'utf-8' ) + + self.assertEqual( response.status, 200 ) + + response_json = json.loads( text ) + + expected_result = { 'status' : CC.STATUS_SUCCESSFUL_AND_NEW, 'hash' : hash.hex() , 'note' : 'test note' } + + wash_example_json_response( expected_result ) + + self.assertEqual( response_json, expected_result ) + + self.assertFalse( os.path.exists( temp_hydrus_png_path ) ) + def _test_add_files_migrate_files( self, connection, set_up_permissions ): diff --git a/static/default/parsers/catbox collection parser.png b/static/default/parsers/catbox collection parser.png new file mode 100644 index 0000000000000000000000000000000000000000..75dbf46bed3a7e9d7db7289f76c5d351c4b8044e GIT binary patch literal 2320 zcmai$X*ARg8^-@L)=?8FYnB;Vijt9$WEuOGB~eHyVnW99WX+lhX)I%%hNe)+qalgP zGDWgRb~AP{mW*{WVR$^B-uKIU-Y?g=&V7Bm@85mSm1JXO0_79u0{{SOW@=~)0N?`) zxbXg~{=6HB0KhwBW~gTuN?mq_`X2u#6m^Wanjv^nT*Vv01A2m&dH%WruMg6rsx_nY zTTr7#GDZDe;h zMuA~z0B=o!#3u?eG*aJ5dR^(v=}Xsfoc<#6cBnJbpx<{U9b0a^g%=IUszwb>7Z&cv;haaCR1CtwSHK7f5Id(YG04tGtg z-xILbg(XZ>ncwA;AZK2Dn*hQHT$=)lZ{%PNC8|Opllo|PzZ8_Cm;~_xx4+1w@q^J_ zoFu*P(%$+KXcKs|2}pM_8Wwy5jfX5b=0VP4U^IvOQWIf-of8WKjn0D-gXQwui~3}D zq+`ZGRgAIL6o?^nN zP^{GCqDRt)*&Jb}^7Ebi1&f46b6J$$DEAcHcHvq5QDu-Q$HA@)QoeV^t4S3Q>?+dd zG`m7031aU;)bUie_SPLujIr~<<#@I7R( z3h!|eLE>6A@0x}uT;nut1kZdO~EOEkw#!%eOT zpt#R19aGN+1xq8sxml~dhJ5o)dI`pTWGCm_97pK}WK?q{BDn+$sQMeZD8u?&OZz1j z4frj0vu{;V<()8@t(ub4!7q&wf-%fGjo51EeY&y1T#fcGgpj%q;)THSM52yW7;eOn z3mlIVj=%}q>jgQay&c#JtZ_RT<3{`%Uf7K8d82kCpybM78by~&Rwpu@6MEl81ll*)R*AX; z9kRE^^L{&07@#j7O0$j)V&@+SiU5@eV#y@vOrxtEW(^2qHWfs?U6`~sTEgKuq2fO) zysvu?EC%w!CT%j_Mew26#biXv-idvZ625q!^f#$?`Pl?=Io)Y)kDPgggC9np_BMO+ z`Il4-xxw+)z+t-nob5eKr@Ue);FuTnR&+2~3P#?u*8*y~Zr7`PwKuBKa;7XpBS)6khFCNw$rDvcPHGK59SUP^hA|HfU!#PV3}+S} zd=@Y;d@*78%oUa~2ae(KyK9Lp+t6b5}A*(U$i4 z6OM}ty=Rv@j`aq-Y2kNmB#CnM(>=NNnd*gi$Un~BL$OuQ_MSloDk59S(sR;$NvzV= z+Xpu^$mXWO8OxXU8I6wkj4}Y_?(028)lw0U?w7hB*)DQ*|Fav*PZ;mT0&w#{jqUUnyGBzJ?`RtJ(3BjW%T zbUD5BlB;_<1xS<4d!fiH;~D#wy$31Y^fN_;NJtVmPX1%zU*sN?d;6fyJgrlSAZE&B zGL4AofKgmIX+y0l?SY@H2E#GCbP;R*w%gIvgn4b`$(FxjkR56mh=ZTru* z%>*ruvuR(Iyo{AuOAjrwr6cZl;pfh`zd44f@70c8iLdUytQokbzu7$_-7s6$kyy{SX8KG_Ek(~CH$M&BLjO~53SIT+D`kuU1K1SQv5OKW};p5RptK>J)Rime4 z7(QY(;jwfEJ9-h*;Li5))cv6oH1LqWI>fL{uxmoWTGv1Nl@bh>U4_i zNV~Vbk_8i1^4Oz)n3xc&OZ9&JjvBPL3-fhs>wK-Ct#lshh3W};%k4ERVMmuVEb3Cb zDAHIznS}8UbD!#fV(P+Kte0IUqBnnJEPswoPZnv+9!7=%#5rI-{`x={R2=cS)>2} literal 0 HcmV?d00001 diff --git a/static/default/parsers/safebooru file page parser.png b/static/default/parsers/safebooru file page parser.png index cb940cb773dc1ba31a9ac71f723913ef5c0798a5..b69a7a824aa0bf24e237506610b698af6f455fb2 100644 GIT binary patch literal 2974 zcma)-Svb@U8^-^(vd!BlOJkiRvQrtNEQ2y+knAz`tfOSlgvm}rQI_mW1|hpJCTl}9 zXe?PqA#0N*TMRz$^__f&-*a$ZzoX~ixv%HGaXm<&R2PmJaLs z84t1hdwPmu&5QG`J^4W9gU#a24K41W?~?4T%-hFq53|@9Q@SkJ;&t4M^{-^#zLJ5M zwchh$`tp~U)IarvOjuMuda`!?YJa!!YydM@PJwY>b8rc;1(*SV0f0wI_(j-1tcl5h zY`U(@n((nPgLy-4Xmfomj9xNAIk15F`(3X_%cxtuo^DiwGG;^o9nUNlMP#VF1|@Qa zhW1RX;A~`;5X80GkG?bhov;5p3+ox)*X{?Bg0Xb3m&S2+WmU zJs?zvA!fua8JOJ}!G=a+ZZcod$B_!qaIgiC=mvWu<9fB{=pq=bsj{9ob4(Z0(g?1( z01Xc}{?vNB99=o&hhOdd#XhKrJr%^D2>{@{fjsN@?(5BRmP3W|0-g3tlLdVC=VQf) zzTYfIfb-ihgK$~B*Hq!prxw32DAnJoB)MiLx%~Pg zDhc_J#v`xaJ)0`Nijj+U_@Fv3D0;}^B;~p*(DYW_1~zjPIOycB`fEj{{^9_MuChWk zhZ}58l)3sS%M)e42|=eKq!c@Ne6#N2lJCfB>mjLqBx$)6E*)9qrIah6x^QcFOjo@# z{&i~~<*fKK?buXqQxIHFTfw2_R)d%L%fCODtFcu$Q#E*ko4c;(rgeFbd!v&;H=3V) z*R4APJ(Lp0zVenb5wbe?BNq1x3K5bpS8y_nN=L!vZ+Vm?n+;!L7`K#T2nw-OtW-tP z_`B--n5gls4cy2U+jni+mSVse29V7oSQ znrtZtKUg@f;rZwyzfO7N`N_!5%FZU9Fnz|Y(AXR}em915o{zJL%qKu*LG7%U*1v8% zB^uroe*4LvW}Q1m{XJ9p+}oEWZxC9Tn(*PzuIBWe=B^5WN8Xx1krGeib z)Qywf5)G1`x^}<0A574ipQFPMgqi;i8!ediC#c%#Uz_OUNVD0sSA10$6{j{QSVJ~+ z_LHNq2yUyC&yAu*3pC2Lo6YC_(Ys8W94;(_zHjRi1EQOxf#rw28NH$p0u1!;8ClUQRbKHZ7t$6gDnLUP%@e z=6SArcIM?gqThg@)4j(y#p?t63yxyRH}T!cp(wn3uzYWU%qlleMjB_SQ#O2bigYa{ zUKSzn`Ny7a3xe-CArh&(dn7n|LDo}{^? zMXDE8X-Jdm4oKvCZE&=ddS}SeGvVa58TGdIvp!2E!AVMeSKcX`;qO(AJ-(ZGdp4tB z`*N{upu@pY0H?5|4 z^oN+vTeE&PdwI4eR0jEg&sj7Z7Os5geNU6Vk=C}#0n-#<*lr&^UO1P@yrQ%B6jVME z+nfQv16j3Z9oZ5mL0XN7G#eaLXc=&k-x0|3@+GFNjHlAi@2rANE1NL8B11T>>~<4r zu38DvZMPs28b8{%K+Zz+r6?>D5`9gKH z+ZblNQrc)TsD6I$@={7#I+If}4cu@nQKTCr>A+W)yW8tZnT^Z_uCZA%k|>EP-pn4C zXqfUtOV80>7NJrlY3LY->qJfo*F!`xFKT8@DT z3chB6g$odXkb502PS{dg;gVKZpKO|Qy6|P^gy++5eZlkEMp_ZvUXc+g>204#wesNr zn#ImhfhJz{IC0MkgzxiKhB;QN0hNnonD9LB4%7A9MIz8k-5`7jhzy$2OCq$} z56ZGIHVI@z6cne*3Y(X09b`2_jH7be8=80>!5MKC7A?GfY+Nt}>;aT?tkg&A=Z({O zQ#k2Ab2BorB&N0RG#P|5nET z;`g)X;le)5Z6lWmPP_A6()Ia&LN|^%a~|LLWK1uCOcQRIc_m0*?jNgSlUlY7ewgUe zjv5;fRAiLp6sFKONB7*H#XQ$2QW;HDMJ3uQUwv3GU%8bsz)@9>L#^a!h?WmM-m8cY zYkL{|t1!!9Ew{h9+_M$+{>QBl`+7&()VyhDAnHD*u#-4k$u+*K6{vRVP>4wI4G1H< zG@_{aS#>wJhI`f_)YApcZ7CHRZ=TZ2^EuG5JO3PY&XURJ5Bm9ygWN{whF*s0q3&?je3B2v*s6i_Zj%$H6z{F~_IVAx@E|8k5SML)j7?@P zGL>)T+<5=XF2J6a+GSck7>64uk$;zdlbdKdJB_Ir`x4RpHNYS@WuLRHLCW#?#gMHw z(}QeDT&Ogf^U-=J<=p36Rm*}fjHYkp&i5!HCKhBtF}TrO%tH^Vq}z=r9Qo`s>r~Jc z1ieEHY*0$|UmH3|OH27-PXjk)FcUdii(Aq7w`^|5C0hBGnJz=ILffwr4m5ZatO>P` z?1wqF?c%E6I$a+)%^oeX9)E^bX{?7MXe_hkay+4KAcRz85l7A=f1(nG=Rj$4WT~TB zxRpcU(pN)~GV?IHN6pr^ST$`V<(}NH(188yRi*S&drJiIPkwxsCju2k@&2}Dw((;_ zz$s7=1N*=>sT3k-5~jW%FBag~oj8PW#}8hGKBURI+_&)A#=ATTf&CM#culctwszTI z1a+{zBqXFDbzx(vEzZ!W3S3H_~_XdTYSXV(BqVqHKysd8E}3R z<(4~2H^`~}ic){xkV95@Ek-~%Gc)?`2mRVzQX^_5MbYtFD^@J&DQZ7w;GosCzm2^r zyLY_QAHw^$Da{=BA$08X@P+S#O*uvwf_L-O+SbmDEy%j`q90M&jdPaClP{DzcVgW0 z(7RCRD1f!49BO-+8gkY4>dyCO~8NEx?OfW)7bkRFeBf1bH5ffcRkBI2K zL>uMP2G@6=@54Rox6axR=j@05aL$`2x@r_)W-tH%6dLNv2mk=xNWh5X-_ATts09FI zVj9W{hCzRJtb$DqOz7M7Uh(eF=dvG7*kQjNTv zZzbV|nwFIkpHz#yLXlDB6RFz{bnbM{03iqXt?+9Sbu_2o3q60jYS>{~vgEXejfO}&r`t4J<@k#>)<^g6BW zcq8f{47O!kF)qs3UteuE9k65UQx(F_WyzfRGYdoz*|Jk(?%2WE2~2x|NJ+wt$71p~ zn&jtC3<)D6`mccwK^*A!>-?RodVz-DC?-e?sP5^ljcmj15zYXpr1}CFtH}VQWrk#O z+nj%7IWpj`0~CIA@xmaerns*apZ9oKZTSFDNkg-EmiC!a;Z+j|`s1qlZ@MWVyD?8fI10~V!!jvM-|u4W>X^2(`39)-9f5u3{BCM=1_rR z{N2jj1ZJc$(*7oJn>yr>9RVG8S^|Xl4+#UI>B{cc3#Vaau zew4&y20#Un*~pK&`q7EC`fqAl{l-%-jYrF4k45Ot*@1#P>5?xp`!F20)3T*JHuky2 z7v7)BGhtL{cpYNylnbNzD#{|)hQ#B{4M__793k@E3HFezwan_yxWzG$Jma`dh8J_= zJb6fKrYsQ&lANXc^}|(8QUDeKdeKtnn}~QDrAKG?*rjBnKgbbO@ms$}hy2-(UpTt4 z)6}gpEeV||Z@LgV=F5U2Hns(UI%D!oX*{o!E7$9LcmCN7IKd6(GAOPLiS#1U3~5Lv zwC&zL^LKld%?j4OF&XV~I^U}Kug5Wb$T5Sq*R%)imt8KkQY)(o{~Uxp^YErtl5XCy5$>qV+4#qMXi(2If#L4pYK z#0&#c;%vY9O{|`%3hLT4FX(M8vzhxFkm0tl?nCIBp?NfBtLv@TN)HLx#w>$9?!>~V zzzgQyA%7mn8AI|xL|e-NBJcZEDM8G;AibDvX-58WDWfVu6KOj?x#5R6_z$~G16S6j zobpq|FTW9^yFS8{VX5a;9KaOn=>}1kUDkIPAd84$>U4yrFqKJW8))%F)6~vYtdy<; zyaA~xePX>HtVW(f8bF?(QKpW5R8g%S2+L>hclpoRnH&#OlFg$|Z$f{SVA;jlZ{CDv z;7tPIVK4+8pA$hF8p^?x;rY>1*q`i*AD=e5qX2KeYbeM2iVw?IV6~Q$;&l-nQE72= zNMH+KfD|D3e;J`FdT!JASHo3-0C$_a$XtGYJy|>C9&daU(THf7e_#9 z^A~D93gLmIyyV?n@Zgl!c(e(xcHGUFv}82-;kl+J3qtLbAPF5zu#r<6gz zb6azBc{?H051Qu2RVKsRC)K`OXnmQEoeZb4`3bPe`C)4TI+-p9mD`)C!?z3ZBL365 z$Q0t2j7}!5Y4`UWd`S+l5|Y?SC!{{symq^wmy8IG zwP3+NOm6Gj9ll5n-a3dmvJ^6C^`QhbQG&$wXLl-* z!z5Y?2bg0yy?r!#U-mJLFy_`qJ>a&A2<&Whzx8DLUC1K7F%=lQ=Mk%mX`mk3=CiMx zrNZKxdzMMHsZ>@8OSCdgPhC#znSsT5r zsRQ+m5LO-cf1Ii0>hZEcb zUy9SwplDCWETgvfQF-^RI~SgNlRo*()m8B0%kez!xz*ab1{lOn+S#e_@8E?>zO1Ja z0n1Dxo?|EJFRmxIrG#~8G4vZPWooy4u`&W7EwTOM^E_U0D2uoax_FzFRSxB^FhZYN7c;d1Ki={sCz+c&=ZzRx^&dsap zxkz_U-3iutaiqCDk%xJi_U!cP%W|N*fcr}|2czL>IrlyOfteH&3#&x2U==v+tiPy* zfO$jT1YBg6t1CEdVG(!s`J}q({DoWRJ9V-G{M^hDDFcNrg@oBjywz#<+csiLxAw;O zw5trUEp&kaP0iZmIg5*rR)#R~9hWvc78qZOeSYnVmXmu<1Ia=mgVU-S!z5NMg4Tg; zDM5G3KPh*~ynvVlZU-!wniQnRG)g<1l}V(Nc=vpLwuVjVC&Mxh8V-Tkz`MeImLm2r7xI51ev62JY2OV)Do=V87WS9ge?@5KIeo~A)#S6 zqO%yGcGs%6g?NA#g$=UY)!hp%_zJ*`; zyd1*?KM)}2m`+{ud-)V*F#558;gXKz!`%l$*TMA}5BVlUQK$fXYjNnHuj{vzP{}=U z5!4Y2*D|H`{EXq4WyX~&8MpIdVNSpRwvP`x+ZKoV;bS&_0O8=0to>@72)HQ?6Ht0Koqu;0gP^ z!7vUP0HD)YGZV+i$u$pw08w+%ySP0yV}zZn?h{AMpFIAZR=X*#ema$;zmmNurZAO1 z++gj1b+&q-5Tpf{=U3$nr)y4uQqMTw9Sul)&PtVV!<|wuT~*1KJsHtJB1AX#udYw8 z1=T)73dS^U;Pj2(-kkZ=AIrHu|LSKCV>lF3AfA?V$BPJOpfF(hEC5!2RX#kyKbT0f zm1GAMb-EWOKs3ZZAnm~K-LsJ0=xCt6eSxF-MUJp)%sK-oZ7P6z?<<@?clhG&KVN2x zi<_Vt3OsQKBdelqadzLjp>o@^Dva38u395&&b1rDhmmldZz!+Wm+bD1xez0zc;5Ei zqqH-Y;DQWL+My()&qPyLn30r~{Ko{w1|FNM58o8^vN$aM=Wxe3_S1mey4i6r>*K-K zh-*n7*Rf+F`a@g=q5Se;8NGh)(($;1Xnv)veC3NC3jL^v zpG;2SlzmnbE_Ln&&&Fv<$}A|mgM&rL7_b?nicbPrnmMx^=PNc068-2 z^pTK!g|apSCy>yW29QOzXRq`qipOXDlkQ4XagZw_-s1C-qI0USR9kXCWE z>c4bqt0;G2^$dbVP_+rAz5@s9TU=dmeSv~13ztDlYamC18aGk9XZ z)zSo60e~9F!1N!4yVhQO&ClMIVTRKm&wddQUzaqbC)_smoTuJyU>+*%lLFPf z@>TV_AuL3Tfpg|g6SV2+W+gOG*)aRk+(;pNkrwLWFmSo785U#eTQ9XIaq^=s$>Ev! z*gA+Q^GGppq+zfi?;SmpZisn*h;_N6T9?^Q66UQMPROk8zf{_s9^#$Y=>~_hGHfWE zAgrwr@o~L)1U1RI zKLetSTniA=P$Bte$^r?GFc4uN;kVj?R>1#<@t;6ET?Tx-f-nHtwSKjr*>%mFdwtku zJNmO7R*ai<^=UTwd9sbvFI=m`vITg{cC)O>kC3SmxXs&!NLi8EuX)BxG`r~7(;)4) ze~_kSOL#*xO`2L&N=&yvp1P6Cl6#h;mArEcb!$;?OPottfyp$~O_c&+XCoy>3|Gu188N%1+x(Z`WEy z=&SviA#@!5EIli&gn(c{5XG3|gP8_^c+r@54TUv zADjKj(Z{Pt#W3%+8N~&8?@{OaE>C}^w(U<3MXBrY5BINKTN|IN9ze842Mkg;ZLK$) za2ESa?P`=R%2r}~YqPK<;PgO9tJACP)h9Td)_DI-rgX#i