Version 376

This commit is contained in:
Hydrus Network Developer 2019-12-04 23:29:32 -06:00
parent d18410121e
commit 129676d9ba
66 changed files with 3244 additions and 2384 deletions

View File

@ -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 &#123;file_id&#125; 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>

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

90
include/ClientGUIStyle.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

1340
include/ClientManagers.py Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -67,7 +67,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 375
SOFTWARE_VERSION = 376
CLIENT_API_VERSION = 11
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

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

View File

@ -370,7 +370,7 @@ def SetEnvTempDir( path ):
if tmp_name in os.environ:
os.putenv( tmp_name, path )
os.environ[ tmp_name ] = path

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

BIN
static/8kun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

7
static/qss/readme.txt Normal file
View File

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