Version 295

This commit is contained in:
Hydrus Network Developer 2018-02-21 15:59:37 -06:00
parent eccc185fcf
commit 4a7f62798c
48 changed files with 2199 additions and 1757 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>&lt;a href="(URL A)" class="thumb"&gt;Forest Glade&lt;/a&gt;</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 "&lt;a href="(URL A)" class="thumb"&gt;Forest Glade&lt;/a&gt;". 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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' ) )

View File

@ -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() )

View File

@ -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 )

View File

@ -59,10 +59,6 @@ def DAEMONCheckImportFolders( controller ):
def DAEMONCheckMouseIdle( controller ):
wx.CallAfter( controller.CheckMouseIdle )
def DAEMONDownloadFiles( controller ):
hashes = controller.Read( 'downloads' )

View File

@ -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 )
#

View File

@ -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()

View File

@ -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 ):

View File

@ -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 )

View File

@ -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' )

View File

@ -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 ) )

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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 ):

View File

@ -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

View File

@ -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 )
#

View File

@ -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 ):

View File

@ -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 ):

View File

@ -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 ):

View File

@ -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 )

View File

@ -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 ):

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -49,7 +49,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 294
SOFTWARE_VERSION = 295
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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 = []

View File

@ -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 = {}

View File

@ -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 ):

View File

@ -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 )

View File

@ -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 ):

View File

@ -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

BIN
static/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

15
test.py
View File

@ -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 )