Version 295
This commit is contained in:
parent
eccc185fcf
commit
4a7f62798c
|
@ -48,6 +48,11 @@ try:
|
|||
|
||||
db_dir = HC.DEFAULT_DB_DIR
|
||||
|
||||
if not HydrusPaths.DirectoryIsWritable( db_dir ):
|
||||
|
||||
db_dir = os.path.join( os.path.expanduser( '~' ), 'Hydrus' )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
db_dir = result.db_dir
|
||||
|
|
|
@ -48,6 +48,11 @@ try:
|
|||
|
||||
db_dir = HC.DEFAULT_DB_DIR
|
||||
|
||||
if not HydrusPaths.DirectoryIsWritable( db_dir ):
|
||||
|
||||
db_dir = os.path.join( os.path.expanduser( '~' ), 'Hydrus' )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
db_dir = result.db_dir
|
||||
|
|
|
@ -8,6 +8,42 @@
|
|||
<div class="content">
|
||||
<h3>changelog</h3>
|
||||
<ul>
|
||||
<li><h3>version 295</h3></li>
|
||||
<ul>
|
||||
<li>fixed the runtimeerror popups that would come up on restore from minimise or main gui move after the complete destruction of a general search page</li>
|
||||
<li>cleaned up some main gui move code generally, and removed a memory leak on the way</li>
|
||||
<li>file queries can now cancel at multiple checkpoints during the first phase, saving a bunch of CPU time on certain large queries that are replaced mid-search</li>
|
||||
<li>after a file query has been going three seconds, a little 'stop' button will appear beside the regular autocomplete input. clicking this will cancel the current query! it will stop when it next hits one of the checkpoints above</li>
|
||||
<li>the floating autocomplete dropdown should be less flickery in some circumstances</li>
|
||||
<li>dejanked some more file query code</li>
|
||||
<li>added a 'clear orphan file records' entry to the database->maintain menu. this looks for and purges orphan file rows as you may have seen a notification about recently. this mostly affects the duplicate filter system</li>
|
||||
<li>fixed up the delete file code to be a bit more robust--it should lead to fewer orphans in future</li>
|
||||
<li>all the parsing edit panels have new layout: they no longer have info panels but instead a help button that points to the html help, and the edit and test panels are now beside each other rather than in notebook pages</li>
|
||||
<li>harmonised a bunch of the parser ui test panel code, refactored how the results are stored</li>
|
||||
<li>the test panel now presents a better 'preview' of what it contains (the actual text control has like 64KB text limit on some OSes and has unreliable text encoding rules, so using it as the raw container for the example data has lead to problems), and we now read and write the example data with a couple of new copy to/paste from clipboard buttons</li>
|
||||
<li>wrote another new test panel for subsidiary page parsers that does the separation formula stuff a bit better. the test results now come back for all posts as well, rather than just the first</li>
|
||||
<li>added a new 'deeply_nested_dialog' frame key to options->gui for the parsing ui to better lay out five or six nested dialogs in a nice 'topleft' way</li>
|
||||
<li>the 'topleft' frame padding is reduced from 50 to 24 pixels to better fit in deeply nested dialogs</li>
|
||||
<li>misc parsing ui improvements and little fixes</li>
|
||||
<li>the manage url classes and manager parsers dialogs now have a better 'add defaults' button that allows you to just select the defaults you want (by name) from a checklistbox</li>
|
||||
<li>wrote a parser for 420chan and added it to the defaults. it should automatically add and link up when you update</li>
|
||||
<li>if the install_dir/db directory is not writable-to (e.g. you have installed the program to a protected location like "C:\Program Files"), the client and server will default to ~/Hydrus as the db directory</li>
|
||||
<li>wrote a new 'TagSummaryGenerator' class that will do 'bunch of tags'->'nice summary string' conversions for the thumbnail banners and export filenames</li>
|
||||
<li>substituted some static tagsummarygenerators to do thumbnail banners</li>
|
||||
<li>did the same for the media viewer top-center namespace summary</li>
|
||||
<li>started some edit ui for tagsummarygenerators--I'll have some proper customisable stuff in the near future</li>
|
||||
<li>moved the background memory maintenance and misc daemons to the new job scheduler, reducing thread count and idle CPU some more</li>
|
||||
<li>added a debug 'show scheduled jobs' entry for the new job scheduler system</li>
|
||||
<li>decompression bomb failures will no longer count towards a subscription's fail count, so having a bunch of them won't abandon a sync</li>
|
||||
<li>fixed and otherwise improved a potential crash condition when a thumbnail panel closes while a menu is popup'd on it</li>
|
||||
<li>to forestall this program instability, the thumbnail window will no longer replace while its menu is open. the behaviour after this delayed window delivery is slightly borked, but it isn't a crash so I'm ok with it for now</li>
|
||||
<li>removed some other jank from the thumbnail media panel swap code</li>
|
||||
<li>non-cancellable modal popups will no longer have the 'close' button. trying to close them with the dialog's X button will still give the 'sorry lad, can't cancel' error</li>
|
||||
<li>rating and file service system predicates for services that no longer exist will now render a neat 'unknown x system predicate' presentation string rather than throwing an error</li>
|
||||
<li>searches in 'all known tags'/'specific tag domain' no longer provide system:untagged, wew</li>
|
||||
<li>some delayed events are now posted in a more thread-safe way</li>
|
||||
<li>misc refactoring</li>
|
||||
</ul>
|
||||
<li><h3>version 294</h3></li>
|
||||
<ul>
|
||||
<li>fixed video scan</li>
|
||||
|
@ -30,7 +66,7 @@
|
|||
<li>if the client fails to initialise the db, it will now try to present the error in a bit of screenshottable-gui before the program quits</li>
|
||||
<li>improved thread watcher error handling when the given url is unwatchable</li>
|
||||
<li>lots of timer related cleanup and tiny fixes</li>
|
||||
<li>mix fixes</li>
|
||||
<li>misc fixes</li>
|
||||
</ul>
|
||||
<li><h3>version 293</h3></li>
|
||||
<ul>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<p><a href="downloader_downloaders.html"><---- Back to Downloaders</a></p>
|
||||
<p><a href="downloader_searches.html"><---- Back to Downloaders</a></p>
|
||||
<h3>putting it all together</h3>
|
||||
<p class="right"><a href="downloader_sharing.html">Now let's share our downloaders ----></a></p>
|
||||
</div>
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
</ul>
|
||||
<p>So we have three components:</p>
|
||||
<ul>
|
||||
<li><b>Downloader:</b> faces the user and converts text input into a series of Gallery URLs.</li>
|
||||
<li><b>Search:</b> faces the user and converts text input into a series of Gallery URLs.</li>
|
||||
<li><b>URL Class:</b> identifies URLs and informs the client how to deal with them.</li>
|
||||
<li><b>Parser:</b> converts data from URLs into hydrus-understandable metadata.</li>
|
||||
</ul>
|
||||
<p>Thread watchers and single page downloaders do not need the 'Downloader' component, as the input in this case <i>is</i> a URL. You drop an imageboard thread URL on the client and it automatically recognises what it is, launches a thread watcher page for it, and finds the correct parser for the output.</p>
|
||||
<p>Thread watchers and single page downloaders do not need the 'Search' component, as the input in this case <i>is</i> a URL. You drop an imageboard thread URL on the client and it automatically recognises what it is, launches a thread watcher page for it, and finds the correct parser for the output.</p>
|
||||
<p class="right"><a href="downloader_url_classes.html">Let's learn about URL Classes ----></a></p>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
<p>There are three main components in the parsing system:</p>
|
||||
<ul>
|
||||
<li><b>Formulae:</b> Take parsable data, search it in some manner, and return 0 to n strings.</li>
|
||||
<li><b>Content Parser:</b> Take parsable data, apply a formula to it to get some strings, and apply a single metadata 'type' and perhaps some additional modifiers.</li>
|
||||
<li><b>Page Parser:</b> Take parsable data, apply content parsers to it, and return all the metadata.</li>
|
||||
<li><b>Content Parsers:</b> Take parsable data, apply a formula to it to get some strings, and apply a single metadata 'type' and perhaps some additional modifiers.</li>
|
||||
<li><b>Page Parsers:</b> Take parsable data, apply content parsers to it, and return all the metadata.</li>
|
||||
</ul>
|
||||
<p>Formulae do the grunt work of parsing and string conversion, content parsers turn the strings into something richer, and page parsers are the containers.</li>
|
||||
<h3>formulae</h3>
|
||||
<h3 id="formulae">formulae</h3>
|
||||
<p>A formula takes some data and returns some strings. The different kinds are:</p>
|
||||
<ul>
|
||||
<li><h3>html</h3></li>
|
||||
<li><h3 id="html_formula">html</h3></li>
|
||||
<li>This takes HTML or a sample of HTML--and any regular sort of XML <i>should</i> also work, it is not at all strict--searches for nodes with certain tag names and/or attributes, and then returns those nodes' particular attribute value, string content, or html beneath.</li>
|
||||
<li>The search occurs in steps:</li>
|
||||
<li>(image of a decent formula with several steps)</li>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<li>Once you have narrowed down the right nodes you want, you can decide what to return. So, given a node of:</li>
|
||||
<li><pre><a href="(URL A)" class="thumb">Forest Glade</a></pre></li>
|
||||
<li>Returning the 'href' attribute would return the string "(URL A)", returning the string content would give "Forest Glade", and returning the full html would give "<a href="(URL A)" class="thumb">Forest Glade</a>". This last choice is useful in complicated situations where you want a second, separated layer of parsing, which we will get to later.</li>
|
||||
<li><h3>json</h3></li>
|
||||
<li><h3 id="json_formula">json</h3></li>
|
||||
<li>This takes some JSON and does a similar style of search:</li>
|
||||
<li>(image of edit formula panel)</li>
|
||||
<li>It is a bit simpler than HTML--if the current node is a list (called an 'Array' in JSON), you can fetch every item or the xth item, and if it is a dictionary (called an 'Object' in JSON), you can fetch a particular string entry. Since you can't jump down several layers with attribute lookups or tag names, you have to go down every layer one at a time. In any case, if you have something like this:</li>
|
||||
|
@ -71,11 +71,11 @@
|
|||
<li>Searching for "posts"->1st list item->"com" will give you the OP's comment, <span class="dealwithit">~AS RAW UNPARSED HTML~</span>.</li>
|
||||
<li>The default is to fetch the final nodes' 'data content', which means coercing simple variables into strings. If the current node is a list or dict, no string is returned.</li>
|
||||
<li>But if you like, you can return the json beneath the current node (which, like HTML, includes the current node). This again will come in useful later.</li>
|
||||
<li><h3>compound</h3></li>
|
||||
<li><h3 id="compound_formula">compound</h3></li>
|
||||
<li>If you want to create a string from multiple parsed strings--for instance by appending the 'tim' and the 'ext' in our json example together--you can use a Compound formula. This fetches multiple lists of strings and tries to place them into a single string using \1 \2 \3 regex substitution syntax:</li>
|
||||
<li>(image of the edit panel--use the thread watcher one with complicated gubbins)</li>
|
||||
<li>This is where the magic happens, sometimes, so keep it in mind if you need to do something cleverer than the data you have seems to provide.</li>
|
||||
<li><h3>context variable</h3></li>
|
||||
<li><h3 id="context_variable_formula">context variable</h3></li>
|
||||
<li>desc</li>
|
||||
<li>ui walkthrough</li>
|
||||
<li>misc</li>
|
||||
|
@ -83,11 +83,11 @@
|
|||
<p>talk about string match and string converter</p>
|
||||
<p>how to test</p>
|
||||
<p>It is a great idea to check the html or json you are trying to parse with your browser. Most web browsers have great developer tools that let you walk through the different nodes in a pretty way. The JSON image above is one of the views Firefox provides if you simply enter a JSON URL.</p>
|
||||
<h3>content parser</h3>
|
||||
<h3 id="content_parsers">content parsers</h3>
|
||||
<p>different types and what they mean</p>
|
||||
<p>hash needs conversion to bytes</p>
|
||||
<p>vetos</p>
|
||||
<h3>page parser</h3>
|
||||
<h3 id="page_parsers">page parsers</h3>
|
||||
<p>pre-parsing conversion example for tumblr</p>
|
||||
<p>example urls are helpful</p>
|
||||
<p>mention vetos again</p>
|
||||
|
@ -100,7 +100,7 @@
|
|||
<p>subsidiary page parsers in the example</p>
|
||||
<p>source time and subject->comment fallback fun</p>
|
||||
<p>The context variable bit to fetch the right board for the file url</p>
|
||||
<p class="right"><a href="downloader_downloaders.html">Let's learn about Downloaders ----></a></p>
|
||||
<p class="right"><a href="downloader_searches.html">Let's learn about Searches ----></a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,13 +1,13 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>downloader - downloaders</title>
|
||||
<title>downloader - searches</title>
|
||||
<link href="hydrus.ico" rel="shortcut icon" />
|
||||
<link href="style.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<p><a href="downloader_parsers.html"><---- Back to Parsers</a></p>
|
||||
<h3>downloaders</h3>
|
||||
<h3>searches</h3>
|
||||
<p class="right"><a href="downloader_completion.html">Now let's put it all together ----></a></p>
|
||||
</div>
|
||||
</body>
|
|
@ -6,9 +6,9 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<a name="repositories"><h3>what is a repository?</h3></a>
|
||||
<a id="repositories"><h3>what is a repository?</h3></a>
|
||||
<p>A <i>repository</i> is a service in the hydrus network that stores a certain kind of information—files or tag mappings, for instance—as submitted by users all over the internet. Those users periodically synchronise with the repository so they know everything that it stores. Sometimes, like with tags, this means creating a complete local copy of everything on the repository. Hydrus network clients never send queries to repositories; they perform queries over their local cache of the repository's data, keeping everything confined to the same computer.</p>
|
||||
<a name="tags"><h3>what is a tag?</h3></a>
|
||||
<a id="tags"><h3>what is a tag?</h3></a>
|
||||
<p><a href="https://en.wikipedia.org/wiki/Tag_(metadata)">wiki</a></p>
|
||||
<p>A <i>tag</i> is a small bit of text describing a single property of something. They make searching easy. Good examples are "flower" or "nicolas cage" or "the sopranos" or "2003". By combining several tags together ( e.g. [ 'tiger woods', 'sports illustrated', '2008' ] or [ 'cosplay', 'the legend of zelda' ] ), a huge image collection is reduced to a tiny and easy-to-digest sample.</p>
|
||||
<p>A good word for the connection of a particular tag to a particular file is <i>mapping</i>.</p>
|
||||
|
@ -18,10 +18,10 @@
|
|||
<li>Searches become far easier when case is not matched. And When case does not matter, what point is there in recording it?</li>
|
||||
</ol>
|
||||
<p>Secondly, leading and trailing whitespace is removed, and multiple whitespace is collapsed to a single character. <pre>' yellow dress '</pre> becomes <pre>'yellow dress'</pre></p>
|
||||
<a name="namespaces"><h3>what is a namespace?</h3></a>
|
||||
<a id="namespaces"><h3>what is a namespace?</h3></a>
|
||||
<p>A <i>namespace</i> is a category that in hydrus prefixes a tag. An example is 'person' in the tag 'person:ron paul'--it lets people and software know that 'ron paul' is a name. You can create any namespace you like; just type one or more words and then a colon, and then the next string of text will have that namespace.</p>
|
||||
<p>The hydrus client gives namespaces different colours so you can pick out important tags more easily in a large list, and you can also search by a particular namespace, even creating complicated predicates like 'give all files that do not have any character tags', for instance.</p>
|
||||
<a name="filenames"><h3>why not use filenames and folders?</h3></a>
|
||||
<a id="filenames"><h3>why not use filenames and folders?</h3></a>
|
||||
<p>As a retrieval method, filenames and folders are less and less useful as the number of files increases. Why?</p>
|
||||
<ul>
|
||||
<li>A filename is not unique; did you mean this "04.jpg" or <i>this</i> "04.jpg" in another folder? Perhaps "04 (3).jpg"?</li>
|
||||
|
@ -32,7 +32,7 @@
|
|||
</ul>
|
||||
<p>So, the client tracks files by their <i>hash</i>.</p>
|
||||
<p>Please do not tag your files with their exact original 'filename.jpg' on my public tag repo. <a href="https://www.youtube.com/watch?v=_yYS0ZZdsnA">Shed the concept of filenames as you would chains.</a></p>
|
||||
<a name="external_files"><h3>can the client manage files from their original locations?</h3></a>
|
||||
<a id="external_files"><h3>can the client manage files from their original locations?</h3></a>
|
||||
<p>When the client imports a file, it makes a quickly accessible but human-ugly copy in its internal database, by default under <i>install_dir/db/client_files</i>. When it needs to access that file again, it always knows where it is, and it can be confident it is what it expects it to be. It never accesses the original again.</p>
|
||||
<p>This storage method is not always convenient, particularly for those who are hesitant about converting to using hydrus completely and also do not want to maintain two large copies of their collections. The question comes up--"can hydrus track files from their original locations, without having to copy them into the db?"</p>
|
||||
<p>The technical answer is, "This support could be added," but I have decided not to, mainly because:</p>
|
||||
|
@ -46,22 +46,22 @@
|
|||
</ul>
|
||||
<p>It is not unusual for new users who ask for this feature to find their feelings change after getting more experience with the software. If desired, path text can be preserved as tags using regexes during import, and getting into the swing of searching by metadata rather than navigating folders often shows how very effective the former is over the latter. Most users eventually import most or all of their collection into hydrus permanently, deleting their old folder structure as they go.</p>
|
||||
<p>For this reason, if you are hesitant about doing things the hydrus way, I advise you try running it on a smaller subset of your collection, say 5,000 files, leaving the original copies completely intact. After a month or two, think about how often you used hydrus to look at the files versus navigating through folders. If you barely used the folders, you probably do not need them any more, but if you used them a lot, then hydrus might not be for you, or it might only be for some sorts of files in your collection.</p>
|
||||
<a name="hashes"><h3>what is a hash?</h3></a>
|
||||
<a id="hashes"><h3>what is a hash?</h3></a>
|
||||
<p><a href="https://en.wikipedia.org/wiki/Hash_function">wiki</a></p>
|
||||
<p>Hashes are a subject you usually have to be a software engineer to find interesting. The simple answer is that they are unique names for things. Hashes make excellent identifiers inside software, as you can safely assume that f099b5823f4e36a4bd6562812582f60e49e818cf445902b504b5533c6a5dad94 refers to one particular file and no other. In the client's normal operation, you will never encounter a file's hash. If you want to see a thumbnail bigger, double-click it; the software handles the mathematics.</p>
|
||||
<p><i>For those who </i>are<i> interested: hydrus uses SHA-256, which spits out 32-byte (256-bit) hashes. The software stores the hash densely, as 32 bytes, only encoding it to 64 hex characters when the user views it or copies to clipboard. SHA-256 is not perfect, but it is a great compromise candidate; it is secure for now, it is reasonably fast, it is available for most programming languages, and newer CPUs perform it more efficiently all the time.</i></p>
|
||||
<a name="access_keys"><h3>what is an access key?</h3></a>
|
||||
<a id="access_keys"><h3>what is an access key?</h3></a>
|
||||
<p>The hydrus network's repositories do not use username/password, but instead a single strong identifier-password like this:</p>
|
||||
<p><i>7ce4dbf18f7af8b420ee942bae42030aab344e91dc0e839260fcd71a4c9879e3</i></p>
|
||||
<p>These hex numbers give you access to a particular account on a particular repository, and are often combined like so:</p>
|
||||
<p><i>7ce4dbf18f7af8b420ee942bae42030aab344e91dc0e839260fcd71a4c9879e3@hostname.com:45871</i></p>
|
||||
<p>They are long enough to be impossible to guess, and also randomly generated, so they reveal nothing personally identifying about you. Many people can use the same access key (and hence the same account) on a repository without consequence, although they will have to share any bandwidth limits, and if one person screws around and gets the account banned, everyone will lose access.</p>
|
||||
<p>The access key is the account. Do not give it to anyone you do not want to have access to the account. An administrator will never need it; instead they will want your <i>account key</i>.</p>
|
||||
<a name="account_keys"><h3>what is an account key?</h3></a>
|
||||
<a id="account_keys"><h3>what is an account key?</h3></a>
|
||||
<p>This is another long string of random hexadecimal that <i>identifies</i> your account without giving away access. If you need to identify yourself to a repository administrator (say, to get your account's permissions modified), you will need to tell them your account key. You can copy it to your clipboard in <i>services->review services</i>.</p>
|
||||
<h3>why aren't my swfs showing?</h3>
|
||||
<p>If an Internet Explorer "Navigation Cancelled" page appears whenever you click on a swf thumbnail, try installing Flash Player for Internet Explorer. Just having it installed for Firefox/Opera is not enough; you need the ActiveX component that comes with the specific IE version. Just boot IE and download/run the installer from Adobe's site.</p>
|
||||
<a name="delays"><h3>why can my friend not see what I just uploaded?</h3></a>
|
||||
<a id="delays"><h3>why can my friend not see what I just uploaded?</h3></a>
|
||||
<p>The repositories do not work like conventional search engines; it takes a short but predictable while for changes to propagate to other users.</p>
|
||||
<p>The client's searches only ever happen over its local cache of what is on the repository. Any changes you make will be delayed for others until their next update occurs. At the moment, the update period is 100,000 seconds, which is about 1 day and 4 hours.</p>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<li>Start your client or server. It may take a few minutes to update its database. I will say in the release post if it is likely to take longer.</li>
|
||||
</ul>
|
||||
<p>Unless the update specifically disables or reconfigures something, all your files and tags and settings will be remembered after the update.</p>
|
||||
<h3>backing up</h3>
|
||||
<h3 id="backing_up">backing up</h3>
|
||||
<p>You <i>do</i> backup, right? <i>Right</i>?</p>
|
||||
<p>I run a backup every week so that if my computer blows up or anything else awful happens, I'll at worst have lost a few days' work. Before I did this, I once lost an entire drive with tens of thousands of files, and it sucked. I encourage backups so you might avoid what I felt. ;_;</p>
|
||||
<p>I use <a href="http://www.abstractspoon.com/tdl_resources.html">ToDoList</a> to remind me of my jobs for the day, including backup tasks, and <a href="http://sourceforge.net/projects/freefilesync/">FreeFileSync</a> to actually mirror over to an external usb drive. I recommend both highly.</p>
|
||||
|
|
|
@ -559,6 +559,7 @@ class GlobalBMPs( object ):
|
|||
|
||||
GlobalBMPs.seed_cache = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'seed_cache.png' ) )
|
||||
|
||||
GlobalBMPs.copy = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'copy.png' ) )
|
||||
GlobalBMPs.paste = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'paste.png' ) )
|
||||
|
||||
GlobalBMPs.eight_chan = wx.Bitmap( os.path.join( HC.STATIC_DIR, '8chan.png' ) )
|
||||
|
|
|
@ -682,7 +682,6 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
if not self._no_daemons:
|
||||
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'CheckMouseIdle', ClientDaemons.DAEMONCheckMouseIdle, period = 10 ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'SynchroniseAccounts', ClientDaemons.DAEMONSynchroniseAccounts, ( 'notify_unknown_accounts', ) ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'SaveDirtyObjects', ClientDaemons.DAEMONSaveDirtyObjects, ( 'important_dirt_to_clean', ), period = 30 ) )
|
||||
|
||||
|
@ -696,6 +695,8 @@ class Controller( HydrusController.HydrusController ):
|
|||
self._daemons.append( HydrusThreading.DAEMONBackgroundWorker( self, 'UPnP', ClientDaemons.DAEMONUPnP, ( 'notify_new_upnp_mappings', ), init_wait = 120, pre_call_wait = 6 ) )
|
||||
|
||||
|
||||
self.CallRepeatingWXSafe( self, 10.0, 10.0, self.CheckMouseIdle )
|
||||
|
||||
if self.db.IsFirstStart():
|
||||
|
||||
message = 'Hi, this looks like the first time you have started the hydrus client.'
|
||||
|
@ -873,7 +874,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
self._menu_open = False
|
||||
|
||||
|
||||
ClientGUIMenus.DestroyMenu( menu )
|
||||
ClientGUIMenus.DestroyMenu( window, menu )
|
||||
|
||||
|
||||
def PrepStringForDisplay( self, text ):
|
||||
|
@ -1127,11 +1128,6 @@ class Controller( HydrusController.HydrusController ):
|
|||
HydrusController.HydrusController.ShutdownView( self )
|
||||
|
||||
|
||||
def StartFileQuery( self, page_key, job_key, search_context ):
|
||||
|
||||
self.CallToThread( self.THREADDoFileQuery, page_key, job_key, search_context )
|
||||
|
||||
|
||||
def SystemBusy( self ):
|
||||
|
||||
if HG.force_idle_mode:
|
||||
|
@ -1189,35 +1185,6 @@ class Controller( HydrusController.HydrusController ):
|
|||
return False
|
||||
|
||||
|
||||
def THREADDoFileQuery( self, page_key, job_key, search_context ):
|
||||
|
||||
QUERY_CHUNK_SIZE = 256
|
||||
|
||||
query_hash_ids = self.Read( 'file_query_ids', search_context )
|
||||
|
||||
media_results = []
|
||||
|
||||
for sub_query_hash_ids in HydrusData.SplitListIntoChunks( query_hash_ids, QUERY_CHUNK_SIZE ):
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return
|
||||
|
||||
|
||||
more_media_results = self.Read( 'media_results_from_ids', sub_query_hash_ids )
|
||||
|
||||
media_results.extend( more_media_results )
|
||||
|
||||
self.pub( 'set_num_query_results', page_key, len( media_results ), len( query_hash_ids ) )
|
||||
|
||||
self.WaitUntilViewFree()
|
||||
|
||||
|
||||
search_context.SetComplete()
|
||||
|
||||
self.pub( 'file_query_done', page_key, job_key, media_results )
|
||||
|
||||
|
||||
def THREADBootEverything( self ):
|
||||
|
||||
try:
|
||||
|
@ -1244,8 +1211,9 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
except Exception as e:
|
||||
|
||||
text = 'A serious error occured while trying to start the program. Its traceback will be shown next. It should have also been written to client.log.'
|
||||
text = 'A serious error occured while trying to start the program. The error will be shown next in a window. More information may have been written to client.log.'
|
||||
|
||||
HydrusData.DebugPrint( 'If the db crashed, another error may be written just above ^.' )
|
||||
HydrusData.DebugPrint( text )
|
||||
|
||||
HydrusData.DebugPrint( traceback.format_exc() )
|
||||
|
|
|
@ -2683,6 +2683,86 @@ class DB( HydrusDB.HydrusDB ):
|
|||
self._service_cache = {}
|
||||
|
||||
|
||||
def _ClearOrphanFileRecords( self ):
|
||||
|
||||
job_key = ClientThreading.JobKey( cancellable = True )
|
||||
|
||||
job_key.SetVariable( 'popup_title', 'clear orphan file records' )
|
||||
|
||||
self._controller.pub( 'modal_message', job_key )
|
||||
|
||||
try:
|
||||
|
||||
job_key.SetVariable( 'popup_text_1', 'looking for orphans' )
|
||||
|
||||
local_file_service_ids = self._GetServiceIds( ( HC.LOCAL_FILE_DOMAIN, HC.LOCAL_FILE_TRASH_DOMAIN ) )
|
||||
|
||||
local_hash_ids = self._STS( self._c.execute( 'SELECT hash_id FROM current_files WHERE service_id IN ' + HydrusData.SplayListForDB( local_file_service_ids ) + ';' ) )
|
||||
|
||||
combined_local_file_service_id = self._GetServiceId( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
|
||||
|
||||
combined_local_hash_ids = self._STS( self._c.execute( 'SELECT hash_id FROM current_files WHERE service_id = ?;', ( combined_local_file_service_id, ) ) )
|
||||
|
||||
in_local_not_in_combined = local_hash_ids.difference( combined_local_hash_ids )
|
||||
in_combined_not_in_local = combined_local_hash_ids.difference( local_hash_ids )
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return
|
||||
|
||||
|
||||
job_key.SetVariable( 'popup_text_1', 'deleting orphans' )
|
||||
|
||||
if len( in_local_not_in_combined ) > 0:
|
||||
|
||||
# these files were deleted from the umbrella service without being cleared from a specific file domain
|
||||
# they are most likely deleted from disk
|
||||
# pushing the 'delete combined' call will flush from the local services as well
|
||||
|
||||
self._DeleteFiles( self._combined_file_service_id, in_local_not_in_combined )
|
||||
|
||||
for hash_id in in_local_not_in_combined:
|
||||
|
||||
self._CacheSimilarFilesDeleteFile( hash_id )
|
||||
|
||||
|
||||
HydrusData.ShowText( 'Found and deleted ' + HydrusData.ConvertIntToPrettyString( len( in_local_not_in_combined ) ) + ' local domain orphan file records.' )
|
||||
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return
|
||||
|
||||
|
||||
if len( in_combined_not_in_local ) > 0:
|
||||
|
||||
# these files were deleted from all specific services but not from the combined service
|
||||
# I have only ever seen one example of this and am not sure how it happened
|
||||
# in any case, the same 'delete combined' call will do the job
|
||||
|
||||
self._DeleteFiles( self._combined_file_service_id, in_combined_not_in_local )
|
||||
|
||||
for hash_id in in_combined_not_in_local:
|
||||
|
||||
self._CacheSimilarFilesDeleteFile( hash_id )
|
||||
|
||||
|
||||
HydrusData.ShowText( 'Found and deleted ' + HydrusData.ConvertIntToPrettyString( len( in_combined_not_in_local ) ) + ' combined domain orphan file records.' )
|
||||
|
||||
|
||||
if len( in_local_not_in_combined ) == 0 and len( in_combined_not_in_local ) == 0:
|
||||
|
||||
HydrusData.ShowText( 'No orphan file records found!' )
|
||||
|
||||
|
||||
finally:
|
||||
|
||||
job_key.SetVariable( 'popup_text_1', 'done!' )
|
||||
|
||||
job_key.Finish()
|
||||
|
||||
|
||||
|
||||
def _CreateDB( self ):
|
||||
|
||||
client_files_default = os.path.join( self._db_dir, 'client_files' )
|
||||
|
@ -2878,13 +2958,26 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
def _DeleteFiles( self, service_id, hash_ids ):
|
||||
|
||||
# the gui sometimes gets out of sync and sends a DELETE FROM TRASH call before the SEND TO TRASH call
|
||||
# in this case, let's make sure the local file domains are clear before deleting from the umbrella domain
|
||||
|
||||
if service_id == self._combined_local_file_service_id:
|
||||
|
||||
local_file_service_ids = self._GetServiceIds( ( HC.LOCAL_FILE_DOMAIN, ) )
|
||||
|
||||
for local_file_service_id in local_file_service_ids:
|
||||
|
||||
self._DeleteFiles( local_file_service_id, hash_ids )
|
||||
|
||||
|
||||
self._DeleteFiles( self._trash_service_id, hash_ids )
|
||||
|
||||
|
||||
service = self._GetService( service_id )
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
splayed_hash_ids = HydrusData.SplayListForDB( hash_ids )
|
||||
|
||||
existing_hash_ids = { hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM current_files WHERE service_id = ? AND hash_id IN ' + splayed_hash_ids + ';', ( service_id, ) ) }
|
||||
existing_hash_ids = self._STS( self._SelectFromList( 'SELECT hash_id FROM current_files WHERE service_id = ' + str( service_id ) + ' AND hash_id IN %s;', hash_ids ) )
|
||||
|
||||
service_info_updates = []
|
||||
|
||||
|
@ -3727,7 +3820,7 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
predicates.append( ClientSearch.Predicate( HC.PREDICATE_TYPE_SYSTEM_EVERYTHING, min_current_count = num_everything ) )
|
||||
|
||||
predicates.extend( [ ClientSearch.Predicate( predicate_type, None ) for predicate_type in [ HC.PREDICATE_TYPE_SYSTEM_UNTAGGED, HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS, HC.PREDICATE_TYPE_SYSTEM_LIMIT, HC.PREDICATE_TYPE_SYSTEM_HASH ] ] )
|
||||
predicates.extend( [ ClientSearch.Predicate( predicate_type, None ) for predicate_type in [ HC.PREDICATE_TYPE_SYSTEM_NUM_TAGS, HC.PREDICATE_TYPE_SYSTEM_LIMIT, HC.PREDICATE_TYPE_SYSTEM_HASH ] ] )
|
||||
|
||||
if have_ratings:
|
||||
|
||||
|
@ -3984,7 +4077,12 @@ class DB( HydrusDB.HydrusDB ):
|
|||
return hash_ids
|
||||
|
||||
|
||||
def _GetHashIdsFromQuery( self, search_context ):
|
||||
def _GetHashIdsFromQuery( self, search_context, job_key = None ):
|
||||
|
||||
if job_key is None:
|
||||
|
||||
job_key = ClientThreading.JobKey( cancellable = True )
|
||||
|
||||
|
||||
self._controller.ResetIdleTimer()
|
||||
|
||||
|
@ -4287,6 +4385,11 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
# at this point, query_hash_ids has something in it
|
||||
|
||||
# hide update files
|
||||
|
@ -4319,6 +4422,11 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
query_hash_ids.difference_update( exclude_query_hash_ids )
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
#
|
||||
|
||||
( file_services_to_include_current, file_services_to_include_pending, file_services_to_exclude_current, file_services_to_exclude_pending ) = system_predicates.GetFileServiceInfo()
|
||||
|
@ -4422,6 +4530,11 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
#
|
||||
|
||||
must_be_local = system_predicates.MustBeLocal() or system_predicates.MustBeArchive()
|
||||
|
@ -4540,6 +4653,11 @@ class DB( HydrusDB.HydrusDB ):
|
|||
query_hash_ids.intersection_update( good_tag_count_hash_ids )
|
||||
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
#
|
||||
|
||||
if 'min_tag_as_number' in simple_preds:
|
||||
|
@ -4560,6 +4678,11 @@ class DB( HydrusDB.HydrusDB ):
|
|||
query_hash_ids.intersection_update( good_hash_ids )
|
||||
|
||||
|
||||
if job_key.IsCancelled():
|
||||
|
||||
return set()
|
||||
|
||||
|
||||
#
|
||||
|
||||
limit = system_predicates.GetLimit()
|
||||
|
@ -10274,8 +10397,6 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
try:
|
||||
|
||||
local_hash_ids = set()
|
||||
|
||||
local_file_service_ids = self._GetServiceIds( ( HC.LOCAL_FILE_DOMAIN, HC.LOCAL_FILE_TRASH_DOMAIN ) )
|
||||
|
||||
local_hash_ids = self._STS( self._c.execute( 'SELECT hash_id FROM current_files WHERE service_id IN ' + HydrusData.SplayListForDB( local_file_service_ids ) + ';' ) )
|
||||
|
@ -10411,6 +10532,48 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 294:
|
||||
|
||||
try:
|
||||
|
||||
domain_manager = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
|
||||
|
||||
#
|
||||
|
||||
existing_parsers = domain_manager.GetParsers()
|
||||
|
||||
new_parsers = list( existing_parsers )
|
||||
|
||||
default_parsers = ClientDefaults.GetDefaultParsers()
|
||||
|
||||
interesting_name = '420chan'
|
||||
|
||||
if True not in ( interesting_name in parser.GetName() for parser in existing_parsers ): # if not already in there
|
||||
|
||||
interesting_new_parsers = [ parser for parser in default_parsers if interesting_name in parser.GetName() ] # add it
|
||||
|
||||
new_parsers.extend( interesting_new_parsers )
|
||||
|
||||
|
||||
domain_manager.SetParsers( new_parsers )
|
||||
|
||||
#
|
||||
|
||||
domain_manager.TryToLinkURLMatchesAndParsers()
|
||||
|
||||
#
|
||||
|
||||
self._SetJSONDump( domain_manager )
|
||||
|
||||
except:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
self.pub_initial_message( 'The client was unable to add some new parsing data. The error has been written to the log--hydrus_dev would be interested in this information.' )
|
||||
|
||||
|
||||
|
||||
|
||||
self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )
|
||||
|
||||
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
|
||||
|
@ -10975,6 +11138,7 @@ class DB( HydrusDB.HydrusDB ):
|
|||
if action == 'analyze': result = self._AnalyzeStaleBigTables( *args, **kwargs )
|
||||
elif action == 'associate_repository_update_hashes': result = self._AssociateRepositoryUpdateHashes( *args, **kwargs )
|
||||
elif action == 'backup': result = self._Backup( *args, **kwargs )
|
||||
elif action == 'clear_orphan_file_records': result = self._ClearOrphanFileRecords( *args, **kwargs )
|
||||
elif action == 'content_updates': result = self._ProcessContentUpdates( *args, **kwargs )
|
||||
elif action == 'db_integrity': result = self._CheckDBIntegrity( *args, **kwargs )
|
||||
elif action == 'delete_hydrus_session_key': result = self._DeleteHydrusSessionKey( *args, **kwargs )
|
||||
|
|
|
@ -59,10 +59,6 @@ def DAEMONCheckImportFolders( controller ):
|
|||
|
||||
|
||||
|
||||
def DAEMONCheckMouseIdle( controller ):
|
||||
|
||||
wx.CallAfter( controller.CheckMouseIdle )
|
||||
|
||||
def DAEMONDownloadFiles( controller ):
|
||||
|
||||
hashes = controller.Read( 'downloads' )
|
||||
|
|
|
@ -273,7 +273,7 @@ def ConvertTextToPixels( window, ( char_cols, char_rows ) ):
|
|||
|
||||
dialog_units = ( char_cols * 4, char_rows * 8 )
|
||||
|
||||
return window.ConvertDialogToPixels( dialog_units )
|
||||
return tuple( window.ConvertDialogToPixels( dialog_units ) ) # convert from _Point_ to a tuple that size methods can deal with
|
||||
|
||||
def ConvertTextToPixelWidth( window, char_cols ):
|
||||
|
||||
|
@ -1040,7 +1040,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
# remember size, remember position, last_size, last_pos, default gravity, default position, maximised, fullscreen
|
||||
self._dictionary[ 'frame_locations' ][ 'file_import_status' ] = ( True, True, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'local_import_filename_tagging' ] = ( True, False, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'main_gui' ] = ( True, True, ( 640, 480 ), ( 20, 20 ), ( -1, -1 ), 'topleft', True, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'main_gui' ] = ( True, True, ( 800, 600 ), ( 20, 20 ), ( -1, -1 ), 'topleft', True, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'manage_options_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'manage_subscriptions_dialog' ] = ( True, True, None, None, ( 1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'manage_tags_dialog' ] = ( False, False, None, None, ( -1, 1 ), 'topleft', False, False )
|
||||
|
@ -1048,6 +1048,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
self._dictionary[ 'frame_locations' ][ 'media_viewer' ] = ( True, True, ( 640, 480 ), ( 70, 70 ), ( -1, -1 ), 'topleft', True, True )
|
||||
self._dictionary[ 'frame_locations' ][ 'regular_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'review_services' ] = ( False, True, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
self._dictionary[ 'frame_locations' ][ 'deeply_nested_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -114,6 +114,9 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
self.Bind( wx.EVT_TIMER, self.TIMEREventUIUpdate, id = ID_TIMER_UI_UPDATE )
|
||||
self.Bind( wx.EVT_TIMER, self.TIMEREventAnimationUpdate, id = ID_TIMER_ANIMATION_UPDATE )
|
||||
|
||||
self.Bind( wx.EVT_MOVE, self.EventMove )
|
||||
self._last_move_pub = 0.0
|
||||
|
||||
self._controller.sub( self, 'AddModalMessage', 'modal_message' )
|
||||
self._controller.sub( self, 'DeleteOldClosedPages', 'delete_old_closed_pages' )
|
||||
self._controller.sub( self, 'NewPageImportHDD', 'new_hdd_import' )
|
||||
|
@ -693,7 +696,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
self._controller.pub( 'notify_new_import_folders' )
|
||||
|
||||
|
||||
def _ClearOrphans( self ):
|
||||
def _ClearOrphanFiles( self ):
|
||||
|
||||
text = 'This will iterate through every file in your database\'s file storage, removing any it does not expect to be there. It may take some time.'
|
||||
text += os.linesep * 2
|
||||
|
@ -732,6 +735,21 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
|
||||
|
||||
def _ClearOrphanFileRecords( self ):
|
||||
|
||||
text = 'This will instruct the database to review its file records and delete any orphans. You typically do not ever see these files and they are basically harmless, but they can offset some file counts confusingly. You probably only need to run this if you can\'t process the apparent last handful of duplicate filter pairs or hydrus dev otherwise told you to try it.'
|
||||
text += os.linesep * 2
|
||||
text += 'It will create a popup message while it works and inform you of the number of orphan records found.'
|
||||
|
||||
with ClientGUIDialogs.DialogYesNo( self, text, yes_label = 'do it', no_label = 'forget it' ) as dlg:
|
||||
|
||||
if dlg.ShowModal() == wx.ID_YES:
|
||||
|
||||
self._controller.Write( 'clear_orphan_file_records' )
|
||||
|
||||
|
||||
|
||||
|
||||
def _DebugMakeSomePopups( self ):
|
||||
|
||||
for i in range( 1, 7 ):
|
||||
|
@ -823,6 +841,11 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
HydrusData.DebugPrint( 'garbage printing finished' )
|
||||
|
||||
|
||||
def _DebugShowScheduledJobs( self ):
|
||||
|
||||
self._controller.DebugShowScheduledJobs()
|
||||
|
||||
|
||||
def _DeleteGUISession( self, name ):
|
||||
|
||||
message = 'Delete session "' + name + '"?'
|
||||
|
@ -1299,7 +1322,8 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
ClientGUIMenus.AppendMenuItem( self, submenu, 'vacuum', 'Defrag the database by completely rebuilding it.', self._VacuumDatabase )
|
||||
ClientGUIMenus.AppendMenuItem( self, submenu, 'analyze', 'Optimise slow queries by running statistical analyses on the database.', self._AnalyzeDatabase )
|
||||
ClientGUIMenus.AppendMenuItem( self, submenu, 'clear orphans', 'Clear out surplus files that have found their way into the file structure.', self._ClearOrphans )
|
||||
ClientGUIMenus.AppendMenuItem( self, submenu, 'clear orphan files', 'Clear out surplus files that have found their way into the file structure.', self._ClearOrphanFiles )
|
||||
ClientGUIMenus.AppendMenuItem( self, submenu, 'clear orphan file records', 'Clear out surplus file records that have not been deleted correctly.', self._ClearOrphanFileRecords )
|
||||
|
||||
ClientGUIMenus.AppendMenu( menu, submenu, 'maintain' )
|
||||
|
||||
|
@ -1642,6 +1666,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
ClientGUIMenus.AppendMenuItem( self, debug, 'force a gui layout now', 'Tell the gui to relayout--useful to test some gui bootup layout issues.', self.Layout )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'flush log', 'Command the log to write any buffered contents to hard drive.', HydrusData.DebugPrint, 'Flushing log' )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'print garbage', 'Print some information about the python garbage to the log.', self._DebugPrintGarbage )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'show scheduled jobs', 'Print some information about the currently scheduled jobs log.', self._DebugShowScheduledJobs )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'clear image rendering cache', 'Tell the image rendering system to forget all current images. This will often free up a bunch of memory immediately.', self._controller.ClearCaches )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'clear db service info cache', 'Delete all cached service info like total number of mappings or files, in case it has become desynchronised. Some parts of the gui may be laggy immediately after this as these numbers are recalculated.', self._DeleteServiceInfo )
|
||||
ClientGUIMenus.AppendMenuItem( self, debug, 'load whole db in disk cache', 'Contiguously read as much of the db as will fit into memory. This will massively speed up any subsequent big job.', self._controller.CallToThread, self._controller.Read, 'load_into_disk_cache' )
|
||||
|
@ -3187,7 +3212,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
title = 'important job'
|
||||
|
||||
|
||||
with ClientGUITopLevelWindows.DialogNullipotentVetoable( self, title ) as dlg:
|
||||
hide_close_button = not job_key.IsCancellable()
|
||||
|
||||
with ClientGUITopLevelWindows.DialogNullipotentVetoable( self, title, hide_close_button = hide_close_button ) as dlg:
|
||||
|
||||
panel = ClientGUIPopupMessages.PopupMessageDialogPanel( dlg, job_key )
|
||||
|
||||
|
@ -3326,6 +3353,18 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
self._notebook.EventMenuFromScreenPosition( screen_position )
|
||||
|
||||
|
||||
def EventMove( self, event ):
|
||||
|
||||
if HydrusData.TimeHasPassedFloat( self._last_move_pub + 0.1 ):
|
||||
|
||||
self._controller.pub( 'main_gui_move_event' )
|
||||
|
||||
self._last_move_pub = HydrusData.GetNowPrecise()
|
||||
|
||||
|
||||
event.Skip()
|
||||
|
||||
|
||||
def TIMEREventAnimationUpdate( self, event ):
|
||||
|
||||
for window in list( self._animation_update_windows ):
|
||||
|
@ -3899,7 +3938,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
self._menus[ name ] = ( menu, label, show )
|
||||
|
||||
ClientGUIMenus.DestroyMenu( old_menu )
|
||||
ClientGUIMenus.DestroyMenu( self, old_menu )
|
||||
|
||||
|
||||
self._dirty_menus = set()
|
||||
|
|
|
@ -42,7 +42,9 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
|
||||
# This turned out to be ugly when I added the manage tags frame, so I've set it to if the tlp has a parent, which basically means "not the main gui"
|
||||
|
||||
if tlp.GetParent() is not None or HC.options[ 'always_embed_autocompletes' ]:
|
||||
not_main_gui = tlp.GetParent() is not None
|
||||
|
||||
if not_main_gui or HC.options[ 'always_embed_autocompletes' ]:
|
||||
|
||||
self._float_mode = False
|
||||
|
||||
|
@ -58,6 +60,9 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
self._last_attempted_dropdown_width = 0
|
||||
self._last_attempted_dropdown_position = ( None, None )
|
||||
|
||||
self._last_move_event_started = 0.0
|
||||
self._last_move_event_occurred = 0.0
|
||||
|
||||
if self._float_mode:
|
||||
|
||||
self._text_ctrl.Bind( wx.EVT_SET_FOCUS, self.EventSetFocus )
|
||||
|
@ -128,7 +133,7 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
self.Bind( wx.EVT_MOVE, self.EventMove )
|
||||
self.Bind( wx.EVT_SIZE, self.EventMove )
|
||||
|
||||
tlp.Bind( wx.EVT_MOVE, self.EventMove )
|
||||
HG.client_controller.sub( self, '_ParentMovedOrResized', 'main_gui_move_event' )
|
||||
|
||||
parent = self
|
||||
|
||||
|
@ -197,6 +202,44 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
def _ParentMovedOrResized( self ):
|
||||
|
||||
if self._float_mode:
|
||||
|
||||
if HydrusData.TimeHasPassedFloat( self._last_move_event_occurred + 1.0 ):
|
||||
|
||||
self._last_move_event_started = HydrusData.GetNowFloat()
|
||||
|
||||
|
||||
self._last_move_event_occurred = HydrusData.GetNowFloat()
|
||||
|
||||
# we'll do smoother move updates for a little bit to stop flickeryness, but after that we'll just hide
|
||||
|
||||
NICE_ANIMATION_GRACE_PERIOD = 0.25
|
||||
|
||||
time_to_delay_these_calls = HydrusData.TimeHasPassedFloat( self._last_move_event_started + NICE_ANIMATION_GRACE_PERIOD )
|
||||
|
||||
if time_to_delay_these_calls:
|
||||
|
||||
self._HideDropdown()
|
||||
|
||||
if self._ShouldShow():
|
||||
|
||||
if self._move_hide_job is None:
|
||||
|
||||
self._move_hide_job = HG.client_controller.CallRepeatingWXSafe( self._dropdown_window, 0.25, 0.0, self.DropdownHideShow )
|
||||
|
||||
|
||||
self._move_hide_job.Delay( 0.25 )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self.DropdownHideShow()
|
||||
|
||||
|
||||
|
||||
|
||||
def _ScheduleListRefresh( self, delay ):
|
||||
|
||||
if self._refresh_list_job is not None and delay == 0.0:
|
||||
|
@ -335,9 +378,7 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
|
||||
try:
|
||||
|
||||
should_show = self._ShouldShow()
|
||||
|
||||
if should_show:
|
||||
if self._ShouldShow():
|
||||
|
||||
self._ShowDropdown()
|
||||
|
||||
|
@ -403,7 +444,7 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
new_event = SelectDownEvent( -1 )
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), new_event )
|
||||
wx.QueueEvent( self.GetEventHandler(), new_event )
|
||||
|
||||
elif key in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and self._text_ctrl.GetValue() == '' and len( self._dropdown_list ) == 0:
|
||||
|
||||
|
@ -416,11 +457,11 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
new_event = ShowNextEvent( -1 )
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), new_event )
|
||||
wx.QueueEvent( self.GetEventHandler(), new_event )
|
||||
|
||||
else:
|
||||
|
||||
# Don't say process/postevent here--it duplicates the event processing at higher levels, leading to 2 x F9, for instance
|
||||
# Don't say QueueEvent here--it duplicates the event processing at higher levels, leading to 2 x F9, for instance
|
||||
self._dropdown_list.EventCharHook( event ) # this typically skips the event, letting the text ctrl take it
|
||||
|
||||
|
||||
|
@ -458,7 +499,7 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
new_event = SelectDownEvent( -1 )
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), new_event )
|
||||
wx.QueueEvent( self.GetEventHandler(), new_event )
|
||||
|
||||
else:
|
||||
|
||||
|
@ -486,27 +527,14 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
if event.GetWheelRotation() > 0: command_type = wx.wxEVT_SCROLLWIN_LINEUP
|
||||
else: command_type = wx.wxEVT_SCROLLWIN_LINEDOWN
|
||||
|
||||
wx.PostEvent( self._dropdown_list.GetEventHandler(), wx.ScrollWinEvent( command_type ) )
|
||||
wx.QueueEvent( self._dropdown_list.GetEventHandler(), wx.ScrollWinEvent( command_type ) )
|
||||
|
||||
|
||||
|
||||
|
||||
def EventMove( self, event ):
|
||||
|
||||
if self._float_mode:
|
||||
|
||||
self._HideDropdown()
|
||||
|
||||
if self._ShouldShow():
|
||||
|
||||
if self._move_hide_job is None:
|
||||
|
||||
self._move_hide_job = HG.client_controller.CallRepeatingWXSafe( self._dropdown_window, 0.25, 0.0, self.DropdownHideShow )
|
||||
|
||||
|
||||
self._move_hide_job.Delay( 0.25 )
|
||||
|
||||
|
||||
self._ParentMovedOrResized()
|
||||
|
||||
event.Skip()
|
||||
|
||||
|
@ -550,6 +578,11 @@ class AutoCompleteDropdown( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
def ForceSizeCalcNow( self ):
|
||||
|
||||
self.DropdownHideShow()
|
||||
|
||||
|
||||
class AutoCompleteDropdownTags( AutoCompleteDropdown ):
|
||||
|
||||
def __init__( self, parent, file_service_key, tag_service_key ):
|
||||
|
|
|
@ -1365,14 +1365,14 @@ class ListBook( wx.Panel ):
|
|||
# this tells any parent scrolled panel to update its virtualsize and recalc its scrollbars
|
||||
event = wx.NotifyEvent( wx.wxEVT_SIZE, self.GetId() )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), event )
|
||||
wx.QueueEvent( self.GetEventHandler(), event )
|
||||
|
||||
# now the virtualsize is updated, we now tell any parent resizing frame/dialog that is interested in resizing that now is the time
|
||||
ClientGUITopLevelWindows.PostSizeChangedEvent( self )
|
||||
|
||||
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, -1 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), event )
|
||||
wx.QueueEvent( self.GetEventHandler(), event )
|
||||
|
||||
|
||||
def AddPage( self, display_name, key, page, select = False ):
|
||||
|
@ -1519,7 +1519,7 @@ class ListBook( wx.Panel ):
|
|||
|
||||
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), event )
|
||||
wx.QueueEvent( self.GetEventHandler(), event )
|
||||
|
||||
if event.IsAllowed():
|
||||
|
||||
|
@ -1599,7 +1599,7 @@ class ListBook( wx.Panel ):
|
|||
|
||||
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), event )
|
||||
wx.QueueEvent( self.GetEventHandler(), event )
|
||||
|
||||
if event.IsAllowed():
|
||||
|
||||
|
@ -3151,7 +3151,7 @@ class ThreadToGUIUpdater( object ):
|
|||
return
|
||||
|
||||
|
||||
wx.PostEvent( self._event_handler, DirtyEvent() )
|
||||
wx.QueueEvent( self._event_handler, DirtyEvent() )
|
||||
|
||||
|
||||
wx.CallAfter( wx_code )
|
||||
|
|
|
@ -2087,7 +2087,7 @@ class DialogCheckFromList( Dialog ):
|
|||
|
||||
Dialog.__init__( self, parent, title )
|
||||
|
||||
self._check_list_box = ClientGUICommon.BetterCheckListBox( self )
|
||||
self._check_list_box = ClientGUICommon.BetterCheckListBox( self, style = wx.LB_EXTENDED )
|
||||
|
||||
self._ok = wx.Button( self, id = wx.ID_OK, label = 'ok' )
|
||||
self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
|
||||
|
|
|
@ -100,7 +100,7 @@ class AddEditDeleteListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
@ -130,7 +130,7 @@ class AddEditDeleteListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def AddDatas( self, datas ):
|
||||
|
@ -140,7 +140,7 @@ class AddEditDeleteListBox( wx.Panel ):
|
|||
self._AddData( data )
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def Bind( self, event, handler ):
|
||||
|
@ -291,7 +291,7 @@ class QueueListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def _Down( self ):
|
||||
|
@ -311,7 +311,7 @@ class QueueListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
@ -341,7 +341,7 @@ class QueueListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def _SwapRows( self, index_a, index_b ):
|
||||
|
@ -387,7 +387,7 @@ class QueueListBox( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def AddDatas( self, datas ):
|
||||
|
@ -397,7 +397,7 @@ class QueueListBox( wx.Panel ):
|
|||
self._AddData( data )
|
||||
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def Bind( self, event, handler ):
|
||||
|
@ -563,7 +563,7 @@ class ListBox( wx.ScrolledWindow ):
|
|||
|
||||
self._SetDirty()
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
|
||||
|
||||
|
||||
def _Deselect( self, index ):
|
||||
|
@ -774,7 +774,7 @@ class ListBox( wx.ScrolledWindow ):
|
|||
|
||||
#self.Scroll( -1, y_to_scroll_to )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to ) )
|
||||
|
||||
elif y > ( start_y * y_unit ) + height - self._text_y:
|
||||
|
||||
|
@ -782,7 +782,7 @@ class ListBox( wx.ScrolledWindow ):
|
|||
|
||||
#self.Scroll( -1, y_to_scroll_to + 2 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to + 2 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to + 2 ) )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -952,6 +952,16 @@ class BetterListCtrlPanel( wx.Panel ):
|
|||
self._button_infos = []
|
||||
|
||||
|
||||
def _AddAllDefaults( self, defaults_callable, add_callable ):
|
||||
|
||||
defaults = defaults_callable()
|
||||
|
||||
for default in defaults:
|
||||
|
||||
add_callable( default )
|
||||
|
||||
|
||||
|
||||
def _AddButton( self, button, enabled_only_on_selection = False, enabled_check_func = None ):
|
||||
|
||||
self._buttonbox.Add( button, CC.FLAGS_VCENTER )
|
||||
|
@ -967,6 +977,30 @@ class BetterListCtrlPanel( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
def _AddSomeDefaults( self, defaults_callable, add_callable ):
|
||||
|
||||
defaults = defaults_callable()
|
||||
|
||||
selected = False
|
||||
|
||||
choice_tuples = [ ( default.GetName(), default, selected ) for default in defaults ]
|
||||
|
||||
import ClientGUIDialogs
|
||||
|
||||
with ClientGUIDialogs.DialogCheckFromList( self, 'select the defaults to add', choice_tuples ) as dlg:
|
||||
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
|
||||
defaults_to_add = dlg.GetChecked()
|
||||
|
||||
for default in defaults_to_add:
|
||||
|
||||
add_callable( default )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _Duplicate( self ):
|
||||
|
||||
dupe_data = self._GetExportObject()
|
||||
|
@ -1184,6 +1218,19 @@ class BetterListCtrlPanel( wx.Panel ):
|
|||
self._UpdateButtons()
|
||||
|
||||
|
||||
def AddDefaultsButton( self, defaults_callable, add_callable ):
|
||||
|
||||
import_menu_items = []
|
||||
|
||||
all_call = HydrusData.Call( self._AddAllDefaults, defaults_callable, add_callable )
|
||||
some_call = HydrusData.Call( self._AddSomeDefaults, defaults_callable, add_callable )
|
||||
|
||||
import_menu_items.append( ( 'normal', 'add them all', 'Load all the defaults.', all_call ) )
|
||||
import_menu_items.append( ( 'normal', 'select from a list', 'Load some of the defaults.', some_call ) )
|
||||
|
||||
self.AddMenuButton( 'add defaults', import_menu_items )
|
||||
|
||||
|
||||
def AddImportExportButtons( self, permitted_object_types, import_add_callable ):
|
||||
|
||||
self._permitted_object_types = permitted_object_types
|
||||
|
|
|
@ -303,7 +303,7 @@ def GenerateDumpMultipartFormDataCTAndBody( fields ):
|
|||
|
||||
event = wx.NotifyEvent( CAPTCHA_FETCH_EVENT_TYPE )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), event )
|
||||
wx.QueueEvent( self.GetEventHandler(), event )
|
||||
|
||||
if event.IsAllowed():
|
||||
|
||||
|
@ -1091,7 +1091,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
|
|||
|
||||
panel = ClientGUIMedia.MediaPanelThumbnails( self._page, self._page_key, CC.COMBINED_LOCAL_FILE_SERVICE_KEY, media_results )
|
||||
|
||||
self._controller.pub( 'swap_media_panel', self._page_key, panel )
|
||||
self._page.SwapMediaPanel( panel )
|
||||
|
||||
|
||||
def _UpdateStatus( self ):
|
||||
|
@ -2846,7 +2846,7 @@ class ManagementPanelPetitions( ManagementPanel ):
|
|||
|
||||
panel.Sort( self._page_key, self._sort_by.GetSort() )
|
||||
|
||||
self._controller.pub( 'swap_media_panel', self._page_key, panel )
|
||||
self._page.SwapMediaPanel( panel )
|
||||
|
||||
|
||||
def EventContentDoubleClick( self, event ):
|
||||
|
@ -3044,6 +3044,8 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
self._query_job_key = ClientThreading.JobKey( cancellable = True )
|
||||
|
||||
self._query_job_key.Finish()
|
||||
|
||||
initial_predicates = file_search_context.GetPredicates()
|
||||
|
||||
if self._search_enabled:
|
||||
|
@ -3055,8 +3057,18 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
synchronised = self._management_controller.GetVariable( 'synchronised' )
|
||||
|
||||
self._searchbox = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._search_panel, self._page_key, file_search_context, media_callable = self._page.GetMedia, synchronised = synchronised )
|
||||
|
||||
self._cancel_search_button = ClientGUICommon.BetterBitmapButton( self._search_panel, CC.GlobalBMPs.stop, self._CancelSearch )
|
||||
|
||||
self._cancel_search_button.Hide()
|
||||
|
||||
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
hbox.Add( self._searchbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
hbox.Add( self._cancel_search_button, CC.FLAGS_VCENTER )
|
||||
|
||||
self._search_panel.Add( self._current_predicates_box, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._search_panel.Add( self._searchbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._search_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
@ -3064,7 +3076,10 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
vbox.Add( self._sort_by, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
vbox.Add( self._collect_by, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
if self._search_enabled: vbox.Add( self._search_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
if self._search_enabled:
|
||||
|
||||
vbox.Add( self._search_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
|
||||
self._MakeCurrentSelectionTagsBox( vbox )
|
||||
|
||||
|
@ -3072,51 +3087,15 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
self._controller.sub( self, 'AddMediaResultsFromQuery', 'add_media_results_from_query' )
|
||||
self._controller.sub( self, 'SearchImmediately', 'notify_search_immediately' )
|
||||
self._controller.sub( self, 'ShowQuery', 'file_query_done' )
|
||||
self._controller.sub( self, 'RefreshQuery', 'refresh_query' )
|
||||
self._controller.sub( self, 'ChangeFileServicePubsub', 'change_file_service' )
|
||||
|
||||
|
||||
def _DoQuery( self ):
|
||||
|
||||
self._controller.ResetIdleTimer()
|
||||
def _CancelSearch( self ):
|
||||
|
||||
self._query_job_key.Cancel()
|
||||
|
||||
self._query_job_key = ClientThreading.JobKey()
|
||||
|
||||
if self._management_controller.GetVariable( 'search_enabled' ):
|
||||
|
||||
if self._management_controller.GetVariable( 'synchronised' ):
|
||||
|
||||
file_search_context = self._searchbox.GetFileSearchContext()
|
||||
|
||||
current_predicates = self._current_predicates_box.GetPredicates()
|
||||
|
||||
file_search_context.SetPredicates( current_predicates )
|
||||
|
||||
self._management_controller.SetVariable( 'file_search_context', file_search_context )
|
||||
|
||||
file_service_key = file_search_context.GetFileServiceKey()
|
||||
|
||||
if len( current_predicates ) > 0:
|
||||
|
||||
self._controller.StartFileQuery( self._page_key, self._query_job_key, file_search_context )
|
||||
|
||||
panel = ClientGUIMedia.MediaPanelLoading( self._page, self._page_key, file_service_key )
|
||||
|
||||
else:
|
||||
|
||||
panel = ClientGUIMedia.MediaPanelThumbnails( self._page, self._page_key, file_service_key, [] )
|
||||
|
||||
|
||||
self._controller.pub( 'swap_media_panel', self._page_key, panel )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self._sort_by.BroadcastSort()
|
||||
|
||||
self._UpdateCancelButton()
|
||||
|
||||
|
||||
def _MakeCurrentSelectionTagsBox( self, sizer ):
|
||||
|
@ -3143,6 +3122,88 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
sizer.Add( tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
|
||||
def _RefreshQuery( self ):
|
||||
|
||||
self._controller.ResetIdleTimer()
|
||||
|
||||
self._query_job_key.Cancel()
|
||||
|
||||
if self._management_controller.GetVariable( 'search_enabled' ):
|
||||
|
||||
if self._management_controller.GetVariable( 'synchronised' ):
|
||||
|
||||
file_search_context = self._searchbox.GetFileSearchContext()
|
||||
|
||||
current_predicates = self._current_predicates_box.GetPredicates()
|
||||
|
||||
file_search_context.SetPredicates( current_predicates )
|
||||
|
||||
self._management_controller.SetVariable( 'file_search_context', file_search_context )
|
||||
|
||||
file_service_key = file_search_context.GetFileServiceKey()
|
||||
|
||||
if len( current_predicates ) > 0:
|
||||
|
||||
self._query_job_key = ClientThreading.JobKey()
|
||||
|
||||
self._controller.CallToThread( self.THREADDoQuery, self._controller, self._page_key, self._query_job_key, file_search_context )
|
||||
|
||||
panel = ClientGUIMedia.MediaPanelLoading( self._page, self._page_key, file_service_key )
|
||||
|
||||
else:
|
||||
|
||||
panel = ClientGUIMedia.MediaPanelThumbnails( self._page, self._page_key, file_service_key, [] )
|
||||
|
||||
|
||||
self._page.SwapMediaPanel( panel )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self._sort_by.BroadcastSort()
|
||||
|
||||
|
||||
|
||||
def _UpdateCancelButton( self ):
|
||||
|
||||
if self._search_enabled:
|
||||
|
||||
do_layout = False
|
||||
|
||||
if self._query_job_key.IsDone():
|
||||
|
||||
if self._cancel_search_button.IsShown():
|
||||
|
||||
self._cancel_search_button.Hide()
|
||||
|
||||
do_layout = True
|
||||
|
||||
|
||||
else:
|
||||
|
||||
# don't show it immediately to save on flickeriness on short queries
|
||||
|
||||
WAIT_PERIOD = 3.0
|
||||
|
||||
can_show = HydrusData.TimeHasPassedFloat( self._query_job_key.GetCreationTime() + WAIT_PERIOD )
|
||||
|
||||
if can_show and not self._cancel_search_button.IsShown():
|
||||
|
||||
self._cancel_search_button.Show()
|
||||
|
||||
do_layout = True
|
||||
|
||||
|
||||
|
||||
if do_layout:
|
||||
|
||||
self.Layout()
|
||||
|
||||
self._searchbox.ForceSizeCalcNow()
|
||||
|
||||
|
||||
|
||||
|
||||
def AddMediaResultsFromQuery( self, query_job_key, media_results ):
|
||||
|
||||
if query_job_key == self._query_job_key:
|
||||
|
@ -3182,7 +3243,7 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
if page_key == self._page_key:
|
||||
|
||||
self._DoQuery()
|
||||
self._RefreshQuery()
|
||||
|
||||
|
||||
|
||||
|
@ -3192,7 +3253,7 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
self._management_controller.SetVariable( 'synchronised', value )
|
||||
|
||||
self._DoQuery()
|
||||
self._RefreshQuery()
|
||||
|
||||
|
||||
|
||||
|
@ -3205,11 +3266,9 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
|
||||
|
||||
def ShowQuery( self, page_key, query_job_key, media_results ):
|
||||
def ShowFinishedQuery( self, query_job_key, media_results ):
|
||||
|
||||
if page_key == self._page_key and query_job_key == self._query_job_key:
|
||||
|
||||
current_predicates = self._current_predicates_box.GetPredicates()
|
||||
if query_job_key == self._query_job_key:
|
||||
|
||||
file_service_key = self._management_controller.GetKey( 'file_service' )
|
||||
|
||||
|
@ -3219,7 +3278,7 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
panel.Sort( self._page_key, self._sort_by.GetSort() )
|
||||
|
||||
self._controller.pub( 'swap_media_panel', self._page_key, panel )
|
||||
self._page.SwapMediaPanel( panel )
|
||||
|
||||
|
||||
|
||||
|
@ -3231,8 +3290,59 @@ class ManagementPanelQuery( ManagementPanel ):
|
|||
|
||||
if len( initial_predicates ) > 0 and not file_search_context.IsComplete():
|
||||
|
||||
wx.CallAfter( self._DoQuery )
|
||||
wx.CallAfter( self._RefreshQuery )
|
||||
|
||||
|
||||
|
||||
|
||||
def THREADDoQuery( self, controller, page_key, query_job_key, search_context ):
|
||||
|
||||
def wx_code():
|
||||
|
||||
query_job_key.Finish()
|
||||
|
||||
if not self:
|
||||
|
||||
return
|
||||
|
||||
|
||||
self.ShowFinishedQuery( query_job_key, media_results )
|
||||
|
||||
|
||||
QUERY_CHUNK_SIZE = 256
|
||||
|
||||
query_hash_ids = controller.Read( 'file_query_ids', search_context, query_job_key )
|
||||
|
||||
if query_job_key.IsCancelled():
|
||||
|
||||
return
|
||||
|
||||
|
||||
media_results = []
|
||||
|
||||
for sub_query_hash_ids in HydrusData.SplitListIntoChunks( query_hash_ids, QUERY_CHUNK_SIZE ):
|
||||
|
||||
if query_job_key.IsCancelled():
|
||||
|
||||
return
|
||||
|
||||
|
||||
more_media_results = controller.Read( 'media_results_from_ids', sub_query_hash_ids )
|
||||
|
||||
media_results.extend( more_media_results )
|
||||
|
||||
controller.pub( 'set_num_query_results', page_key, len( media_results ), len( query_hash_ids ) )
|
||||
|
||||
controller.WaitUntilViewFree()
|
||||
|
||||
|
||||
search_context.SetComplete()
|
||||
|
||||
wx.CallAfter( wx_code )
|
||||
|
||||
|
||||
def TIMERPageUpdate( self ):
|
||||
|
||||
self._UpdateCancelButton()
|
||||
|
||||
|
||||
management_panel_types_to_classes[ MANAGEMENT_TYPE_QUERY ] = ManagementPanelQuery
|
||||
|
|
|
@ -2381,7 +2381,7 @@ class MediaPanelThumbnails( MediaPanel ):
|
|||
|
||||
self.Scroll( -1, y_to_scroll_to )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to ) )
|
||||
|
||||
elif y > ( start_y * y_unit ) + height - ( thumbnail_span_height * percent_visible ):
|
||||
|
||||
|
@ -2389,7 +2389,7 @@ class MediaPanelThumbnails( MediaPanel ):
|
|||
|
||||
self.Scroll( -1, y_to_scroll_to + 2 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to + 2 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to + 2 ) )
|
||||
|
||||
|
||||
|
||||
|
@ -3995,80 +3995,31 @@ class Thumbnail( Selectable ):
|
|||
|
||||
wx_bmp.Destroy()
|
||||
|
||||
namespaces = self.GetTagsManager().GetCombinedNamespaces( ( 'creator', 'series', 'title', 'volume', 'chapter', 'page' ) )
|
||||
|
||||
creators = namespaces[ 'creator' ]
|
||||
series = namespaces[ 'series' ]
|
||||
titles = namespaces[ 'title' ]
|
||||
volumes = namespaces[ 'volume' ]
|
||||
chapters = namespaces[ 'chapter' ]
|
||||
pages = namespaces[ 'page' ]
|
||||
|
||||
new_options = HG.client_controller.new_options
|
||||
|
||||
if new_options.GetBoolean( 'show_thumbnail_page' ):
|
||||
|
||||
collections_string = ''
|
||||
namespace_info = []
|
||||
|
||||
if len( volumes ) > 0:
|
||||
|
||||
if len( volumes ) == 1:
|
||||
|
||||
( volume, ) = volumes
|
||||
|
||||
collections_string = 'v' + HydrusData.ToUnicode( volume )
|
||||
|
||||
else:
|
||||
|
||||
volumes_sorted = HydrusTags.SortNumericTags( volumes )
|
||||
|
||||
collections_string_append = 'v' + HydrusData.ToUnicode( volumes_sorted[0] ) + '-' + HydrusData.ToUnicode( volumes_sorted[-1] )
|
||||
|
||||
|
||||
namespace_info.append( ( 'volume', 'v', '-' ) )
|
||||
namespace_info.append( ( 'chapter', 'c', '-' ) )
|
||||
namespace_info.append( ( 'page', 'p', '-' ) )
|
||||
|
||||
if len( chapters ) > 0:
|
||||
|
||||
if len( chapters ) == 1:
|
||||
|
||||
( chapter, ) = chapters
|
||||
|
||||
collections_string_append = 'c' + HydrusData.ToUnicode( chapter )
|
||||
|
||||
else:
|
||||
|
||||
chapters_sorted = HydrusTags.SortNumericTags( chapters )
|
||||
|
||||
collections_string_append = 'c' + HydrusData.ToUnicode( chapters_sorted[0] ) + '-' + HydrusData.ToUnicode( chapters_sorted[-1] )
|
||||
|
||||
|
||||
if len( collections_string ) > 0: collections_string += '-' + collections_string_append
|
||||
else: collections_string = collections_string_append
|
||||
|
||||
tags_summary_generator = ClientTags.TagSummaryGenerator( namespace_info, '-' )
|
||||
|
||||
if len( pages ) > 0:
|
||||
|
||||
if len( pages ) == 1:
|
||||
|
||||
( page, ) = pages
|
||||
|
||||
collections_string_append = 'p' + HydrusData.ToUnicode( page )
|
||||
|
||||
else:
|
||||
|
||||
pages_sorted = HydrusTags.SortNumericTags( pages )
|
||||
|
||||
collections_string_append = 'p' + HydrusData.ToUnicode( pages_sorted[0] ) + '-' + HydrusData.ToUnicode( pages_sorted[-1] )
|
||||
|
||||
|
||||
if len( collections_string ) > 0: collections_string += '-' + collections_string_append
|
||||
else: collections_string = collections_string_append
|
||||
|
||||
tags = self.GetTagsManager().GetCurrent( CC.COMBINED_TAG_SERVICE_KEY )
|
||||
|
||||
if len( collections_string ) > 0:
|
||||
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
||||
|
||||
tags = siblings_manager.CollapseTags( CC.COMBINED_TAG_SERVICE_KEY, tags )
|
||||
|
||||
lower_summary = tags_summary_generator.GenerateSummary( tags )
|
||||
|
||||
if len( lower_summary ) > 0:
|
||||
|
||||
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
|
||||
|
||||
( text_x, text_y ) = dc.GetTextExtent( collections_string )
|
||||
( text_x, text_y ) = dc.GetTextExtent( lower_summary )
|
||||
|
||||
top_left_x = width - text_x - CC.THUMBNAIL_BORDER
|
||||
top_left_y = height - text_y - CC.THUMBNAIL_BORDER
|
||||
|
@ -4081,37 +4032,33 @@ class Thumbnail( Selectable ):
|
|||
|
||||
dc.DrawRectangle( top_left_x - 1, top_left_y - 1, text_x + 2, text_y + 2 )
|
||||
|
||||
dc.DrawText( collections_string, top_left_x, top_left_y )
|
||||
dc.DrawText( lower_summary, top_left_x, top_left_y )
|
||||
|
||||
|
||||
|
||||
if new_options.GetBoolean( 'show_thumbnail_title_banner' ):
|
||||
|
||||
namespace_info = []
|
||||
|
||||
namespace_info.append( ( 'creator', '', ', ' ) )
|
||||
namespace_info.append( ( 'series', '', ', ' ) )
|
||||
namespace_info.append( ( 'title', '', ', ' ) )
|
||||
|
||||
tags_summary_generator = ClientTags.TagSummaryGenerator( namespace_info, ' - ' )
|
||||
|
||||
tags = self.GetTagsManager().GetCurrent( CC.COMBINED_TAG_SERVICE_KEY )
|
||||
|
||||
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
||||
|
||||
upper_info_string = ''
|
||||
tags = siblings_manager.CollapseTags( CC.COMBINED_TAG_SERVICE_KEY, tags )
|
||||
|
||||
if len( creators ) > 0:
|
||||
|
||||
upper_info_string = ', '.join( creators )
|
||||
|
||||
if len( series ) > 0 or len( titles ) > 0: upper_info_string += ' - '
|
||||
|
||||
upper_summary = tags_summary_generator.GenerateSummary( tags )
|
||||
|
||||
if len( series ) > 0:
|
||||
|
||||
upper_info_string += ', '.join( series )
|
||||
|
||||
elif len( titles ) > 0:
|
||||
|
||||
upper_info_string += ', '.join( titles )
|
||||
|
||||
|
||||
if len( upper_info_string ) > 0:
|
||||
if len( upper_summary ) > 0:
|
||||
|
||||
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
|
||||
|
||||
( text_x, text_y ) = dc.GetTextExtent( upper_info_string )
|
||||
( text_x, text_y ) = dc.GetTextExtent( upper_summary )
|
||||
|
||||
top_left_x = int( ( width - text_x ) / 2 )
|
||||
top_left_y = CC.THUMBNAIL_BORDER
|
||||
|
@ -4124,7 +4071,7 @@ class Thumbnail( Selectable ):
|
|||
|
||||
dc.DrawRectangle( 0, top_left_y - 1, width, text_y + 2 )
|
||||
|
||||
dc.DrawText( upper_info_string, top_left_x, top_left_y )
|
||||
dc.DrawText( upper_summary, top_left_x, top_left_y )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import collections
|
||||
import HydrusData
|
||||
import HydrusGlobals as HG
|
||||
import os
|
||||
import wx
|
||||
|
||||
menus_to_submenus = collections.defaultdict( set )
|
||||
|
@ -99,6 +100,11 @@ def UnbindMenuItems( menu ):
|
|||
|
||||
for ( menu_item, event_handler ) in menu_item_data:
|
||||
|
||||
if not event_handler: # under some circumstances, this has been deleted before the menu was
|
||||
|
||||
continue
|
||||
|
||||
|
||||
event_handler.Unbind( wx.EVT_MENU, source = menu_item )
|
||||
|
||||
|
||||
|
@ -116,12 +122,21 @@ def UnbindMenuItems( menu ):
|
|||
del menus_to_submenus[ menu ]
|
||||
|
||||
|
||||
def DestroyMenu( menu ):
|
||||
def DestroyMenu( window, menu ):
|
||||
|
||||
UnbindMenuItems( menu )
|
||||
|
||||
menu.is_dead = True
|
||||
|
||||
# if the window we just popupmenu'd on is dead now (i.e. it died while the menu was open), destroying the menu will cause a crash and letting the event continue will cause a crash
|
||||
|
||||
if not window:
|
||||
|
||||
message = 'A window just died before its menu could be safely destroyed! If an exception were not raised here, the program would crash! If you know you did something tricky, please avoid this in future. If you think you did something normal, please let hydrus dev know.'
|
||||
|
||||
raise Exception( message )
|
||||
|
||||
|
||||
menu.Destroy()
|
||||
|
||||
def GetEventCallable( callable, *args, **kwargs ):
|
||||
|
|
|
@ -434,7 +434,6 @@ class Page( wx.SplitterWindow ):
|
|||
|
||||
|
||||
self._controller.sub( self, 'SetPrettyStatus', 'new_page_status' )
|
||||
self._controller.sub( self, 'SwapMediaPanel', 'swap_media_panel' )
|
||||
|
||||
|
||||
def _SetPrettyStatus( self, status ):
|
||||
|
@ -446,6 +445,16 @@ class Page( wx.SplitterWindow ):
|
|||
|
||||
def _SwapMediaPanel( self, new_panel ):
|
||||
|
||||
# if a new media page comes in while its menu is open, we can enter program instability.
|
||||
# so let's just put it off.
|
||||
|
||||
if self._controller.MenuIsOpen():
|
||||
|
||||
self._controller.CallLaterWXSafe( self, 0.5, self._SwapMediaPanel, new_panel )
|
||||
|
||||
return
|
||||
|
||||
|
||||
self._preview_panel.SetMedia( None )
|
||||
|
||||
self._media_panel.ClearPageKey()
|
||||
|
@ -459,6 +468,8 @@ class Page( wx.SplitterWindow ):
|
|||
|
||||
self._media_panel = new_panel
|
||||
|
||||
self._controller.pub( 'refresh_page_name', self._page_key )
|
||||
|
||||
|
||||
def CleanBeforeDestroy( self ):
|
||||
|
||||
|
@ -703,14 +714,9 @@ class Page( wx.SplitterWindow ):
|
|||
|
||||
|
||||
|
||||
def SwapMediaPanel( self, page_key, new_panel ):
|
||||
def SwapMediaPanel( self, new_panel ):
|
||||
|
||||
if page_key == self._page_key:
|
||||
|
||||
self._SwapMediaPanel( new_panel )
|
||||
|
||||
self._controller.pub( 'refresh_page_name', self._page_key )
|
||||
|
||||
self._SwapMediaPanel( new_panel )
|
||||
|
||||
|
||||
def TestAbleToClose( self ):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2966,6 +2966,99 @@ class EditTagImportOptions( ClientGUIScrolledPanels.EditPanel ):
|
|||
return tag_import_options
|
||||
|
||||
|
||||
class EditTagSummaryGeneratorPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, tag_summary_generator ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
|
||||
|
||||
namespace_info_panel = ClientGUIListCtrl.BetterListCtrlPanel( edit_panel )
|
||||
|
||||
# add in a way to maintain order
|
||||
# I guess an enumerated column and some up/down buttons on the panel
|
||||
|
||||
self._namespace_info = ClientGUIListCtrl.BetterListCtrl( namespace_info_panel, 'tag_summary_generator_namespace_info', 8, 24, [ ( 'namespace', -1 ), ( 'prefix', 8 ), ( 'separator', 8 ) ], self._ConvertNamespaceInfoToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
|
||||
|
||||
namespace_info_panel.SetListCtrl( self._namespace_info )
|
||||
|
||||
namespace_info_panel.AddButton( 'add', self._Add )
|
||||
namespace_info_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
|
||||
namespace_info_panel.AddButton( 'delete', self._Delete, enabled_only_on_selection = True )
|
||||
|
||||
self._separator = wx.TextCtrl( edit_panel )
|
||||
|
||||
# example panel
|
||||
# multilinelistpanel
|
||||
# readonly result
|
||||
|
||||
#
|
||||
|
||||
( namespace_info, separator ) = tag_summary_generator.ToTuple()
|
||||
|
||||
self._namespace_info.SetData( namespace_info )
|
||||
self._separator.SetValue( separator )
|
||||
|
||||
#
|
||||
|
||||
edit_panel.Add( self._namespace_info, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
edit_panel.Add( ClientGUICommon.WrapInText( self._separator, edit_panel, 'separator' ), CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.Add( edit_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
|
||||
def _Add( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _ConvertNamespaceInfoToListCtrlTuples( self, namespace_info ):
|
||||
|
||||
( namespace, prefix, separator ) = namespace_info
|
||||
|
||||
if namespace == '':
|
||||
|
||||
pretty_namespace = 'unnamespaced'
|
||||
|
||||
else:
|
||||
|
||||
pretty_namespace = namespace
|
||||
|
||||
|
||||
pretty_prefix = prefix
|
||||
pretty_separator = separator
|
||||
|
||||
display_tuple = ( pretty_namespace, pretty_prefix, pretty_separator )
|
||||
sort_tuple = ( namespace, prefix, separator )
|
||||
|
||||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _Delete( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
# order based on the new enumerated column
|
||||
|
||||
namespace_info = []
|
||||
separator = self._separator.GetValue()
|
||||
|
||||
ClientTags.TagSummaryGenerator( namespace_info, separator )
|
||||
|
||||
|
||||
class EditURLMatchPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, url_match ):
|
||||
|
@ -3432,7 +3525,7 @@ class EditURLMatchesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._list_ctrl_panel.AddSeparator()
|
||||
self._list_ctrl_panel.AddImportExportButtons( ( ClientNetworkingDomain.URLMatch, ), self._AddURLMatch )
|
||||
self._list_ctrl_panel.AddSeparator()
|
||||
self._list_ctrl_panel.AddButton( 'add the defaults', self._AddDefaults )
|
||||
self._list_ctrl_panel.AddDefaultsButton( ClientDefaults.GetDefaultURLMatches, self._AddURLMatch )
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -5631,7 +5631,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
def _OKParent( self ):
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ClientGUITopLevelWindows.OKEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ClientGUITopLevelWindows.OKEvent( -1 ) )
|
||||
|
||||
|
||||
def _ProcessApplicationCommand( self, command ):
|
||||
|
@ -6342,7 +6342,7 @@ class ManageTagsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
def OK( self ):
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), ClientGUITopLevelWindows.OKEvent( -1 ) )
|
||||
wx.QueueEvent( self.GetEventHandler(), ClientGUITopLevelWindows.OKEvent( -1 ) )
|
||||
|
||||
|
||||
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
||||
|
@ -6598,72 +6598,6 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
|
||||
|
||||
class ManageURLMatchesPanel( ClientGUIScrolledPanels.ManagePanel ):
|
||||
|
||||
def __init__( self, parent ):
|
||||
|
||||
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
|
||||
|
||||
self._url_matches = ClientGUIListCtrl.BetterListCtrl( self, 'manage_url_matches', 15, 30, [ ( 'name', 20 ), ( 'example url', -1 ) ], self._ConvertURLMatchToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
|
||||
|
||||
# add, edit, delete buttons
|
||||
# it would be nice to wrap this up in a panel rather than writing it out manually
|
||||
|
||||
#
|
||||
|
||||
# load them from the db, populate the listctrl
|
||||
|
||||
self._original_names = set()
|
||||
# set self._original_names
|
||||
|
||||
#
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.Add( self._url_matches, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
|
||||
def _Add( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _ConvertURLMatchToListCtrlTuples( self, url_match ):
|
||||
|
||||
name = url_match.GetName()
|
||||
example_url = url_match.GetExampleURL()
|
||||
|
||||
display_tuple = ( name, example_url )
|
||||
sort_tuple = display_tuple
|
||||
|
||||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _Delete( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def CommitChanges( self ):
|
||||
|
||||
datas = self._url_matches.GetData()
|
||||
|
||||
datas_names = { data.GetName() for data in datas }
|
||||
|
||||
to_delete = [ name for name in self._original_names if name not in datas_names ]
|
||||
|
||||
# save datas to db
|
||||
|
||||
# delete names from db
|
||||
|
||||
|
||||
class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
|
||||
|
||||
def __init__( self, parent, missing_locations ):
|
||||
|
|
|
@ -148,7 +148,7 @@ class TimeDeltaButton( wx.Button ):
|
|||
|
||||
new_event = TimeDeltaEvent( 0 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), new_event )
|
||||
wx.QueueEvent( self.GetEventHandler(), new_event )
|
||||
|
||||
|
||||
|
||||
|
@ -293,7 +293,7 @@ class TimeDeltaCtrl( wx.Panel ):
|
|||
|
||||
new_event = TimeDeltaEvent( 0 )
|
||||
|
||||
wx.PostEvent( self.GetEventHandler(), new_event )
|
||||
wx.QueueEvent( self.GetEventHandler(), new_event )
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
|
|
@ -11,7 +11,7 @@ import wx
|
|||
|
||||
( OKEvent, EVT_OK ) = wx.lib.newevent.NewCommandEvent()
|
||||
|
||||
CHILD_POSITION_PADDING = 50
|
||||
CHILD_POSITION_PADDING = 24
|
||||
FUZZY_PADDING = 15
|
||||
|
||||
def GetDisplayPosition( window ):
|
||||
|
@ -147,7 +147,7 @@ def PostSizeChangedEvent( window ):
|
|||
|
||||
event = CC.SizeChangedEvent( -1 )
|
||||
|
||||
wx.PostEvent( window.GetEventHandler(), event )
|
||||
wx.QueueEvent( window.GetEventHandler(), event )
|
||||
|
||||
def SaveTLWSizeAndPosition( tlw, frame_key ):
|
||||
|
||||
|
@ -530,10 +530,17 @@ class DialogNullipotent( DialogThatTakesScrollablePanelClose ):
|
|||
|
||||
class DialogNullipotentVetoable( DialogThatTakesScrollablePanelClose ):
|
||||
|
||||
def __init__( self, parent, title, style_override = None ):
|
||||
def __init__( self, parent, title, style_override = None, hide_close_button = False ):
|
||||
|
||||
DialogThatTakesScrollablePanelClose.__init__( self, parent, title, style_override = style_override )
|
||||
|
||||
if hide_close_button:
|
||||
|
||||
self._close.Hide()
|
||||
|
||||
self.Bind( wx.EVT_CLOSE, self.EventOK ) # the close event no longer goes to the default button, since it is hidden, wew
|
||||
|
||||
|
||||
|
||||
def DoOK( self ):
|
||||
|
||||
|
|
|
@ -474,7 +474,7 @@ class FileImportJob( object ):
|
|||
|
||||
if HydrusImageHandling.IsDecompressionBomb( self._temp_path ):
|
||||
|
||||
raise HydrusExceptions.SizeException( 'Image seems to be a Decompression Bomb!' )
|
||||
raise HydrusExceptions.DecompressionBombException( 'Image seems to be a Decompression Bomb!' )
|
||||
|
||||
|
||||
|
||||
|
@ -4465,9 +4465,20 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
seed.SetStatus( status, exception = e )
|
||||
|
||||
|
||||
# DataMissing is a quick thing to avoid subscription abandons when lots of deleted files in e621 (or any other booru)
|
||||
# this should be richer in any case in the new system
|
||||
if not isinstance( e, HydrusExceptions.DataMissing ):
|
||||
if isinstance( e, HydrusExceptions.DecompressionBombException ):
|
||||
|
||||
job_key.SetVariable( 'popup_text_2', x_out_of_y + 'file failed: decompression bomb' )
|
||||
|
||||
time.sleep( 1 )
|
||||
|
||||
elif isinstance( e, HydrusExceptions.DataMissing ):
|
||||
|
||||
# DataMissing is a quick thing to avoid subscription abandons when lots of deleted files in e621 (or any other booru)
|
||||
# this should be richer in any case in the new system
|
||||
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
error_count += 1
|
||||
|
||||
|
@ -5540,12 +5551,12 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
parser = HG.client_controller.network_engine.domain_manager.GetParser( url_to_check )
|
||||
|
||||
parse_context = {}
|
||||
parsing_context = {}
|
||||
|
||||
parse_context[ 'thread_url' ] = self._thread_url
|
||||
parse_context[ 'url' ] = url_to_check
|
||||
parsing_context[ 'thread_url' ] = self._thread_url
|
||||
parsing_context[ 'url' ] = url_to_check
|
||||
|
||||
all_parse_results = parser.Parse( parse_context, data )
|
||||
all_parse_results = parser.Parse( parsing_context, data )
|
||||
|
||||
subject = ClientParsing.GetTitleFromAllParseResults( all_parse_results )
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import ClientData
|
|||
import ClientFiles
|
||||
import ClientRatings
|
||||
import ClientSearch
|
||||
import ClientTags
|
||||
import HydrusConstants as HC
|
||||
import HydrusTags
|
||||
import os
|
||||
|
@ -1686,101 +1687,26 @@ class MediaSingleton( Media ):
|
|||
|
||||
def GetTitleString( self ):
|
||||
|
||||
title_string = ''
|
||||
namespace_info = []
|
||||
|
||||
namespace_info.append( ( 'creator', '', ', ' ) )
|
||||
namespace_info.append( ( 'series', '', ', ' ) )
|
||||
namespace_info.append( ( 'title', '', ', ' ) )
|
||||
namespace_info.append( ( 'volume', 'v', '-' ) )
|
||||
namespace_info.append( ( 'chapter', 'c', '-' ) )
|
||||
namespace_info.append( ( 'page', 'p', '-' ) )
|
||||
|
||||
tags_summary_generator = ClientTags.TagSummaryGenerator( namespace_info, ' - ' )
|
||||
|
||||
tags = self.GetTagsManager().GetCurrent( CC.COMBINED_TAG_SERVICE_KEY )
|
||||
|
||||
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
|
||||
|
||||
namespaces = self._media_result.GetTagsManager().GetCombinedNamespaces( ( 'creator', 'series', 'title', 'volume', 'chapter', 'page' ) )
|
||||
tags = siblings_manager.CollapseTags( CC.COMBINED_TAG_SERVICE_KEY, tags )
|
||||
|
||||
creators = namespaces[ 'creator' ]
|
||||
series = namespaces[ 'series' ]
|
||||
titles = namespaces[ 'title' ]
|
||||
volumes = namespaces[ 'volume' ]
|
||||
chapters = namespaces[ 'chapter' ]
|
||||
pages = namespaces[ 'page' ]
|
||||
summary = tags_summary_generator.GenerateSummary( tags )
|
||||
|
||||
if len( creators ) > 0:
|
||||
|
||||
title_string_append = ', '.join( creators )
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
if len( series ) > 0:
|
||||
|
||||
title_string_append = ', '.join( series )
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
if len( titles ) > 0:
|
||||
|
||||
title_string_append = ', '.join( titles )
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
if len( volumes ) > 0:
|
||||
|
||||
if len( volumes ) == 1:
|
||||
|
||||
( volume, ) = volumes
|
||||
|
||||
title_string_append = 'volume ' + HydrusData.ToUnicode( volume )
|
||||
|
||||
else:
|
||||
|
||||
volumes_sorted = HydrusTags.SortNumericTags( volumes )
|
||||
|
||||
title_string_append = 'volumes ' + HydrusData.ToUnicode( volumes_sorted[0] ) + '-' + HydrusData.ToUnicode( volumes_sorted[-1] )
|
||||
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
if len( chapters ) > 0:
|
||||
|
||||
if len( chapters ) == 1:
|
||||
|
||||
( chapter, ) = chapters
|
||||
|
||||
title_string_append = 'chapter ' + HydrusData.ToUnicode( chapter )
|
||||
|
||||
else:
|
||||
|
||||
chapters_sorted = HydrusTags.SortNumericTags( chapters )
|
||||
|
||||
title_string_append = 'chapters ' + HydrusData.ToUnicode( chapters_sorted[0] ) + '-' + HydrusData.ToUnicode( chapters_sorted[-1] )
|
||||
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
if len( pages ) > 0:
|
||||
|
||||
if len( pages ) == 1:
|
||||
|
||||
( page, ) = pages
|
||||
|
||||
title_string_append = 'page ' + HydrusData.ToUnicode( page )
|
||||
|
||||
else:
|
||||
|
||||
pages_sorted = HydrusTags.SortNumericTags( pages )
|
||||
|
||||
title_string_append = 'pages ' + HydrusData.ToUnicode( pages_sorted[0] ) + '-' + HydrusData.ToUnicode( pages_sorted[-1] )
|
||||
|
||||
|
||||
if len( title_string ) > 0: title_string += ' - ' + title_string_append
|
||||
else: title_string = title_string_append
|
||||
|
||||
|
||||
return title_string
|
||||
return summary
|
||||
|
||||
|
||||
def HasAnyOfTheseHashes( self, hashes ):
|
||||
|
|
|
@ -326,6 +326,19 @@ def GetURLsFromParseResults( results, desired_url_types ):
|
|||
|
||||
return url_list
|
||||
|
||||
def MakeParsedTextPretty( parsed_text ):
|
||||
|
||||
try:
|
||||
|
||||
parsed_text = unicode( parsed_text )
|
||||
|
||||
except UnicodeDecodeError:
|
||||
|
||||
parsed_text = repr( parsed_text )
|
||||
|
||||
|
||||
return parsed_text
|
||||
|
||||
def RenderJSONParseRule( parse_rule ):
|
||||
|
||||
if parse_rule is None:
|
||||
|
@ -389,26 +402,31 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
|
|||
self._string_converter = string_converter
|
||||
|
||||
|
||||
def _ParseRawContents( self, parse_context, data ):
|
||||
def _GetParsePrettySeparator( self ):
|
||||
|
||||
return os.linesep
|
||||
|
||||
|
||||
def _ParseRawContents( self, parsing_context, data ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def Parse( self, parse_context, data ):
|
||||
def Parse( self, parsing_context, data ):
|
||||
|
||||
raw_contents = self._ParseRawContents( parse_context, data )
|
||||
raw_texts = self._ParseRawContents( parsing_context, data )
|
||||
|
||||
contents = []
|
||||
texts = []
|
||||
|
||||
for raw_content in raw_contents:
|
||||
for raw_text in raw_texts:
|
||||
|
||||
try:
|
||||
|
||||
self._string_match.Test( raw_content )
|
||||
self._string_match.Test( raw_text )
|
||||
|
||||
content = self._string_converter.Convert( raw_content )
|
||||
text = self._string_converter.Convert( raw_text )
|
||||
|
||||
contents.append( content )
|
||||
texts.append( text )
|
||||
|
||||
except HydrusExceptions.ParseException:
|
||||
|
||||
|
@ -416,7 +434,22 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
return contents
|
||||
return texts
|
||||
|
||||
|
||||
def ParsePretty( self, parsing_context, data ):
|
||||
|
||||
texts = self.Parse( parsing_context, data )
|
||||
|
||||
pretty_texts = [ MakeParsedTextPretty( text ) for text in texts ]
|
||||
|
||||
pretty_texts = [ '*** ' + HydrusData.ConvertIntToPrettyString( len( pretty_texts ) ) + ' RESULTS BEGIN ***' ] + pretty_texts + [ '*** RESULTS END ***' ]
|
||||
|
||||
separator = self._GetParsePrettySeparator()
|
||||
|
||||
result = separator.join( pretty_texts )
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def ParsesSeparatedContent( self ):
|
||||
|
@ -479,7 +512,7 @@ class ParseFormulaCompound( ParseFormula ):
|
|||
self._string_converter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_converter )
|
||||
|
||||
|
||||
def _ParseRawContents( self, parse_context, data ):
|
||||
def _ParseRawContents( self, parsing_context, data ):
|
||||
|
||||
def get_stream_data( index, s ):
|
||||
|
||||
|
@ -501,7 +534,7 @@ class ParseFormulaCompound( ParseFormula ):
|
|||
|
||||
for formula in self._formulae:
|
||||
|
||||
stream = formula.Parse( parse_context, data )
|
||||
stream = formula.Parse( parsing_context, data )
|
||||
|
||||
if len( stream ) == 0: # no contents were found for one of the /1 replace components, so no valid strings can be made.
|
||||
|
||||
|
@ -598,13 +631,13 @@ class ParseFormulaContextVariable( ParseFormula ):
|
|||
self._string_converter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_converter )
|
||||
|
||||
|
||||
def _ParseRawContents( self, parse_context, data ):
|
||||
def _ParseRawContents( self, parsing_context, data ):
|
||||
|
||||
raw_contents = []
|
||||
|
||||
if self._variable_name in parse_context:
|
||||
if self._variable_name in parsing_context:
|
||||
|
||||
raw_contents.append( parse_context[ self._variable_name ] )
|
||||
raw_contents.append( parsing_context[ self._variable_name ] )
|
||||
|
||||
|
||||
return raw_contents
|
||||
|
@ -704,6 +737,18 @@ class ParseFormulaHTML( ParseFormula ):
|
|||
return tags
|
||||
|
||||
|
||||
def _GetParsePrettySeparator( self ):
|
||||
|
||||
if self._content_to_fetch == HTML_CONTENT_HTML:
|
||||
|
||||
return os.linesep * 2
|
||||
|
||||
else:
|
||||
|
||||
return os.linesep
|
||||
|
||||
|
||||
|
||||
def _GetRawContentFromTag( self, tag ):
|
||||
|
||||
if self._content_to_fetch == HTML_CONTENT_ATTRIBUTE:
|
||||
|
@ -797,9 +842,16 @@ class ParseFormulaHTML( ParseFormula ):
|
|||
self._string_converter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_converter )
|
||||
|
||||
|
||||
def _ParseRawContents( self, parse_context, data ):
|
||||
def _ParseRawContents( self, parsing_context, data ):
|
||||
|
||||
root = bs4.BeautifulSoup( data, 'lxml' )
|
||||
try:
|
||||
|
||||
root = bs4.BeautifulSoup( data, 'lxml' )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
raise HydrusExceptions.ParseException( 'Unable to parse that HTML: ' + HydrusData.ToUnicode( e ) )
|
||||
|
||||
|
||||
tags = self._FindHTMLTags( root )
|
||||
|
||||
|
@ -970,6 +1022,18 @@ class ParseFormulaJSON( ParseFormula ):
|
|||
self._content_to_fetch = content_to_fetch
|
||||
|
||||
|
||||
def _GetParsePrettySeparator( self ):
|
||||
|
||||
if self._content_to_fetch == JSON_CONTENT_JSON:
|
||||
|
||||
return os.linesep * 2
|
||||
|
||||
else:
|
||||
|
||||
return os.linesep
|
||||
|
||||
|
||||
|
||||
def _GetRawContentsFromJSON( self, j ):
|
||||
|
||||
roots = ( j, )
|
||||
|
@ -1066,9 +1130,16 @@ class ParseFormulaJSON( ParseFormula ):
|
|||
self._string_converter = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_converter )
|
||||
|
||||
|
||||
def _ParseRawContents( self, parse_context, data ):
|
||||
def _ParseRawContents( self, parsing_context, data ):
|
||||
|
||||
j = json.loads( data )
|
||||
try:
|
||||
|
||||
j = json.loads( data )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
raise HydrusExceptions.ParseException( 'Unable to parse that JSON: ' + HydrusData.ToUnicode( e ) )
|
||||
|
||||
|
||||
raw_contents = self._GetRawContentsFromJSON( j )
|
||||
|
||||
|
@ -1234,9 +1305,9 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
|
|||
return { ( self._name, self._content_type, self._additional_info ) }
|
||||
|
||||
|
||||
def Parse( self, parse_context, data ):
|
||||
def Parse( self, parsing_context, data ):
|
||||
|
||||
parsed_texts = self._formula.Parse( parse_context, data )
|
||||
parsed_texts = self._formula.Parse( parsing_context, data )
|
||||
|
||||
if self._content_type == HC.CONTENT_TYPE_VETO:
|
||||
|
||||
|
@ -1265,6 +1336,30 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def ParsePretty( self, parsing_context, data ):
|
||||
|
||||
try:
|
||||
|
||||
parse_results = self.Parse( parsing_context, data )
|
||||
|
||||
results = [ ConvertParseResultToPrettyString( parse_result ) for parse_result in parse_results ]
|
||||
|
||||
except HydrusExceptions.VetoException as e:
|
||||
|
||||
results = [ 'veto: ' + HydrusData.ToUnicode( e ) ]
|
||||
|
||||
|
||||
result_lines = [ '*** ' + HydrusData.ConvertIntToPrettyString( len( results ) ) + ' RESULTS BEGIN ***' ]
|
||||
|
||||
result_lines.extend( results )
|
||||
|
||||
result_lines.append( '*** RESULTS END ***' )
|
||||
|
||||
results_text = os.linesep.join( result_lines )
|
||||
|
||||
return results_text
|
||||
|
||||
|
||||
def SetName( self, name ):
|
||||
|
||||
self._name = name
|
||||
|
@ -1411,7 +1506,7 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
return self._string_converter
|
||||
|
||||
|
||||
def Parse( self, parse_context, page_data ):
|
||||
def Parse( self, parsing_context, page_data ):
|
||||
|
||||
try:
|
||||
|
||||
|
@ -1430,7 +1525,7 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
|
||||
try:
|
||||
|
||||
whole_page_parse_results.extend( content_parser.Parse( parse_context, converted_page_data ) )
|
||||
whole_page_parse_results.extend( content_parser.Parse( parsing_context, converted_page_data ) )
|
||||
|
||||
except HydrusExceptions.VetoException:
|
||||
|
||||
|
@ -1464,11 +1559,11 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
|
||||
for ( formula, page_parser ) in self._sub_page_parsers:
|
||||
|
||||
posts = formula.Parse( parse_context, converted_page_data )
|
||||
posts = formula.Parse( parsing_context, converted_page_data )
|
||||
|
||||
for post in posts:
|
||||
|
||||
page_parser_all_parse_results = page_parser.Parse( parse_context, post )
|
||||
page_parser_all_parse_results = page_parser.Parse( parsing_context, post )
|
||||
|
||||
for page_parser_parse_results in page_parser_all_parse_results:
|
||||
|
||||
|
@ -1483,6 +1578,36 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
|
|||
return all_parse_results
|
||||
|
||||
|
||||
def ParsePretty( self, parsing_context, page_data ):
|
||||
|
||||
try:
|
||||
|
||||
all_parse_results = self.Parse( parsing_context, page_data )
|
||||
|
||||
pretty_groups_of_parse_results = [ os.linesep.join( [ ConvertParseResultToPrettyString( parse_result ) for parse_result in parse_results ] ) for parse_results in all_parse_results ]
|
||||
|
||||
group_separator = os.linesep * 2 + '*** SEPARATE FILE RESULTS BREAK ***' + os.linesep * 2
|
||||
|
||||
pretty_parse_result_text = group_separator.join( pretty_groups_of_parse_results )
|
||||
|
||||
except HydrusExceptions.VetoException as e:
|
||||
|
||||
pretty_parse_result_text = HydrusData.ToUnicode( e )
|
||||
|
||||
|
||||
result_lines = []
|
||||
|
||||
result_lines.append( '*** ' + HydrusData.ConvertIntToPrettyString( len( all_parse_results ) ) + ' RESULTS BEGIN ***' + os.linesep )
|
||||
|
||||
result_lines.append( pretty_parse_result_text )
|
||||
|
||||
result_lines.append( os.linesep + '*** RESULTS END ***' )
|
||||
|
||||
results_text = os.linesep.join( result_lines )
|
||||
|
||||
return results_text
|
||||
|
||||
|
||||
def RegenerateParserKey( self ):
|
||||
|
||||
self._parser_key = HydrusData.GenerateKey()
|
||||
|
|
|
@ -5,6 +5,7 @@ import ClientTags
|
|||
import datetime
|
||||
import HydrusConstants as HC
|
||||
import HydrusData
|
||||
import HydrusExceptions
|
||||
import HydrusGlobals as HG
|
||||
import HydrusSerialisable
|
||||
import HydrusTags
|
||||
|
@ -1228,52 +1229,59 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
( operator, value, service_key ) = self._value
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
pretty_value = HydrusData.ToUnicode( value )
|
||||
|
||||
if service_type == HC.LOCAL_RATING_LIKE:
|
||||
try:
|
||||
|
||||
if value == 0:
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
pretty_value = HydrusData.ToUnicode( value )
|
||||
|
||||
if service_type == HC.LOCAL_RATING_LIKE:
|
||||
|
||||
pretty_value = 'dislike'
|
||||
if value == 0:
|
||||
|
||||
pretty_value = 'dislike'
|
||||
|
||||
elif value == 1:
|
||||
|
||||
pretty_value = 'like'
|
||||
|
||||
|
||||
elif value == 1:
|
||||
elif service_type == HC.LOCAL_RATING_NUMERICAL:
|
||||
|
||||
pretty_value = 'like'
|
||||
if isinstance( value, float ):
|
||||
|
||||
allow_zero = service.AllowZero()
|
||||
num_stars = service.GetNumStars()
|
||||
|
||||
if allow_zero:
|
||||
|
||||
star_range = num_stars
|
||||
|
||||
else:
|
||||
|
||||
star_range = num_stars - 1
|
||||
|
||||
|
||||
pretty_x = int( round( value * star_range ) )
|
||||
pretty_y = num_stars
|
||||
|
||||
if not allow_zero:
|
||||
|
||||
pretty_x += 1
|
||||
|
||||
|
||||
pretty_value = HydrusData.ConvertValueRangeToPrettyString( pretty_x, pretty_y )
|
||||
|
||||
|
||||
|
||||
elif service_type == HC.LOCAL_RATING_NUMERICAL:
|
||||
base += u' for ' + service.GetName() + u' ' + operator + u' ' + pretty_value
|
||||
|
||||
if isinstance( value, float ):
|
||||
|
||||
allow_zero = service.AllowZero()
|
||||
num_stars = service.GetNumStars()
|
||||
|
||||
if allow_zero:
|
||||
|
||||
star_range = num_stars
|
||||
|
||||
else:
|
||||
|
||||
star_range = num_stars - 1
|
||||
|
||||
|
||||
pretty_x = int( round( value * star_range ) )
|
||||
pretty_y = num_stars
|
||||
|
||||
if not allow_zero:
|
||||
|
||||
pretty_x += 1
|
||||
|
||||
|
||||
pretty_value = HydrusData.ConvertValueRangeToPrettyString( pretty_x, pretty_y )
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
base = u'system:unknown rating service system predicate'
|
||||
|
||||
|
||||
base += u' for ' + service.GetName() + u' ' + operator + u' ' + pretty_value
|
||||
|
||||
|
||||
elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_SIMILAR_TO:
|
||||
|
@ -1303,9 +1311,16 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
|
|||
if current_or_pending == HC.CONTENT_STATUS_PENDING: base += u' pending to '
|
||||
else: base += u' currently in '
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
base += service.GetName()
|
||||
try:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
base += service.GetName()
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
base = u'unknown file service system predicate'
|
||||
|
||||
|
||||
|
||||
elif self._predicate_type == HC.PREDICATE_TYPE_SYSTEM_TAG_AS_NUMBER:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import ClientConstants as CC
|
||||
import ClientGUIDialogs
|
||||
import collections
|
||||
import HydrusConstants as HC
|
||||
import HydrusData
|
||||
import HydrusGlobals as HG
|
||||
import HydrusSerialisable
|
||||
import HydrusTagArchive
|
||||
import HydrusTags
|
||||
import os
|
||||
|
@ -239,3 +241,99 @@ def RenderTag( tag, render_for_user ):
|
|||
return namespace + connector + subtag
|
||||
|
||||
|
||||
class TagSummaryGenerator( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR
|
||||
SERIALISABLE_NAME = 'Tag Summary Generator'
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, namespace_info = None, separator = None ):
|
||||
|
||||
if namespace_info is None:
|
||||
|
||||
namespace_info = []
|
||||
|
||||
namespace_info.append( ( 'creator', '', ', ' ) )
|
||||
namespace_info.append( ( 'series', '', ', ' ) )
|
||||
namespace_info.append( ( 'title', '', ', ' ) )
|
||||
|
||||
|
||||
if separator is None:
|
||||
|
||||
separator = ' - '
|
||||
|
||||
|
||||
self._namespace_info = namespace_info
|
||||
self._separator = separator
|
||||
|
||||
self._UpdateNamespaceLookup()
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return ( self._namespace_info, self._separator )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._namespace_info, self._separator ) = serialisable_info
|
||||
|
||||
self._namespace_info = [ tuple( row ) for row in self._namespace_info ]
|
||||
|
||||
self._UpdateNamespaceLookup()
|
||||
|
||||
|
||||
def _UpdateNamespaceLookup( self ):
|
||||
|
||||
self._interesting_namespaces = { namespace for ( namespace, prefix, separator ) in self._namespace_info }
|
||||
|
||||
|
||||
def GenerateSummary( self, tags, max_length = None ):
|
||||
|
||||
namespaces_to_subtags = collections.defaultdict( list )
|
||||
|
||||
for tag in tags:
|
||||
|
||||
( namespace, subtag ) = HydrusTags.SplitTag( tag )
|
||||
|
||||
if namespace in self._interesting_namespaces:
|
||||
|
||||
namespaces_to_subtags[ namespace ].append( subtag )
|
||||
|
||||
|
||||
|
||||
for l in namespaces_to_subtags.values():
|
||||
|
||||
l.sort()
|
||||
|
||||
|
||||
namespace_texts = []
|
||||
|
||||
for ( namespace, prefix, separator ) in self._namespace_info:
|
||||
|
||||
subtags = namespaces_to_subtags[ namespace ]
|
||||
|
||||
if len( subtags ) > 0:
|
||||
|
||||
namespace_text = prefix + separator.join( namespaces_to_subtags[ namespace ] )
|
||||
|
||||
namespace_texts.append( namespace_text )
|
||||
|
||||
|
||||
|
||||
summary = self._separator.join( namespace_texts )
|
||||
|
||||
if max_length is not None:
|
||||
|
||||
summary = summary[:max_length]
|
||||
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def ToTuple( self ):
|
||||
|
||||
return ( self._namespace_info, self._separator )
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR ] = TagSummaryGenerator
|
||||
|
|
|
@ -15,6 +15,8 @@ class JobKey( object ):
|
|||
|
||||
self._key = HydrusData.GenerateKey()
|
||||
|
||||
self._creation_time = HydrusData.GetNowFloat()
|
||||
|
||||
self._pausable = pausable
|
||||
self._cancellable = cancellable
|
||||
self._only_when_idle = only_when_idle
|
||||
|
@ -170,6 +172,11 @@ class JobKey( object ):
|
|||
|
||||
|
||||
|
||||
def GetCreationTime( self ):
|
||||
|
||||
return self._creation_time
|
||||
|
||||
|
||||
def GetIfHasVariable( self, name ):
|
||||
|
||||
with self._variable_lock:
|
||||
|
|
|
@ -49,7 +49,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 18
|
||||
SOFTWARE_VERSION = 294
|
||||
SOFTWARE_VERSION = 295
|
||||
|
||||
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
||||
|
|
|
@ -218,6 +218,17 @@ class HydrusController( object ):
|
|||
return job
|
||||
|
||||
|
||||
def CallRepeating( self, period, delay, func, *args, **kwargs ):
|
||||
|
||||
call = HydrusData.Call( func, *args, **kwargs )
|
||||
|
||||
job = HydrusThreading.RepeatingJob( self, self._job_scheduler, call, period, initial_delay = delay )
|
||||
|
||||
self._job_scheduler.AddJob( job )
|
||||
|
||||
return job
|
||||
|
||||
|
||||
def CallToThread( self, callable, *args, **kwargs ):
|
||||
|
||||
if HG.callto_report_mode:
|
||||
|
@ -296,6 +307,13 @@ class HydrusController( object ):
|
|||
|
||||
|
||||
|
||||
def DebugShowScheduledJobs( self ):
|
||||
|
||||
summary = self._job_scheduler.GetPrettyJobSummary()
|
||||
|
||||
HydrusData.ShowText( summary )
|
||||
|
||||
|
||||
def GetBootTime( self ):
|
||||
|
||||
return self._timestamps[ 'boot' ]
|
||||
|
@ -353,13 +371,13 @@ class HydrusController( object ):
|
|||
|
||||
if not self._no_daemons:
|
||||
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'SleepCheck', HydrusDaemons.DAEMONSleepCheck, period = 120 ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'MaintainMemoryFast', HydrusDaemons.DAEMONMaintainMemoryFast, period = 60 ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONWorker( self, 'MaintainMemorySlow', HydrusDaemons.DAEMONMaintainMemorySlow, period = 300 ) )
|
||||
|
||||
self._daemons.append( HydrusThreading.DAEMONBackgroundWorker( self, 'MaintainDB', HydrusDaemons.DAEMONMaintainDB, period = 300, init_wait = 60 ) )
|
||||
|
||||
|
||||
self.CallRepeating( 120.0, 10.0, self.SleepCheck )
|
||||
self.CallRepeating( 60.0, 10.0, self.MaintainMemoryFast )
|
||||
self.CallRepeating( 300.0, 10.0, self.MaintainMemorySlow )
|
||||
|
||||
|
||||
def IsFirstStart( self ):
|
||||
|
||||
|
@ -378,6 +396,11 @@ class HydrusController( object ):
|
|||
pass
|
||||
|
||||
|
||||
def MaintainMemoryFast( self ):
|
||||
|
||||
self.pub( 'memory_maintenance_pulse' )
|
||||
|
||||
|
||||
def MaintainMemorySlow( self ):
|
||||
|
||||
sys.stdout.flush()
|
||||
|
|
|
@ -11,15 +11,3 @@ def DAEMONMaintainDB( controller ):
|
|||
|
||||
controller.MaintainDB()
|
||||
|
||||
def DAEMONMaintainMemoryFast( controller ):
|
||||
|
||||
controller.pub( 'memory_maintenance_pulse' )
|
||||
|
||||
def DAEMONMaintainMemorySlow( controller ):
|
||||
|
||||
controller.MaintainMemorySlow()
|
||||
|
||||
def DAEMONSleepCheck( controller ):
|
||||
|
||||
controller.SleepCheck()
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ class MimeException( Exception ): pass
|
|||
class NameException( Exception ): pass
|
||||
class ShutdownException( Exception ): pass
|
||||
class SizeException( Exception ): pass
|
||||
class DecompressionBombException( SizeException ): pass
|
||||
class VetoException( Exception ): pass
|
||||
|
||||
class ParseException( Exception ): pass
|
||||
|
|
|
@ -242,6 +242,21 @@ def DeletePath( path ):
|
|||
|
||||
|
||||
|
||||
def DirectoryIsWritable( path ):
|
||||
|
||||
try:
|
||||
|
||||
t = tempfile.TemporaryFile( dir = path )
|
||||
|
||||
t.close()
|
||||
|
||||
return True
|
||||
|
||||
except:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def FilterFreePaths( paths ):
|
||||
|
||||
free_paths = []
|
||||
|
|
|
@ -75,6 +75,7 @@ SERIALISABLE_TYPE_SEED = 57
|
|||
SERIALISABLE_TYPE_PAGE_PARSER = 58
|
||||
SERIALISABLE_TYPE_PARSE_FORMULA_COMPOUND = 59
|
||||
SERIALISABLE_TYPE_PARSE_FORMULA_CONTEXT_VARIABLE = 60
|
||||
SERIALISABLE_TYPE_TAG_SUMMARY_GENERATOR = 61
|
||||
|
||||
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}
|
||||
|
||||
|
|
|
@ -378,6 +378,22 @@ class JobScheduler( threading.Thread ):
|
|||
self._new_job_arrived.set()
|
||||
|
||||
|
||||
def GetPrettyJobSummary( self ):
|
||||
|
||||
with self._waiting_lock:
|
||||
|
||||
num_jobs = len( self._waiting )
|
||||
|
||||
job_lines = [ repr( job ) for job in self._waiting ]
|
||||
|
||||
lines = [ HydrusData.ConvertIntToPrettyString( num_jobs ) + ' jobs:' ] + job_lines
|
||||
|
||||
text = os.linesep.join( lines )
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
||||
def JobCancelled( self ):
|
||||
|
||||
self._cancel_filter_needed.set()
|
||||
|
@ -473,7 +489,7 @@ class SchedulableJob( object ):
|
|||
|
||||
def __repr__( self ):
|
||||
|
||||
return 'Schedulable Job: ' + repr( self._work_callable )
|
||||
return repr( self.__class__ ) + ': ' + repr( self._work_callable ) + ' next in ' + HydrusData.ConvertTimeDeltaToPrettyString( self._next_work_time - HydrusData.GetNowFloat() )
|
||||
|
||||
|
||||
def _BootWorker( self ):
|
||||
|
|
|
@ -13,7 +13,7 @@ import HydrusGlobals as HG
|
|||
|
||||
def DoClick( click, panel ):
|
||||
|
||||
wx.PostEvent( panel, click )
|
||||
wx.QueueEvent( panel, click )
|
||||
|
||||
wx.YieldIfNeeded()
|
||||
|
||||
|
@ -82,13 +82,6 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
#
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
current_y = 5
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( current_y )
|
||||
|
||||
all_clickable_indices = GetAllClickableIndices( panel )
|
||||
|
||||
self.assertEqual( len( all_clickable_indices.keys() ), len( terms ) )
|
||||
|
@ -98,6 +91,9 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
for ( index, y ) in all_clickable_indices.items():
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( y )
|
||||
|
||||
DoClick( click, panel )
|
||||
|
@ -109,6 +105,11 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
current_y = 5
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( current_y )
|
||||
|
||||
while panel._GetIndexUnderMouse( click ) is not None:
|
||||
|
||||
current_y += 5
|
||||
|
@ -122,7 +123,7 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
#
|
||||
|
||||
click.SetControlDown( True )
|
||||
|
||||
|
||||
if len( all_clickable_indices.keys() ) > 2:
|
||||
|
||||
|
@ -130,6 +131,11 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
for index in indices:
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
click.SetControlDown( True )
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( all_clickable_indices[ index ] )
|
||||
|
||||
DoClick( click, panel )
|
||||
|
@ -140,8 +146,6 @@ class TestListBoxes( unittest.TestCase ):
|
|||
self.assertEqual( panel.GetSelectedNamespaceColours(), dict( expected_selected_terms ) )
|
||||
|
||||
|
||||
click.SetControlDown( False )
|
||||
|
||||
#
|
||||
|
||||
random_index = random.choice( all_clickable_indices.keys() )
|
||||
|
@ -157,6 +161,11 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
current_y = 5
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( current_y )
|
||||
|
||||
while panel._GetIndexUnderMouse( click ) is not None:
|
||||
|
||||
current_y += 5
|
||||
|
@ -168,6 +177,9 @@ class TestListBoxes( unittest.TestCase ):
|
|||
|
||||
# select the random index
|
||||
|
||||
click = wx.MouseEvent( wx.wxEVT_LEFT_DOWN )
|
||||
|
||||
click.SetX( 10 )
|
||||
click.SetY( all_clickable_indices[ random_index ] )
|
||||
|
||||
DoClick( click, panel )
|
||||
|
|
|
@ -14,15 +14,15 @@ import HydrusGlobals as HG
|
|||
|
||||
def HitButton( button ):
|
||||
|
||||
wx.PostEvent( button, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = button.GetId() ) )
|
||||
wx.QueueEvent( button, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = button.GetId() ) )
|
||||
|
||||
def HitCancelButton( window ):
|
||||
|
||||
wx.PostEvent( window, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = wx.ID_CANCEL ) )
|
||||
wx.QueueEvent( window, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = wx.ID_CANCEL ) )
|
||||
|
||||
def HitOKButton( window ):
|
||||
|
||||
wx.PostEvent( window, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = wx.ID_OK ) )
|
||||
wx.QueueEvent( window, wx.CommandEvent( commandEventType = wx.EVT_BUTTON.typeId, id = wx.ID_OK ) )
|
||||
|
||||
def CancelChildDialog( window ):
|
||||
|
||||
|
|
|
@ -48,6 +48,11 @@ try:
|
|||
|
||||
db_dir = HC.DEFAULT_DB_DIR
|
||||
|
||||
if not HydrusPaths.DirectoryIsWritable( db_dir ):
|
||||
|
||||
db_dir = os.path.join( os.path.expanduser( '~' ), 'Hydrus' )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
db_dir = result.db_dir
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 663 B |
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
15
test.py
15
test.py
|
@ -269,6 +269,11 @@ class Controller( object ):
|
|||
|
||||
CallToThreadLongRunning = CallToThread
|
||||
|
||||
def CallLaterWXSafe( self, *args, **kwargs ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def DBCurrentlyDoingJob( self ):
|
||||
|
||||
return False
|
||||
|
@ -338,6 +343,11 @@ class Controller( object ):
|
|||
return self._reads[ name ]
|
||||
|
||||
|
||||
def RegisterUIUpdateWindow( self, window ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def ReportDataUsed( self, num_bytes ):
|
||||
|
||||
pass
|
||||
|
@ -423,11 +433,6 @@ class Controller( object ):
|
|||
self._cookies[ name ] = value
|
||||
|
||||
|
||||
def StartFileQuery( self, page_key, job_key, search_context ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def TidyUp( self ):
|
||||
|
||||
time.sleep( 2 )
|
||||
|
|
Loading…
Reference in New Issue