Version 376
|
@ -8,6 +8,52 @@
|
|||
<div class="content">
|
||||
<h3>changelog</h3>
|
||||
<ul>
|
||||
<li><h3>version 376</h3></li>
|
||||
<ul>
|
||||
<li>subscriptions:</li>
|
||||
<li>wrote a new subscription manager to better look after subscription scheduling</li>
|
||||
<li>rather than checking every four hours or after manage subs dialog close, subscriptions now record an indication of when they are next due for work, whether that is the estimated next check time or when bandwidth is free on remaining file downloads, and launch in a fifteen-minute window around that time. delays due to previous errors or user cancels are also taken into account. this reduces background cpu and i/o greatly for clients with large subs</li>
|
||||
<li>if a sub is paused, or all its queries are paused, it will now never be reloaded after first load until a change via the manage subs dialog</li>
|
||||
<li>furthermore, if a single sub takes a very long time to work, the whole sublist can re-cycle if they come up due for more work before it is finished</li>
|
||||
<li>if a sub query is DEAD but still has outstanding files to download, it will no longer automatically pause</li>
|
||||
<li>subs now clean up more tidily if they are running on a program exit</li>
|
||||
<li>the subscription popup now shows check/file progress based on the number of queries that appear to have pending work. instead of 'query 300/450' with 420 that aren't due, you'll get 'query 12/30'. if a query becomes due during a round of checking, another round of checking will run</li>
|
||||
<li>if a subscription fails to load from the db, the error is handled better and no more subs will run in that boot</li>
|
||||
<li>improved subscription startup checking logic, tightening up various paused/dead/cansync tests</li>
|
||||
<li>improved subscription interrupt checking logic, tightening checks on global network pause and various shutdown scenarios</li>
|
||||
<li>cleaned up some more subscription code in prep for data storage breakup</li>
|
||||
<li>.</li>
|
||||
<li>qt:</li>
|
||||
<li>added experimental Qt style settings to the new options->style page! all users should now be able to set Fusion style, and perhaps some alternate OS styles. advanced users are invited to play around with QSS stylesheets (although be warned that some of hydrus's custom colour system overrides QSS, so more work is needed here), which will be extended and made user-friendly in coming weeks</li>
|
||||
<li>fixed tab position calculations for all tab/media drag and drops for tab bars that are centered or otherwise positioned far off top-left alignment</li>
|
||||
<li>fixed tab drag and drop event object handling for macOS. tab and media DnD is now enabled for macOS</li>
|
||||
<li>the popup toaster can now unhide if an on-top-of-parent non-modal frame (like review services) is focused (so hitting 'process now' should show you the work)</li>
|
||||
<li>fixed a variety of old hacky wx close-window veto tests. the 'close client?' confirmation dialog will now reliably veto a close requent on 'no'/cancel, dialog close events that are vetoed (such as closing the manage tags dialog with pending tags) will now veto more than just the first time, and several bad media viewer archive/dupe filtering cancel and end-of-window events should now work more cleanly and correctly. users who had crashes at the end of filtering may find they are stable again</li>
|
||||
<li>as a quick patch against some multiline notes and statuses, list controls now force single-line text in all cells</li>
|
||||
<li>list controls now tooltip all cells</li>
|
||||
<li>fixed the shutdown splash not updating after the daemons shut down (lmao)</li>
|
||||
<li>'modal' message dialogs, which are created by blocking maintenance tasks such as vacuum, will no longer raise the program to the foreground on creation</li>
|
||||
<li>should have fixed the taglist vertical positioning jank that could occur in the row after a tag with a tall emoji unicode character (and also sometimes kanji/hangul)</li>
|
||||
<li>fixed a typo bug that was throwing an error for the upnp port widget in the local client server management panel when 'allow non local connections' was checked</li>
|
||||
<li>improved stability of bandwidth review panel bandwidth rules refresh</li>
|
||||
<li>improved stability of review services bandwidth rules refresh</li>
|
||||
<li>improved some dialog cleanup code</li>
|
||||
<li>reverted a bad environment-setting change put in last week that was causing some running-from-source users trouble</li>
|
||||
<li>misc qt code cleanup</li>
|
||||
<li>.</li>
|
||||
<li>the rest:</li>
|
||||
<li>updated the default pixiv tag search downloader to one submitted by a user. it now uses their api</li>
|
||||
<li>updated the default twitter username lookup to a downloader submitted by a user. it fetches just the media tweet feed, making it more efficient. also added (but not linked by default) is a new tweet parser that can fetch most videos using a third-party site, advanced users may wish to play with this</li>
|
||||
<li>added a {file_id} term for file export phrases that substitutes a unique and permanent numerical file identifier</li>
|
||||
<li>fixed an issue where idle maintenance jobs could sometimes sneak in a few milliseconds of work during certain long shut down pauses, such as while waiting for a 'should I do shutdown work?' dialog to return. program shutdown should be snappier for many users as forced startup delays in these calls will no longer trigger</li>
|
||||
<li>added a date 'encode' string transformation rule, which takes an integer timestamp and converts it to a pretty date string. the date rules are now renamed to the clearer 'datestring to timestamp' and vice versa</li>
|
||||
<li>fixed page parser edit panel's 'test parse' when string transformations perform pre-parsing conversion. the handling and passing of test data for all the panels here is cleaned up throughout</li>
|
||||
<li>system:limit predicate edit panel now has a small label describing its sampling behaviour</li>
|
||||
<li>updated the various 8chan links in the client and help to 8kun, let me know if I missed any, and added Endchan bunker link to help menu</li>
|
||||
<li>improved some misc status text handling across the program</li>
|
||||
<li>refactored cache and manager code into different, simpler files</li>
|
||||
<li>updated sqlite on windows build to 3.30.1</li>
|
||||
</ul>
|
||||
<li><h3>version 375</h3></li>
|
||||
<ul>
|
||||
<li>qt:</li>
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
<div class="content">
|
||||
<h3>contact and links</h3>
|
||||
<p>Please send me all your bug reports, questions, ideas, and comments. It is always interesting to see how other people are using my software and what they generally think of it. Most of the changes every week are suggested by users.</p>
|
||||
<p>You can contact me by email, twitter, tumblr, discord, or the 8chan board--I do not mind which. I'm not active on github (I use it mostly as a mirror of my home dev environment) and do not check its messages or issues. I often like to spend a day or so to think before replying to non-urgent messages, but I do try to reply to everything.</p>
|
||||
<p>You can contact me by email, twitter, tumblr, discord, or the 8kun/Endchan boards--I do not mind which. I'm not active on github (I use it mostly as a mirror of my home dev environment) and do not check its messages or issues. I often like to spend a day or so to think before replying to non-urgent messages, but I do try to reply to everything.</p>
|
||||
<p>I am on the discord on Saturday afternoon, USA time, and Wednesday after I put the release out. If that is not a good time for you, feel free to leave me a DM and I will get to you when I can. There are also plenty of other hydrus users who idle who would be happy to help with any sort of support question.</p>
|
||||
<p>I delete all tweets and resolved email conversations after three months. So, if you think you are waiting for a reply, or I said I was going to work on something you care about and seem to have forgotten, please do nudge me.</p>
|
||||
<p>If you have a problem with something on someone else's server, please, <span class="warning">do not come to me</span>, as I cannot help. If your ex-gf's nudes have leaked onto the internet or you just find something terribly offensive, I cannot help you at all.</p>
|
||||
<p>Anyway:</p>
|
||||
<ul>
|
||||
<li><a href="https://hydrusnetwork.github.io/hydrus/">homepage</a></li>
|
||||
<li><a href="https://8ch.net/hydrus/index.html">8chan board</a> (<a href="https://endchan.net/hydrus/">endchan bunker</a>)</li>
|
||||
<li><a href="https://8kun.top/hydrus/index.html">8kun board</a> (<a href="https://endchan.net/hydrus/">endchan bunker</a>)</li>
|
||||
<li><a href="http://hydrus.tumblr.com">tumblr</a> (<a href="http://hydrus.tumblr.com/rss">rss</a>)</li>
|
||||
<li><a href="https://github.com/hydrusnetwork/hydrus/releases">new downloads</a></li>
|
||||
<li><a href="https://www.mediafire.com/hydrus">old downloads</a></li>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
<li><b>application/x-7z-compressed</b> (.7z)</li>
|
||||
</ul>
|
||||
<p>Although some support is imperfect for the complicated filetypes. Most videos will not play audio yet, some animated gifs with unusual transparency will render like static, and flash cannot embed into Linux or macOS. When something does not render how you want, right-clicking on its thumbnail presents the option 'open externally', which will open the file in the appropriate default program (e.g. ACDSee, VLC).</p>
|
||||
<p>The client can also download files from several websites, including 4chan and 8chan, many boorus, and gallery sites like deviant art and hentai foundry. You will learn more about this later.</p>
|
||||
<p>The client can also download files from several websites, including 4chan and other imageboards, many boorus, and gallery sites like deviant art and hentai foundry. You will learn more about this later.</p>
|
||||
<h3>inbox and archiving</h3>
|
||||
<p>The client sends newly imported files to an <b>inbox</b>, just like your email. Inbox acts like a tag, matched by 'system:inbox'. A small envelope icon is drawn in the top corner of all inbox files:</p>
|
||||
<p><img src="fresh_imports.png" /></p>
|
||||
|
|
|
@ -587,7 +587,7 @@ class GlobalPixmaps( object ):
|
|||
GlobalPixmaps.copy = QG.QPixmap( os.path.join(HC.STATIC_DIR,'copy.png') )
|
||||
GlobalPixmaps.paste = QG.QPixmap( os.path.join(HC.STATIC_DIR,'paste.png') )
|
||||
|
||||
GlobalPixmaps.eight_chan = QG.QPixmap( os.path.join(HC.STATIC_DIR,'8chan.png') )
|
||||
GlobalPixmaps.eight_kun = QG.QPixmap( os.path.join(HC.STATIC_DIR,'8kun.png') )
|
||||
GlobalPixmaps.twitter = QG.QPixmap( os.path.join(HC.STATIC_DIR,'twitter.png') )
|
||||
GlobalPixmaps.tumblr = QG.QPixmap( os.path.join(HC.STATIC_DIR,'tumblr.png') )
|
||||
GlobalPixmaps.discord = QG.QPixmap( os.path.join(HC.STATIC_DIR,'discord.png') )
|
||||
|
|
|
@ -14,6 +14,9 @@ from . import ClientDownloading
|
|||
from . import ClientFiles
|
||||
from . import ClientGUIMenus
|
||||
from . import ClientGUIShortcuts
|
||||
from . import ClientGUIStyle
|
||||
from . import ClientImportSubscriptions
|
||||
from . import ClientManagers
|
||||
from . import ClientNetworking
|
||||
from . import ClientNetworkingBandwidth
|
||||
from . import ClientNetworkingDomain
|
||||
|
@ -79,7 +82,7 @@ class App( QW.QApplication ):
|
|||
# Since aboutToQuit gets called not only on external shutdown events (like user logging off), but even if we explicitely call QApplication.exit(),
|
||||
# this check will make sure that we only do an emergency exit if it's really necessary (i.e. QApplication.exit() wasn't called by us).
|
||||
if not QW.QApplication.instance().property( 'normal_exit' ):
|
||||
|
||||
|
||||
HG.emergency_exit = True
|
||||
|
||||
if hasattr( HG.client_controller, 'gui' ):
|
||||
|
@ -195,6 +198,23 @@ class Controller( HydrusController.HydrusController ):
|
|||
self.SafeShowCriticalMessage( 'shutdown error', traceback.format_exc() )
|
||||
|
||||
|
||||
def _ShutdownSubscriptionsManager( self ):
|
||||
|
||||
self.subscriptions_manager.Shutdown()
|
||||
|
||||
started = HydrusData.GetNow()
|
||||
|
||||
while not self.subscriptions_manager.IsShutdown():
|
||||
|
||||
time.sleep( 0.1 )
|
||||
|
||||
if HydrusData.TimeHasPassed( started + 30 ):
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
def AcquirePageKey( self ):
|
||||
|
||||
with self._page_key_lock:
|
||||
|
@ -416,7 +436,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
if HG.program_is_shutting_down:
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if HG.force_idle_mode:
|
||||
|
@ -483,7 +503,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
if HG.program_is_shutting_down:
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if self._idle_started is not None and HydrusData.TimeHasPassed( self._idle_started + 3600 ):
|
||||
|
@ -609,6 +629,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
HG.emergency_exit = True
|
||||
|
||||
self.Exit()
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -699,7 +720,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
self.pub( 'splash_set_status_subtext', 'services' )
|
||||
|
||||
self.services_manager = ClientCaches.ServicesManager( self )
|
||||
self.services_manager = ClientManagers.ServicesManager( self )
|
||||
|
||||
self.pub( 'splash_set_status_subtext', 'options' )
|
||||
|
||||
|
@ -809,7 +830,7 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
self.local_booru_manager = ClientCaches.LocalBooruCache( self )
|
||||
|
||||
self.file_viewing_stats_manager = ClientCaches.FileViewingStatsManager( self )
|
||||
self.file_viewing_stats_manager = ClientManagers.FileViewingStatsManager( self )
|
||||
|
||||
self.pub( 'splash_set_status_subtext', 'tag display' )
|
||||
|
||||
|
@ -828,18 +849,19 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
self.pub( 'splash_set_status_subtext', 'tag siblings' )
|
||||
|
||||
self.tag_siblings_manager = ClientCaches.TagSiblingsManager( self )
|
||||
self.tag_siblings_manager = ClientManagers.TagSiblingsManager( self )
|
||||
|
||||
self.pub( 'splash_set_status_subtext', 'tag parents' )
|
||||
|
||||
self.tag_parents_manager = ClientCaches.TagParentsManager( self )
|
||||
self._managers[ 'undo' ] = ClientCaches.UndoManager( self )
|
||||
self.tag_parents_manager = ClientManagers.TagParentsManager( self )
|
||||
self._managers[ 'undo' ] = ClientManagers.UndoManager( self )
|
||||
|
||||
def qt_code():
|
||||
|
||||
self._caches[ 'images' ] = ClientCaches.RenderedImageCache( self )
|
||||
self._caches[ 'thumbnail' ] = ClientCaches.ThumbnailCache( self )
|
||||
self.bitmap_manager = ClientCaches.BitmapManager( self )
|
||||
|
||||
self.bitmap_manager = ClientManagers.BitmapManager( self )
|
||||
|
||||
CC.GlobalPixmaps.STATICInitialise()
|
||||
|
||||
|
@ -885,14 +907,46 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
self.pub( 'splash_set_title_text', 'booting gui\u2026' )
|
||||
|
||||
self.subscriptions_manager = ClientImportSubscriptions.SubscriptionsManager( self )
|
||||
|
||||
def qt_code_gui():
|
||||
|
||||
ClientGUIStyle.InitialiseDefaults()
|
||||
|
||||
qt_style_name = self.new_options.GetNoneableString( 'qt_style_name' )
|
||||
|
||||
if qt_style_name is not None:
|
||||
|
||||
try:
|
||||
|
||||
ClientGUIStyle.SetStyle( qt_style_name )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.Print( 'Could not load Qt style: {}'.format( e ) )
|
||||
|
||||
|
||||
|
||||
qt_stylesheet_name = self.new_options.GetNoneableString( 'qt_stylesheet_name' )
|
||||
|
||||
if qt_stylesheet_name is not None:
|
||||
|
||||
try:
|
||||
|
||||
ClientGUIStyle.SetStylesheet( qt_stylesheet_name )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.Print( 'Could not load Qt stylesheet: {}'.format( e ) )
|
||||
|
||||
|
||||
|
||||
self.gui = ClientGUI.FrameGUI( self )
|
||||
|
||||
self.ResetIdleTimer()
|
||||
|
||||
|
||||
self.CallBlockingToQt(self._splash, qt_code_gui)
|
||||
self.CallBlockingToQt( self._splash, qt_code_gui )
|
||||
|
||||
# ShowText will now popup as a message, as popup message manager has overwritten the hooks
|
||||
|
||||
|
@ -904,7 +958,6 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
if not HG.no_daemons:
|
||||
|
||||
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'SynchroniseSubscriptions', ClientDaemons.DAEMONSynchroniseSubscriptions, ( 'notify_restart_subs_sync_daemon', 'notify_new_subscriptions' ), period = 4 * 3600, init_wait = 60, pre_call_wait = 3 ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'MaintainTrash', ClientDaemons.DAEMONMaintainTrash, init_wait = 120 ) )
|
||||
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'SynchroniseRepositories', ClientDaemons.DAEMONSynchroniseRepositories, ( 'notify_restart_repo_sync_daemon', 'notify_new_permissions', 'wake_idle_workers' ), period = 4 * 3600, pre_call_wait = 1 ) )
|
||||
|
||||
|
@ -1423,6 +1476,8 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
def ShutdownModel( self ):
|
||||
|
||||
self.pub( 'splash_set_status_text', 'saving and exiting objects' )
|
||||
|
||||
if self._is_booted:
|
||||
|
||||
self.file_viewing_stats_manager.Flush()
|
||||
|
@ -1437,12 +1492,20 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
if not HG.emergency_exit:
|
||||
|
||||
self.pub( 'splash_set_status_text', 'waiting for subscriptions to exit' )
|
||||
|
||||
self._ShutdownSubscriptionsManager()
|
||||
|
||||
self.pub( 'splash_set_status_text', 'waiting for daemons to exit' )
|
||||
|
||||
self._ShutdownDaemons()
|
||||
|
||||
self.pub( 'splash_set_status_subtext', '' )
|
||||
|
||||
if HG.do_idle_shutdown_work:
|
||||
|
||||
self.pub( 'splash_set_status_text', 'waiting for idle shutdown work' )
|
||||
|
||||
try:
|
||||
|
||||
self.DoIdleShutdownWork()
|
||||
|
|
|
@ -13189,6 +13189,38 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 375:
|
||||
|
||||
try:
|
||||
|
||||
domain_manager = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
|
||||
|
||||
domain_manager.Initialise()
|
||||
|
||||
#
|
||||
|
||||
domain_manager.OverwriteDefaultGUGs( ( 'pixiv tag search', 'twitter username lookup' ) )
|
||||
domain_manager.OverwriteDefaultURLClasses( ( 'pixiv search api', 'twitter tweets api - media only' ) )
|
||||
domain_manager.OverwriteDefaultParsers( ( 'pixiv tag search api parser', 'twitter tweet parser (video from koto.reisen)', 'twitter media tweets api parser' ) )
|
||||
|
||||
#
|
||||
|
||||
domain_manager.TryToLinkURLClassesAndParsers()
|
||||
|
||||
#
|
||||
|
||||
self._SetJSONDump( domain_manager )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
message = 'Trying to update some parsers failed! Please let hydrus dev know!'
|
||||
|
||||
self.pub_initial_message( message )
|
||||
|
||||
|
||||
|
||||
self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )
|
||||
|
||||
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
|
||||
|
|
|
@ -166,167 +166,3 @@ def DAEMONSynchroniseRepositories( controller ):
|
|||
|
||||
|
||||
|
||||
|
||||
class SubscriptionJob( object ):
|
||||
|
||||
def __init__( self, controller, name ):
|
||||
|
||||
self._controller = controller
|
||||
self._name = name
|
||||
self._job_done = threading.Event()
|
||||
|
||||
|
||||
def _DoWork( self ):
|
||||
|
||||
if HG.subscription_report_mode:
|
||||
|
||||
HydrusData.ShowText( 'Subscription "' + self._name + '" about to start.' )
|
||||
|
||||
|
||||
subscription = self._controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION, self._name )
|
||||
|
||||
subscription.Sync()
|
||||
|
||||
|
||||
def IsDone( self ):
|
||||
|
||||
return self._job_done.is_set()
|
||||
|
||||
|
||||
def Work( self ):
|
||||
|
||||
try:
|
||||
|
||||
self._DoWork()
|
||||
|
||||
finally:
|
||||
|
||||
self._job_done.set()
|
||||
|
||||
|
||||
|
||||
def DAEMONSynchroniseSubscriptions( controller ):
|
||||
|
||||
def filter_finished_jobs( subs_jobs ):
|
||||
|
||||
done_indices = [ i for ( i, ( thread, job ) ) in enumerate( subs_jobs ) if job.IsDone() ]
|
||||
|
||||
done_indices.reverse()
|
||||
|
||||
for i in done_indices:
|
||||
|
||||
del subs_jobs[ i ]
|
||||
|
||||
|
||||
|
||||
def wait_for_free_slot( controller, subs_jobs, max_simultaneous_subscriptions ):
|
||||
|
||||
time.sleep( 0.1 )
|
||||
|
||||
while True:
|
||||
|
||||
p1 = controller.options[ 'pause_subs_sync' ]
|
||||
p2 = HydrusThreading.IsThreadShuttingDown()
|
||||
p3 = controller.new_options.GetBoolean( 'pause_all_new_network_traffic' )
|
||||
|
||||
if p1 or p2 or p3:
|
||||
|
||||
if HG.subscription_report_mode:
|
||||
|
||||
HydrusData.ShowText( 'Subscriptions cancelling. Global sub pause is {}, sub daemon thread shutdown status is {}, and global network pause is {}.'.format( p1, p2, p3 ) )
|
||||
|
||||
|
||||
if p2:
|
||||
|
||||
for ( thread, job ) in subs_jobs:
|
||||
|
||||
HydrusThreading.ShutdownThread( thread )
|
||||
|
||||
|
||||
|
||||
raise HydrusExceptions.CancelledException( 'subs cancelling or thread shutting down' )
|
||||
|
||||
|
||||
filter_finished_jobs( subs_jobs )
|
||||
|
||||
if len( subs_jobs ) < max_simultaneous_subscriptions:
|
||||
|
||||
return
|
||||
|
||||
|
||||
time.sleep( 1.0 )
|
||||
|
||||
|
||||
|
||||
def wait_for_all_finished( subs_jobs ):
|
||||
|
||||
while True:
|
||||
|
||||
filter_finished_jobs( subs_jobs )
|
||||
|
||||
if len( subs_jobs ) == 0:
|
||||
|
||||
return
|
||||
|
||||
|
||||
time.sleep( 1.0 )
|
||||
|
||||
|
||||
|
||||
if HG.subscription_report_mode:
|
||||
|
||||
HydrusData.ShowText( 'Subscription daemon started a run.' )
|
||||
|
||||
|
||||
subscription_names = list( controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ) )
|
||||
|
||||
if controller.new_options.GetBoolean( 'process_subs_in_random_order' ):
|
||||
|
||||
random.shuffle( subscription_names )
|
||||
|
||||
else:
|
||||
|
||||
subscription_names.sort()
|
||||
|
||||
|
||||
HG.subscriptions_running = True
|
||||
|
||||
subs_jobs = []
|
||||
|
||||
try:
|
||||
|
||||
for name in subscription_names:
|
||||
|
||||
max_simultaneous_subscriptions = controller.new_options.GetInteger( 'max_simultaneous_subscriptions' )
|
||||
|
||||
try:
|
||||
|
||||
wait_for_free_slot( controller, subs_jobs, max_simultaneous_subscriptions )
|
||||
|
||||
except HydrusExceptions.CancelledException:
|
||||
|
||||
break
|
||||
|
||||
|
||||
job = SubscriptionJob( controller, name )
|
||||
|
||||
thread = threading.Thread( target = job.Work, name = 'subscription thread' )
|
||||
|
||||
thread.start()
|
||||
|
||||
subs_jobs.append( ( thread, job ) )
|
||||
|
||||
# while we initialise the queue, don't hammer the cpu
|
||||
if len( subs_jobs ) < max_simultaneous_subscriptions:
|
||||
|
||||
time.sleep( 1.0 )
|
||||
|
||||
|
||||
|
||||
wait_for_all_finished( subs_jobs )
|
||||
|
||||
finally:
|
||||
|
||||
HG.subscriptions_running = False
|
||||
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ def GetClientDefaultOptions():
|
|||
options[ 'hpos' ] = 400
|
||||
options[ 'vpos' ] = -240
|
||||
options[ 'thumbnail_cache_size' ] = 25 * 1048576
|
||||
options[ 'preview_cache_size' ] = 15 * 1048576
|
||||
options[ 'fullscreen_cache_size' ] = 150 * 1048576
|
||||
options[ 'thumbnail_dimensions' ] = [ 150, 125 ]
|
||||
options[ 'password' ] = None
|
||||
|
|
|
@ -113,10 +113,7 @@ def DoFileExportDragDrop( window, page_key, media, alt_down ):
|
|||
|
||||
hashes = [ m.GetHash() for m in media ]
|
||||
|
||||
if not HC.PLATFORM_MACOS:
|
||||
|
||||
data_object.setHydrusFiles( page_key, hashes )
|
||||
|
||||
data_object.setHydrusFiles( page_key, hashes )
|
||||
|
||||
# old way of doing this that makes some external programs (discord) reject it
|
||||
'''
|
||||
|
@ -168,7 +165,7 @@ class FileDropTarget( QC.QObject ):
|
|||
if event.type() == QC.QEvent.Drop:
|
||||
|
||||
if self.OnDrop( event.pos().x(), event.pos().y() ):
|
||||
|
||||
|
||||
event.setDropAction( self.OnData( event.mimeData(), event.proposedAction() ) )
|
||||
event.accept()
|
||||
|
||||
|
@ -183,90 +180,92 @@ class FileDropTarget( QC.QObject ):
|
|||
|
||||
def OnData( self, mime_data, result ):
|
||||
|
||||
if mime_data.formats():
|
||||
media_dnd = isinstance( mime_data, QMimeDataHydrusFiles )
|
||||
urls_dnd = mime_data.hasUrls()
|
||||
text_dnd = mime_data.hasText()
|
||||
|
||||
if media_dnd and self._media_callable is not None:
|
||||
|
||||
if isinstance( mime_data, QMimeDataHydrusFiles ) and self._media_callable is not None:
|
||||
result = mime_data.hydrusFiles()
|
||||
|
||||
if result is not None:
|
||||
|
||||
result = mime_data.hydrusFiles()
|
||||
( page_key, hashes ) = result
|
||||
|
||||
if result is not None:
|
||||
if page_key is not None:
|
||||
|
||||
( page_key, hashes ) = result
|
||||
|
||||
if page_key is not None:
|
||||
|
||||
QP.CallAfter( self._media_callable, page_key, hashes ) # callafter so we can terminate dnd event now
|
||||
|
||||
|
||||
|
||||
result = QC.Qt.MoveAction
|
||||
|
||||
# old way of doing it that messed up discord et al
|
||||
'''
|
||||
elif mime_data.formats().count( 'application/hydrus-media' ) and self._media_callable is not None:
|
||||
|
||||
mview = mime_data.data( 'application/hydrus-media' )
|
||||
|
||||
data_bytes = mview.data()
|
||||
|
||||
data_str = str( data_bytes, 'utf-8' )
|
||||
|
||||
(encoded_page_key, encoded_hashes) = json.loads( data_str )
|
||||
|
||||
if encoded_page_key is not None:
|
||||
|
||||
page_key = bytes.fromhex( encoded_page_key )
|
||||
hashes = [ bytes.fromhex( encoded_hash ) for encoded_hash in encoded_hashes ]
|
||||
|
||||
QP.CallAfter( self._media_callable, page_key, hashes ) # callafter so we can terminate dnd event now
|
||||
|
||||
|
||||
|
||||
result = QC.Qt.MoveAction
|
||||
|
||||
# old way of doing it that messed up discord et al
|
||||
'''
|
||||
elif mime_data.formats().count( 'application/hydrus-media' ) and self._media_callable is not None:
|
||||
|
||||
mview = mime_data.data( 'application/hydrus-media' )
|
||||
|
||||
result = QC.Qt.MoveAction
|
||||
'''
|
||||
elif mime_data.hasUrls() and self._filenames_callable is not None:
|
||||
data_bytes = mview.data()
|
||||
|
||||
data_str = str( data_bytes, 'utf-8' )
|
||||
|
||||
(encoded_page_key, encoded_hashes) = json.loads( data_str )
|
||||
|
||||
if encoded_page_key is not None:
|
||||
|
||||
paths = []
|
||||
urls = []
|
||||
page_key = bytes.fromhex( encoded_page_key )
|
||||
hashes = [ bytes.fromhex( encoded_hash ) for encoded_hash in encoded_hashes ]
|
||||
|
||||
QP.CallAfter( self._media_callable, page_key, hashes ) # callafter so we can terminate dnd event now
|
||||
|
||||
for url in mime_data.urls():
|
||||
|
||||
result = QC.Qt.MoveAction
|
||||
'''
|
||||
elif urls_dnd and self._filenames_callable is not None:
|
||||
|
||||
paths = []
|
||||
urls = []
|
||||
|
||||
for url in mime_data.urls():
|
||||
|
||||
if url.isLocalFile():
|
||||
|
||||
if url.isLocalFile():
|
||||
|
||||
paths.append( os.path.normpath( url.toLocalFile() ) )
|
||||
|
||||
else:
|
||||
|
||||
urls.append( url.url() )
|
||||
|
||||
paths.append( os.path.normpath( url.toLocalFile() ) )
|
||||
|
||||
else:
|
||||
|
||||
urls.append( url.url() )
|
||||
|
||||
|
||||
if len( paths ) > 0:
|
||||
|
||||
if len( paths ) > 0:
|
||||
|
||||
QP.CallAfter( self._filenames_callable, paths ) # callafter to terminate dnd event now
|
||||
|
||||
|
||||
if len( urls ) > 0:
|
||||
|
||||
for url in urls:
|
||||
|
||||
QP.CallAfter( self._filenames_callable, paths ) # callafter to terminate dnd event now
|
||||
QP.CallAfter( self._url_callable, url ) # callafter to terminate dnd event now
|
||||
|
||||
|
||||
if len( urls ) > 0:
|
||||
|
||||
for url in urls:
|
||||
|
||||
QP.CallAfter( self._url_callable, url ) # callafter to terminate dnd event now
|
||||
|
||||
|
||||
|
||||
result = QC.Qt.IgnoreAction
|
||||
|
||||
elif mime_data.hasText() and self._url_callable is not None:
|
||||
|
||||
text = mime_data.text()
|
||||
|
||||
QP.CallAfter( self._url_callable, text ) # callafter to terminate dnd event now
|
||||
|
||||
result = QC.Qt.CopyAction
|
||||
|
||||
else:
|
||||
|
||||
result = QC.Qt.MoveAction
|
||||
|
||||
|
||||
result = QC.Qt.IgnoreAction
|
||||
|
||||
elif text_dnd and self._url_callable is not None:
|
||||
|
||||
text = mime_data.text()
|
||||
|
||||
QP.CallAfter( self._url_callable, text ) # callafter to terminate dnd event now
|
||||
|
||||
result = QC.Qt.CopyAction
|
||||
|
||||
|
||||
else:
|
||||
|
||||
result = QC.Qt.IgnoreAction
|
||||
|
||||
|
||||
return result
|
||||
|
|
|
@ -84,6 +84,12 @@ def GenerateExportFilename( destination_directory, media, terms, append_number =
|
|||
|
||||
filename += hash.hex()
|
||||
|
||||
elif term == 'file_id':
|
||||
|
||||
hash_id = media.GetHashId()
|
||||
|
||||
filename += str( hash_id )
|
||||
|
||||
|
||||
elif term_type == 'tag':
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ from . import ClientGUIScrolledPanelsEdit
|
|||
from . import ClientGUIScrolledPanelsManagement
|
||||
from . import ClientGUIScrolledPanelsReview
|
||||
from . import ClientGUIShortcuts
|
||||
from . import ClientGUIStyle
|
||||
from . import ClientGUITags
|
||||
from . import ClientGUITopLevelWindows
|
||||
from . import ClientMedia
|
||||
|
@ -384,7 +385,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
self._widget_event_filter.EVT_LEFT_DCLICK( self.EventFrameNewPage )
|
||||
self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventFrameNewPage )
|
||||
self._widget_event_filter.EVT_RIGHT_DOWN( self.EventFrameNotebookMenu )
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventClose )
|
||||
self._widget_event_filter.EVT_SET_FOCUS( self.EventFocus )
|
||||
self._widget_event_filter.EVT_ICONIZE( self.EventIconize )
|
||||
|
||||
|
@ -1878,7 +1878,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
#self._controller.CallLaterQtSafe(self, 1.0, self.adjustSize ) # some i3 thing--doesn't layout main gui on init for some reason
|
||||
|
||||
self._controller.CallLaterQtSafe(self, last_session_save_period_minutes * 60, self.SaveLastSession)
|
||||
self._controller.CallLaterQtSafe(self, last_session_save_period_minutes * 60, self.AutoSaveLastSession)
|
||||
|
||||
self._clipboard_watcher_repeating_job = self._controller.CallRepeatingQtSafe(self, 1.0, 1.0, self.REPEATINGClipboardWatcher)
|
||||
|
||||
|
@ -2298,6 +2298,27 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
dlg.exec()
|
||||
|
||||
|
||||
qt_style_name = self._controller.new_options.GetNoneableString( 'qt_style_name' )
|
||||
qt_stylesheet_name = self._controller.new_options.GetNoneableString( 'qt_stylesheet_name' )
|
||||
|
||||
if qt_style_name is None:
|
||||
|
||||
ClientGUIStyle.SetStyle( ClientGUIStyle.ORIGINAL_STYLE )
|
||||
|
||||
else:
|
||||
|
||||
ClientGUIStyle.SetStyle( qt_style_name )
|
||||
|
||||
|
||||
if qt_stylesheet_name is None:
|
||||
|
||||
ClientGUIStyle.ClearStylesheet()
|
||||
|
||||
else:
|
||||
|
||||
ClientGUIStyle.SetStylesheet( qt_stylesheet_name )
|
||||
|
||||
|
||||
self._controller.pub( 'wake_daemons' )
|
||||
self.SetStatusBarDirty()
|
||||
self._controller.pub( 'refresh_page_name' )
|
||||
|
@ -2414,6 +2435,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
HG.client_controller.Write( 'serialisables_overwrite', [ HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ], subscriptions )
|
||||
|
||||
HG.client_controller.subscriptions_manager.NewSubscriptions( subscriptions )
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2427,7 +2450,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
try:
|
||||
|
||||
if HG.subscriptions_running:
|
||||
if HG.client_controller.subscriptions_manager.SubscriptionsRunning():
|
||||
|
||||
job_key = ClientThreading.JobKey()
|
||||
|
||||
|
@ -2437,7 +2460,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
controller.pub( 'message', job_key )
|
||||
|
||||
while HG.subscriptions_running:
|
||||
while HG.client_controller.subscriptions_manager.SubscriptionsRunning():
|
||||
|
||||
time.sleep( 0.1 )
|
||||
|
||||
|
@ -2486,7 +2509,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
try:
|
||||
|
||||
controller.CallBlockingToQt(self, qt_do_it, subscriptions, original_pause_status)
|
||||
controller.CallBlockingToQt( self, qt_do_it, subscriptions, original_pause_status )
|
||||
|
||||
except HydrusExceptions.QtDeadWindowException:
|
||||
|
||||
|
@ -2497,8 +2520,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
controller.options[ 'pause_subs_sync' ] = original_pause_status
|
||||
|
||||
controller.pub( 'notify_new_subscriptions' )
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2694,7 +2715,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
HC.options[ 'pause_subs_sync' ] = not HC.options[ 'pause_subs_sync' ]
|
||||
|
||||
self._controller.pub( 'notify_restart_subs_sync_daemon' )
|
||||
self._controller.subscriptions_manager.Wake()
|
||||
|
||||
elif sync_type == 'export_folders':
|
||||
|
||||
|
@ -3556,7 +3577,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
hide_close_button = not job_key.IsCancellable()
|
||||
|
||||
with ClientGUITopLevelWindows.DialogNullipotent( self, title, hide_buttons = hide_close_button ) as dlg:
|
||||
with ClientGUITopLevelWindows.DialogNullipotent( self, title, hide_buttons = hide_close_button, do_not_activate = True ) as dlg:
|
||||
|
||||
panel = ClientGUIPopupMessages.PopupMessageDialogPanel( dlg, job_key )
|
||||
|
||||
|
@ -3567,6 +3588,29 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
|
||||
|
||||
def AutoSaveLastSession( self ):
|
||||
|
||||
only_save_last_session_during_idle = self._controller.new_options.GetBoolean( 'only_save_last_session_during_idle' )
|
||||
|
||||
if only_save_last_session_during_idle and not self._controller.CurrentlyIdle():
|
||||
|
||||
next_call_delay = 60
|
||||
|
||||
else:
|
||||
|
||||
if HC.options[ 'default_gui_session' ] == 'last session':
|
||||
|
||||
self._notebook.SaveGUISession( 'last session' )
|
||||
|
||||
|
||||
last_session_save_period_minutes = self._controller.new_options.GetInteger( 'last_session_save_period_minutes' )
|
||||
|
||||
next_call_delay = last_session_save_period_minutes * 60
|
||||
|
||||
|
||||
self._controller.CallLaterQtSafe( self, next_call_delay, self.AutoSaveLastSession )
|
||||
|
||||
|
||||
def DeleteAllClosedPages( self ):
|
||||
|
||||
deletee_pages = [ page for ( time_closed, page ) in self._closed_pages ]
|
||||
|
@ -3635,13 +3679,13 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
|
||||
|
||||
def EventClose( self, event ):
|
||||
def closeEvent( self, event ):
|
||||
|
||||
exit_allowed = self.Exit()
|
||||
|
||||
if not exit_allowed:
|
||||
|
||||
return True # was: event.ignore()
|
||||
event.ignore()
|
||||
|
||||
|
||||
|
||||
|
@ -4126,7 +4170,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
if self._controller.new_options.GetBoolean( 'advanced_mode' ):
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( submenu, 'nudge subscriptions awake', 'Tell the subs daemon to wake up, just in case any subs are due.', self._controller.pub, 'notify_restart_subs_sync_daemon' )
|
||||
ClientGUIMenus.AppendMenuItem( submenu, 'nudge subscriptions awake', 'Tell the subs daemon to wake up, just in case any subs are due.', self._controller.subscriptions_manager.ClearCacheAndWake )
|
||||
|
||||
|
||||
ClientGUIMenus.AppendSeparator( submenu )
|
||||
|
@ -4325,7 +4369,8 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
links = QW.QMenu( menu )
|
||||
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, 'site', 'Open hydrus\'s website, which is mostly a mirror of the local help.', CC.GlobalPixmaps.file_repository, ClientPaths.LaunchURLInWebBrowser, 'https://hydrusnetwork.github.io/hydrus/' )
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, '8chan board', 'Open hydrus dev\'s 8chan board, where he makes release posts and other status updates. Much other discussion also occurs.', CC.GlobalPixmaps.eight_chan, ClientPaths.LaunchURLInWebBrowser, 'https://8ch.net/hydrus/index.html' )
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, '8kun board', 'Open hydrus dev\'s 8kun board, where he makes release posts and other status updates.', CC.GlobalPixmaps.eight_kun, ClientPaths.LaunchURLInWebBrowser, 'https://8kun.top/hydrus/index.html' )
|
||||
site = ClientGUIMenus.AppendMenuItem( links, 'Endchan board bunker', 'Open hydrus dev\'s Endchan board, the bunker for when 8kun is unavailable.', ClientPaths.LaunchURLInWebBrowser, 'https://endchan.net/hydrus/index.html' )
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, 'twitter', 'Open hydrus dev\'s twitter, where he makes general progress updates and emergency notifications.', CC.GlobalPixmaps.twitter, ClientPaths.LaunchURLInWebBrowser, 'https://twitter.com/hydrusnetwork' )
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, 'tumblr', 'Open hydrus dev\'s tumblr, where he makes release posts and other status updates.', CC.GlobalPixmaps.tumblr, ClientPaths.LaunchURLInWebBrowser, 'http://hydrus.tumblr.com/' )
|
||||
site = ClientGUIMenus.AppendMenuBitmapItem( links, 'discord', 'Open a discord channel where many hydrus users congregate. Hydrus dev visits regularly.', CC.GlobalPixmaps.discord, ClientPaths.LaunchURLInWebBrowser, 'https://discord.gg/vy8CUB4' )
|
||||
|
@ -4417,6 +4462,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
ClientGUIMenus.AppendMenuItem( data_actions, 'run slow memory maintenance', 'Tell all the slow caches to maintain themselves.', self._controller.MaintainMemorySlow )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'review threads', 'Show current threads and what they are doing.', self._ReviewThreads )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'show scheduled jobs', 'Print some information about the currently scheduled jobs log.', self._DebugShowScheduledJobs )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'subscription manager snapshot', 'Have the subscription system show what it is doing.', self._controller.subscriptions_manager.ShowSnapshot )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'flush log', 'Command the log to write any buffered contents to hard drive.', HydrusData.DebugPrint, 'Flushing log' )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'print garbage', 'Print some information about the python garbage to the log.', self._DebugPrintGarbage )
|
||||
ClientGUIMenus.AppendMenuItem( data_actions, 'take garbage snapshot', 'Capture current garbage object counts.', self._DebugTakeGarbageSnapshot )
|
||||
|
@ -5539,29 +5585,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
|
||||
|
||||
|
||||
def SaveLastSession( self ):
|
||||
|
||||
only_save_last_session_during_idle = self._controller.new_options.GetBoolean( 'only_save_last_session_during_idle' )
|
||||
|
||||
if only_save_last_session_during_idle and not self._controller.CurrentlyIdle():
|
||||
|
||||
next_call_delay = 60
|
||||
|
||||
else:
|
||||
|
||||
if HC.options[ 'default_gui_session' ] == 'last session':
|
||||
|
||||
self._notebook.SaveGUISession( 'last session' )
|
||||
|
||||
|
||||
last_session_save_period_minutes = self._controller.new_options.GetInteger( 'last_session_save_period_minutes' )
|
||||
|
||||
next_call_delay = last_session_save_period_minutes * 60
|
||||
|
||||
|
||||
self._controller.CallLaterQtSafe(self, next_call_delay, self.SaveLastSession)
|
||||
|
||||
|
||||
def SetMediaFocus( self ):
|
||||
|
||||
self._SetMediaFocus()
|
||||
|
|
|
@ -518,9 +518,10 @@ class AutoCompleteDropdown( QW.QWidget ):
|
|||
|
||||
self._dropdown_window = QW.QFrame( self )
|
||||
|
||||
self._dropdown_window.setWindowFlags( QC.Qt.Tool | QC.Qt.FramelessWindowHint )
|
||||
|
||||
self._dropdown_window.setAttribute( QC.Qt.WA_ShowWithoutActivating )
|
||||
|
||||
self._dropdown_window.setWindowFlags( QC.Qt.Tool | QC.Qt.FramelessWindowHint )
|
||||
self._dropdown_window.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised )
|
||||
self._dropdown_window.setLineWidth( 2 )
|
||||
|
||||
|
@ -995,6 +996,8 @@ class AutoCompleteDropdown( QW.QWidget ):
|
|||
|
||||
HG.client_controller.gui.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def EventKillFocus( self, event ):
|
||||
|
||||
|
|
|
@ -188,7 +188,12 @@ class FastThreadToGUIUpdater( object ):
|
|||
# if not, we won't get bungled up with 10,000+ pubsub events in the event queue
|
||||
def Update( self, *args, **kwargs ):
|
||||
|
||||
if HG.view_shutdown:
|
||||
if HG.model_shutdown:
|
||||
|
||||
return
|
||||
|
||||
|
||||
if self._win is None:
|
||||
|
||||
return
|
||||
|
||||
|
|
|
@ -926,11 +926,27 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
self.destroyed.connect( HG.client_controller.gui.MaintainCanvasFrameReferences )
|
||||
|
||||
|
||||
def close( self ):
|
||||
def closeEvent( self, event ):
|
||||
|
||||
self._canvas_window.CleanBeforeDestroy()
|
||||
|
||||
ClientGUITopLevelWindows.FrameThatResizes.close( self )
|
||||
if self._canvas_window is not None:
|
||||
|
||||
can_close = self._canvas_window.TryToDoPreClose()
|
||||
|
||||
if can_close:
|
||||
|
||||
self._canvas_window.CleanBeforeDestroy()
|
||||
|
||||
ClientGUITopLevelWindows.FrameThatResizes.closeEvent( self, event )
|
||||
|
||||
else:
|
||||
|
||||
event.ignore()
|
||||
|
||||
|
||||
else:
|
||||
|
||||
ClientGUITopLevelWindows.FrameThatResizes.closeEvent( self, event )
|
||||
|
||||
|
||||
|
||||
def FullscreenSwitch( self ):
|
||||
|
@ -1008,8 +1024,6 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
# just to reinforce, as Qt sometimes sets none focus for this window until it goes off and back on
|
||||
self._canvas_window.setFocus( QC.Qt.OtherFocusReason )
|
||||
|
||||
self._widget_event_filter.EVT_CLOSE( self._canvas_window.EventClose )
|
||||
|
||||
|
||||
def TakeFocusForUser( self ):
|
||||
|
||||
|
@ -1043,8 +1057,6 @@ class Canvas( QW.QWidget ):
|
|||
|
||||
self._maintain_pan_and_zoom = False
|
||||
|
||||
self._closing = False
|
||||
|
||||
self._service_keys_to_services = {}
|
||||
|
||||
self._current_media = None
|
||||
|
@ -1954,24 +1966,21 @@ class Canvas( QW.QWidget ):
|
|||
|
||||
def resizeEvent( self, event ):
|
||||
|
||||
if not self._closing:
|
||||
( my_width, my_height ) = self.size().toTuple()
|
||||
|
||||
if self._current_media is not None:
|
||||
|
||||
( my_width, my_height ) = self.size().toTuple()
|
||||
( media_width, media_height ) = self._media_container.size().toTuple()
|
||||
|
||||
if self._current_media is not None:
|
||||
if my_width != media_width or my_height != media_height:
|
||||
|
||||
( media_width, media_height ) = self._media_container.size().toTuple()
|
||||
self._ReinitZoom()
|
||||
|
||||
if my_width != media_width or my_height != media_height:
|
||||
|
||||
self._ReinitZoom()
|
||||
|
||||
self._ResetMediaWindowCenterPosition()
|
||||
|
||||
self._ResetMediaWindowCenterPosition()
|
||||
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
def FlipActiveCustomShortcutName( self, name ):
|
||||
|
@ -2775,23 +2784,21 @@ class CanvasWithHovers( CanvasWithDetails ):
|
|||
HG.client_controller.sub( self, 'FullscreenSwitch', 'canvas_fullscreen_switch' )
|
||||
|
||||
|
||||
def _Close( self ):
|
||||
|
||||
self._closing = True
|
||||
|
||||
self.parentWidget().close()
|
||||
|
||||
|
||||
def _GenerateHoverTopFrame( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def _TryToCloseWindow( self ):
|
||||
|
||||
self.window().close()
|
||||
|
||||
|
||||
def CloseFromHover( self, canvas_key ):
|
||||
|
||||
if canvas_key == self._canvas_key:
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
|
||||
|
||||
|
@ -2918,6 +2925,13 @@ class CanvasWithHovers( CanvasWithDetails ):
|
|||
|
||||
|
||||
|
||||
def TryToDoPreClose( self ):
|
||||
|
||||
can_close = True
|
||||
|
||||
return can_close
|
||||
|
||||
|
||||
class CanvasFilterDuplicates( CanvasWithHovers ):
|
||||
|
||||
def __init__( self, parent, file_search_context, both_files_match ):
|
||||
|
@ -2959,7 +2973,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
QP.CallAfter( self._ShowNewPair )
|
||||
|
||||
|
||||
def _Close( self ):
|
||||
def TryToDoPreClose( self ):
|
||||
|
||||
num_committable = self._GetNumCommittableDecisions()
|
||||
|
||||
|
@ -2978,7 +2992,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
self._GoBack()
|
||||
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
elif result == QW.QDialog.Accepted:
|
||||
|
||||
|
@ -2991,7 +3005,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
HG.client_controller.pub( 'refresh_dupe_page_numbers' )
|
||||
|
||||
CanvasWithHovers._Close( self )
|
||||
return True
|
||||
|
||||
|
||||
def _CommitProcessed( self, blocking = True ):
|
||||
|
@ -3276,7 +3290,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
QW.QMessageBox.critical( self, 'Error', 'Due to an unexpected series of events (likely a series of file deletes), the duplicate filter has no valid pair to back up to. It will now close.' )
|
||||
|
||||
self._Close()
|
||||
self.window().deleteLater()
|
||||
|
||||
return
|
||||
|
||||
|
@ -3409,7 +3423,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
QW.QMessageBox.critical( self, 'Error', 'Due to an unexpected series of events (likely a series of file deletes), the duplicate filter has no valid pair to back up to. It will now close.' )
|
||||
|
||||
self._Close()
|
||||
self.window().deleteLater()
|
||||
|
||||
return
|
||||
|
||||
|
@ -3478,7 +3492,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
QW.QMessageBox.critical( self, 'Error', 'It seems an entire batch of pairs were unable to be displayed. The duplicate filter will now close.' )
|
||||
|
||||
self._Close()
|
||||
self.window().deleteLater()
|
||||
|
||||
return
|
||||
|
||||
|
@ -3503,7 +3517,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
self._CommitProcessed( blocking = True )
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
return
|
||||
|
||||
|
@ -3585,11 +3599,6 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
|
||||
|
||||
def EventClose( self, event ):
|
||||
|
||||
if not self._closing: self._Close()
|
||||
|
||||
|
||||
def EventMouse( self, event ):
|
||||
|
||||
if self._IShouldCatchShortcutEvent( event = event ):
|
||||
|
@ -3662,7 +3671,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
if key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return, QC.Qt.Key_Escape ):
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
|
@ -3820,7 +3829,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
QW.QMessageBox.information( self, 'Information', 'All pairs have been filtered!' )
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
|
||||
def qt_continue( unprocessed_pairs ):
|
||||
|
@ -3866,11 +3875,11 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
|
|||
HG.client_controller.pub( 'set_focus', self._page_key, None )
|
||||
|
||||
|
||||
def _Close( self ):
|
||||
def TryToDoPreClose( self ):
|
||||
|
||||
HG.client_controller.pub( 'set_focus', self._page_key, self._current_media )
|
||||
|
||||
CanvasWithHovers._Close( self )
|
||||
return CanvasWithHovers.TryToDoPreClose( self )
|
||||
|
||||
|
||||
def _GetIndexString( self ):
|
||||
|
@ -3975,7 +3984,7 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
|
|||
|
||||
if self.HasNoMedia():
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
elif self.HasMedia( self._current_media ):
|
||||
|
||||
|
@ -4026,11 +4035,6 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
|
|||
|
||||
|
||||
|
||||
def EventClose( self, event ):
|
||||
|
||||
if not self._closing: self._Close()
|
||||
|
||||
|
||||
def EventFullscreenSwitch( self, event ):
|
||||
|
||||
self.parentWidget().FullscreenSwitch()
|
||||
|
@ -4061,7 +4065,7 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
|
|||
|
||||
if self.HasNoMedia():
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
elif self.HasMedia( self._current_media ):
|
||||
|
||||
|
@ -4116,75 +4120,72 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
|
||||
|
||||
def _Close( self ):
|
||||
def TryToDoPreClose( self ):
|
||||
|
||||
if self._IShouldCatchShortcutEvent():
|
||||
if len( self._kept ) > 0 or len( self._deleted ) > 0:
|
||||
|
||||
if len( self._kept ) > 0 or len( self._deleted ) > 0:
|
||||
label = 'keep ' + HydrusData.ToHumanInt( len( self._kept ) ) + ' and delete ' + HydrusData.ToHumanInt( len( self._deleted ) ) + ' files?'
|
||||
|
||||
( result, cancelled ) = ClientGUIDialogsQuick.GetFinishFilteringAnswer( self, label )
|
||||
|
||||
if cancelled:
|
||||
|
||||
label = 'keep ' + HydrusData.ToHumanInt( len( self._kept ) ) + ' and delete ' + HydrusData.ToHumanInt( len( self._deleted ) ) + ' files?'
|
||||
if self._current_media in self._kept:
|
||||
|
||||
self._kept.remove( self._current_media )
|
||||
|
||||
|
||||
result, cancelled = ClientGUIDialogsQuick.GetFinishFilteringAnswer( self, label )
|
||||
if self._current_media in self._deleted:
|
||||
|
||||
self._deleted.remove( self._current_media )
|
||||
|
||||
|
||||
if cancelled:
|
||||
return False
|
||||
|
||||
elif result == QW.QDialog.Accepted:
|
||||
|
||||
def process_in_thread( service_keys_and_content_updates ):
|
||||
|
||||
if self._current_media in self._kept:
|
||||
for ( service_key, content_update ) in service_keys_and_content_updates:
|
||||
|
||||
self._kept.remove( self._current_media )
|
||||
HG.client_controller.WriteSynchronous( 'content_updates', { service_key : [ content_update ] } )
|
||||
|
||||
|
||||
if self._current_media in self._deleted:
|
||||
|
||||
self._deleted.remove( self._current_media )
|
||||
|
||||
|
||||
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
|
||||
self._kept_hashes = [ media.GetHash() for media in self._kept ]
|
||||
|
||||
service_keys_and_content_updates = []
|
||||
|
||||
reason = 'Deleted in Archive/Delete filter.'
|
||||
|
||||
for chunk_of_hashes in HydrusData.SplitListIntoChunks( self._deleted_hashes, 64 ):
|
||||
|
||||
return
|
||||
service_keys_and_content_updates.append( ( CC.LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, chunk_of_hashes, reason = reason ) ) )
|
||||
|
||||
elif result == QW.QDialog.Accepted:
|
||||
|
||||
service_keys_and_content_updates.append( ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) ) )
|
||||
|
||||
HG.client_controller.CallToThread( process_in_thread, service_keys_and_content_updates )
|
||||
|
||||
self._kept = set()
|
||||
self._deleted = set()
|
||||
|
||||
self._current_media = self._GetFirst() # so the pubsub on close is better
|
||||
|
||||
if HC.options[ 'remove_filtered_files' ]:
|
||||
|
||||
def process_in_thread( service_keys_and_content_updates ):
|
||||
|
||||
for ( service_key, content_update ) in service_keys_and_content_updates:
|
||||
|
||||
HG.client_controller.WriteSynchronous( 'content_updates', { service_key : [ content_update ] } )
|
||||
|
||||
|
||||
all_hashes = set()
|
||||
|
||||
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
|
||||
self._kept_hashes = [ media.GetHash() for media in self._kept ]
|
||||
all_hashes.update( self._deleted_hashes )
|
||||
all_hashes.update( self._kept_hashes )
|
||||
|
||||
service_keys_and_content_updates = []
|
||||
|
||||
reason = 'Deleted in Archive/Delete filter.'
|
||||
|
||||
for chunk_of_hashes in HydrusData.SplitListIntoChunks( self._deleted_hashes, 64 ):
|
||||
|
||||
service_keys_and_content_updates.append( ( CC.LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, chunk_of_hashes, reason = reason ) ) )
|
||||
|
||||
|
||||
service_keys_and_content_updates.append( ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) ) )
|
||||
|
||||
HG.client_controller.CallToThread( process_in_thread, service_keys_and_content_updates )
|
||||
|
||||
self._kept = set()
|
||||
self._deleted = set()
|
||||
|
||||
self._current_media = self._GetFirst() # so the pubsub on close is better
|
||||
|
||||
if HC.options[ 'remove_filtered_files' ]:
|
||||
|
||||
all_hashes = set()
|
||||
|
||||
all_hashes.update( self._deleted_hashes )
|
||||
all_hashes.update( self._kept_hashes )
|
||||
|
||||
HG.client_controller.pub( 'remove_media', self._page_key, all_hashes )
|
||||
|
||||
HG.client_controller.pub( 'remove_media', self._page_key, all_hashes )
|
||||
|
||||
|
||||
|
||||
CanvasMediaList._Close( self )
|
||||
|
||||
|
||||
return CanvasMediaList.TryToDoPreClose( self )
|
||||
|
||||
|
||||
def _Delete( self, media = None, reason = None, file_service_key = None ):
|
||||
|
@ -4196,8 +4197,14 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
self._deleted.add( self._current_media )
|
||||
|
||||
if self._current_media == self._GetLast(): self._Close()
|
||||
else: self._ShowNext()
|
||||
if self._current_media == self._GetLast():
|
||||
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
self._ShowNext()
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
@ -4211,15 +4218,21 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
self._kept.add( self._current_media )
|
||||
|
||||
if self._current_media == self._GetLast(): self._Close()
|
||||
else: self._ShowNext()
|
||||
if self._current_media == self._GetLast():
|
||||
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
self._ShowNext()
|
||||
|
||||
|
||||
|
||||
def _Skip( self ):
|
||||
|
||||
if self._current_media == self._GetLast():
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
|
@ -4330,7 +4343,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
if key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return, QC.Qt.Key_Escape ):
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
|
@ -4377,7 +4390,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
elif action == 'launch_the_archive_delete_filter':
|
||||
|
||||
self._Close()
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
|
@ -4563,8 +4576,8 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
|
|||
self._timer_slideshow_job = None
|
||||
self._timer_slideshow_interval = 0
|
||||
|
||||
self._widget_event_filter.EVT_LEFT_DCLICK( self.EventClose )
|
||||
self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventClose )
|
||||
self._widget_event_filter.EVT_LEFT_DCLICK( self.EventMouseClose )
|
||||
self._widget_event_filter.EVT_MIDDLE_DOWN( self.EventMouseClose )
|
||||
|
||||
if first_hash is None:
|
||||
|
||||
|
@ -4587,6 +4600,11 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
|
|||
HG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' )
|
||||
|
||||
|
||||
def EventMouseClose( self, event ):
|
||||
|
||||
self._TryToCloseWindow()
|
||||
|
||||
|
||||
def _PausePlaySlideshow( self ):
|
||||
|
||||
if self._timer_slideshow_job is not None:
|
||||
|
@ -4855,7 +4873,10 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
|
|||
if modifier == QC.Qt.NoModifier and key in CC.DELETE_KEYS: self._Delete()
|
||||
elif modifier == QC.Qt.ShiftModifier and key in CC.DELETE_KEYS: self._Undelete()
|
||||
elif modifier == QC.Qt.NoModifier and key in ( QC.Qt.Key_Space, ): self._PausePlaySlideshow()
|
||||
elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return, QC.Qt.Key_Escape ): self._Close()
|
||||
elif key in ( QC.Qt.Key_Enter, QC.Qt.Key_Return, QC.Qt.Key_Escape ):
|
||||
|
||||
self._TryToCloseWindow()
|
||||
|
||||
else:
|
||||
|
||||
CanvasMediaListNavigable.keyPressEvent( self, event )
|
||||
|
@ -4907,7 +4928,7 @@ class MediaContainer( QW.QWidget ):
|
|||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
# If I do not set this, macOS goes 100% CPU endless repaint events!
|
||||
# My guess is it is due to the borked layout
|
||||
# My guess is it due to the borked layout
|
||||
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
|
||||
|
||||
self._media = None
|
||||
|
|
|
@ -930,6 +930,7 @@ class ExportPatternButton( BetterButton ):
|
|||
|
||||
ClientGUIMenus.AppendSeparator( menu )
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'unique numerical file id - {file_id}', 'copy "{file_id}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{file_id}' )
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'the file\'s hash - {hash}', 'copy "{hash}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{hash}' )
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'all the file\'s tags - {tags}', 'copy "{tags}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{tags}' )
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'all the file\'s non-namespaced tags - {nn tags}', 'copy "{nn tags}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{nn tags}' )
|
||||
|
|
|
@ -649,7 +649,7 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._transformation_type = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
for t_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_REVERSE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ):
|
||||
for t_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_ENCODE, ClientParsing.STRING_TRANSFORMATION_DECODE, ClientParsing.STRING_TRANSFORMATION_REVERSE, ClientParsing.STRING_TRANSFORMATION_REGEX_SUB, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ):
|
||||
|
||||
self._transformation_type.addItem( ClientParsing.transformation_type_str_lookup[ t_type], t_type )
|
||||
|
||||
|
@ -660,7 +660,8 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._data_regex_pattern = QW.QLineEdit( self )
|
||||
self._data_regex_repl = QW.QLineEdit( self )
|
||||
self._data_date_link = ClientGUICommon.BetterHyperLink( self, 'link to date info', 'https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior' )
|
||||
self._data_timezone = ClientGUICommon.BetterChoice( self )
|
||||
self._data_timezone_decode = ClientGUICommon.BetterChoice( self )
|
||||
self._data_timezone_encode = ClientGUICommon.BetterChoice( self )
|
||||
self._data_timezone_offset = QP.MakeQSpinBox( self, min=-86400, max=86400 )
|
||||
|
||||
for e in ( 'hex', 'base64' ):
|
||||
|
@ -668,9 +669,12 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._data_encoding.addItem( e, e )
|
||||
|
||||
|
||||
self._data_timezone.addItem( 'GMT', HC.TIMEZONE_GMT )
|
||||
self._data_timezone.addItem( 'Local', HC.TIMEZONE_LOCAL )
|
||||
self._data_timezone.addItem( 'Offset', HC.TIMEZONE_OFFSET )
|
||||
self._data_timezone_decode.addItem( 'UTC', HC.TIMEZONE_GMT )
|
||||
self._data_timezone_decode.addItem( 'Local', HC.TIMEZONE_LOCAL )
|
||||
self._data_timezone_decode.addItem( 'Offset', HC.TIMEZONE_OFFSET )
|
||||
|
||||
self._data_timezone_encode.addItem( 'UTC', HC.TIMEZONE_GMT )
|
||||
self._data_timezone_encode.addItem( 'Local', HC.TIMEZONE_LOCAL )
|
||||
|
||||
#
|
||||
|
||||
|
@ -696,9 +700,16 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
( phrase, timezone_type, timezone_offset ) = data
|
||||
|
||||
self._data_text.setText( phrase )
|
||||
self._data_timezone.SetValue( timezone_type )
|
||||
self._data_timezone_decode.SetValue( timezone_type )
|
||||
self._data_timezone_offset.setValue( timezone_offset )
|
||||
|
||||
elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE:
|
||||
|
||||
( phrase, timezone_type ) = data
|
||||
|
||||
self._data_text.setText( phrase )
|
||||
self._data_timezone_encode.SetValue( timezone_type )
|
||||
|
||||
elif data is not None:
|
||||
|
||||
if isinstance( data, int ):
|
||||
|
@ -721,8 +732,9 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
rows.append( ( 'regex pattern: ', self._data_regex_pattern ) )
|
||||
rows.append( ( 'regex replacement: ', self._data_regex_repl ) )
|
||||
rows.append( ( 'date info: ', self._data_date_link ) )
|
||||
rows.append( ( 'date timezone: ', self._data_timezone ) )
|
||||
rows.append( ( 'date decode timezone: ', self._data_timezone_decode ) )
|
||||
rows.append( ( 'timezone offset: ', self._data_timezone_offset ) )
|
||||
rows.append( ( 'date encode timezone: ', self._data_timezone_encode ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
|
@ -737,7 +749,8 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._transformation_type.currentIndexChanged.connect( self._UpdateDataControls )
|
||||
self._data_encoding.currentIndexChanged.connect( self._UpdateDataControls )
|
||||
self._data_timezone.currentIndexChanged.connect( self._UpdateDataControls )
|
||||
self._data_timezone_decode.currentIndexChanged.connect( self._UpdateDataControls )
|
||||
self._data_timezone_encode.currentIndexChanged.connect( self._UpdateDataControls )
|
||||
|
||||
|
||||
def _UpdateDataControls( self ):
|
||||
|
@ -747,8 +760,9 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._data_encoding.setEnabled( False )
|
||||
self._data_regex_pattern.setEnabled( False )
|
||||
self._data_regex_repl.setEnabled( False )
|
||||
self._data_timezone.setEnabled( False )
|
||||
self._data_timezone_decode.setEnabled( False )
|
||||
self._data_timezone_offset.setEnabled( False )
|
||||
self._data_timezone_encode.setEnabled( False )
|
||||
|
||||
transformation_type = self._transformation_type.GetValue()
|
||||
|
||||
|
@ -756,19 +770,23 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._data_encoding.setEnabled( True )
|
||||
|
||||
elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE ):
|
||||
elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_PREPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_APPEND_TEXT, ClientParsing.STRING_TRANSFORMATION_DATE_DECODE, ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE ):
|
||||
|
||||
self._data_text.setEnabled( True )
|
||||
|
||||
if transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE:
|
||||
|
||||
self._data_timezone.setEnabled( True )
|
||||
self._data_timezone_decode.setEnabled( True )
|
||||
|
||||
if self._data_timezone.GetValue() == HC.TIMEZONE_OFFSET:
|
||||
if self._data_timezone_decode.GetValue() == HC.TIMEZONE_OFFSET:
|
||||
|
||||
self._data_timezone_offset.setEnabled( True )
|
||||
|
||||
|
||||
elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE:
|
||||
|
||||
self._data_timezone_encode.setEnabled( True )
|
||||
|
||||
|
||||
elif transformation_type in ( ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_REMOVE_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING, ClientParsing.STRING_TRANSFORMATION_CLIP_TEXT_FROM_END, ClientParsing.STRING_TRANSFORMATION_INTEGER_ADDITION ):
|
||||
|
||||
|
@ -816,11 +834,18 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_DECODE:
|
||||
|
||||
phrase = self._data_text.text()
|
||||
timezone_time = self._data_timezone.GetValue()
|
||||
timezone_time = self._data_timezone_decode.GetValue()
|
||||
timezone_offset = self._data_timezone_offset.value()
|
||||
|
||||
data = ( phrase, timezone_time, timezone_offset )
|
||||
|
||||
elif transformation_type == ClientParsing.STRING_TRANSFORMATION_DATE_ENCODE:
|
||||
|
||||
phrase = self._data_text.text()
|
||||
timezone_time = self._data_timezone_encode.GetValue()
|
||||
|
||||
data = ( phrase, timezone_time )
|
||||
|
||||
else:
|
||||
|
||||
data = None
|
||||
|
|
|
@ -102,6 +102,10 @@ class Dialog( QP.Dialog ):
|
|||
|
||||
QP.Dialog.__init__( self, parent )
|
||||
|
||||
self.setWindowFlags( style )
|
||||
|
||||
self.setWindowTitle( title )
|
||||
|
||||
if parent is not None and position == 'topleft':
|
||||
|
||||
parent_tlp = self.parentWidget().window()
|
||||
|
@ -115,9 +119,6 @@ class Dialog( QP.Dialog ):
|
|||
pos = None
|
||||
|
||||
|
||||
self.setWindowTitle( title )
|
||||
self.setWindowFlags( style )
|
||||
|
||||
if pos: self.move( pos )
|
||||
|
||||
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
|
||||
|
@ -193,9 +194,9 @@ class DialogChooseNewServiceMethod( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
self._should_register = False
|
||||
|
||||
|
@ -281,9 +282,9 @@ class DialogGenerateNewAccounts( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._ok.setFocus, QC.Qt.OtherFocusReason )
|
||||
|
||||
|
@ -437,11 +438,11 @@ class DialogInputLocalBooruShare( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 350 )
|
||||
size_hint.setWidth( max( size_hint.width(), 350 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._ok.setFocus, QC.Qt.OtherFocusReason)
|
||||
|
||||
|
@ -550,9 +551,9 @@ class DialogInputNamespaceRegex( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._ok.setFocus, QC.Qt.OtherFocusReason)
|
||||
|
||||
|
@ -640,11 +641,11 @@ class DialogInputTags( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 300 )
|
||||
size_hint.setWidth( max( size_hint.width(), 300 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._tag_box.setFocus, QC.Qt.OtherFocusReason)
|
||||
|
||||
|
@ -736,9 +737,9 @@ class DialogInputUPnPMapping( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._ok.setFocus, QC.Qt.OtherFocusReason)
|
||||
|
||||
|
@ -889,9 +890,9 @@ class DialogModifyAccounts( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( self._exit.setFocus, QC.Qt.OtherFocusReason)
|
||||
|
||||
|
@ -1012,12 +1013,12 @@ class DialogSelectFromURLTree( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 640 )
|
||||
y = max( y, 640 )
|
||||
size_hint.setWidth( max( size_hint.width(), 640 ) )
|
||||
size_hint.setHeight( max( size_hint.height(), 640 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
|
||||
def _AddDirectory( self, root, children ):
|
||||
|
@ -1129,16 +1130,16 @@ class DialogSelectImageboard( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
if x < 320: x = 320
|
||||
if y < 640: y = 640
|
||||
size_hint.setWidth( max( size_hint.width(), 320 ) )
|
||||
size_hint.setHeight( max( size_hint.height(), 640 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
|
||||
def EventActivate( self, item, column ):
|
||||
|
||||
|
||||
data_object = item.data( 0, QC.Qt.UserRole )
|
||||
|
||||
if data_object is None: item.setExpanded( not item.isExpanded() )
|
||||
|
@ -1228,11 +1229,11 @@ class DialogTextEntry( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 250 )
|
||||
size_hint.setWidth( max( size_hint.width(), 250 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
|
||||
def _CheckText( self ):
|
||||
|
@ -1329,11 +1330,11 @@ class DialogYesYesNo( Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 250 )
|
||||
size_hint.setWidth( max( size_hint.width(), 250 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
QP.CallAfter( yes_buttons[0].setFocus, QC.Qt.OtherFocusReason )
|
||||
|
||||
|
|
|
@ -111,9 +111,9 @@ class DialogManageRatings( ClientGUIDialogs.Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
#
|
||||
|
||||
|
@ -354,11 +354,11 @@ class DialogManageUPnP( ClientGUIDialogs.Dialog ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
x = max( x, 760 )
|
||||
size_hint.setWidth( max( size_hint.width(), 760 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -42,7 +42,9 @@ def GetFinishFilteringAnswer( win, label ):
|
|||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
return ( dlg.exec(), dlg.WasCancelled() )
|
||||
result = ( dlg.exec(), dlg.WasCancelled() )
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def GetInterstitialFilteringAnswer( win, label ):
|
||||
|
@ -53,7 +55,9 @@ def GetInterstitialFilteringAnswer( win, label ):
|
|||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
return dlg.exec()
|
||||
result = dlg.exec()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def GetYesNo( win, message, title = 'Are you sure?', yes_label = 'yes', no_label = 'no', auto_yes_time = None, auto_no_time = None, check_for_cancelled = False ):
|
||||
|
|
|
@ -396,7 +396,7 @@ If you select synchronise, be careful!'''
|
|||
run_regularly = self._run_regularly.isChecked()
|
||||
|
||||
period = self._period.GetValue()
|
||||
|
||||
|
||||
if self._path.GetPath() in ( '', None ):
|
||||
|
||||
raise HydrusExceptions.VetoException( 'You must enter a folder path to export to!' )
|
||||
|
|
|
@ -52,12 +52,12 @@ class ShowKeys( ClientGUITopLevelWindows.Frame ):
|
|||
|
||||
self.setLayout( vbox )
|
||||
|
||||
( x, y ) = QP.GetEffectiveMinSize( self )
|
||||
size_hint = self.sizeHint()
|
||||
|
||||
if x < 500: x = 500
|
||||
if y < 200: y = 200
|
||||
size_hint.setWidth( max( size_hint.width(), 500 ) )
|
||||
size_hint.setHeight( max( size_hint.height(), 200 ) )
|
||||
|
||||
QP.SetInitialSize( self, (x,y) )
|
||||
QP.SetInitialSize( self, size_hint )
|
||||
|
||||
self.show()
|
||||
|
||||
|
|
|
@ -355,9 +355,9 @@ def IsQtAncestor( child, ancestor, through_tlws = False ):
|
|||
|
||||
def NotebookScreenToHitTest( notebook, screen_position ):
|
||||
|
||||
position = notebook.mapFromGlobal( screen_position )
|
||||
tab_pos = notebook.tabBar().mapFromGlobal( screen_position )
|
||||
|
||||
return notebook.tabBar().tabAt( position )
|
||||
return notebook.tabBar().tabAt( tab_pos )
|
||||
|
||||
def SetBitmapButtonBitmap( button, bitmap ):
|
||||
|
||||
|
|
|
@ -28,11 +28,13 @@ class FullscreenHoverFrame( QW.QFrame ):
|
|||
QW.QFrame.__init__( self, parent )
|
||||
|
||||
self.setWindowFlags( QC.Qt.FramelessWindowHint | QC.Qt.Tool )
|
||||
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised )
|
||||
self.setLineWidth( 2 )
|
||||
|
||||
self.setAttribute( QC.Qt.WA_ShowWithoutActivating )
|
||||
self.setAttribute( QC.Qt.WA_DeleteOnClose )
|
||||
|
||||
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised )
|
||||
self.setLineWidth( 2 )
|
||||
|
||||
self._my_canvas = my_canvas
|
||||
self._canvas_key = canvas_key
|
||||
self._current_media = None
|
||||
|
@ -1163,7 +1165,7 @@ class FullscreenHoverFrameTopRight( FullscreenHoverFrame ):
|
|||
|
||||
self._last_seen_urls = list( urls )
|
||||
|
||||
QP.ClearLayout( self._urls_vbox, delete_widgets=True )
|
||||
QP.ClearLayout( self._urls_vbox, delete_widgets = True )
|
||||
|
||||
url_tuples = HG.client_controller.network_engine.domain_manager.ConvertURLsToMediaViewerTuples( urls )
|
||||
|
||||
|
@ -1172,7 +1174,10 @@ class FullscreenHoverFrameTopRight( FullscreenHoverFrame ):
|
|||
link = ClientGUICommon.BetterHyperLink( self, display_string, url )
|
||||
|
||||
QP.AddToLayout( self._urls_vbox, link, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self.layout().addStretch( 1 )
|
||||
|
||||
|
||||
|
||||
|
||||
self._SizeAndPosition()
|
||||
|
@ -1180,7 +1185,7 @@ class FullscreenHoverFrameTopRight( FullscreenHoverFrame ):
|
|||
|
||||
def wheelEvent( self, event ):
|
||||
|
||||
QW.QApplication.sendEvent(self.parentWidget(), event)
|
||||
QW.QApplication.sendEvent( self.parentWidget(), event )
|
||||
|
||||
|
||||
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
||||
|
|
|
@ -1278,13 +1278,13 @@ class ListBox( QW.QScrollArea ):
|
|||
|
||||
( x, y ) = ( x_start, current_index * text_height )
|
||||
|
||||
( text_width, text_height ) = painter.fontMetrics().size( QC.Qt.TextSingleLine, text ).toTuple()
|
||||
( this_text_width, this_text_height ) = painter.fontMetrics().size( QC.Qt.TextSingleLine, text ).toTuple()
|
||||
|
||||
painter.drawText( QC.QRectF( x, y, text_width, text_height ), text )
|
||||
painter.drawText( QC.QRectF( x, y, this_text_width, this_text_height ), text )
|
||||
|
||||
if there_is_more_than_one_text:
|
||||
|
||||
x_start += text_width
|
||||
x_start += this_text_width
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ def SafeNoneInt( value ):
|
|||
def SafeNoneStr( value ):
|
||||
|
||||
return '' if value is None else value
|
||||
|
||||
|
||||
class BetterListCtrl( QW.QTreeWidget ):
|
||||
|
||||
|
@ -74,8 +73,10 @@ class BetterListCtrl( QW.QTreeWidget ):
|
|||
|
||||
|
||||
self.headerItem().setText( i, name )
|
||||
|
||||
self.setColumnWidth( i, width )
|
||||
|
||||
|
||||
# Technically this is the previous behavior, but the two commented lines might work better in some cases (?)
|
||||
self.header().setStretchLastSection( False )
|
||||
self.header().setSectionResizeMode( resize_column - 1 , QW.QHeaderView.Stretch )
|
||||
|
@ -110,8 +111,17 @@ class BetterListCtrl( QW.QTreeWidget ):
|
|||
|
||||
for i in range( len( display_tuple ) ):
|
||||
|
||||
append_item.setText( i, display_tuple[i] )
|
||||
text = display_tuple[i]
|
||||
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
append_item.setText( i, text )
|
||||
append_item.setToolTip( i, text )
|
||||
|
||||
|
||||
self.addTopLevelItem( append_item )
|
||||
|
||||
index = self.topLevelItemCount() - 1
|
||||
|
@ -241,11 +251,19 @@ class BetterListCtrl( QW.QTreeWidget ):
|
|||
|
||||
for ( column_index, value ) in enumerate( display_tuple ):
|
||||
|
||||
existing_value = self.topLevelItem( index ).text( column_index )
|
||||
if len( value ) > 0:
|
||||
|
||||
value = value.splitlines()[0]
|
||||
|
||||
|
||||
tree_widget_item = self.topLevelItem( index )
|
||||
|
||||
existing_value = tree_widget_item.text( column_index )
|
||||
|
||||
if existing_value != value:
|
||||
|
||||
self.topLevelItem( index ).setText( column_index, value )
|
||||
tree_widget_item.setText( column_index, value )
|
||||
tree_widget_item.setToolTip( column_index, value )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1780,12 +1780,16 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
|
||||
position = event.pos()
|
||||
|
||||
tab_index = self.tabBar().tabAt( position )
|
||||
screen_pos = self.mapToGlobal( position )
|
||||
|
||||
tab_pos = self.tabBar().mapFromGlobal( screen_pos )
|
||||
|
||||
tab_index = self.tabBar().tabAt( tab_pos )
|
||||
|
||||
if tab_index == -1:
|
||||
|
||||
self.ChooseNewPage()
|
||||
|
||||
|
||||
else:
|
||||
|
||||
return True # was: event.ignore()
|
||||
|
@ -1815,7 +1819,11 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
|
||||
position = event.pos()
|
||||
|
||||
tab_index = self.tabBar().tabAt( position )
|
||||
screen_pos = self.mapToGlobal( position )
|
||||
|
||||
tab_pos = self.tabBar().mapFromGlobal( screen_pos )
|
||||
|
||||
tab_index = self.tabBar().tabAt( tab_pos )
|
||||
|
||||
if tab_index == -1:
|
||||
|
||||
|
|
|
@ -834,8 +834,15 @@ class ReviewServicePanel( QW.QWidget ):
|
|||
self._address = ClientGUICommon.BetterStaticText( self )
|
||||
self._functional = ClientGUICommon.BetterStaticText( self )
|
||||
self._bandwidth_summary = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
self._bandwidth_panel = QW.QWidget( self )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
self._bandwidth_panel.setLayout( vbox )
|
||||
|
||||
self._rule_widgets = []
|
||||
|
||||
#
|
||||
|
||||
self._Refresh()
|
||||
|
@ -871,24 +878,29 @@ class ReviewServicePanel( QW.QWidget ):
|
|||
|
||||
self._bandwidth_summary.setText( bandwidth_summary )
|
||||
|
||||
QP.DestroyChildren( self._bandwidth_panel )
|
||||
vbox = self._bandwidth_panel.layout()
|
||||
|
||||
b_gauges = []
|
||||
for rule_widget in self._rule_widgets:
|
||||
|
||||
vbox.removeWidget( rule_widget )
|
||||
|
||||
rule_widget.deleteLater()
|
||||
|
||||
|
||||
self._rule_widgets = []
|
||||
|
||||
bandwidth_rows = self._service.GetBandwidthStringsAndGaugeTuples()
|
||||
|
||||
b_vbox = QP.VBoxLayout()
|
||||
|
||||
for ( status, ( value, range ) ) in bandwidth_rows:
|
||||
|
||||
gauge = ClientGUICommon.TextAndGauge( self._bandwidth_panel )
|
||||
|
||||
gauge.SetValue( status, value, range )
|
||||
|
||||
QP.AddToLayout( b_vbox, gauge, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
self._rule_widgets.append( gauge )
|
||||
|
||||
QP.AddToLayout( vbox, gauge, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
|
||||
self._bandwidth_panel.setLayout( b_vbox )
|
||||
|
||||
|
||||
def ServiceUpdated( self, service ):
|
||||
|
@ -916,8 +928,15 @@ class ReviewServicePanel( QW.QWidget ):
|
|||
self._status_st = ClientGUICommon.BetterStaticText( self )
|
||||
self._next_sync_st = ClientGUICommon.BetterStaticText( self )
|
||||
self._bandwidth_summary = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
self._bandwidth_panel = QW.QWidget( self )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
self._bandwidth_panel.setLayout( vbox )
|
||||
|
||||
self._rule_widgets = []
|
||||
|
||||
self._refresh_account_button = ClientGUICommon.BetterButton( self, 'refresh account', self._RefreshAccount )
|
||||
self._copy_account_key_button = ClientGUICommon.BetterButton( self, 'copy account key', self._CopyAccountKey )
|
||||
self._permissions_button = ClientGUICommon.MenuButton( self, 'see special permissions', [] )
|
||||
|
@ -986,24 +1005,29 @@ class ReviewServicePanel( QW.QWidget ):
|
|||
|
||||
self._bandwidth_summary.setText( bandwidth_summary )
|
||||
|
||||
QP.DestroyChildren( self._bandwidth_panel )
|
||||
vbox = self._bandwidth_panel.layout()
|
||||
|
||||
b_gauges = []
|
||||
for rule_widget in self._rule_widgets:
|
||||
|
||||
vbox.removeWidget( rule_widget )
|
||||
|
||||
rule_widget.deleteLater()
|
||||
|
||||
|
||||
self._rule_widgets = []
|
||||
|
||||
bandwidth_rows = account.GetBandwidthStringsAndGaugeTuples()
|
||||
|
||||
b_vbox = QP.VBoxLayout()
|
||||
|
||||
for ( status, ( value, range ) ) in bandwidth_rows:
|
||||
|
||||
gauge = ClientGUICommon.TextAndGauge( self._bandwidth_panel )
|
||||
|
||||
gauge.SetValue( status, value, range )
|
||||
|
||||
QP.AddToLayout( b_vbox, gauge, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
self._rule_widgets.append( gauge )
|
||||
|
||||
QP.AddToLayout( vbox, gauge, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
|
||||
self._bandwidth_panel.setLayout( b_vbox )
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -636,7 +636,7 @@ class EditCompoundFormulaPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
|
||||
|
||||
panel = EditFormulaPanel( dlg, existing_formula, self._test_panel.GetTestContext )
|
||||
panel = EditFormulaPanel( dlg, existing_formula, self._test_panel.GetTestContextForChild )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
@ -681,7 +681,7 @@ class EditCompoundFormulaPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self, 'edit formula', frame_key = 'deeply_nested_dialog' ) as dlg:
|
||||
|
||||
panel = EditFormulaPanel( dlg, old_formula, self._test_panel.GetTestContext )
|
||||
panel = EditFormulaPanel( dlg, old_formula, self._test_panel.GetTestContextForChild )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
@ -1823,6 +1823,14 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
test_panel = ClientGUICommon.StaticBox( self, 'test' )
|
||||
|
||||
QP.SetBackgroundColour( test_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
||||
self._test_panel = TestPanel( test_panel, self.GetValue, test_context = test_context )
|
||||
|
||||
#
|
||||
|
||||
self._edit_panel = ClientGUICommon.StaticBox( self, 'edit' )
|
||||
|
||||
QP.SetBackgroundColour( self._edit_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
@ -1909,15 +1917,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
( name, content_type, formula, sort_type, sort_asc, additional_info ) = content_parser.ToTuple()
|
||||
|
||||
self._formula = EditFormulaPanel( self._edit_panel, formula, self.GetTestContext )
|
||||
|
||||
#
|
||||
|
||||
test_panel = ClientGUICommon.StaticBox( self, 'test' )
|
||||
|
||||
QP.SetBackgroundColour( test_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
||||
self._test_panel = TestPanel( test_panel, self.GetValue, test_context = test_context )
|
||||
self._formula = EditFormulaPanel( self._edit_panel, formula, self._test_panel.GetTestContextForChild )
|
||||
|
||||
#
|
||||
|
||||
|
@ -2164,11 +2164,6 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
|
||||
|
||||
def GetTestContext( self ):
|
||||
|
||||
return self._test_panel.GetTestContext()
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
name = self._name.text()
|
||||
|
@ -2965,19 +2960,6 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
|
||||
content_parsers_panel = QW.QWidget( edit_notebook )
|
||||
|
||||
QP.SetBackgroundColour( content_parsers_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
||||
#
|
||||
|
||||
permitted_content_types = [ HC.CONTENT_TYPE_URLS, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_TYPE_HASH, HC.CONTENT_TYPE_TIMESTAMP, HC.CONTENT_TYPE_TITLE, HC.CONTENT_TYPE_VETO ]
|
||||
|
||||
self._content_parsers = EditContentParsersPanel( content_parsers_panel, self.GetTestContext, permitted_content_types )
|
||||
|
||||
#
|
||||
|
||||
test_panel = ClientGUICommon.StaticBox( self, 'test' )
|
||||
|
||||
QP.SetBackgroundColour( test_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
@ -2998,6 +2980,18 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._test_panel = TestPanelPageParserSubsidiary( test_panel, self.GetValue, self._string_converter.GetValue, self.GetFormula, test_context = test_context )
|
||||
|
||||
|
||||
#
|
||||
|
||||
content_parsers_panel = QW.QWidget( edit_notebook )
|
||||
|
||||
QP.SetBackgroundColour( content_parsers_panel, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
||||
#
|
||||
|
||||
permitted_content_types = [ HC.CONTENT_TYPE_URLS, HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_TYPE_HASH, HC.CONTENT_TYPE_TIMESTAMP, HC.CONTENT_TYPE_TITLE, HC.CONTENT_TYPE_VETO ]
|
||||
|
||||
self._content_parsers = EditContentParsersPanel( content_parsers_panel, self._test_panel.GetTestContextForChild, permitted_content_types )
|
||||
|
||||
#
|
||||
|
||||
name = parser.GetName()
|
||||
|
@ -3139,7 +3133,7 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self, 'edit sub page parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
||||
|
||||
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_context = self._test_panel.GetTestContext() )
|
||||
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_context = self._test_panel.GetTestContextForChild() )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
@ -3207,7 +3201,7 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self, 'edit sub page parser', frame_key = 'deeply_nested_dialog' ) as dlg:
|
||||
|
||||
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_context = self._test_panel.GetTestContext() )
|
||||
panel = EditPageParserPanel( dlg, page_parser, formula = formula, test_context = self._test_panel.GetTestContextForChild() )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
|
@ -3294,11 +3288,6 @@ class EditPageParserPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
HG.client_controller.CallToThread( wait_and_do_it, network_job )
|
||||
|
||||
|
||||
def GetTestContext( self ):
|
||||
|
||||
return self._test_panel.GetTestContext()
|
||||
|
||||
|
||||
def GetFormula( self ):
|
||||
|
||||
return self._formula.GetValue()
|
||||
|
@ -4503,6 +4492,11 @@ class TestPanel( QW.QWidget ):
|
|||
return ( example_parsing_context, self._example_data_raw )
|
||||
|
||||
|
||||
def GetTestContextForChild( self ):
|
||||
|
||||
return self.GetTestContext()
|
||||
|
||||
|
||||
def SetExampleData( self, example_data ):
|
||||
|
||||
self._SetExampleData( example_data )
|
||||
|
@ -4662,7 +4656,7 @@ class TestPanelPageParser( TestPanel ):
|
|||
self._example_data_post_conversion_preview.setPlainText( preview )
|
||||
|
||||
|
||||
def GetTestContext( self ):
|
||||
def GetTestContextForChild( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
|
@ -4775,7 +4769,7 @@ class TestPanelPageParserSubsidiary( TestPanelPageParser ):
|
|||
self._example_data_post_separation_preview.setPlainText( preview )
|
||||
|
||||
|
||||
def GetTestContext( self ):
|
||||
def GetTestContextForChild( self ):
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
|
||||
|
@ -4801,15 +4795,15 @@ class TestPanelPageParserSubsidiary( TestPanelPageParser ):
|
|||
|
||||
try:
|
||||
|
||||
example_parsing_context = self._example_parsing_context.GetValue()
|
||||
( example_parsing_context, example_data ) = self.GetTestContext()
|
||||
|
||||
if formula is None:
|
||||
|
||||
posts = [ self._example_data_raw ]
|
||||
posts = [ example_data ]
|
||||
|
||||
else:
|
||||
|
||||
posts = formula.Parse( example_parsing_context, self._example_data_raw )
|
||||
posts = formula.Parse( example_parsing_context, example_data )
|
||||
|
||||
|
||||
pretty_texts = []
|
||||
|
|
|
@ -536,9 +536,11 @@ class PopupMessageManager( QW.QWidget ):
|
|||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
self.setWindowFlags( QC.Qt.Tool | QC.Qt.FramelessWindowHint )
|
||||
self.setSizePolicy( QW.QSizePolicy.MinimumExpanding, QW.QSizePolicy.Preferred )
|
||||
|
||||
self.setAttribute( QC.Qt.WA_ShowWithoutActivating )
|
||||
|
||||
self.setSizePolicy( QW.QSizePolicy.MinimumExpanding, QW.QSizePolicy.Preferred )
|
||||
|
||||
QP.SetBackgroundColour( self, QP.GetSystemColour( QG.QPalette.Button ) )
|
||||
|
||||
self._last_best_size_i_fit_on = ( 0, 0 )
|
||||
|
@ -711,25 +713,32 @@ class PopupMessageManager( QW.QWidget ):
|
|||
|
||||
try:
|
||||
|
||||
parent = self.parentWidget()
|
||||
gui_frame = self.parentWidget()
|
||||
|
||||
possibly_on_hidden_virtual_desktop = not ClientGUITopLevelWindows.MouseIsOnMyDisplay( parent )
|
||||
possibly_on_hidden_virtual_desktop = not ClientGUITopLevelWindows.MouseIsOnMyDisplay( gui_frame )
|
||||
|
||||
going_to_bug_out_at_hide_or_show = possibly_on_hidden_virtual_desktop
|
||||
|
||||
current_focus_tlp = QW.QApplication.activeWindow()
|
||||
|
||||
main_gui_is_active = current_focus_tlp in ( self, parent )
|
||||
main_gui_is_active = current_focus_tlp in ( self, gui_frame )
|
||||
|
||||
on_top_frame_is_active = False
|
||||
|
||||
if not main_gui_is_active:
|
||||
if not main_gui_is_active and current_focus_tlp is not None:
|
||||
|
||||
c_f_tlp_is_child_frame_of_main_gui = current_focus_tlp is not None and current_focus_tlp.parentWidget() == parent
|
||||
c_f_tlp_is_resizing_frame = isinstance( current_focus_tlp, ClientGUITopLevelWindows.FrameThatResizes )
|
||||
|
||||
if c_f_tlp_is_child_frame_of_main_gui:
|
||||
frame_parent = current_focus_tlp.parentWidget()
|
||||
|
||||
if c_f_tlp_is_resizing_frame and frame_parent is not None:
|
||||
|
||||
on_top_frame_is_active = True
|
||||
c_f_tlp_is_child_frame_of_main_gui = frame_parent.window() == gui_frame
|
||||
|
||||
if c_f_tlp_is_child_frame_of_main_gui:
|
||||
|
||||
on_top_frame_is_active = True
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -739,16 +748,16 @@ class PopupMessageManager( QW.QWidget ):
|
|||
|
||||
if there_is_stuff_to_display:
|
||||
|
||||
( parent_width, parent_height ) = parent.size().toTuple()
|
||||
( parent_width, parent_height ) = gui_frame.size().toTuple()
|
||||
|
||||
( my_width, my_height ) = self.size().toTuple()
|
||||
|
||||
my_x = ( parent_width - my_width ) - 20
|
||||
my_y = ( parent_height - my_height ) - 25
|
||||
|
||||
if parent.isVisible():
|
||||
if gui_frame.isVisible():
|
||||
|
||||
my_position = ClientGUIFunctions.ClientToScreen( parent, ( my_x, my_y ) )
|
||||
my_position = ClientGUIFunctions.ClientToScreen( gui_frame, ( my_x, my_y ) )
|
||||
|
||||
if my_position != self.pos():
|
||||
|
||||
|
@ -758,7 +767,7 @@ class PopupMessageManager( QW.QWidget ):
|
|||
|
||||
# Unhiding tends to raise the main gui tlp, which is annoying if a media viewer window has focus
|
||||
# Qt port note: the on_top_frame_is_active part was uncommented originally, but it IS annoying since it leads to flickering (e.g. open the options window with this uncommented to see it in action)
|
||||
show_is_not_annoying = main_gui_is_active or self._DisplayingError() # or on_top_frame_is_active
|
||||
show_is_not_annoying = main_gui_is_active or self._DisplayingError() or on_top_frame_is_active
|
||||
|
||||
ok_to_show = show_is_not_annoying and not going_to_bug_out_at_hide_or_show
|
||||
|
||||
|
@ -783,7 +792,7 @@ class PopupMessageManager( QW.QWidget ):
|
|||
|
||||
HydrusData.Print( traceback.format_exc() )
|
||||
|
||||
QW.QMessageBox.critical( HG.client_controller.gui, 'Error', text )
|
||||
QW.QMessageBox.critical( gui_frame, 'Error', text )
|
||||
|
||||
self._update_job.Cancel()
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from . import HydrusConstants as HC
|
|||
from . import HydrusData
|
||||
from . import HydrusGlobals as HG
|
||||
from . import HydrusText
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
from qtpy import QtCore as QC
|
||||
|
@ -29,6 +30,7 @@ class InputFileSystemPredicate( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._predicates = []
|
||||
|
||||
label = None
|
||||
editable_pred_panel_classes = []
|
||||
static_pred_buttons = []
|
||||
|
||||
|
@ -78,6 +80,10 @@ class InputFileSystemPredicate( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
elif predicate_type == HC.PREDICATE_TYPE_SYSTEM_LIMIT:
|
||||
|
||||
label = 'Please note that, for now, system:limit generally samples randomly from the full search results.'
|
||||
label += os.linesep
|
||||
label += 'It will not clip the n largest/longest/most tagged files given a particular file sort.'
|
||||
|
||||
static_pred_buttons.append( StaticSystemPredicateButton( self, ( ClientSearch.Predicate( HC.PREDICATE_TYPE_SYSTEM_LIMIT, 64 ), ) ) )
|
||||
static_pred_buttons.append( StaticSystemPredicateButton( self, ( ClientSearch.Predicate( HC.PREDICATE_TYPE_SYSTEM_LIMIT, 256 ), ) ) )
|
||||
static_pred_buttons.append( StaticSystemPredicateButton( self, ( ClientSearch.Predicate( HC.PREDICATE_TYPE_SYSTEM_LIMIT, 1024 ), ) ) )
|
||||
|
@ -135,6 +141,13 @@ class InputFileSystemPredicate( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
if label is not None:
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self, label = label )
|
||||
|
||||
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
|
||||
for button in static_pred_buttons:
|
||||
|
||||
QP.AddToLayout( vbox, button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
|
|
@ -50,6 +50,7 @@ class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPane
|
|||
|
||||
parent.SetCancelled( True )
|
||||
parent.done( QW.QDialog.Rejected )
|
||||
|
||||
|
||||
self._back = ClientGUICommon.BetterButton( self, 'back to filtering', cancel_callback, parent )
|
||||
|
||||
|
|
|
@ -3866,7 +3866,7 @@ But if 2 is--and is also perhaps accompanied by many 'could not parse' errors--t
|
|||
file_velocity = checker_options.GetRawCurrentVelocity( query.GetFileSeedCache(), last_check_time )
|
||||
pretty_file_velocity = checker_options.GetPrettyCurrentVelocity( query.GetFileSeedCache(), last_check_time, no_prefix = True )
|
||||
|
||||
estimate = self._original_subscription.GetBandwidthWaitingEstimate( query )
|
||||
estimate = query.GetBandwidthWaitingEstimate( self._original_subscription.GetName() )
|
||||
|
||||
if estimate == 0:
|
||||
|
||||
|
@ -5063,7 +5063,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
for subscription in self._subscriptions.GetData():
|
||||
|
||||
if subscription.HasQuerySearchText( search_text ):
|
||||
if subscription.HasQuerySearchTextFragment( search_text ):
|
||||
|
||||
selectee_subscriptions.append( subscription )
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ from . import ClientMedia
|
|||
from . import ClientRatings
|
||||
from . import ClientSerialisable
|
||||
from . import ClientServices
|
||||
from . import ClientGUIStyle
|
||||
from . import ClientGUITime
|
||||
import collections
|
||||
from . import HydrusConstants as HC
|
||||
|
@ -1176,7 +1177,7 @@ class ManageClientServicesPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
if self._allow_non_local_connections.isChecked():
|
||||
|
||||
self._upnp.setValue( None )
|
||||
self._upnp.SetValue( None )
|
||||
|
||||
self._upnp.setEnabled( False )
|
||||
|
||||
|
@ -1524,6 +1525,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
self._listbook.AddPage( 'downloading', 'downloading', self._DownloadingPanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'duplicates', 'duplicates', self._DuplicatesPanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'importing', 'importing', self._ImportingPanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'style', 'style', self._StylePanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'tag presentation', 'tag presentation', self._TagPresentationPanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'tag suggestions', 'tag suggestions', self._TagSuggestionsPanel( self._listbook, self._new_options ) )
|
||||
self._listbook.AddPage( 'tags', 'tags', self._TagsPanel( self._listbook, self._new_options ) )
|
||||
|
@ -3818,6 +3820,112 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
|
||||
|
||||
class _StylePanel( QW.QWidget ):
|
||||
|
||||
def __init__( self, parent, new_options ):
|
||||
|
||||
QW.QWidget.__init__( self, parent )
|
||||
|
||||
self._new_options = new_options
|
||||
|
||||
#
|
||||
|
||||
self._qt_style_name = ClientGUICommon.BetterChoice( self )
|
||||
self._qt_stylesheet_name = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._qt_style_name.addItem( 'use default ("{}")'.format( ClientGUIStyle.ORIGINAL_STYLE ), None )
|
||||
|
||||
try:
|
||||
|
||||
for name in ClientGUIStyle.GetAvailableStyles():
|
||||
|
||||
self._qt_style_name.addItem( name, name )
|
||||
|
||||
|
||||
except HydrusExceptions.DataMissing as e:
|
||||
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
|
||||
self._qt_stylesheet_name.addItem( 'use default', None )
|
||||
|
||||
try:
|
||||
|
||||
for name in ClientGUIStyle.GetAvailableStylesheets():
|
||||
|
||||
self._qt_stylesheet_name.addItem( name, name )
|
||||
|
||||
|
||||
except HydrusExceptions.DataMissing as e:
|
||||
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
|
||||
#
|
||||
|
||||
self._qt_style_name.SetValue( self._new_options.GetNoneableString( 'qt_style_name' ) )
|
||||
self._qt_stylesheet_name.SetValue( self._new_options.GetNoneableString( 'qt_stylesheet_name' ) )
|
||||
|
||||
#
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
#
|
||||
|
||||
text = 'This is experimental! Some custom colours in hydrus do not play well with QSS theming yet! All feedback on errors and any preferred systems is appreciated.'
|
||||
text += os.linesep * 2
|
||||
text += 'The current styles are what your Qt has available, the stylesheets are what .css and .qss files are currently in install_dir/static/qss.'
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self, label = text )
|
||||
|
||||
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'Qt style:', self._qt_style_name ) )
|
||||
rows.append( ( 'Qt stylesheet:', self._qt_stylesheet_name ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
self.setLayout( vbox )
|
||||
|
||||
self._qt_style_name.currentIndexChanged.connect( self.StyleChanged )
|
||||
self._qt_stylesheet_name.currentIndexChanged.connect( self.StyleChanged )
|
||||
|
||||
|
||||
def StyleChanged( self ):
|
||||
|
||||
qt_style_name = self._qt_style_name.GetValue()
|
||||
qt_stylesheet_name = self._qt_stylesheet_name.GetValue()
|
||||
|
||||
if qt_style_name is None:
|
||||
|
||||
ClientGUIStyle.SetStyle( ClientGUIStyle.ORIGINAL_STYLE )
|
||||
|
||||
else:
|
||||
|
||||
ClientGUIStyle.SetStyle( qt_style_name )
|
||||
|
||||
|
||||
if qt_stylesheet_name is None:
|
||||
|
||||
ClientGUIStyle.ClearStylesheet()
|
||||
|
||||
else:
|
||||
|
||||
ClientGUIStyle.SetStylesheet( qt_stylesheet_name )
|
||||
|
||||
|
||||
|
||||
def UpdateOptions( self ):
|
||||
|
||||
self._new_options.SetNoneableString( 'qt_style_name', self._qt_style_name.GetValue() )
|
||||
self._new_options.SetNoneableString( 'qt_stylesheet_name', self._qt_stylesheet_name.GetValue() )
|
||||
|
||||
|
||||
|
||||
class _TagsPanel( QW.QWidget ):
|
||||
|
||||
def __init__( self, parent, new_options ):
|
||||
|
|
|
@ -1880,6 +1880,7 @@ class ReviewAllBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
help += 'Please note that this system bases its calendar dates on UTC/GMT time (it helps servers and clients around the world stay in sync a bit easier). This has no bearing on what, for instance, the \'past 24 hours\' means, but monthly transitions may occur a few hours off whatever your midnight is.'
|
||||
help += os.linesep * 2
|
||||
help += 'If you do not understand what is going on here, you can safely leave it alone. The default settings make for a _reasonable_ and polite profile that will not accidentally cause you to download way too much in one go or piss off servers by being too aggressive. If you want to throttle your client, the simplest way is to add a simple rule like \'500MB per day\' to the global context.'
|
||||
|
||||
QW.QMessageBox.information( self, 'Information', help )
|
||||
|
||||
|
||||
|
@ -3433,8 +3434,6 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
self._bandwidth_rules = self._controller.network_engine.bandwidth_manager.GetRules( self._network_context )
|
||||
self._bandwidth_tracker = self._controller.network_engine.bandwidth_manager.GetTracker( self._network_context )
|
||||
|
||||
self._last_fetched_rule_rows = set()
|
||||
|
||||
#
|
||||
|
||||
info_panel = ClientGUICommon.StaticBox( self, 'description' )
|
||||
|
@ -3463,6 +3462,13 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
self._rules_rows_panel = QW.QWidget( rules_panel )
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
self._rules_rows_panel.setLayout( vbox )
|
||||
|
||||
self._last_fetched_rule_rows = set()
|
||||
self._rule_widgets = []
|
||||
|
||||
self._use_default_rules_button = ClientGUICommon.BetterButton( rules_panel, 'use default rules', self._UseDefaultRules )
|
||||
self._edit_rules_button = ClientGUICommon.BetterButton( rules_panel, 'edit rules', self._EditRules )
|
||||
|
||||
|
@ -3538,9 +3544,9 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
#
|
||||
|
||||
self._rules_job = HG.client_controller.CallRepeatingQtSafe(self, 0.5, 5.0, self._UpdateRules)
|
||||
self._rules_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, self._UpdateRules )
|
||||
|
||||
self._update_job = HG.client_controller.CallRepeatingQtSafe(self, 0.5, 1.0, self._Update)
|
||||
self._update_job = HG.client_controller.CallRepeatingQtSafe( self, 0.5, 1.0, self._Update )
|
||||
|
||||
|
||||
def _EditRules( self ):
|
||||
|
@ -3595,8 +3601,6 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
def _UpdateRules( self ):
|
||||
|
||||
changes_made = False
|
||||
|
||||
if self._network_context.IsDefault() or self._network_context == ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT:
|
||||
|
||||
if self._use_default_rules_button.isVisible():
|
||||
|
@ -3604,8 +3608,6 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
self._uses_default_rules_st.hide()
|
||||
self._use_default_rules_button.hide()
|
||||
|
||||
changes_made = True
|
||||
|
||||
|
||||
else:
|
||||
|
||||
|
@ -3619,8 +3621,6 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
self._use_default_rules_button.hide()
|
||||
|
||||
changes_made = True
|
||||
|
||||
|
||||
else:
|
||||
|
||||
|
@ -3632,8 +3632,6 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
self._use_default_rules_button.show()
|
||||
|
||||
changes_made = True
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -3643,9 +3641,16 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
self._last_fetched_rule_rows = rule_rows
|
||||
|
||||
QP.DestroyChildren( self._rules_rows_panel )
|
||||
vbox = self._rules_rows_panel.layout()
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
for rule_widget in self._rule_widgets:
|
||||
|
||||
vbox.removeWidget( rule_widget )
|
||||
|
||||
rule_widget.deleteLater()
|
||||
|
||||
|
||||
self._rule_widgets = []
|
||||
|
||||
for ( status, ( v, r ) ) in rule_rows:
|
||||
|
||||
|
@ -3653,13 +3658,11 @@ class ReviewNetworkContextBandwidthPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
tg.SetValue( status, v, r )
|
||||
|
||||
self._rule_widgets.append( tg )
|
||||
|
||||
QP.AddToLayout( vbox, tg, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
|
||||
self._rules_rows_panel.setLayout( vbox )
|
||||
|
||||
changes_made = True
|
||||
|
||||
|
||||
|
||||
def _UseDefaultRules( self ):
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
from . import HydrusConstants as HC
|
||||
from . import HydrusData
|
||||
from . import HydrusExceptions
|
||||
import os
|
||||
from qtpy import QtWidgets as QW
|
||||
|
||||
STYLESHEET_DIR = os.path.join( HC.BASE_DIR, 'static', 'qss' )
|
||||
|
||||
ORIGINAL_STYLE = None
|
||||
ORIGINAL_STYLESHEET = None
|
||||
|
||||
def ClearStylesheet():
|
||||
|
||||
QW.QApplication.instance().setStyleSheet( ORIGINAL_STYLESHEET )
|
||||
|
||||
def GetAvailableStyles():
|
||||
|
||||
# so eventually expand this to do QStylePlugin or whatever we are doing to add more QStyles
|
||||
|
||||
return list( QW.QStyleFactory.keys() )
|
||||
|
||||
def GetCurrentStyleName():
|
||||
|
||||
return QW.QApplication.instance().style().objectName()
|
||||
|
||||
def GetAvailableStylesheets():
|
||||
|
||||
if not os.path.exists( STYLESHEET_DIR ) or not os.path.isdir( STYLESHEET_DIR ):
|
||||
|
||||
raise HydrusExceptions.DataMissing( 'Stylesheet dir "{}" is missing or not a directory!'.format( STYLESHEET_DIR ) )
|
||||
|
||||
|
||||
stylesheet_filenames = []
|
||||
|
||||
extensions = [ '.qss', '.css' ]
|
||||
|
||||
for filename in os.listdir( STYLESHEET_DIR ):
|
||||
|
||||
if True in ( filename.endswith( ext ) for ext in extensions ):
|
||||
|
||||
stylesheet_filenames.append( filename )
|
||||
|
||||
|
||||
|
||||
return stylesheet_filenames
|
||||
|
||||
def InitialiseDefaults():
|
||||
|
||||
global ORIGINAL_STYLE
|
||||
|
||||
ORIGINAL_STYLE = GetCurrentStyleName()
|
||||
|
||||
global ORIGINAL_STYLESHEET
|
||||
|
||||
ORIGINAL_STYLESHEET = QW.QApplication.instance().styleSheet()
|
||||
|
||||
def SetStyle( name ):
|
||||
|
||||
current_name = GetCurrentStyleName()
|
||||
|
||||
if name == current_name:
|
||||
|
||||
return
|
||||
|
||||
|
||||
if name in QW.QStyleFactory.keys():
|
||||
|
||||
QW.QApplication.instance().setStyle( QW.QStyleFactory.create( name ) )
|
||||
|
||||
else:
|
||||
|
||||
raise HydrusExceptions.DataMissing( 'Style "{}" does not exist!'.format( name ) )
|
||||
|
||||
|
||||
def SetStylesheet( filename ):
|
||||
|
||||
path = os.path.join( STYLESHEET_DIR, filename )
|
||||
|
||||
if not os.path.exists( path ):
|
||||
|
||||
raise HydrusExceptions.DataMissing( 'Stylesheet "{}" does not exist!'.format( path ) )
|
||||
|
||||
|
||||
with open( path, 'r', encoding = 'utf-8' ) as f:
|
||||
|
||||
qss = f.read()
|
||||
|
||||
|
||||
QW.QApplication.instance().setStyleSheet( qss )
|
||||
|
|
@ -16,6 +16,7 @@ from . import ClientGUIScrolledPanelsEdit
|
|||
from . import ClientGUIScrolledPanelsReview
|
||||
from . import ClientGUIShortcuts
|
||||
from . import ClientGUITagSuggestions
|
||||
from . import ClientManagers
|
||||
from . import ClientMedia
|
||||
from . import ClientMigration
|
||||
from . import ClientTags
|
||||
|
@ -2287,8 +2288,8 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._children, ( 12, 6 ) )
|
||||
|
||||
QP.SetInitialSize( self._children, (-1,preview_height) )
|
||||
QP.SetInitialSize( self._parents, (-1,preview_height) )
|
||||
self._children.setMinimumHeight( preview_height )
|
||||
self._parents.setMinimumHeight( preview_height )
|
||||
|
||||
expand_parents = True
|
||||
|
||||
|
@ -2611,9 +2612,9 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
if potential_parent in current_children:
|
||||
|
||||
simple_children_to_parents = ClientCaches.BuildSimpleChildrenToParents( current_pairs )
|
||||
simple_children_to_parents = ClientManagers.BuildSimpleChildrenToParents( current_pairs )
|
||||
|
||||
if ClientCaches.LoopInSimpleChildrenToParents( simple_children_to_parents, potential_child, potential_parent ):
|
||||
if ClientManagers.LoopInSimpleChildrenToParents( simple_children_to_parents, potential_child, potential_parent ):
|
||||
|
||||
QW.QMessageBox.critical( self, 'Error', 'Adding '+potential_child+'->'+potential_parent+' would create a loop!' )
|
||||
|
||||
|
@ -3105,7 +3106,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._old_siblings, ( 12, 6 ) )
|
||||
|
||||
QP.SetInitialSize( self._old_siblings, (-1,preview_height) )
|
||||
self._old_siblings.setMinimumHeight( preview_height )
|
||||
|
||||
expand_parents = False
|
||||
|
||||
|
|
|
@ -216,7 +216,7 @@ def SetInitialTLWSizeAndPosition( tlw, frame_key ):
|
|||
|
||||
else:
|
||||
|
||||
( min_width, min_height ) = QP.GetEffectiveMinSize( tlw )
|
||||
( min_width, min_height ) = tlw.sizeHint().toTuple()
|
||||
|
||||
( width, height ) = GetSafeSize( tlw, ( min_width, min_height ), default_gravity )
|
||||
|
||||
|
@ -321,12 +321,16 @@ def SlideOffScreenTLWUpAndLeft( tlw ):
|
|||
|
||||
class NewDialog( QP.Dialog ):
|
||||
|
||||
def __init__( self, parent, title ):
|
||||
def __init__( self, parent, title, do_not_activate = False ):
|
||||
|
||||
QP.Dialog.__init__( self, parent )
|
||||
self.setWindowTitle( title )
|
||||
|
||||
self._consumed_esc_to_cancel = False
|
||||
if do_not_activate:
|
||||
|
||||
self.setAttribute( QC.Qt.WA_ShowWithoutActivating )
|
||||
|
||||
|
||||
self.setWindowTitle( title )
|
||||
|
||||
self._last_move_pub = 0.0
|
||||
|
||||
|
@ -377,20 +381,21 @@ class NewDialog( QP.Dialog ):
|
|||
|
||||
if not self.isModal(): # in some rare cases (including spammy AutoHotkey, looks like), this can be fired before the dialog can clean itself up
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
|
||||
if not self._ReadyToClose( value ):
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
|
||||
if value == QW.QDialog.Rejected:
|
||||
|
||||
if not self._CanCancel():
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
|
||||
self.SetCancelled( True )
|
||||
|
||||
|
||||
|
@ -398,7 +403,7 @@ class NewDialog( QP.Dialog ):
|
|||
|
||||
if not self._CanOK():
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
|
||||
self._SaveOKPosition()
|
||||
|
@ -439,6 +444,8 @@ class NewDialog( QP.Dialog ):
|
|||
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def CleanBeforeDestroy( self ):
|
||||
|
||||
|
@ -450,13 +457,19 @@ class NewDialog( QP.Dialog ):
|
|||
self._TryEndModal( QW.QDialog.Accepted )
|
||||
|
||||
|
||||
def EventClose( self, event ):
|
||||
def closeEvent( self, event ):
|
||||
|
||||
if not self or not QP.isValid( self ):
|
||||
|
||||
return
|
||||
|
||||
self._TryEndModal( QW.QDialog.Rejected )
|
||||
|
||||
was_ended = self._TryEndModal( QW.QDialog.Rejected )
|
||||
|
||||
if not was_ended:
|
||||
|
||||
event.ignore()
|
||||
|
||||
|
||||
|
||||
def EventDialogButtonApply( self ):
|
||||
|
@ -514,9 +527,7 @@ class NewDialog( QP.Dialog ):
|
|||
|
||||
event_from_us = current_focus is not None and ClientGUIFunctions.IsQtAncestor( current_focus, self )
|
||||
|
||||
if event_from_us and key == QC.Qt.Key_Escape and not self._consumed_esc_to_cancel:
|
||||
|
||||
self._consumed_esc_to_cancel = True
|
||||
if event_from_us and key == QC.Qt.Key_Escape:
|
||||
|
||||
self._TryEndModal( QW.QDialog.Rejected )
|
||||
|
||||
|
@ -528,11 +539,11 @@ class NewDialog( QP.Dialog ):
|
|||
|
||||
class DialogThatResizes( NewDialog ):
|
||||
|
||||
def __init__( self, parent, title, frame_key ):
|
||||
def __init__( self, parent, title, frame_key, do_not_activate = False ):
|
||||
|
||||
self._frame_key = frame_key
|
||||
|
||||
NewDialog.__init__( self, parent, title )
|
||||
NewDialog.__init__( self, parent, title, do_not_activate = do_not_activate )
|
||||
|
||||
|
||||
def _SaveOKPosition( self ):
|
||||
|
@ -542,12 +553,12 @@ class DialogThatResizes( NewDialog ):
|
|||
|
||||
class DialogThatTakesScrollablePanel( DialogThatResizes ):
|
||||
|
||||
def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = False ):
|
||||
def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = False, do_not_activate = False ):
|
||||
|
||||
self._panel = None
|
||||
self._hide_buttons = hide_buttons
|
||||
|
||||
DialogThatResizes.__init__( self, parent, title, frame_key )
|
||||
DialogThatResizes.__init__( self, parent, title, frame_key, do_not_activate = do_not_activate )
|
||||
|
||||
self._InitialiseButtons()
|
||||
|
||||
|
@ -676,8 +687,6 @@ class DialogApplyCancel( DialogThatTakesScrollablePanel ):
|
|||
self._apply.setVisible( False )
|
||||
self._cancel.setVisible( False )
|
||||
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventClose ) # the close event no longer goes to the default button, since it is hidden, wew
|
||||
|
||||
|
||||
|
||||
class DialogEdit( DialogApplyCancel ):
|
||||
|
@ -768,6 +777,7 @@ class Frame( QW.QWidget ):
|
|||
|
||||
self.setWindowFlags( QC.Qt.Window )
|
||||
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
|
||||
|
||||
self.setAttribute( QC.Qt.WA_DeleteOnClose )
|
||||
|
||||
self._new_options = HG.client_controller.new_options
|
||||
|
@ -893,7 +903,7 @@ class FrameThatTakesScrollablePanel( FrameThatResizes ):
|
|||
FrameThatResizes.__init__( self, parent, title, frame_key )
|
||||
|
||||
self._ok = QW.QPushButton( 'close', self )
|
||||
self._ok.clicked.connect( self.EventClose )
|
||||
self._ok.clicked.connect( self.close )
|
||||
|
||||
|
||||
def CleanBeforeDestroy( self ):
|
||||
|
@ -920,11 +930,6 @@ class FrameThatTakesScrollablePanel( FrameThatResizes ):
|
|||
|
||||
|
||||
|
||||
def EventClose( self ):
|
||||
|
||||
self.close()
|
||||
|
||||
|
||||
def GetPanel( self ):
|
||||
|
||||
return self._panel
|
||||
|
@ -934,7 +939,10 @@ class FrameThatTakesScrollablePanel( FrameThatResizes ):
|
|||
|
||||
self._panel = panel
|
||||
|
||||
if hasattr( self._panel, 'okSignal' ): self._panel.okSignal.connect( self.EventClose )
|
||||
if hasattr( self._panel, 'okSignal' ):
|
||||
|
||||
self._panel.okSignal.connect( self.close )
|
||||
|
||||
|
||||
vbox = QP.VBoxLayout()
|
||||
|
||||
|
|
|
@ -218,7 +218,10 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._file_status = text
|
||||
|
||||
|
@ -332,7 +335,10 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._gallery_status = text
|
||||
|
||||
|
|
|
@ -190,7 +190,10 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._current_action = text
|
||||
|
||||
|
|
|
@ -617,7 +617,10 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._watcher_status = text
|
||||
|
||||
|
@ -627,7 +630,10 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._subject = text
|
||||
|
||||
|
@ -928,7 +934,10 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
self._file_status = text
|
||||
|
||||
|
|
|
@ -148,7 +148,10 @@ def THREADDownloadURL( job_key, url, url_string ):
|
|||
|
||||
def status_hook( text ):
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
job_key.SetVariable( 'popup_text_1', text )
|
||||
|
||||
|
@ -219,7 +222,10 @@ def THREADDownloadURLs( job_key, urls, title ):
|
|||
|
||||
def status_hook( text ):
|
||||
|
||||
text = text.splitlines()[0]
|
||||
if len( text ) > 0:
|
||||
|
||||
text = text.splitlines()[0]
|
||||
|
||||
|
||||
job_key.SetVariable( 'popup_text_2', text )
|
||||
|
||||
|
|
|
@ -473,6 +473,181 @@ def GetMediasTagCount( pool, tag_service_key, tag_display_type ):
|
|||
|
||||
return ( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count )
|
||||
|
||||
class MediaResult( object ):
|
||||
|
||||
def __init__( self, file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager ):
|
||||
|
||||
self._file_info_manager = file_info_manager
|
||||
self._tags_manager = tags_manager
|
||||
self._locations_manager = locations_manager
|
||||
self._ratings_manager = ratings_manager
|
||||
self._file_viewing_stats_manager = file_viewing_stats_manager
|
||||
|
||||
|
||||
def DeletePending( self, service_key ):
|
||||
|
||||
try:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
return
|
||||
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
if service_type in HC.TAG_SERVICES:
|
||||
|
||||
self._tags_manager.DeletePending( service_key )
|
||||
|
||||
elif service_type in HC.FILE_SERVICES:
|
||||
|
||||
self._locations_manager.DeletePending( service_key )
|
||||
|
||||
|
||||
|
||||
def Duplicate( self ):
|
||||
|
||||
file_info_manager = self._file_info_manager.Duplicate()
|
||||
tags_manager = self._tags_manager.Duplicate()
|
||||
locations_manager = self._locations_manager.Duplicate()
|
||||
ratings_manager = self._ratings_manager.Duplicate()
|
||||
file_viewing_stats_manager = self._file_viewing_stats_manager.Duplicate()
|
||||
|
||||
return MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager )
|
||||
|
||||
|
||||
def GetDuration( self ):
|
||||
|
||||
return self._file_info_manager.duration
|
||||
|
||||
|
||||
def GetFileInfoManager( self ):
|
||||
|
||||
return self._file_info_manager
|
||||
|
||||
|
||||
def GetFileViewingStatsManager( self ):
|
||||
|
||||
return self._file_viewing_stats_manager
|
||||
|
||||
|
||||
def GetHash( self ):
|
||||
|
||||
return self._file_info_manager.hash
|
||||
|
||||
|
||||
def GetHashId( self ):
|
||||
|
||||
return self._file_info_manager.hash_id
|
||||
|
||||
|
||||
def GetInbox( self ):
|
||||
|
||||
return self._locations_manager.GetInbox()
|
||||
|
||||
|
||||
def GetLocationsManager( self ):
|
||||
|
||||
return self._locations_manager
|
||||
|
||||
|
||||
def GetMime( self ):
|
||||
|
||||
return self._file_info_manager.mime
|
||||
|
||||
|
||||
def GetNumFrames( self ):
|
||||
|
||||
return self._file_info_manager.num_frames
|
||||
|
||||
|
||||
def GetNumWords( self ):
|
||||
|
||||
return self._file_info_manager.num_words
|
||||
|
||||
|
||||
def GetRatingsManager( self ):
|
||||
|
||||
return self._ratings_manager
|
||||
|
||||
|
||||
def GetResolution( self ):
|
||||
|
||||
return ( self._file_info_manager.width, self._file_info_manager.height )
|
||||
|
||||
|
||||
def GetSize( self ):
|
||||
|
||||
return self._file_info_manager.size
|
||||
|
||||
|
||||
def GetTagsManager( self ):
|
||||
|
||||
return self._tags_manager
|
||||
|
||||
|
||||
def HasAudio( self ):
|
||||
|
||||
return self._file_info_manager.has_audio is True
|
||||
|
||||
|
||||
def IsStaticImage( self ):
|
||||
|
||||
return self._file_info_manager.mime in HC.IMAGES and self._file_info_manager.duration in ( None, 0 )
|
||||
|
||||
|
||||
def ProcessContentUpdate( self, service_key, content_update ):
|
||||
|
||||
try:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
return
|
||||
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
if service_type in HC.TAG_SERVICES:
|
||||
|
||||
self._tags_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
elif service_type in HC.FILE_SERVICES:
|
||||
|
||||
if content_update.GetDataType() == HC.CONTENT_TYPE_FILE_VIEWING_STATS:
|
||||
|
||||
self._file_viewing_stats_manager.ProcessContentUpdate( content_update )
|
||||
|
||||
else:
|
||||
|
||||
self._locations_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
|
||||
elif service_type in HC.RATINGS_SERVICES:
|
||||
|
||||
self._ratings_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
|
||||
|
||||
def ResetService( self, service_key ):
|
||||
|
||||
self._tags_manager.ResetService( service_key )
|
||||
self._locations_manager.ResetService( service_key )
|
||||
|
||||
|
||||
def SetTagsManager( self, tags_manager ):
|
||||
|
||||
self._tags_manager = tags_manager
|
||||
|
||||
|
||||
def ToTuple( self ):
|
||||
|
||||
return ( self._file_info_manager, self._tags_manager, self._locations_manager, self._ratings_manager )
|
||||
|
||||
|
||||
class DuplicatesManager( object ):
|
||||
|
||||
def __init__( self, service_keys_to_dupe_statuses_to_counts ):
|
||||
|
@ -1919,7 +2094,7 @@ class MediaCollection( MediaList, Media ):
|
|||
|
||||
class MediaSingleton( Media ):
|
||||
|
||||
def __init__( self, media_result ):
|
||||
def __init__( self, media_result: MediaResult ):
|
||||
|
||||
Media.__init__( self )
|
||||
|
||||
|
@ -1951,6 +2126,11 @@ class MediaSingleton( Media ):
|
|||
return self._media_result.GetHash()
|
||||
|
||||
|
||||
def GetHashId( self ):
|
||||
|
||||
return self._media_result.GetHashId()
|
||||
|
||||
|
||||
def GetHashes( self, has_location = None, discriminant = None, not_uploaded_to = None, ordered = False ):
|
||||
|
||||
if self.MatchesDiscriminant( has_location = has_location, discriminant = discriminant, not_uploaded_to = not_uploaded_to ):
|
||||
|
@ -2263,181 +2443,6 @@ class MediaSingleton( Media ):
|
|||
|
||||
|
||||
|
||||
class MediaResult( object ):
|
||||
|
||||
def __init__( self, file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager ):
|
||||
|
||||
self._file_info_manager = file_info_manager
|
||||
self._tags_manager = tags_manager
|
||||
self._locations_manager = locations_manager
|
||||
self._ratings_manager = ratings_manager
|
||||
self._file_viewing_stats_manager = file_viewing_stats_manager
|
||||
|
||||
|
||||
def DeletePending( self, service_key ):
|
||||
|
||||
try:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
return
|
||||
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
if service_type in HC.TAG_SERVICES:
|
||||
|
||||
self._tags_manager.DeletePending( service_key )
|
||||
|
||||
elif service_type in HC.FILE_SERVICES:
|
||||
|
||||
self._locations_manager.DeletePending( service_key )
|
||||
|
||||
|
||||
|
||||
def Duplicate( self ):
|
||||
|
||||
file_info_manager = self._file_info_manager.Duplicate()
|
||||
tags_manager = self._tags_manager.Duplicate()
|
||||
locations_manager = self._locations_manager.Duplicate()
|
||||
ratings_manager = self._ratings_manager.Duplicate()
|
||||
file_viewing_stats_manager = self._file_viewing_stats_manager.Duplicate()
|
||||
|
||||
return MediaResult( file_info_manager, tags_manager, locations_manager, ratings_manager, file_viewing_stats_manager )
|
||||
|
||||
|
||||
def GetDuration( self ):
|
||||
|
||||
return self._file_info_manager.duration
|
||||
|
||||
|
||||
def GetFileInfoManager( self ):
|
||||
|
||||
return self._file_info_manager
|
||||
|
||||
|
||||
def GetFileViewingStatsManager( self ):
|
||||
|
||||
return self._file_viewing_stats_manager
|
||||
|
||||
|
||||
def GetHash( self ):
|
||||
|
||||
return self._file_info_manager.hash
|
||||
|
||||
|
||||
def GetHashId( self ):
|
||||
|
||||
return self._file_info_manager.hash_id
|
||||
|
||||
|
||||
def GetInbox( self ):
|
||||
|
||||
return self._locations_manager.GetInbox()
|
||||
|
||||
|
||||
def GetLocationsManager( self ):
|
||||
|
||||
return self._locations_manager
|
||||
|
||||
|
||||
def GetMime( self ):
|
||||
|
||||
return self._file_info_manager.mime
|
||||
|
||||
|
||||
def GetNumFrames( self ):
|
||||
|
||||
return self._file_info_manager.num_frames
|
||||
|
||||
|
||||
def GetNumWords( self ):
|
||||
|
||||
return self._file_info_manager.num_words
|
||||
|
||||
|
||||
def GetRatingsManager( self ):
|
||||
|
||||
return self._ratings_manager
|
||||
|
||||
|
||||
def GetResolution( self ):
|
||||
|
||||
return ( self._file_info_manager.width, self._file_info_manager.height )
|
||||
|
||||
|
||||
def GetSize( self ):
|
||||
|
||||
return self._file_info_manager.size
|
||||
|
||||
|
||||
def GetTagsManager( self ):
|
||||
|
||||
return self._tags_manager
|
||||
|
||||
|
||||
def HasAudio( self ):
|
||||
|
||||
return self._file_info_manager.has_audio is True
|
||||
|
||||
|
||||
def IsStaticImage( self ):
|
||||
|
||||
return self._file_info_manager.mime in HC.IMAGES and self._file_info_manager.duration in ( None, 0 )
|
||||
|
||||
|
||||
def ProcessContentUpdate( self, service_key, content_update ):
|
||||
|
||||
try:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
except HydrusExceptions.DataMissing:
|
||||
|
||||
return
|
||||
|
||||
|
||||
service_type = service.GetServiceType()
|
||||
|
||||
if service_type in HC.TAG_SERVICES:
|
||||
|
||||
self._tags_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
elif service_type in HC.FILE_SERVICES:
|
||||
|
||||
if content_update.GetDataType() == HC.CONTENT_TYPE_FILE_VIEWING_STATS:
|
||||
|
||||
self._file_viewing_stats_manager.ProcessContentUpdate( content_update )
|
||||
|
||||
else:
|
||||
|
||||
self._locations_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
|
||||
elif service_type in HC.RATINGS_SERVICES:
|
||||
|
||||
self._ratings_manager.ProcessContentUpdate( service_key, content_update )
|
||||
|
||||
|
||||
|
||||
def ResetService( self, service_key ):
|
||||
|
||||
self._tags_manager.ResetService( service_key )
|
||||
self._locations_manager.ResetService( service_key )
|
||||
|
||||
|
||||
def SetTagsManager( self, tags_manager ):
|
||||
|
||||
self._tags_manager = tags_manager
|
||||
|
||||
|
||||
def ToTuple( self ):
|
||||
|
||||
return ( self._file_info_manager, self._tags_manager, self._locations_manager, self._ratings_manager )
|
||||
|
||||
|
||||
class MediaSort( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_MEDIA_SORT
|
||||
|
|
|
@ -314,6 +314,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
self._dictionary[ 'noneable_strings' ][ 'media_background_bmp_path' ] = None
|
||||
self._dictionary[ 'noneable_strings' ][ 'http_proxy' ] = None
|
||||
self._dictionary[ 'noneable_strings' ][ 'https_proxy' ] = None
|
||||
self._dictionary[ 'noneable_strings' ][ 'qt_style_name' ] = None
|
||||
self._dictionary[ 'noneable_strings' ][ 'qt_stylesheet_name' ] = None
|
||||
|
||||
self._dictionary[ 'strings' ] = {}
|
||||
|
||||
|
|
|
@ -2795,6 +2795,7 @@ STRING_TRANSFORMATION_REVERSE = 8
|
|||
STRING_TRANSFORMATION_REGEX_SUB = 9
|
||||
STRING_TRANSFORMATION_DATE_DECODE = 10
|
||||
STRING_TRANSFORMATION_INTEGER_ADDITION = 11
|
||||
STRING_TRANSFORMATION_DATE_ENCODE = 12
|
||||
|
||||
transformation_type_str_lookup = {}
|
||||
|
||||
|
@ -2808,8 +2809,9 @@ transformation_type_str_lookup[ STRING_TRANSFORMATION_CLIP_TEXT_FROM_BEGINNING ]
|
|||
transformation_type_str_lookup[ STRING_TRANSFORMATION_CLIP_TEXT_FROM_END ] = 'take the end of the string'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_REVERSE ] = 'reverse text'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_REGEX_SUB ] = 'regex substitution'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_DATE_DECODE ] = 'date decode'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_DATE_DECODE ] = 'datestring to timestamp'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_INTEGER_ADDITION ] = 'integer addition'
|
||||
transformation_type_str_lookup[ STRING_TRANSFORMATION_DATE_ENCODE ] = 'timestamp to datestring'
|
||||
|
||||
class StringConverter( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
|
@ -2985,6 +2987,34 @@ class StringConverter( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
s = str( timestamp )
|
||||
|
||||
elif transformation_type == STRING_TRANSFORMATION_DATE_ENCODE:
|
||||
|
||||
( phrase, timezone ) = data
|
||||
|
||||
try:
|
||||
|
||||
timestamp = int( s )
|
||||
|
||||
except:
|
||||
|
||||
raise Exception( '"{}" was not an integer!'.format( s ) )
|
||||
|
||||
|
||||
if timezone == HC.TIMEZONE_GMT:
|
||||
|
||||
# user wants a UTC string, so we need UTC struct
|
||||
|
||||
struct_time = time.gmtime( timestamp )
|
||||
|
||||
elif timezone == HC.TIMEZONE_LOCAL:
|
||||
|
||||
# user wants a local string, so we need localtime
|
||||
|
||||
struct_time = time.localtime( timestamp )
|
||||
|
||||
|
||||
s = time.strftime( phrase, struct_time )
|
||||
|
||||
elif transformation_type == STRING_TRANSFORMATION_INTEGER_ADDITION:
|
||||
|
||||
delta = data
|
||||
|
@ -3063,7 +3093,11 @@ class StringConverter( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
elif transformation_type == STRING_TRANSFORMATION_DATE_DECODE:
|
||||
|
||||
return 'date decode: ' + repr( data )
|
||||
return 'datestring to timestamp: ' + repr( data )
|
||||
|
||||
elif transformation_type == STRING_TRANSFORMATION_DATE_ENCODE:
|
||||
|
||||
return 'timestamp to datestring: ' + repr( data )
|
||||
|
||||
elif transformation_type == STRING_TRANSFORMATION_INTEGER_ADDITION:
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 18
|
||||
SOFTWARE_VERSION = 375
|
||||
SOFTWARE_VERSION = 376
|
||||
CLIENT_API_VERSION = 11
|
||||
|
||||
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
|
|
@ -17,7 +17,6 @@ db_synchronous_override = None
|
|||
|
||||
import_folders_running = False
|
||||
export_folders_running = False
|
||||
subscriptions_running = False
|
||||
|
||||
callto_report_mode = False
|
||||
db_report_mode = False
|
||||
|
|
|
@ -370,7 +370,7 @@ def SetEnvTempDir( path ):
|
|||
|
||||
if tmp_name in os.environ:
|
||||
|
||||
os.putenv( tmp_name, path )
|
||||
os.environ[ tmp_name ] = path
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ if not 'QT_API' in os.environ:
|
|||
|
||||
import PySide2
|
||||
|
||||
os.putenv( 'QT_API', 'pyside2' )
|
||||
os.environ[ 'QT_API' ] = 'pyside2'
|
||||
|
||||
except ImportError:
|
||||
|
||||
|
@ -382,44 +382,31 @@ class TabBar( QW.QTabBar ):
|
|||
|
||||
def dragEnterEvent(self, event):
|
||||
|
||||
if event.mimeData().formats():
|
||||
|
||||
event.accept()
|
||||
if 'application/hydrus-tab' in event.mimeData().formats():
|
||||
|
||||
event.ignore()
|
||||
|
||||
else:
|
||||
|
||||
event.ignore()
|
||||
|
||||
event.accept()
|
||||
|
||||
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
|
||||
if event.mimeData().formats():
|
||||
|
||||
def dragMoveEvent( self, event ):
|
||||
|
||||
if 'application/hydrus-tab' not in event.mimeData().formats():
|
||||
|
||||
tab_index = self.tabAt( event.pos() )
|
||||
|
||||
|
||||
if tab_index != -1:
|
||||
|
||||
shift_down = event.keyboardModifiers() & QC.Qt.ShiftModifier
|
||||
|
||||
follow_dropped_page = not shift_down
|
||||
|
||||
new_options = HG.client_controller.new_options
|
||||
|
||||
if new_options.GetBoolean( 'reverse_page_shift_drag_behaviour' ):
|
||||
|
||||
follow_dropped_page = not follow_dropped_page
|
||||
|
||||
|
||||
if follow_dropped_page:
|
||||
|
||||
self.parentWidget().setCurrentIndex( tab_index )
|
||||
|
||||
self.parentWidget().setCurrentIndex( tab_index )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
event.ignore()
|
||||
|
||||
|
||||
|
||||
def lastClickedTabIndex( self ):
|
||||
|
@ -455,7 +442,7 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
self._tab_bar = self.tabBar()
|
||||
|
||||
self._supplementary_drop_target = None
|
||||
|
||||
|
||||
|
||||
def _LayoutPagesHelper( self ):
|
||||
|
||||
|
@ -521,15 +508,11 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
return
|
||||
|
||||
|
||||
if HC.PLATFORM_MACOS:
|
||||
|
||||
return
|
||||
|
||||
my_mouse_pos = e.pos()
|
||||
global_mouse_pos = self.mapToGlobal( my_mouse_pos )
|
||||
tab_bar_mouse_pos = self._tab_bar.mapFromGlobal( global_mouse_pos )
|
||||
|
||||
global_pos = self.mapToGlobal( e.pos() )
|
||||
pos_in_tab = self._tab_bar.mapFromGlobal( global_pos )
|
||||
|
||||
if not self._tab_bar.rect().contains( pos_in_tab ):
|
||||
if not self._tab_bar.rect().contains( tab_bar_mouse_pos ):
|
||||
|
||||
return
|
||||
|
||||
|
@ -553,6 +536,8 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
|
||||
mimeData = QC.QMimeData()
|
||||
|
||||
mimeData.setData( 'application/hydrus-tab', b'' )
|
||||
|
||||
drag = QG.QDrag( self._tab_bar )
|
||||
|
||||
drag.setMimeData( mimeData )
|
||||
|
@ -561,7 +546,10 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
|
||||
cursor = QG.QCursor( QC.Qt.OpenHandCursor )
|
||||
|
||||
drag.setHotSpot( e.pos() - pos_in_tab )
|
||||
drag.setHotSpot( QC.QPoint( 0, 0 ) )
|
||||
|
||||
# this puts the tab pixmap exactly where we picked it up, but it looks bad
|
||||
# drag.setHotSpot( tab_bar_mouse_pos - tab_rect.topLeft() )
|
||||
|
||||
drag.setDragCursor( cursor.pixmap(), QC.Qt.MoveAction )
|
||||
|
||||
|
@ -575,7 +563,7 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
return QW.QTabWidget.dragEnterEvent( self, e )
|
||||
|
||||
|
||||
if not e.mimeData().formats():
|
||||
if 'application/hydrus-tab' in e.mimeData().formats():
|
||||
|
||||
e.accept()
|
||||
|
||||
|
@ -586,10 +574,14 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
|
||||
|
||||
def dragMoveEvent( self, event ):
|
||||
|
||||
|
||||
#if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.pos() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event )
|
||||
|
||||
tab_index = self._tab_bar.tabAt( event.pos() )
|
||||
screen_pos = self.mapToGlobal( event.pos() )
|
||||
|
||||
tab_pos = self._tab_bar.mapFromGlobal( screen_pos )
|
||||
|
||||
tab_index = self._tab_bar.tabAt( tab_pos )
|
||||
|
||||
if tab_index != -1:
|
||||
|
||||
|
@ -598,7 +590,7 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
self.setCurrentIndex( tab_index )
|
||||
|
||||
|
||||
if event.mimeData().formats():
|
||||
if 'application/hydrus-tab' not in event.mimeData().formats():
|
||||
|
||||
event.reject()
|
||||
|
||||
|
@ -640,7 +632,7 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
return QW.QTabWidget.dropEvent( self, e )
|
||||
|
||||
|
||||
if len( e.mimeData().formats() ): #Page dnd has no associated mime data
|
||||
if 'application/hydrus-tab' not in e.mimeData().formats(): #Page dnd has no associated mime data
|
||||
|
||||
e.ignore()
|
||||
|
||||
|
@ -681,8 +673,12 @@ class TabWidgetWithDnD( QW.QTabWidget ):
|
|||
e.accept()
|
||||
|
||||
counter = self.count()
|
||||
|
||||
dropped_on_tab_index = self.tabBar().tabAt( e.pos() )
|
||||
|
||||
screen_pos = self.mapToGlobal( e.pos() )
|
||||
|
||||
tab_pos = self.tabBar().mapFromGlobal( screen_pos )
|
||||
|
||||
dropped_on_tab_index = self.tabBar().tabAt( tab_pos )
|
||||
|
||||
if source_notebook == self and dropped_on_tab_index == source_page_index:
|
||||
|
||||
|
@ -1128,26 +1124,7 @@ def AdjustColour( colour, percent ):
|
|||
percent = percent / 100
|
||||
|
||||
return QG.QColor( colour.red() + colour.red() * percent, colour.green() + colour.green() * percent, colour.blue() + colour.blue() * percent, colour.alpha() )
|
||||
|
||||
|
||||
def DestroyChildren( widget ):
|
||||
|
||||
if not widget.layout(): return
|
||||
|
||||
ClearLayout( widget.layout(), delete_widgets = True )
|
||||
|
||||
# This creates a new hidden widget, reparents the layout to it then deletes the widget.
|
||||
# Not the nicest solution but otherwise widget.setLayout( None ) refused to work...
|
||||
tmp = QW.QWidget()
|
||||
tmp.setVisible( False )
|
||||
|
||||
tmp.setLayout( widget.layout() )
|
||||
|
||||
tmp.deleteLater()
|
||||
|
||||
widget.setLayout( None )
|
||||
|
||||
|
||||
|
||||
class BusyCursor:
|
||||
|
||||
def __enter__( self ):
|
||||
|
@ -1268,10 +1245,6 @@ def Unsplit( splitter, widget ):
|
|||
widget.setVisible( False )
|
||||
|
||||
|
||||
def GetEffectiveMinSize( widget ):
|
||||
|
||||
return widget.sizeHint().toTuple() #widget.minimumSize().toTuple()
|
||||
|
||||
def GetSystemColour( colour ):
|
||||
|
||||
return QG.QPalette().color( colour )
|
||||
|
@ -1590,6 +1563,7 @@ class AboutBox( QW.QDialog ):
|
|||
QW.QDialog.__init__( self, parent )
|
||||
|
||||
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
|
||||
|
||||
self.setAttribute( QC.Qt.WA_DeleteOnClose )
|
||||
|
||||
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
|
||||
|
@ -2022,9 +1996,12 @@ class Dialog( QW.QDialog ):
|
|||
return self
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
def __exit__( self, exc_type, exc_val, exc_tb ):
|
||||
|
||||
self.deleteLater()
|
||||
if isValid( self ):
|
||||
|
||||
self.deleteLater()
|
||||
|
||||
|
||||
|
||||
class PasswordEntryDialog( Dialog ):
|
||||
|
|
|
@ -3,6 +3,7 @@ from . import ClientConstants as CC
|
|||
from . import ClientAPI
|
||||
from . import ClientLocalServer
|
||||
from . import ClientLocalServerResources
|
||||
from . import ClientManagers
|
||||
from . import ClientMedia
|
||||
from . import ClientRatings
|
||||
from . import ClientSearch
|
||||
|
@ -744,8 +745,8 @@ class TestClientAPI( unittest.TestCase ):
|
|||
|
||||
HG.test_controller.SetRead( 'tag_parents', tag_parents )
|
||||
|
||||
HG.test_controller.tag_siblings_manager = ClientCaches.TagSiblingsManager( HG.test_controller )
|
||||
HG.test_controller.tag_parents_manager = ClientCaches.TagParentsManager( HG.test_controller )
|
||||
HG.test_controller.tag_siblings_manager = ClientManagers.TagSiblingsManager( HG.test_controller )
|
||||
HG.test_controller.tag_parents_manager = ClientManagers.TagParentsManager( HG.test_controller )
|
||||
|
||||
# ok, now with
|
||||
|
||||
|
@ -962,8 +963,6 @@ class TestClientAPI( unittest.TestCase ):
|
|||
|
||||
expected_answer = {}
|
||||
|
||||
expected_answer = { 'url_type' : HC.URL_TYPE_WATCHABLE, 'url_type_string' : 'watchable url', 'match_name' : '8chan thread', 'can_parse' : True }
|
||||
|
||||
expected_answer[ 'normalised_url' ] = normalised_url
|
||||
expected_answer[ 'url_type' ] = HC.URL_TYPE_WATCHABLE
|
||||
expected_answer[ 'url_type_string' ] = 'watchable url'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from . import ClientConstants as CC
|
||||
from . import ClientGUIManagement
|
||||
from . import ClientManagers
|
||||
from . import ClientNetworking
|
||||
from . import ClientCaches
|
||||
from . import ClientServices
|
||||
|
@ -38,7 +39,7 @@ class TestManagers( unittest.TestCase ):
|
|||
|
||||
HG.test_controller.SetRead( 'services', services )
|
||||
|
||||
services_manager = ClientCaches.ServicesManager( HG.client_controller )
|
||||
services_manager = ClientManagers.ServicesManager( HG.client_controller )
|
||||
|
||||
#
|
||||
|
||||
|
@ -82,7 +83,7 @@ class TestManagers( unittest.TestCase ):
|
|||
command_1_inverted = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_INBOX, { hash_1 } ) ] }
|
||||
command_2_inverted = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, { hash_2 } ) ] }
|
||||
|
||||
undo_manager = ClientCaches.UndoManager( HG.client_controller )
|
||||
undo_manager = ClientManagers.UndoManager( HG.client_controller )
|
||||
|
||||
#
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from . import ClientConstants as CC
|
|||
from . import ClientDB
|
||||
from . import ClientImportFileSeeds
|
||||
from . import ClientImportOptions
|
||||
from . import ClientManagers
|
||||
from . import ClientMigration
|
||||
from . import ClientServices
|
||||
from . import ClientTags
|
||||
|
@ -155,7 +156,7 @@ class TestMigration( unittest.TestCase ):
|
|||
|
||||
self.WriteSynchronous( 'update_services', services )
|
||||
|
||||
self.services_manager = ClientCaches.ServicesManager( self )
|
||||
self.services_manager = ClientManagers.ServicesManager( self )
|
||||
|
||||
|
||||
def _do_fake_imports( self ):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import collections
|
||||
from . import ClientCaches
|
||||
from . import ClientConstants as CC
|
||||
from . import ClientManagers
|
||||
from . import ClientMedia
|
||||
from . import ClientSearch
|
||||
from . import ClientTags
|
||||
|
@ -674,7 +675,7 @@ class TestTagParents( unittest.TestCase ):
|
|||
|
||||
HG.test_controller.SetRead( 'tag_parents', tag_parents )
|
||||
|
||||
cls._tag_parents_manager = ClientCaches.TagParentsManager( HG.client_controller )
|
||||
cls._tag_parents_manager = ClientManagers.TagParentsManager( HG.client_controller )
|
||||
|
||||
|
||||
def test_expand_predicates( self ):
|
||||
|
@ -812,7 +813,7 @@ class TestTagSiblings( unittest.TestCase ):
|
|||
|
||||
HG.test_controller.SetRead( 'tag_siblings', tag_siblings )
|
||||
|
||||
cls._tag_siblings_manager = ClientCaches.TagSiblingsManager( HG.test_controller )
|
||||
cls._tag_siblings_manager = ClientManagers.TagSiblingsManager( HG.test_controller )
|
||||
|
||||
|
||||
def test_collapse_predicates( self ):
|
||||
|
|
|
@ -15,6 +15,7 @@ from . import HydrusGlobals as HG
|
|||
from . import ClientAPI
|
||||
from . import ClientDefaults
|
||||
from . import ClientFiles
|
||||
from . import ClientManagers
|
||||
from . import ClientNetworking
|
||||
from . import ClientNetworkingBandwidth
|
||||
from . import ClientNetworkingDomain
|
||||
|
@ -252,7 +253,7 @@ class Controller( object ):
|
|||
|
||||
self._managers = {}
|
||||
|
||||
self.services_manager = ClientCaches.ServicesManager( self )
|
||||
self.services_manager = ClientManagers.ServicesManager( self )
|
||||
self.client_files_manager = ClientFiles.ClientFilesManager( self )
|
||||
|
||||
self.parsing_cache = ClientCaches.ParsingCache()
|
||||
|
@ -270,12 +271,12 @@ class Controller( object ):
|
|||
self.CallToThreadLongRunning( self.network_engine.MainLoop )
|
||||
|
||||
self.tag_display_manager = ClientTags.TagDisplayManager()
|
||||
self.tag_siblings_manager = ClientCaches.TagSiblingsManager( self )
|
||||
self.tag_parents_manager = ClientCaches.TagParentsManager( self )
|
||||
self._managers[ 'undo' ] = ClientCaches.UndoManager( self )
|
||||
self.tag_siblings_manager = ClientManagers.TagSiblingsManager( self )
|
||||
self.tag_parents_manager = ClientManagers.TagParentsManager( self )
|
||||
self._managers[ 'undo' ] = ClientManagers.UndoManager( self )
|
||||
self.server_session_manager = HydrusSessions.HydrusSessionManagerServer()
|
||||
|
||||
self.bitmap_manager = ClientCaches.BitmapManager( self )
|
||||
self.bitmap_manager = ClientManagers.BitmapManager( self )
|
||||
|
||||
self.local_booru_manager = ClientCaches.LocalBooruCache( self )
|
||||
self.client_api_manager = ClientAPI.APIManager()
|
||||
|
|
BIN
static/8chan.png
Before Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,7 @@
|
|||
Place a .css or .qss Qt Stylesheet file in here, and hydrus will provide it as an UI stylesheet option.
|
||||
|
||||
I think to do this properly we'll want folders so we can include additional assets like images.
|
||||
|
||||
Here's some examples, there are some QSS files buried here:
|
||||
|
||||
https://wiki.qt.io/Gallery_of_Qt_CSS_Based_Styles
|