Version 374

This commit is contained in:
Hydrus Network Developer 2019-11-20 17:10:46 -06:00
parent 69019f844f
commit 4d4f39984e
57 changed files with 818 additions and 409 deletions

View File

@ -22,7 +22,7 @@ try:
import argparse
argparser = argparse.ArgumentParser( description = 'hydrus network client (windowed)' )
argparser = argparse.ArgumentParser( description = 'hydrus network client (console)' )
argparser.add_argument( '-d', '--db_dir', help = 'set an external db location' )
argparser.add_argument( '--temp_dir', help = 'override the program\'s temporary directory' )
@ -38,7 +38,7 @@ try:
db_dir = HC.DEFAULT_DB_DIR
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_OSX_APP:
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_MACOS_APP:
db_dir = HC.USERPATH_DB_DIR
@ -103,8 +103,6 @@ try:
from include import HydrusData
import sys
from include import HydrusLogger
import traceback
@ -134,19 +132,36 @@ except Exception as e:
pass
error_trace = traceback.format_exc()
print( error_trace )
if 'db_dir' in locals() and os.path.exists( db_dir ):
error_trace = traceback.format_exc()
emergency_dir = db_dir
dest_path = os.path.join( db_dir, 'crash.log' )
else:
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
emergency_dir = os.path.expanduser( '~' )
possible_desktop = os.path.join( emergency_dir, 'Desktop' )
if os.path.exists( possible_desktop ) and os.path.isdir( possible_desktop ):
f.write( error_trace )
emergency_dir = possible_desktop
print( 'Critical boot error occurred! Details written to crash.log!' )
dest_path = os.path.join( emergency_dir, 'hydrus_crash.log' )
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
f.write( error_trace )
print( 'Critical boot error occurred! Details written to hydrus_crash.log in either db dir or user dir!' )
import sys
sys.exit( 1 )

View File

@ -38,7 +38,7 @@ try:
db_dir = HC.DEFAULT_DB_DIR
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_OSX_APP:
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_MACOS_APP:
db_dir = HC.USERPATH_DB_DIR
@ -103,8 +103,6 @@ try:
from include import HydrusData
import sys
from include import HydrusLogger
import traceback
@ -134,19 +132,36 @@ except Exception as e:
pass
error_trace = traceback.format_exc()
print( error_trace )
if 'db_dir' in locals() and os.path.exists( db_dir ):
error_trace = traceback.format_exc()
emergency_dir = db_dir
dest_path = os.path.join( db_dir, 'crash.log' )
else:
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
emergency_dir = os.path.expanduser( '~' )
possible_desktop = os.path.join( emergency_dir, 'Desktop' )
if os.path.exists( possible_desktop ) and os.path.isdir( possible_desktop ):
f.write( error_trace )
emergency_dir = possible_desktop
print( 'Critical boot error occurred! Details written to crash.log!' )
dest_path = os.path.join( emergency_dir, 'hydrus_crash.log' )
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
f.write( error_trace )
print( 'Critical boot error occurred! Details written to hydrus_crash.log in either db dir or user dir!' )
import sys
sys.exit( 1 )

View File

@ -61,7 +61,7 @@
<p>At some point, you will probably encounter a PIL error when importing a file. PIL is the Python Image Library, the code I use to manipulate image files. Some files are kooky, and just won't load with it. I can't fix these errors, since PIL is not mine. Just gotta deal with it.</p>
<p>If the PIL error'ing file is one you particularly care about, I suggest you import it into photoshop or similar and save it again. Photoshop should be clever enough to parse the file's weirdness, and then it'll hopefully save again to a simpler format that PIL, and hence the client, will be able to understand.</p>
<h3>busted up gifs</h3>
<p>Animated gifs are a real pain in the neck. The standard permits odd palettes and colourspaces, and PIL has a hard time parsing it all. I try my best to compensate, but some still break for reasons I can't fathom. I have fixed most of this on Windows by moving to OpenCV for gif rendering, but it still affects Linux and OS X.</p>
<p>Animated gifs are a real pain in the neck. The standard permits odd palettes and colourspaces, and PIL has a hard time parsing it all. I try my best to compensate, but some still break for reasons I can't fathom. I have fixed most of this on Windows by moving to OpenCV for gif rendering, but it still affects Linux and macOS.</p>
<p>So, some gifs will have a coloured first frame but grey frames thereafter; or they will have odd washy noise all over; or they will just be black. The file isn't broken, the client is just looking at it wrong.</p>
<h3>setting a password</h3>
<p>the client offers a very simple password system, enough to keep out noobs. You can set it at <i>database->set a password</i>. It will thereafter ask for the password every time you start the program, and will not open without it. However none of the database is encrypted, and someone with enough enthusiasm or a tool and access to your computer can still very easily see what files you have. The password is mainly to stop idle snoops checking your images if you are away from your machine.</p>

View File

@ -8,6 +8,57 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 374</h3></li>
<ul>
<li>qt environment/build:</li>
<li>macOS build is useable! tab drag and drop position calculation doesn't work yet, so intra-client file DnDs and tab rearrange DnDs are disabled for now. borderless fullscreen is also disabled, feedback on this vs maximise would be appreciated</li>
<li>fixed a critical bug in the macOS release that was resulting in 100% CPU repaint loop for the canvas viewer when media was loaded (wew). this may have affected certain other platforms in some situations</li>
<li>the linux build has a variety of common library files removed, letting your OS rely on higher compatibility system defaults. this _should_ clean up font and other issues for users running on very new/old system libraries. if you cannot run 374, please let me know your distro and version and any error messages</li>
<li>the special linux running from source document is updated, including info about Arch and PyQt5</li>
<li>fixed a windows build issue that meant some animated gifs were not able to load and render correctly</li>
<li>fixed a precise time fetching issue for users running from source with python 3.8</li>
<li>high dpi scaling should have improved support. please report on bad layout issues and other artifacts</li>
<li>fixed creating a serialised object png when using PyQt5</li>
<li>fixed file save dialogs with filetype filters when using PyQt5</li>
<li>fixed an important menubar related memory leak</li>
<li>_seem_ to have fixed an important media viewer memory leak</li>
<li>.</li>
<li>qt ui fixes:</li>
<li>fixed pages not collecting and sorting on creation if they do not have to, which restores the 'preserve flat unsorted order' behaviour of session loads and file drag and drop page tab creations</li>
<li>fixed the cursor not unhiding on move in the media viewer when over an animation or static image</li>
<li>fixed the issue where a new thumbnail panel would double-up with the old one for half a second if a menu caused the panel swap</li>
<li>reworked the elided (text that cuts off...) label code to more reliably work on single lines, which fits our purposes. the network job control (esppecially on subscription popups) and top hover window should now show their long statuses without changing their parent panel's layout</li>
<li>updated a variety of old text-wrap-width wx-hacks texts to instead auto-fill available space</li>
<li>the various downloaders should now be careful about handling large status texts. if a multiline error or html page slips in to a status somewhere, your download pages' lists should no longer go nuts with very tall spam-filled status cells</li>
<li>hydrus->discord drag and drop should be fixed if the BUGFIX is on!</li>
<li>fixed page tab drag and drop to do live drag selection with 'do not follow' behaviour (this is switched by holding down shift during drag), and, in this case, got it to return to the original page's neighbour/parent once the drop is complete</li>
<li>fixed 'center' dialogs positioning on the center of their parent windows, rather than the center of the primary screen</li>
<li>fixed the hover windows not passing shortcuts up to the media viewer when not consumed</li>
<li>fixed some misc 'can I consume a shortcut' focus/active checking code</li>
<li>fixed the various hide/parents/siblings tag menu items for tags with counts</li>
<li>fixed the main gui and other non-dialog windows remembering their pre-maximise/fullscreen sizes if set to remember size and previously closing while maximised/fullscreened</li>
<li>menubar menus should now show description text in the main gui statusbar on mouseover of their items</li>
<li>fixed a bad menu initialisation in the canvas preview panel</li>
<li>fixed a little page splitter bork and improved size of preview window on initial boot</li>
<li>fixed the edit notes dialog when launched from the media viewer</li>
<li>fixed a couple of text edit issues in edit url class panel</li>
<li>fixed page up/down scroll for taglists</li>
<li>fixed page down scroll for thumbnail grid, and fixed page up/down distance</li>
<li>fixed thumbnails not scrolling into view if they are keyboard-selected slightly off screen but within the scroll option percentage threshold</li>
<li>misc layout and style cleanup</li>
<li>misc refactoring</li>
<li>.</li>
<li>misc:</li>
<li>you can now set the maximum size of duplicate filter pair batches (default 250) under options->duplicates</li>
<li>when an ipfs service fails to pin a file and returns no hash or the empty multihash, this is now recognised, info dumped to log, a simple popup message sent, and the job continued. this is just a patch--better error handling here will come later</li>
<li>if the client or server are launched with a custom temp_dir that does not exist, it will now attempt to create it (previously errored out)</li>
<li>fixed a clean exit after certain client boot fail error handling, and repeated cleaner exit for the server</li>
<li>added some new memory profiling actions to the help->debug menu</li>
<li>parallel subscriptions should now initialise with less of an aggresive CPU spike</li>
<li>if the client or server crash before the application can be launched, the crash log is now called hydrus_crash.log. if the db dir is not yet established, it will now try to find and put it in your desktop and, failing that, then your user dir</li>
<li>the client no longer prints 'booting db' twice</li>
<li>a variety of misc code cleanup and fixes</li>
</ul>
<li><h3>version 373</h3></li>
<ul>
<li>qt:</li>

View File

@ -46,7 +46,7 @@
<li>client -d="D:\media\my_hydrus_database"</li>
<li><i>--or--</i></li>
<li>client --db_dir="G:\misc documents\New Folder (3)\DO NOT ENTER"</li>
<li><i>--or, for OS X--</i></li>
<li><i>--or, for macOS--</i></li>
<li>open -n -a "Hydrus Network.app" --args -d="/path/to/db"</li>
</ul>
<p>And it will instead use the given path. If no database is found, it will similarly create a new empty one at that location. You can use any path that is valid in your system, but I would not advise using network locations and so on, as the database works best with some clever device locking calls these interfaces may not provide.</p>

View File

@ -75,7 +75,7 @@
<li><b>application/zip</b> (.zip)</li>
<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 OS X. 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>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>
<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>

View File

@ -18,7 +18,7 @@
<li>If you know what you are doing and want a little more control, get the .zip. Don't extract it to Program Files unless you are willing to run it as administrator every time (it stores all its user data inside its own folder). You probably want something like D:\hydrus.</li>
<li><i>Note if you run &lt;Win10, you may need <a href="https://www.microsoft.com/en-us/download/details.aspx?id=48145">Visual C++ Redistributable for Visual Studio 2015</a>, if you don't already have it for vidya. If you run Win7, you will need some/all core OS updates released before 2017.</i></li>
</ul>
<p>for OS X:</p>
<p>for macOS:</p>
<ul>
<li>Get the .dmg App. Open it, drag it to Applications, and check the readme inside.</li>
</ul>
@ -33,7 +33,7 @@
<li>If you know Python, you can <a href="running_from_source.html">run from source</a>.</li>
</ul>
<p>Hydrus stores all its data&#x2014;options, files, subscriptions, <i>everything</i>&#x2014;entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine if you manually move it.</p>
<p><span class="warning">However, for OS X users:</span> the Hydrus App is <b>non-portable</b> and puts your database in ~/Library/Hydrus (i.e. /Users/[You]/Library/Hydrus). You can update simply by replacing the old App with the new, but if you wish to backup, you should be looking at ~/Library/Hydrus, not the App itself.</p>
<p><span class="warning">However, for macOS users:</span> the Hydrus App is <b>non-portable</b> and puts your database in ~/Library/Hydrus (i.e. /Users/[You]/Library/Hydrus). You can update simply by replacing the old App with the new, but if you wish to backup, you should be looking at ~/Library/Hydrus, not the App itself.</p>
<h3>updating</h3>
<p class="warning">Hydrus is imageboard-tier software, wild and fun but unprofessional. It is written by one Anon spinning a lot of plates. Mistakes happen from time to time, usually in the update process. There are also no training wheels to stop you from accidentally overwriting your whole db if you screw around. Be careful when updating. Make backups beforehand!</p>
<p>The update process:<p>

View File

@ -16,7 +16,7 @@
<p>But that by simply deleting the <i>libX11.so.6</i> file in the hydrus install directory, he was able to boot. I presume this meant my hydrus build was then relying on his local libX11.so, which happened to have better API compatibility. If you receive a similar error, you might like to try the same sort of thing. Let me know if you discover anything!</p>
<h3>what you will need</h3>
<p>You will need basic python experience, python 3.x and a number of python modules. Most of it you can get through pip.</p>
<p>If you are on Linux or OS X, or if you are on Windows and have an existing python you do not want to stomp all over with new modules, I recommend you create a virtual environment:</p>
<p>If you are on Linux or macOS, or if you are on Windows and have an existing python you do not want to stomp all over with new modules, I recommend you create a virtual environment:</p>
<p><i>Note, if you are on Linux, it may be easier to use your package manager instead of messing around with venv. A user has written a great summary with all needed packages <a href="running_from_source_linux_packages.txt">here</a>.</i></p>
<p>If you do want to create a new venv environment:</p>
<ul>
@ -51,7 +51,7 @@
<p>For Windows, depending on which compiler you are using, pip can have problems building some modules like lz4 and lxml. <a href="http://www.lfd.uci.edu/~gohlke/pythonlibs/">This page</a> has a lot of prebuilt binaries--I have found it very helpful many times. You may want to update python's sqlite3.dll as well--you can get it <a href="https://www.sqlite.org/download.html">here</a>, and just drop it in C:\Python37\DLLs or wherever you have python installed. I have a fair bit of experience with Windows python, so send me a mail if you need help.</a>
<p>If you don't have ffmpeg in your PATH and you want to import videos, you will need to put a static <a href="https://ffmpeg.org/">FFMPEG</a> executable in the install_dir/bin directory. Have a look at how I do it in the extractable compiled releases if you can't figure it out. On Windows, you can copy the exe from one of those releases, or just download the latest static build right from the FFMPEG site.</a>
<p>Once you have everything set up, client.pyw and server.py should look for and run off client.db and server.db just like the executables. They will look in the 'db' directory by default, or anywhere you point them with the "-d" parameter, again just like the executables.</p>
<p>I develop hydrus on and am most experienced with Windows, so the program is more stable and reasonable on that. I do not have as much experience with Linux or OS X, so I would particularly appreciate your Linux/OS X bug reports and any informed suggestions.</p>
<p>I develop hydrus on and am most experienced with Windows, so the program is more stable and reasonable on that. I do not have as much experience with Linux or macOS, so I would particularly appreciate your Linux/macOS bug reports and any informed suggestions.</p>
<h3>my code</h3>
<p>Unlike most software people, I am more INFJ than INTP/J. My coding style is unusual and unprofessional, and everything is pretty much hacked together. Please look through the source if you are interested in how things work and ask me if you don't understand something. I'm constantly throwing new code together and then cleaning and overhauling it down the line.</p>
<p>I work strictly alone, so while I am very interested in detailed bug reports or suggestions for good libraries to use, I am not looking for pull requests. Everything I do is <a href="https://github.com/sirkris/WTFPL/blob/master/WTFPL.md">WTFPL</a>, so feel free to fork and play around with things on your end as much as you like.</p>

View File

@ -7,6 +7,14 @@ Why use distro packages instead of pip?
The following lists should work for any recent release of the named distros and derivatives. However, for some modules, some distros might ship versions
that are too old. These should be installed through pip until the distro adds/updates the package.
IMPORTANT PySide2 + Python 3.8 compatibility notice
===================================================
This especially applies to Arch and other rolling release distro users.
Slower distros most likely won't upgrade right away to Python 3.8 and thus won't have this problem.
If you are using Python 3.8 or newer, you need at least Qt version 5.14 and the corresponding PySide2 version.
Otherwise you will get a `TypeError: 'Shiboken.ObjectType' object is not iterable` error on startup. To temporarily work around this by using PyQt5,
install PyQt5 instead of PySide2, then either remove PySide2 or set the QT_API environment variable to PyQt5.
Arch Linux, Manjaro
===================
Install from the AUR: https://aur.archlinux.org/packages/hydrus/
@ -17,19 +25,25 @@ Note that you can use PyQt5 instead of PySide2 (however PySide2 is recommended).
Ubuntu, Debian, Linux Mint
==========================
Note: if you are using an older release, the python3-pyside2.* packages listed below might not be available. If that is the case,
install PySide2 with:
pip3 install --user pyside2
Required:
python3-chardet python3-html5lib python3-bs4 python3-lxml
python3-nose python3-numpy python3-opencv python3-six python3-pil
python3-psutil python3-openssl python3-yaml python3-requests
python3-send2trash python3-service-identity python3-twisted
ffmpeg python3-pyside2.qtwidgets python3-pyside2.qtcore python3-pyside2.qtgui
ffmpeg python3-pyside2.qtwidgets python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtcharts
You also need python3-qtpy, but the version shipped by these distros (as of this writing) is too old. At least version 1.8 is required. Until the distros catch up, it is recommended that you install
this module with pip instead of the package manager.
You might need to install the shiboken2 module from pip since it looks like it is not currently packaged.
this module with pip instead of the package manager, like this:
pip3 install --user qtpy
You might need to install the shiboken2 module from pip since it looks like it is not currently packaged:
pip3 install --user shiboken2
Instead of PySide2 (the python3-pyside2.* packages above and shiboken2), you can also use PyQt5 (python3-pyqt5, python3-sip), though PySide2 is recommended.
Optional:
python3-lz4 python3-pysocks python3-matplotlib python3-mock python3-httmock
python3-lz4 python3-pysocks python3-mock python3-httmock
pylzma doesn't seem to be packaged, but it is not essential.
@ -51,7 +65,7 @@ python3-shiboken2
Instead of PySide2 (python3-pyside2, python3-shiboken2) you can also use PyQt5 (python3-sip, python3-qt5).
The optional stuff:
python3-lz4 python3-pysocks python3-matplotlib python3-matplotlib-qt5 python3-httmock
python3-lz4 python3-pysocks python3-httmock qt5-qtcharts
mock and pylzma doesn't seem to be packaged, and ffmpeg isn't in the base Fedora repo (due to licensing/patents scare?),
but can be installed from the rpmfusion repo (https://rpmfusion.org/). From these, only ffmpeg is necessary for normal users.
@ -73,7 +87,7 @@ You might need to install the shiboken2 module from pip since it looks like it i
Instead of PySide2 (python3-pyside2 and the shiboken2 module) you can also use PyQt5 (python3-sip, python3-qt5).
Optional:
python3-lz4 python3-pylzma python3-PySocks python3-matplotlib python3-matplotlib-qt5
python3-mock python3-httmock
python3-lz4 python3-pylzma python3-PySocks
python3-mock python3-httmock libqt5-qtcharts
Package search: https://software.opensuse.org/search

View File

@ -13,7 +13,7 @@
<li>A <b>server</b> is an instantiation of the hydrus server executable (e.g. server.exe in Windows). It has a complicated and flexible database that can run many different services in parallel.</li>
<li>A <b>service</b> sits on a port (e.g. 45871) and responds to certain http requests (e.g. /file or /update) that the hydrus client can plug into. A service might be a repository for a certain kind of data, the administration interface to manage what services run on a server, or anything else.</li>
</ul>
<p>Setting up a hydrus server is easy compared to, say, Apache. There are no .conf files to mess about with, and everything is controlled through the client. When started, the server will place an icon in your system tray in Windows or open a small frame in Linux or OS X. To close the server, either right-click the system tray icon and select exit, or just close the frame.</p>
<p>Setting up a hydrus server is easy compared to, say, Apache. There are no .conf files to mess about with, and everything is controlled through the client. When started, the server will place an icon in your system tray in Windows or open a small frame in Linux or macOS. To close the server, either right-click the system tray icon and select exit, or just close the frame.</p>
<p>The basic process for setting up a server is:</p>
<ul>
<li>Start the server.</li>
@ -54,4 +54,4 @@
<p>Remember that everything is breaking all the time. Make regular backups, and you'll minimise your problems.</p>
</div>
</body>
</html>
</html>

View File

@ -7,7 +7,7 @@
<body>
<div class="content">
<h3>getting it to work on wine</h3>
<p>Several Linux and OS X users have found success running hydrus with Wine. Here is a post from a Linux dude:</p>
<p>Several Linux and macOS users have found success running hydrus with Wine. Here is a post from a Linux dude:</p>
<i>
<p>Some things I picked up on after extended use:</p>
<ul>
@ -30,4 +30,4 @@
<p>If you get the client running in Wine, please let me know how you get on!</p>
</div>
</body>
</html>
</html>

View File

@ -311,7 +311,7 @@ SHORTCUT_KEY_SPECIAL_F10 = 26
SHORTCUT_KEY_SPECIAL_F11 = 27
SHORTCUT_KEY_SPECIAL_F12 = 28
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
DELETE_KEYS = ( QC.Qt.Key_Backspace, QC.Qt.Key_Delete )

View File

@ -1207,6 +1207,12 @@ class Controller( HydrusController.HydrusController ):
def Run( self ):
QP.MonkeyPatchMissingMethods()
if hasattr( QC.Qt, 'AA_EnableHighDpiScaling' ):
QW.QApplication.setAttribute( QC.Qt.AA_EnableHighDpiScaling, True )
self.app = App( sys.argv )
HydrusData.Print( 'booting controller\u2026' )

View File

@ -198,8 +198,6 @@ class DB( HydrusDB.HydrusDB ):
HydrusDB.HydrusDB.__init__( self, controller, db_dir, db_name )
self._controller.pub( 'splash_set_title_text', 'booting db\u2026' )
def _AddFilesInfo( self, rows, overwrite = False ):
@ -3020,7 +3018,7 @@ class DB( HydrusDB.HydrusDB ):
result = self._c.execute( 'SELECT DISTINCT smaller_media_id, larger_media_id, distance FROM ' + table_join + ' WHERE ' + predicate_string + ' LIMIT 2500;' ).fetchall()
MAX_BATCH_SIZE = 250
MAX_BATCH_SIZE = HG.client_controller.new_options.GetInteger( 'duplicate_filter_max_batch_size' )
batch_of_pairs_of_media_ids = []
seen_media_ids = set()

View File

@ -320,6 +320,12 @@ def DAEMONSynchroniseSubscriptions( controller ):
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 )

View File

@ -654,7 +654,7 @@ class Credentials( HydrusData.HydrusYAMLBase ):
return connection_string
def HasAccessKey( self ): return self._access_key is not None and self._access_key is not ''
def HasAccessKey( self ): return self._access_key is not None and self._access_key != ''
def SetAccessKey( self, access_key ): self._access_key = access_key

View File

@ -18,7 +18,7 @@ def GetClientDefaultOptions():
options[ 'play_dumper_noises' ] = True
options[ 'export_path' ] = None
options[ 'hpos' ] = 400
options[ 'vpos' ] = 700
options[ 'vpos' ] = -240
options[ 'thumbnail_cache_size' ] = 25 * 1048576
options[ 'preview_cache_size' ] = 15 * 1048576
options[ 'fullscreen_cache_size' ] = 150 * 1048576

View File

@ -1,4 +1,5 @@
from . import ClientGUIFunctions
from . import HydrusConstants as HC
from . import HydrusGlobals as HG
from . import HydrusPaths
import json
@ -8,11 +9,32 @@ from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from . import QtPorting as QP
# we do this because some programs like discord will disallow exports with additional custom mimetypes (like 'application/hydrus-files')
# as this is only ever an internal transfer, and as the python mimedata object is preserved through the dnd, we can just tack this info on with a subclass and python variables
class QMimeDataHydrusFiles( QC.QMimeData ):
def __init__( self ):
QC.QMimeData.__init__( self )
self._hydrus_files = None
def hydrusFiles( self ):
return self._hydrus_files
def setHydrusFiles( self, page_key, hashes ):
self._hydrus_files = ( page_key, hashes )
def DoFileExportDragDrop( window, page_key, media, alt_down ):
drop_source = QG.QDrag( window )
data_object = QC.QMimeData()
data_object = QMimeDataHydrusFiles()
#
@ -91,6 +113,13 @@ 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 )
# old way of doing this that makes some external programs (discord) reject it
'''
if page_key is None:
encoded_page_key = None
@ -107,7 +136,7 @@ def DoFileExportDragDrop( window, page_key, media, alt_down ):
data_bytes = bytes( data_str, 'utf-8' )
data_object.setData( 'application/hydrus-media', data_bytes )
'''
#
drop_source.setMimeData( data_object )
@ -155,9 +184,27 @@ class FileDropTarget( QC.QObject ):
def OnData( self, mime_data, result ):
if mime_data.formats():
if mime_data.formats().count( 'application/hydrus-media' ) 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:
( 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()
@ -175,7 +222,7 @@ class FileDropTarget( QC.QObject ):
result = QC.Qt.MoveAction
'''
elif mime_data.hasUrls() and self._filenames_callable is not None:
paths = []
@ -229,10 +276,10 @@ class FileDropTarget( QC.QObject ):
screen_position = ClientGUIFunctions.ClientToScreen( self._parent, ( x, y ) )
drop_tlp = QW.QApplication.topLevelAt( screen_position )
my_tlp = self._parent.window()
drop_tlw = QW.QApplication.topLevelAt( screen_position )
my_tlw = self._parent.window()
if drop_tlp == my_tlp:
if drop_tlw == my_tlw:
return True

View File

@ -55,7 +55,7 @@ regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_OTHER_HASHES ] =
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_DELETE_NEIGHBOUR_DUPES ] = 'Sometimes, a file metadata regeneration will mean a new filetype and thus a new file extension. If the existing, incorrectly named file is in use, it must be copied rather than renamed, and so there is a spare duplicate left over after the operation. This jobs cleans up the duplicate at a later time.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE ] = 'This checks to see if the file is present in the file system as expected. If it is not, the internal file record in the database is removed, just as if the file were deleted. Use this if you have manually deleted or otherwise lost a number of files from your file structure and need hydrus to re-sync with what it has. Missing files will have their known URLs exported to your database directory if you wish to attempt to re-download them.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_DATA ] = 'This does the same check as the \'present\' job, and if the file is where it is expected, it ensures its file content, byte-for-byte, is correct. This is a heavy job, so be wary. Files that are incorrect will be exported to your database directory along with their known URLs.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_FIX_PERMISSIONS ] = 'This ensures that files in the file system are readable and writeable. For Linux/OS X users, it specifically sets 644. If you wish to run this job on Linux/OS X, ensure you are first the file owner of all your files.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_FIX_PERMISSIONS ] = 'This ensures that files in the file system are readable and writeable. For Linux/macOS users, it specifically sets 644. If you wish to run this job on Linux/macOS, ensure you are first the file owner of all your files.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_CHECK_SIMILAR_FILES_MEMBERSHIP ] = 'This checks to see if files should be in the similar files system, and if they are falsely in or falsely out, it will remove their record or queue them up for a search as appropriate. It is useful to repair database damage.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_SIMILAR_FILES_METADATA ] = 'This forces a regeneration of the file\'s similar-files \'phashes\'. It is not useful unless you know there is missing data to repair.'
regen_file_enum_to_description_lookup[ REGENERATE_FILE_DATA_JOB_FILE_MODIFIED_TIMESTAMP ] = 'This rechecks the file\'s modified timestamp and saves it to the database.'

View File

@ -53,6 +53,7 @@ from . import HydrusText
from . import HydrusVideoHandling
import os
import PIL
import random
import re
import sqlite3
import ssl
@ -177,7 +178,16 @@ def THREADUploadPending( service_key ):
hash = media_result.GetHash()
mime = media_result.GetMime()
service.PinFile( hash, mime )
try:
service.PinFile( hash, mime )
except HydrusExceptions.DataMissing:
HydrusData.ShowText( 'File {} could not be pinned!'.format( hash.hexh() ) )
continue
else:
@ -277,6 +287,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._notebook = ClientGUIPages.PagesNotebook( self, self._controller, 'top page notebook' )
self._garbage_snapshot = collections.Counter()
self._last_clipboard_watched_text = ''
self._clipboard_watcher_destination_page_watcher = None
self._clipboard_watcher_destination_page_urls = None
@ -1053,6 +1065,26 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._controller.CallToThread( do_it, self._controller )
def _DebugLongTextPopup( self ):
words = [ 'test', 'a', 'longish', 'statictext', 'm8' ]
text = random.choice( words )
job_key = ClientThreading.JobKey()
job_key.SetVariable( 'popup_text_1', text )
self._controller.pub( 'message', job_key )
for i in range( 2, 64 ):
text += ' {}'.format( random.choice( words ) )
self._controller.CallLater( i * 0.2, job_key.SetVariable, 'popup_text_1', text )
def _DebugMakeParentlessTextCtrl( self ):
with QP.Dialog( None, title = 'parentless debug dialog' ) as dlg:
@ -1122,6 +1154,48 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
def _DebugShowGarbageDifferences( self ):
count = collections.Counter()
for o in gc.get_objects():
count[ type( o ) ] += 1
count.subtract( self._garbage_snapshot )
text = 'Garbage differences start here:'
to_print = list( count.items() )
to_print.sort( key = lambda pair: -pair[1] )
for ( t, count ) in to_print:
if count == 0:
continue
text += os.linesep + '{}: {}'.format( t, HydrusData.ToHumanInt( count ) )
HydrusData.ShowText( text )
def _DebugTakeGarbageSnapshot( self ):
count = collections.Counter()
for o in gc.get_objects():
count[ type( o ) ] += 1
self._garbage_snapshot = count
def _DebugPrintGarbage( self ):
HydrusData.ShowText( 'Printing garbage to log' )
@ -1166,10 +1240,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for o in gc.get_objects():
if 'FullscreenHover' in str( type( o ) ):
objects_to_inspect.add( o )
# add objects to inspect here
count[ type( o ) ] += 1
@ -1367,6 +1438,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
return index
return -1
@ -1394,9 +1467,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
i_and_e_submenu = QW.QMenu( self )
i_and_e_submenu = QW.QMenu( menu )
submenu = QW.QMenu( self )
submenu = QW.QMenu( i_and_e_submenu )
ClientGUIMenus.AppendMenuCheckItem( submenu, 'import folders', 'Pause the client\'s import folders.', HC.options['pause_import_folders_sync'], self._PauseSync, 'import_folders' )
ClientGUIMenus.AppendMenuCheckItem( submenu, 'export folders', 'Pause the client\'s export folders.', HC.options['pause_export_folders_sync'], self._PauseSync, 'export_folders' )
@ -1409,7 +1482,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( import_folder_names ) > 0:
submenu = QW.QMenu( self )
submenu = QW.QMenu( i_and_e_submenu )
if len( import_folder_names ) > 1:
@ -1429,7 +1502,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( export_folder_names ) > 0:
submenu = QW.QMenu( self )
submenu = QW.QMenu( i_and_e_submenu )
if len( export_folder_names ) > 1:
@ -1456,7 +1529,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
open = QW.QMenu( self )
open = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( open, 'installation directory', 'Open the installation directory for this client.', self._OpenInstallFolder )
ClientGUIMenus.AppendMenuItem( open, 'database directory', 'Open the database directory for this instance of the client.', self._OpenDBFolder )
@ -1509,7 +1582,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
undo_pages = QW.QMenu( self )
undo_pages = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( undo_pages, 'clear all', 'Remove all closed pages from memory.', self.DeleteAllClosedPages )
@ -1527,6 +1600,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
args.reverse() # so that recently closed are at the top
for ( index, name ) in args:
ClientGUIMenus.AppendMenuItem( undo_pages, name, 'Restore this page.', self._UnclosePage, index )
@ -1556,7 +1630,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenuItem( menu, 'refresh', 'If the current page has a search, refresh it.', self._Refresh )
splitter_menu = QW.QMenu( self )
splitter_menu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( splitter_menu, 'show/hide', 'Show or hide the panels on the left.', self._ShowHideSplitters )
@ -1578,11 +1652,11 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
gui_session_names = self._controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_GUI_SESSION )
sessions = QW.QMenu( self )
sessions = QW.QMenu( menu )
if len( gui_session_names ) > 0:
load = QW.QMenu( self )
load = QW.QMenu( sessions )
for name in gui_session_names:
@ -1591,9 +1665,10 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( sessions, load, 'clear and load' )
append = QW.QMenu( self )
append = QW.QMenu( sessions )
for name in gui_session_names:
ClientGUIMenus.AppendMenuItem( append, name, 'Append this session to whatever pages are already open.', self._notebook.AppendGUISession, name )
@ -1603,7 +1678,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( gui_session_names_to_backup_timestamps ) > 0:
append_backup = QW.QMenu( self )
append_backup = QW.QMenu( sessions )
rows = list( gui_session_names_to_backup_timestamps.items() )
@ -1611,7 +1686,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for ( name, timestamps ) in rows:
submenu = QW.QMenu( self )
submenu = QW.QMenu( append_backup )
for timestamp in timestamps:
ClientGUIMenus.AppendMenuItem( submenu, HydrusData.ConvertTimestampToPrettyTime( timestamp ), 'Append this backup session to whatever pages are already open.', self._notebook.AppendGUISessionBackup, name, timestamp )
@ -1624,7 +1699,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
save = QW.QMenu( self )
save = QW.QMenu( sessions )
for name in gui_session_names:
@ -1640,7 +1715,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( set( gui_session_names ).difference( ClientGUIPages.RESERVED_SESSION_NAMES ) ) > 0:
delete = QW.QMenu( self )
delete = QW.QMenu( sessions )
for name in gui_session_names:
@ -1662,7 +1737,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
search_menu = QW.QMenu( self )
search_menu = QW.QMenu( menu )
services = self._controller.services_manager.GetServices()
@ -1687,7 +1762,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( petition_resolvable_repositories ) > 0:
petition_menu = QW.QMenu( self )
petition_menu = QW.QMenu( menu )
for service in petition_resolvable_repositories:
ClientGUIMenus.AppendMenuItem( petition_menu, service.GetName(), 'Open a new petition page for ' + service.GetName() + '.', self._notebook.NewPagePetitions, service.GetServiceKey(), on_deepest_notebook=True )
@ -1698,7 +1773,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
download_menu = QW.QMenu( self )
download_menu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( download_menu, 'url download', 'Open a new tab to download some separate urls.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_url_downloader_page' ) )
ClientGUIMenus.AppendMenuItem( download_menu, 'watcher', 'Open a new tab to watch threads or other updating locations.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_watcher_downloader_page' ) )
@ -1713,7 +1788,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if has_ipfs:
download_popup_menu = QW.QMenu( self )
download_popup_menu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( download_popup_menu, 'an ipfs multihash', 'Enter an IPFS multihash and attempt to import whatever is returned.', self._StartIPFSDownload )
@ -1722,7 +1797,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
special_menu = QW.QMenu( self )
special_menu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( special_menu, 'page of pages', 'Open a new tab that can hold more tabs.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_page_of_pages' ) )
ClientGUIMenus.AppendMenuItem( special_menu, 'duplicates processing', 'Open a new tab to discover and filter duplicate files.', self.ProcessApplicationCommand, ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'new_duplicate_filter_page' ) )
@ -1733,7 +1808,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
special_command_menu = QW.QMenu( self )
special_command_menu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( special_command_menu, 'clear all multiwatcher highlights', 'Command all multiwatcher pages to clear their highlighted watchers.', HG.client_controller.pub, 'clear_multiwatcher_highlights' )
@ -1777,9 +1852,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
file_maintenance_menu = QW.QMenu( self )
file_maintenance_menu = QW.QMenu( submenu )
ClientGUIMenus.AppendMenuItem( file_maintenance_menu, 'review scheduled jobs', 'Review outstanding jobs, and schedule new ones.', self._ReviewFileMaintenance )
ClientGUIMenus.AppendSeparator( file_maintenance_menu )
@ -1806,18 +1881,19 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenuItem( submenu, 'clear orphan file records', 'Clear out surplus file records that have not been deleted correctly.', self._ClearOrphanFileRecords )
if self._controller.new_options.GetBoolean( 'advanced_mode' ):
ClientGUIMenus.AppendMenuItem( submenu, 'clear orphan tables', 'Clear out surplus db tables that have not been deleted correctly.', self._ClearOrphanTables )
ClientGUIMenus.AppendMenu( menu, submenu, 'maintain' )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'database integrity', 'Have the database examine all its records for internal consistency.', self._CheckDBIntegrity )
ClientGUIMenus.AppendMenu( menu, submenu, 'check' )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'autocomplete cache', 'Delete and recreate the tag autocomplete cache, fixing any miscounts.', self._RegenerateACCache )
ClientGUIMenus.AppendMenuItem( submenu, 'similar files search tree', 'Delete and recreate the similar files search tree.', self._RegenerateSimilarFilesTree )
@ -1826,7 +1902,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'clear all file viewing statistics', 'Delete all file viewing records from the database.', self._ClearFileViewingStats )
ClientGUIMenus.AppendMenuItem( submenu, 'cull file viewing statistics based on current min/max values', 'Cull your file viewing statistics based on minimum and maximum permitted time deltas.', self._CullFileViewingStats )
@ -1887,7 +1963,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
menu = QW.QMenu( self )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'commit', 'Upload ' + name + '\'s pending content.', self._UploadPending, service_key )
ClientGUIMenus.AppendMenuItem( submenu, 'forget', 'Clear ' + name + '\'s pending content.', self._DeletePending, service_key )
@ -1919,7 +1995,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
menu = QW.QMenu( self )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
pause_all_new_network_traffic = self._controller.new_options.GetBoolean( 'pause_all_new_network_traffic' )
@ -1936,7 +2012,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'review bandwidth usage', 'See where you are consuming data.', self._ReviewBandwidth )
ClientGUIMenus.AppendMenuItem( submenu, 'review current network jobs', 'Review the jobs currently running in the network engine.', self._ReviewNetworkJobs )
@ -1951,7 +2027,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
if not ClientParsing.HTML5LIB_IS_OK:
@ -1973,7 +2049,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( submenu )
clipboard_menu = QW.QMenu( self )
clipboard_menu = QW.QMenu( submenu )
ClientGUIMenus.AppendMenuCheckItem( clipboard_menu, 'watcher urls', 'Automatically import watcher URLs that enter the clipboard just as if you drag-and-dropped them onto the ui.', self._controller.new_options.GetBoolean( 'watch_clipboard_for_watcher_urls' ), self._FlipClipboardWatcher, 'watch_clipboard_for_watcher_urls' )
ClientGUIMenus.AppendMenuCheckItem( clipboard_menu, 'other recognised urls', 'Automatically import recognised URLs that enter the clipboard just as if you drag-and-dropped them onto the ui.', self._controller.new_options.GetBoolean( 'watch_clipboard_for_other_recognised_urls' ), self._FlipClipboardWatcher, 'watch_clipboard_for_other_recognised_urls' )
@ -1993,7 +2069,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenuItem( submenu, 'manage logins', 'Edit which domains you wish to log in to.', self._ManageLogins )
debug_menu = QW.QMenu( self )
debug_menu = QW.QMenu( submenu )
ClientGUIMenus.AppendMenuItem( debug_menu, 'do tumblr GDPR click-through', 'Do a manual click-through for the tumblr GDPR page.', self._controller.CallLater, 0.0, self._controller.network_engine.login_manager.LoginTumblrGDPR )
@ -2003,7 +2079,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
#
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'manage gallery url generators', 'Manage the client\'s GUGs, which convert search terms into URLs.', self._ManageGUGs )
ClientGUIMenus.AppendMenuItem( submenu, 'manage url classes', 'Configure which URLs the client can recognise.', self._ManageURLClasses )
@ -2034,7 +2110,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
tag_services = self._controller.services_manager.GetServices( ( HC.TAG_REPOSITORY, ) )
file_services = self._controller.services_manager.GetServices( ( HC.FILE_REPOSITORY, ) )
submenu = QW.QMenu( self )
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuCheckItem( submenu, 'repositories synchronisation', 'Pause the client\'s synchronisation with hydrus repositories.', HC.options['pause_repo_sync'], self._PauseSync, 'repo' )
@ -2055,11 +2131,11 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if len( admin_repositories ) > 0 or len( server_admins ) > 0:
admin_menu = QW.QMenu( self )
admin_menu = QW.QMenu( menu )
for service in admin_repositories:
submenu = QW.QMenu( self )
submenu = QW.QMenu( admin_menu )
service_key = service.GetServiceKey()
@ -2093,7 +2169,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
for service in server_admins:
submenu = QW.QMenu( self )
submenu = QW.QMenu( admin_menu )
service_key = service.GetServiceKey()
@ -2164,7 +2240,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenuItem( menu, 'help and getting started guide', 'Open hydrus\'s local help in your web browser.', ClientPaths.LaunchPathInWebBrowser, os.path.join( HC.HELP_DIR, 'index.html' ) )
links = QW.QMenu( self )
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' )
@ -2200,9 +2276,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendSeparator( menu )
debug = QW.QMenu( self )
debug = QW.QMenu( menu )
debug_modes = QW.QMenu( self )
debug_modes = QW.QMenu( debug )
ClientGUIMenus.AppendMenuCheckItem( debug_modes, 'force idle mode', 'Make the client consider itself idle and fire all maintenance routines right now. This may hang the gui for a while.', HG.force_idle_mode, self._SwitchBoolean, 'force_idle_mode' )
ClientGUIMenus.AppendMenuCheckItem( debug_modes, 'no page limit mode', 'Let the user create as many pages as they want with no warnings or prohibitions.', HG.no_page_limit_mode, self._SwitchBoolean, 'no_page_limit_mode' )
@ -2211,7 +2287,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( debug, debug_modes, 'debug modes' )
profile_modes = QW.QMenu( self )
profile_modes = QW.QMenu( debug )
ClientGUIMenus.AppendMenuCheckItem( profile_modes, 'db profile mode', 'Run detailed \'profiles\' on every database query and dump this information to the log (this is very useful for hydrus dev to have, if something is running slow for you!).', HG.db_profile_mode, self._SwitchBoolean, 'db_profile_mode' )
ClientGUIMenus.AppendMenuCheckItem( profile_modes, 'menu profile mode', 'Run detailed \'profiles\' on menu actions.', HG.menu_profile_mode, self._SwitchBoolean, 'menu_profile_mode' )
@ -2220,7 +2296,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( debug, profile_modes, 'profile modes' )
report_modes = QW.QMenu( self )
report_modes = QW.QMenu( debug )
ClientGUIMenus.AppendMenuCheckItem( report_modes, 'callto report mode', 'Report whenever the thread pool is given a task.', HG.callto_report_mode, self._SwitchBoolean, 'callto_report_mode' )
ClientGUIMenus.AppendMenuCheckItem( report_modes, 'daemon report mode', 'Have the daemons report whenever they fire their jobs.', HG.daemon_report_mode, self._SwitchBoolean, 'daemon_report_mode' )
@ -2239,9 +2315,10 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( debug, report_modes, 'report modes' )
gui_actions = QW.QMenu( self )
gui_actions = QW.QMenu( debug )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make some popups', 'Throw some varied popups at the message manager, just to check it is working.', self._DebugMakeSomePopups )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a long text popup', 'Make a popup with text that will grow in size.', self._DebugLongTextPopup )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a popup in five seconds', 'Throw a delayed popup at the message manager, giving you time to minimise or otherwise alter the client before it arrives.', self._controller.CallLater, 5, HydrusData.ShowText, 'This is a delayed popup message.' )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a modal popup in five seconds', 'Throw up a delayed modal popup to test with. It will stay alive for five seconds.', self._DebugMakeDelayedModalPopup )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a new page in five seconds', 'Throw a delayed page at the main notebook, giving you time to minimise or otherwise alter the client before it arrives.', self._controller.CallLater, 5, self._controller.pub, 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY )
@ -2252,7 +2329,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( debug, gui_actions, 'gui actions' )
data_actions = QW.QMenu( self )
data_actions = QW.QMenu( debug )
ClientGUIMenus.AppendMenuItem( data_actions, 'run fast memory maintenance', 'Tell all the fast caches to maintain themselves.', self._controller.MaintainMemoryFast )
ClientGUIMenus.AppendMenuItem( data_actions, 'run slow memory maintenance', 'Tell all the slow caches to maintain themselves.', self._controller.MaintainMemorySlow )
@ -2260,6 +2337,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenuItem( data_actions, 'show scheduled jobs', 'Print some information about the currently scheduled jobs log.', self._DebugShowScheduledJobs )
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 )
ClientGUIMenus.AppendMenuItem( data_actions, 'show garbage snapshot changes', 'Show object count differences from the last snapshot.', self._DebugShowGarbageDifferences )
ClientGUIMenus.AppendMenuItem( data_actions, 'enable truncated image loading', 'Enable the truncated image loading to test out broken jpegs.', self._EnableLoadTruncatedImages )
ClientGUIMenus.AppendMenuItem( data_actions, 'clear image rendering cache', 'Tell the image rendering system to forget all current images. This will often free up a bunch of memory immediately.', self._controller.ClearCaches )
ClientGUIMenus.AppendMenuItem( data_actions, 'clear thumbnail cache', 'Tell the thumbnail cache to forget everything and redraw all current thumbs.', self._controller.pub, 'reset_thumbnail_cache' )
@ -2268,7 +2347,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
ClientGUIMenus.AppendMenu( debug, data_actions, 'data actions' )
network_actions = QW.QMenu( self )
network_actions = QW.QMenu( debug )
ClientGUIMenus.AppendMenuItem( network_actions, 'fetch a url', 'Fetch a URL using the network engine as per normal.', self._DebugFetchAURL )
@ -2305,9 +2384,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
menu.menuAction().setProperty( 'hydrus_menubar_name', name )
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
menu.setTitle( label ) # causes bugs in os x if this is not here
menu.setTitle( label ) # causes bugs in macOS if this is not here
@ -4281,11 +4360,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
dialog_open = False
tlps = QW.QApplication.topLevelWidgets()
tlws = QW.QApplication.topLevelWidgets()
for tlp in tlps:
for tlw in tlws:
if isinstance( tlp, QP.Dialog ) and tlp.isModal():
if isinstance( tlw, QP.Dialog ) and tlw.isModal():
dialog_open = True
@ -4558,9 +4637,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HydrusData.PrintException( e )
for tlp in QW.QApplication.topLevelWidgets():
for tlw in QW.QApplication.topLevelWidgets():
tlp.hide()
tlw.hide()
if HG.emergency_exit:
@ -4879,9 +4958,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def PresentImportedFilesToPage( self, hashes, page_name ):
tlp = self.window()
tlw = self.window()
if tlp.isMinimized() and not self._notebook.HasMediaPageName( page_name ):
if tlw.isMinimized() and not self._notebook.HasMediaPageName( page_name ):
self._controller.CallLaterQtSafe(self, 10.0, self.PresentImportedFilesToPage, hashes, page_name)
@ -5031,10 +5110,10 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
name = self._dirty_menus.pop()
( menu_or_none, label ) = self._GenerateMenuInfo( name )
old_menu_index = self._FindMenuBarIndex( name )
( menu_or_none, label ) = self._GenerateMenuInfo( name )
if old_menu_index == -1:
if menu_or_none is not None:
@ -5064,11 +5143,14 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if len( self._menubar.actions() ) > insert_index:
action_before = self._menubar.actions()[ insert_index ]
else:
action_before = None
menu.setParent( self )
self._menubar.insertMenu( action_before, menu )
@ -5078,9 +5160,16 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._menubar.actions()[ old_menu_index ].setText( label )
if menu_or_none is not None:
ClientGUIMenus.DestroyMenu( self, menu_or_none )
else:
old_menu = self._menubar.actions()[ old_menu_index ]
old_action = self._menubar.actions()[ old_menu_index ]
old_menu = old_action.menu()
if menu_or_none is not None:
@ -5089,17 +5178,17 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
menu.setTitle( label )
menu.setParent( self )
self._menubar.insertMenu( old_menu, menu )
self._menubar.removeAction( old_menu )
self._menubar.insertMenu( old_action, menu )
self._menubar.removeAction( old_action )
else:
self._menubar.removeAction( old_menu )
self._menubar.removeAction( old_action )
ClientGUIMenus.DestroyMenu( self, old_menu )
@ -5388,9 +5477,9 @@ class FrameSplashPanel( QW.QWidget ):
( title_text, status_text, status_subtext ) = self._my_status.GetTexts()
painter.setBackground( QG.QBrush( QP.GetSystemColour( QG.QPalette.Window ) ) )
painter.setBackground( QG.QBrush( QP.GetSystemColour( QG.QPalette.Base ) ) )
# painter.eraseRect( painter.viewport() )
painter.eraseRect( painter.viewport() )
#
@ -5399,8 +5488,6 @@ class FrameSplashPanel( QW.QWidget ):
painter.drawPixmap( x, y, self._hydrus_pixmap )
painter.setPen( QG.QPen( QC.Qt.black ) )
painter.setFont( QW.QApplication.font() )
y += 166 + 15
@ -5592,8 +5679,6 @@ class FrameSplash( QW.QWidget ):
self.setWindowIcon( QG.QIcon( self._controller.frame_icon_pixmap ) )
QP.SetBackgroundColour( self, QC.Qt.white )
self._my_panel = FrameSplashPanel( self, self._controller )
self._vbox = QP.VBoxLayout()
@ -5602,7 +5687,7 @@ class FrameSplash( QW.QWidget ):
self.setLayout( self._vbox )
QP.Center( self )
QP.CenterOnScreen( self )
self.show()

View File

@ -523,8 +523,6 @@ class AutoCompleteDropdown( QW.QWidget ):
self._dropdown_window.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Raised )
self._dropdown_window.setLineWidth( 2 )
QP.SetBackgroundColour( self._dropdown_window, QP.GetSystemColour( QG.QPalette.Button ) )
self._dropdown_window.move( ClientGUIFunctions.ClientToScreen( self._text_ctrl, ( 0, 0 ) ) )
self._dropdown_window_widget_event_filter = QP.WidgetEventFilter( self._dropdown_window )
@ -888,7 +886,7 @@ class AutoCompleteDropdown( QW.QWidget ):
raw_control_modifier = QC.Qt.ControlModifier
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
raw_control_modifier = QC.Qt.MetaModifier # This way raw_control_modifier always means the Control key, even on Mac. See Qt docs.
@ -1136,7 +1134,7 @@ class AutoCompleteDropdown( QW.QWidget ):
def setFocus( self, focus_reason = QC.Qt.OtherFocusReason ):
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
QP.CallAfter( self._text_ctrl.setFocus, focus_reason )

View File

@ -238,6 +238,8 @@ class Animation( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self.setMouseTracking( True )
self._media = None
self._drag_happened = False
@ -651,7 +653,8 @@ class AnimationBar( QW.QWidget ):
self._it_was_playing = False
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_MOUSE_EVENTS( self.EventMouse )
self._widget_event_filter.EVT_MOUSE_EVENTS( self.EventMouse )
def _DrawBlank( self, painter ):
@ -920,11 +923,6 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
HG.client_controller.gui.RegisterCanvasFrameReference( self )
def l():
HydrusData.ShowText( QP.isValid( self ) )
self.destroyed.connect( HG.client_controller.gui.MaintainCanvasFrameReferences )
@ -943,7 +941,7 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
else:
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
return
@ -986,6 +984,11 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
return command_processed
def minimumSizeHint( self ):
return QC.QSize( 240, 180 )
def SetCanvas( self, canvas_window ):
self._canvas_window = canvas_window
@ -1403,11 +1406,11 @@ class Canvas( QW.QWidget ):
dlg.SetPanel( panel )
QP.CallAfter( control.setFocus, QC.Qt.OtherFocusReason )
QP.CallAfter( control.SetInsertionPointEnd )
QP.CallAfter( control.moveCursor, QG.QTextCursor.End )
if dlg.exec() == QW.QDialog.Accepted:
notes = control.plainText()
notes = control.toPlainText()
hash = media.GetHash()
@ -1717,7 +1720,7 @@ class Canvas( QW.QWidget ):
if self._media_window_pos == self._media_container.pos():
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
self._media_container.update()
@ -1912,6 +1915,18 @@ class Canvas( QW.QWidget ):
def event( self, event ):
if event.type() == QC.QEvent.LayoutRequest:
return True
else:
return QW.QWidget.event( self, event )
def CleanBeforeDestroy( self ):
self.SetMedia( None )
@ -2402,7 +2417,7 @@ class CanvasPanel( Canvas ):
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
copy_hash_menu = QW.QMenu( copy_hash_menu )
copy_hash_menu = QW.QMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Open this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Open this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
@ -2740,7 +2755,7 @@ class CanvasWithHovers( CanvasWithDetails ):
self._timer_cursor_hide_job = None
self._widget_event_filter.EVT_MOTION( self.EventDrag )
self._widget_event_filter.EVT_MOTION( self.EventMouseMove )
HG.client_controller.sub( self, 'Close', 'canvas_close' )
HG.client_controller.sub( self, 'FullscreenSwitch', 'canvas_fullscreen_switch' )
@ -2782,7 +2797,7 @@ class CanvasWithHovers( CanvasWithDetails ):
return True # was: event.ignore()
def EventDrag( self, event ):
def EventMouseMove( self, event ):
CC.CAN_HIDE_MOUSE = True
@ -3579,7 +3594,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
elif event.buttons() != QC.Qt.NoButton and event.type() == QC.QEvent.MouseMove:
self.EventDrag( event )
self.EventMouseMove( event )
else:
@ -4252,7 +4267,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
elif event.buttons() != QC.Qt.NoButton and event.type() == QC.QEvent.MouseMove:
self.EventDrag( event )
self.EventMouseMove( event )
else:
@ -4877,6 +4892,10 @@ 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
self.setAttribute( QC.Qt.WA_OpaquePaintEvent, True )
self._media = None
self._show_action = None
@ -4886,6 +4905,8 @@ class MediaContainer( QW.QWidget ):
self._embed_button_widget_event_filter = QP.WidgetEventFilter( self._embed_button )
self._embed_button_widget_event_filter.EVT_LEFT_DOWN( self.EventEmbedButton )
self.setMouseTracking( True )
self._animation_window = Animation( self )
self._animation_bar = AnimationBar( self )
self._static_image_window = StaticImage( self )
@ -5396,6 +5417,8 @@ class StaticImage( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self.setMouseTracking( True )
self._media = None
self._first_background_drawn = False

View File

@ -413,7 +413,7 @@ class BetterStaticText( QP.EllipsizedLabel ):
self.setText( label )
def setText( self, text ):
# this doesn't need mnemonic escape _unless_ a buddy is set, wew lad
@ -424,12 +424,6 @@ class BetterStaticText( QP.EllipsizedLabel ):
QP.EllipsizedLabel.setText( self, text )
if self._wrap_width is not None:
self.setWordWrap( True )
self.setMaximumWidth( self._wrap_width )
if self._tooltip_label:
self.setToolTip( text )
@ -437,17 +431,6 @@ class BetterStaticText( QP.EllipsizedLabel ):
def SetWrapWidth( self, wrap_width ):
self._wrap_width = wrap_width
if self._wrap_width is not None:
self.setWordWrap( True )
self.setMaximumWidth( wrap_width )
class BetterHyperLink( BetterStaticText ):
def __init__( self, parent, label, url ):
@ -463,7 +446,7 @@ class BetterHyperLink( BetterStaticText ):
self.setOpenExternalLinks( True )
self.setText( '<a href="{}">{}</a>'.format( url, label ) )
class BufferedWindow( QW.QWidget ):
@ -569,6 +552,7 @@ class CheckboxCollect( QW.QWidget ):
self._collect_unmatched.currentIndexChanged.connect( self.CollectValuesChanged )
self._collect_comboctrl.itemChanged.connect( self.CollectValuesChanged )
def GetValue( self ):
return self._media_collect
@ -594,8 +578,6 @@ class CheckboxCollect( QW.QWidget ):
class CheckboxManager( object ):
def GetCurrentValue( self ):
@ -735,9 +717,6 @@ class ChoiceSort( QW.QWidget ):
self.setLayout( hbox )
self._sort_type_choice.currentIndexChanged.connect( self.EventSortTypeChoice )
self._sort_asc_choice.currentIndexChanged.connect( self.EventSortAscChoice )
HG.client_controller.sub( self, 'ACollectHappened', 'collect_media' )
HG.client_controller.sub( self, 'BroadcastSort', 'do_page_sort' )
@ -757,6 +736,9 @@ class ChoiceSort( QW.QWidget ):
self._sort_type_choice.currentIndexChanged.connect( self.EventSortTypeChoice )
self._sort_asc_choice.currentIndexChanged.connect( self.EventSortAscChoice )
def _BroadcastSort( self ):

View File

@ -132,7 +132,7 @@ class Dialog( QP.Dialog ):
if parent is not None and position == 'center':
QP.CallAfter( QP.Center, self )
QP.CallAfter( QP.CenterOnWindow, parent, self )
HG.client_controller.ResetIdleTimer()
@ -1207,7 +1207,7 @@ class DialogTextEntry( Dialog ):
QP.AddToLayout( hbox, self._cancel, CC.FLAGS_SMALL_INDENT )
st_message = ClientGUICommon.BetterStaticText( self, message )
st_message.SetWrapWidth( 480 )
st_message.setWordWrap( True )
vbox = QP.VBoxLayout()
@ -1317,8 +1317,7 @@ class DialogYesYesNo( Dialog ):
vbox = QP.VBoxLayout()
text = ClientGUICommon.BetterStaticText( self, message )
text.SetWrapWidth( 480 )
text.setWordWrap( True )
QP.AddToLayout( vbox, text, CC.FLAGS_BIG_INDENT )
QP.AddToLayout( vbox, hbox, CC.FLAGS_BUTTON_SIZER )

View File

@ -284,8 +284,7 @@ synchronise - try to export the files to the directory, overwriting if the files
If you select synchronise, be careful!'''
st = ClientGUICommon.BetterStaticText( self._type_box, label = text )
st.SetWrapWidth( 440 )
st.setWordWrap( True )
self._type_box.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
self._type_box.Add( self._type, CC.FLAGS_EXPAND_PERPENDICULAR )

View File

@ -379,47 +379,34 @@ def SetBitmapButtonBitmap( button, bitmap ):
button.last_bitmap = bitmap
def TLPHasFocus( window ):
def TLPIsActive( window ):
return window.window().hasFocus()
return window.window() == QW.QApplication.activeWindow()
def WindowOrAnyTLPChildHasFocus( window ):
if window == QW.QApplication.activeWindow(): return True
active_window = QW.QApplication.activeWindow()
focus = QW.QApplication.focusWidget()
while focus is not None:
if window == active_window:
if focus == window:
return True
widget = QW.QApplication.focusWidget()
if widget is None:
widget = active_window
while widget is not None:
if widget == window:
return True
focus = focus.parentWidget()
return False
def WindowOrSameTLPChildHasFocus( window ):
if window == QW.QApplication.activeWindow(): return True
focus = QW.QApplication.focusWidget()
while focus is not None:
if focus == window:
return True
if focus == focus.window():
return False
focus = focus.parentWidget()
widget = widget.parentWidget()
return False

View File

@ -71,7 +71,7 @@ class FullscreenHoverFrame( QW.QFrame ):
changes_occurred = should_resize or self.pos() != QP.TupleToQPoint( my_ideal_position )
if HC.PLATFORM_OSX and changes_occurred and self._always_on_top:
if HC.PLATFORM_MACOS and changes_occurred and self._always_on_top:
self.raise_()
@ -80,6 +80,13 @@ class FullscreenHoverFrame( QW.QFrame ):
def keyPressEvent( self, event ):
# sendEvent here does some shortcutoverride pain in the neck
self._my_canvas.keyPressEvent( event )
def SetDisplayMedia( self, canvas_key, media ):
if canvas_key == self._canvas_key:
@ -105,7 +112,7 @@ class FullscreenHoverFrame( QW.QFrame ):
self.show()
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
( mouse_x, mouse_y ) = QG.QCursor.pos().toTuple()
@ -554,9 +561,13 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
self._top_hbox = QP.HBoxLayout()
self._top_hbox.setContentsMargins( 0, 0, 0, 2 )
self._title_text = ClientGUICommon.BetterStaticText( self, 'title', ellipsize_end = True )
self._info_text = ClientGUICommon.BetterStaticText( self, 'info', ellipsize_end = True )
self._title_text.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
self._info_text.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
self._PopulateLeftButtons()
QP.AddToLayout( self._top_hbox, (10,10), CC.FLAGS_EXPAND_BOTH_WAYS )
self._PopulateCenterButtons()
@ -566,8 +577,8 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._top_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._title_text, None, QC.Qt.AlignHCenter )
QP.AddToLayout( vbox, self._info_text, None, QC.Qt.AlignHCenter )
QP.AddToLayout( vbox, self._title_text, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._info_text, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
@ -667,7 +678,7 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
fullscreen_switch = ClientGUICommon.BetterBitmapButton( self, CC.GlobalPixmaps.fullscreen_switch, HG.client_controller.pub, 'canvas_fullscreen_switch', self._canvas_key )
fullscreen_switch.setToolTip( 'fullscreen switch' )
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
fullscreen_switch.hide()
@ -744,13 +755,6 @@ class FullscreenHoverFrameTop( FullscreenHoverFrame ):
else:
my_width = self.size().width()
my_wrap_width = my_width - 20
self._title_text.SetWrapWidth( my_wrap_width )
self._info_text.SetWrapWidth( my_wrap_width )
label = self._current_media.GetTitleString()
if len( label ) > 0:

View File

@ -1481,9 +1481,9 @@ class ListBox( QW.QScrollArea ):
visible_rect = QP.ScrollAreaVisibleRect( self )
visible_rect_y = visible_rect.y()
visible_rect_height = visible_rect.height()
self._num_rows_per_page = visible_rect_y // self._text_y
self._num_rows_per_page = visible_rect_height // self._text_y
self._SetVirtualSize()
@ -1581,6 +1581,25 @@ class ListBoxTags( ListBox ):
raise NotImplementedError()
def _GetTagFromTerm( self, term ):
if isinstance( term, ClientSearch.Predicate ):
if term.GetType() == HC.PREDICATE_TYPE_TAG:
return term.GetValue()
else:
return None
else:
return term
def _GetTextsAndColours( self, term ):
namespace_colours = self._GetNamespaceColours()
@ -1721,7 +1740,9 @@ class ListBoxTags( ListBox ):
def _ProcessMenuTagEvent( self, command ):
tags = [ self._GetTextFromTerm( term ) for term in self._selected_terms ]
tags = [ self._GetTagFromTerm( term ) for term in self._selected_terms ]
tags = [ tag for tag in tags if tag is not None ]
if command in ( 'hide', 'hide_namespace' ):

View File

@ -678,7 +678,8 @@ class ManagementPanel( QW.QScrollArea ):
self.setFrameShape( QW.QFrame.NoFrame )
self.setWidget( QW.QWidget() )
self.setWidgetResizable( True )
self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Plain )
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
self.setLineWidth( 2 )
#self.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff )
self.setVerticalScrollBarPolicy( QC.Qt.ScrollBarAsNeeded )
@ -3526,7 +3527,6 @@ class ManagementPanelPetitions( ManagementPanel ):
self._reason_text = QW.QTextEdit( self._petition_panel )
self._reason_text.setReadOnly( True )
self._reason_text.setMinimumHeight( 80 )
self._reason_text.setTextColor( QC.Qt.black )
check_all = ClientGUICommon.BetterButton( self._petition_panel, 'check all', self._CheckAll )
flip_selected = ClientGUICommon.BetterButton( self._petition_panel, 'flip selected', self._FlipSelected )

View File

@ -2769,6 +2769,7 @@ class MediaPanelThumbnails( MediaPanel ):
self._clean_canvas_pages = {}
self._dirty_canvas_pages = []
self._num_rows_per_canvas_page = 1
self._num_rows_per_actual_page = 1
MediaPanel.__init__( self, parent, page_key, file_service_key, media_results )
@ -3252,9 +3253,10 @@ class MediaPanelThumbnails( MediaPanel ):
( thumbnail_span_width, thumbnail_span_height ) = self._GetThumbnailSpanDimensions()
num_rows = ( client_height // thumbnail_span_height ) // 2 # roughly half a client_height's worth of thumbs
num_rows = ( client_height // thumbnail_span_height )
self._num_rows_per_canvas_page = max( 1, num_rows )
self._num_rows_per_actual_page = max( 1, num_rows )
self._num_rows_per_canvas_page = max( 1, num_rows // 2 )
self._num_columns = max( 1, client_width // thumbnail_span_width )
@ -3352,8 +3354,7 @@ class MediaPanelThumbnails( MediaPanel ):
elif y > visible_rect_y + visible_rect_height - ( thumbnail_span_height * percent_visible ):
self.ensureVisible( 0, y )
self.ensureVisible( 0, y + thumbnail_span_height )
@ -3477,7 +3478,7 @@ class MediaPanelThumbnails( MediaPanel ):
self._Select( 'none' )
elif event.key() in ( QC.Qt.Key_PageUp, QC.Qt.Key_PageUp ):
elif event.key() in ( QC.Qt.Key_PageUp, QC.Qt.Key_PageDown ):
if event.key() == QC.Qt.Key_PageUp:
@ -3490,7 +3491,7 @@ class MediaPanelThumbnails( MediaPanel ):
shift = event.modifiers() & QC.Qt.ShiftModifier
self._MoveFocusedThumbnail( self._num_rows_per_canvas_page * direction, 0, shift )
self._MoveFocusedThumbnail( self._num_rows_per_actual_page * direction, 0, shift )
else:
@ -3678,7 +3679,7 @@ class MediaPanelThumbnails( MediaPanel ):
media_has_inbox = num_inbox > 0
media_has_archive = num_archive > 0
menu = QW.QMenu()
menu = QW.QMenu( self.window() )
if self._focused_media is not None:
@ -4659,7 +4660,7 @@ class MediaPanelThumbnails( MediaPanel ):
QP.AddShortcut( self, QC.Qt.ControlModifier, QC.Qt.Key_A, self._Select, 'all' ),
QP.AddShortcut( self, QC.Qt.ControlModifier, QC.Qt.Key_Space, ctrl_space_callback, self )
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
QP.AddShortcut( self, QC.Qt.NoModifier, QC.Qt.Key_Back, self._Delete )
QP.AddShortcut( self, QC.Qt.ShiftModifier, QC.Qt.Key_Back, self._Undelete )

View File

@ -1,4 +1,5 @@
import collections
from . import HydrusConstants as HC
from . import HydrusData
from . import HydrusGlobals as HG
import os
@ -21,8 +22,14 @@ def AppendMenuBitmapItem( menu, label, description, bitmap, callable, *args, **k
menu_item = QW.QAction( menu )
if HC.PLATFORM_MACOS:
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( label )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
menu_item.setWhatsThis( description )
@ -39,9 +46,15 @@ def AppendMenuCheckItem( menu, label, description, initial_value, callable, *arg
label = SanitiseLabel( label )
menu_item = QW.QAction( menu )
if HC.PLATFORM_MACOS:
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( label )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
menu_item.setWhatsThis( description )
@ -59,9 +72,15 @@ def AppendMenuItem( menu, label, description, callable, *args, **kwargs ):
label = SanitiseLabel( label )
menu_item = QW.QAction( menu )
if HC.PLATFORM_MACOS:
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( label )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
menu_item.setWhatsThis( description )
@ -77,10 +96,17 @@ def AppendMenuLabel( menu, label, description = '' ):
description = ''
menu_item = QW.QAction( menu )
if HC.PLATFORM_MACOS:
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( label )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
menu_item.setWhatsThis( description )

View File

@ -402,7 +402,8 @@ class Page( QW.QSplitter ):
file_service_key = self._management_controller.GetKey( 'file_service' )
self._preview_panel = QW.QFrame( self._search_preview_split )
self._preview_panel.setFrameStyle( QW.QFrame.Box | QW.QFrame.Plain )
self._preview_panel.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
self._preview_panel.setLineWidth( 2 )
self._preview_canvas = ClientGUICanvas.CanvasPanel( self._preview_panel, self._page_key )
@ -428,7 +429,7 @@ class Page( QW.QSplitter ):
self._search_preview_split.widget( 1 ).setMinimumHeight( 180 )
self._search_preview_split.setStretchFactor( 0, 1 )
self._search_preview_split.setStretchFactor( 1, 0 )
self._search_preview_split._handle_event_filter = QP.WidgetEventFilter( self._search_preview_split.handle( 1 ) )
self._search_preview_split._handle_event_filter.EVT_LEFT_DCLICK( self.EventPreviewUnsplit )
@ -453,13 +454,6 @@ class Page( QW.QSplitter ):
# if a new media page comes in while its menu is open, we can enter program instability.
# so let's just put it off.
if self._controller.MenuIsOpen():
self._controller.CallLaterQtSafe( self, 0.5, self._SwapMediaPanel, new_panel )
return
previous_sizes = self.sizes()
self._preview_canvas.SetMedia( None )
@ -479,16 +473,32 @@ class Page( QW.QSplitter ):
self._media_panel.setParent( None )
old_panel = self._media_panel
self.addWidget( new_panel )
self.setSizes( previous_sizes )
self._media_panel.deleteLater()
self.setStretchFactor( 1, 1 )
self._media_panel = new_panel
self._controller.pub( 'refresh_page_name', self._page_key )
def clean_up_old_panel():
if self._controller.MenuIsOpen():
self._controller.CallLaterQtSafe( self, 0.5, clean_up_old_panel )
return
old_panel.deleteLater()
clean_up_old_panel()
def CheckAbleToClose( self ):
@ -699,7 +709,7 @@ class Page( QW.QSplitter ):
def SetupSplits( self ):
QP.SplitVertically( self, self._search_preview_split, self._media_panel, HC.options[ 'hpos' ] )
QP.SplitHorizontally( self._search_preview_split, self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )

View File

@ -2698,8 +2698,6 @@ The formula should attempt to parse full or relative urls. If the url is relativ
info_st = ClientGUICommon.BetterStaticText( info_panel, label = message )
info_st.SetWrapWidth( 400 )
#
self._name.setText( name )
@ -3548,7 +3546,7 @@ And pass that html to a number of 'parsing children' that will each look through
info_st = ClientGUICommon.BetterStaticText( info_panel, label = message )
info_st.SetWrapWidth( 400 )
info_st.setWordWrap( True )
#

View File

@ -70,13 +70,13 @@ class PopupMessage( PopupWindow ):
QP.SetMinClientSize( self, ( wrap_width, -1 ) )
self._title.SetWrapWidth( wrap_width )
self._title.setWordWrap( True )
self._title_ev = QP.WidgetEventFilter( self._title )
self._title_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._title.hide()
self._text_1 = ClientGUICommon.BetterStaticText( self, ellipsize_end = True )
self._text_1.SetWrapWidth( wrap_width )
self._text_1 = ClientGUICommon.BetterStaticText( self )
self._text_1.setWordWrap( True )
self._text_1_ev = QP.WidgetEventFilter( self._text_1 )
self._text_1_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._text_1.hide()
@ -86,8 +86,8 @@ class PopupMessage( PopupWindow ):
self._gauge_1_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._gauge_1.hide()
self._text_2 = ClientGUICommon.BetterStaticText( self, ellipsize_end = True )
self._text_2.SetWrapWidth( wrap_width )
self._text_2 = ClientGUICommon.BetterStaticText( self )
self._text_2.setWordWrap( True )
self._text_2_ev = QP.WidgetEventFilter( self._text_2 )
self._text_2_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._text_2.hide()
@ -98,7 +98,6 @@ class PopupMessage( PopupWindow ):
self._gauge_2.hide()
self._text_yes_no = ClientGUICommon.BetterStaticText( self )
self._text_yes_no.SetWrapWidth( wrap_width )
self._text_yes_no_ev = QP.WidgetEventFilter( self._text_yes_no )
self._text_yes_no_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._text_yes_no.hide()
@ -128,9 +127,9 @@ class PopupMessage( PopupWindow ):
self._show_tb_button.hide()
self._tb_text = ClientGUICommon.BetterStaticText( self )
self._tb_text.SetWrapWidth( wrap_width )
self._tb_text_ev = QP.WidgetEventFilter( self._tb_text )
self._tb_text_ev.EVT_RIGHT_DOWN( self.EventDismiss )
self._tb_text.setWordWrap( True )
self._tb_text.hide()
self._copy_tb_button = ClientGUICommon.BetterButton( self, 'copy traceback information', self.CopyTB )
@ -277,10 +276,6 @@ class PopupMessage( PopupWindow ):
self._show_tb_button.setText( 'hide traceback' )
popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )
wrap_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._title, popup_message_character_width )
self._tb_text.show()
@ -311,10 +306,6 @@ class PopupMessage( PopupWindow ):
def UpdateMessage( self ):
popup_message_character_width = HG.client_controller.new_options.GetInteger( 'popup_message_character_width' )
wrap_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._title, popup_message_character_width )
paused = self._job_key.IsPaused()
title = self._job_key.GetIfHasVariable( 'popup_title' )

View File

@ -94,8 +94,7 @@ class QuestionYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
vbox = QP.VBoxLayout()
text = ClientGUICommon.BetterStaticText( self, message )
text.SetWrapWidth( 480 )
text.setWordWrap( True )
QP.AddToLayout( vbox, text )
QP.AddToLayout( vbox, hbox, CC.FLAGS_BUTTON_SIZER )

View File

@ -200,14 +200,11 @@ class EditAdvancedORPredicates( ClientGUIScrolledPanels.EditPanel ):
summary = 'Enter a complicated tag search here as text, such as \'( blue eyes and blonde hair ) or ( green eyes and red hair )\', and this should turn it into hydrus-compatible search predicates.'
summary += os.linesep * 2
summary += 'Accepted operators: not (!, -), and (&&), or (||), implies (=>), xor, xnor (iff, <=>), nand, nor.'
summary += os.linesep
summary += os.linesep * 2
summary += 'Parentheses work the usual way. \ can be used to escape characters (e.g. to search for tags including parentheses)'
st = ClientGUICommon.BetterStaticText( self, summary )
width = ClientGUIFunctions.ConvertTextToPixelWidth( st, 96 )
st.SetWrapWidth( width )
st.setWordWrap( True )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
@ -328,8 +325,7 @@ class EditBandwidthRulesPanel( ClientGUIScrolledPanels.EditPanel ):
if summary != '':
st = ClientGUICommon.BetterStaticText( self, summary )
st.SetWrapWidth( 250 )
st.setWordWrap( True )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -5340,8 +5336,7 @@ class EditTagDisplayManagerPanel( ClientGUIScrolledPanels.EditPanel ):
intro = 'Please note this new system is under construction. It is neither completely functional nor as efficient as intended.'
st = ClientGUICommon.BetterStaticText( self, intro )
st.SetWrapWidth( min_width - 50 )
st.setWordWrap( True )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._tag_services, CC.FLAGS_EXPAND_BOTH_WAYS )
@ -6639,7 +6634,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ):
if dlg_default.exec() == QW.QDialog.Accepted:
default = dlg_default.value()
default = dlg_default.GetValue()
if default == '':
@ -6699,7 +6694,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ):
if dlg_default.exec() == QW.QDialog.Accepted:
new_default = dlg_default.value()
new_default = dlg_default.GetValue()
if new_default == '':

View File

@ -2017,6 +2017,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._duplicate_comparison_score_more_tags = QP.MakeQSpinBox( weights_panel, min=0, max=100 )
self._duplicate_comparison_score_older = QP.MakeQSpinBox( weights_panel, min=0, max=100 )
self._duplicate_filter_max_batch_size = QP.MakeQSpinBox( self, min = 10, max = 1024 )
#
self._duplicate_comparison_score_higher_jpeg_quality.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_higher_jpeg_quality' ) )
@ -2028,6 +2030,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._duplicate_comparison_score_more_tags.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_more_tags' ) )
self._duplicate_comparison_score_older.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_older' ) )
self._duplicate_filter_max_batch_size.setValue( self._new_options.GetInteger( 'duplicate_filter_max_batch_size' ) )
#
rows = []
@ -2046,8 +2050,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
label = 'When processing potential duplicate pairs in the duplicate filter, the client tries to present the \'best\' file first. It judges the two files on a variety of potential differences, each with a score. The file with the greatest total score is presented first. Here you can tinker with these scores.'
st = ClientGUICommon.BetterStaticText( weights_panel, label )
st.SetWrapWidth( 640 )
st.setWordWrap( True )
weights_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
weights_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@ -2056,7 +2059,15 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, weights_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, weights_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
rows = []
rows.append( ( 'Max size of duplicate filter pair batches:', self._duplicate_filter_max_batch_size ) )
gridbox = ClientGUICommon.WrapInGrid( self, rows )
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
self.setLayout( vbox )
@ -2072,6 +2083,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetInteger( 'duplicate_comparison_score_more_tags', self._duplicate_comparison_score_more_tags.value() )
self._new_options.SetInteger( 'duplicate_comparison_score_older', self._duplicate_comparison_score_older.value() )
self._new_options.SetInteger( 'duplicate_filter_max_batch_size', self._duplicate_filter_max_batch_size.value() )
class _DefaultFileSystemPredicatesPanel( QW.QWidget ):
@ -2191,8 +2204,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
text = 'Setting a specific web browser path here--like \'C:\\program files\\firefox\\firefox.exe "%path%"\'--can help with the \'share->open->in web browser\' command, which is buggy working with OS defaults, particularly on Windows. It also fixes #anchors, which are dropped in some OSes using default means. Use the same %path% format for the \'open externally\' commands below.'
st = ClientGUICommon.BetterStaticText( mime_panel, text )
st.SetWrapWidth( 800 )
st.setWordWrap( True )
mime_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -3050,8 +3062,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
text += 'If the client believes the system is busy, it will generally not start jobs.'
st = ClientGUICommon.BetterStaticText( self._jobs_panel, label = text )
st.SetWrapWidth( 550 )
st.setWordWrap( True )
self._jobs_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
self._jobs_panel.Add( self._idle_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
@ -3837,8 +3848,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
desc = 'These tags will appear in your tag autocomplete results area, under the \'favourites\' tab.'
favourites_st = ClientGUICommon.BetterStaticText( favourites_panel, desc )
favourites_st.SetWrapWidth( 400 )
favourites_st.setWordWrap( True )
expand_parents = False
@ -5950,8 +5960,7 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
st = ClientGUICommon.BetterStaticText( self, text )
st.SetWrapWidth( 640 )
st.setWordWrap( True )
columns = [ ( 'missing location', -1 ), ( 'expected subdirectory', 23 ), ( 'correct location', 36 ), ( 'now ok?', 9 ) ]

View File

@ -1101,10 +1101,7 @@ class MigrateTagsPanel( ClientGUIScrolledPanels.ReviewPanel ):
message += 'You may need to restart your client to see their effect.'
st = ClientGUICommon.BetterStaticText( self, message )
width = ClientGUIFunctions.ConvertTextToPixelWidth( st, 96 )
st.SetWrapWidth( width )
st.setWordWrap( True )
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._migration_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

View File

@ -37,7 +37,7 @@ def ConvertKeyEventToShortcut( event ):
modifiers.append( CC.SHORTCUT_MODIFIER_ALT )
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
ctrl = QC.Qt.MetaModifier
@ -163,14 +163,14 @@ def IShouldCatchShortcutEvent( evt_handler, event = None, child_tlp_classes_who_
do_focus_test = True
if event is not None and isinstance( event, QG.QWheelEvent ):
do_focus_test = False
if do_focus_test:
if not ClientGUIFunctions.WindowOrSameTLPChildHasFocus( evt_handler ):
if not ClientGUIFunctions.TLPIsActive( evt_handler ):
if child_tlp_classes_who_can_pass_up is not None:

View File

@ -156,6 +156,11 @@ def MouseIsOnMyDisplay( window ):
def SaveTLWSizeAndPosition( tlw, frame_key ):
if tlw.isMinimized():
return
new_options = HG.client_controller.new_options
( remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen ) = new_options.GetFrameLocation( frame_key )
@ -246,19 +251,22 @@ def SetInitialTLWSizeAndPosition( tlw, frame_key ):
elif default_position == 'center':
QP.CallAfter( QP.Center, tlw )
if parent is not None:
QP.CenterOnWindow( parent, tlw )
# Comment from before the Qt port: if these aren't callafter, the size and pos calls don't stick if a restore event happens
if maximised:
QP.CallAfter( tlw.showMaximized )
tlw.showMaximized()
if fullscreen and not HC.PLATFORM_OSX:
if fullscreen and not HC.PLATFORM_MACOS:
QP.CallAfter( tlw.showFullScreen )
tlw.showFullScreen()
def SlideOffScreenTLWUpAndLeft( tlw ):
@ -824,7 +832,8 @@ class FrameThatResizes( Frame ):
def EventSizeAndPositionChanged( self, event ):
if not self.isMinimized(): SaveTLWSizeAndPosition( self, self._frame_key )
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
HG.client_controller.CallLaterQtSafe( self, 0.1, SaveTLWSizeAndPosition, self, self._frame_key )
return True # was: event.ignore()
@ -844,7 +853,8 @@ class MainFrameThatResizes( MainFrame ):
def EventSizeAndPositionChanged( self, event ):
if not self.isMinimized(): SaveTLWSizeAndPosition( self, self._frame_key )
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
HG.client_controller.CallLaterQtSafe( self, 0.1, SaveTLWSizeAndPosition, self, self._frame_key )
return True # was: event.ignore()

View File

@ -218,6 +218,8 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._file_status = text
@ -330,6 +332,8 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._gallery_status = text

View File

@ -190,6 +190,8 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._current_action = text

View File

@ -555,6 +555,8 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
def status_hook( text ):
text = text.splitlines()[0]
job_key.SetVariable( 'popup_text_2', x_out_of_y + text )
@ -827,6 +829,8 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
def status_hook( text ):
text = text.splitlines()[0]
job_key.SetVariable( 'popup_text_1', prefix + ': ' + text )

View File

@ -617,6 +617,8 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._watcher_status = text
@ -625,6 +627,8 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._subject = text
@ -924,6 +928,8 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
with self._lock:
text = text.splitlines()[0]
self._file_status = text

View File

@ -148,6 +148,8 @@ def THREADDownloadURL( job_key, url, url_string ):
def status_hook( text ):
text = text.splitlines()[0]
job_key.SetVariable( 'popup_text_1', text )
@ -217,6 +219,8 @@ def THREADDownloadURLs( job_key, urls, title ):
def status_hook( text ):
text = text.splitlines()[0]
job_key.SetVariable( 'popup_text_2', text )

View File

@ -64,7 +64,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'show_related_tags' ] = False
self._dictionary[ 'booleans' ][ 'show_file_lookup_script_tags' ] = False
self._dictionary[ 'booleans' ][ 'hide_message_manager_on_gui_iconise' ] = HC.PLATFORM_OSX
self._dictionary[ 'booleans' ][ 'hide_message_manager_on_gui_iconise' ] = HC.PLATFORM_MACOS
self._dictionary[ 'booleans' ][ 'hide_message_manager_on_gui_deactive' ] = False
self._dictionary[ 'booleans' ][ 'load_images_with_pil' ] = False
@ -223,6 +223,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'integers' ][ 'popup_message_character_width' ] = 56
self._dictionary[ 'integers' ][ 'duplicate_filter_max_batch_size' ] = 250
self._dictionary[ 'integers' ][ 'video_thumbnail_percentage_in' ] = 35
self._dictionary[ 'integers' ][ 'duplicate_comparison_score_higher_jpeg_quality' ] = 10

View File

@ -113,7 +113,14 @@ def CreateTopImage( width, title, payload_description, text ):
data_bytearray = top_qt_image.bits()
data_bytes = bytes( data_bytearray )
if QP.qtpy.PYSIDE2:
data_bytes = bytes( data_bytearray )
elif QP.qtpy.PYQT5:
data_bytes = data_bytearray.asstring( top_height * width * 3 )
top_image_rgb = numpy.fromstring( data_bytes, dtype = 'uint8' ).reshape( ( top_height, width, 3 ) )

View File

@ -2444,7 +2444,16 @@ class ServiceIPFS( ServiceRemote ):
if len( result ) == 0:
multihash = self.PinFile( hash, mime )
try:
multihash = self.PinFile( hash, mime )
except HydrusExceptions.DataMissing:
HydrusData.ShowText( 'File {} could not be pinned!'.format( hash.hexh() ) )
continue
else:
@ -2620,8 +2629,30 @@ class ServiceIPFS( ServiceRemote ):
j = json.loads( parsing_text )
if 'Hash' not in j:
message = 'IPFS was unable to pin--returned no hash!'
HydrusData.Print( message )
HydrusData.Print( parsing_text )
raise HydrusExceptions.DataMissing( message )
multihash = j[ 'Hash' ]
EMPTY_IPFS_HASH = 'bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'
if multihash == EMPTY_IPFS_HASH:
message = 'IPFS was unable to pin--returned empty multihash!'
HydrusData.Print( message )
HydrusData.Print( parsing_text )
raise HydrusExceptions.DataMissing( message )
( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) )
file_info_manager = media_result.GetFileInfoManager()

View File

@ -28,15 +28,15 @@ else:
PLATFORM_WINDOWS = False
PLATFORM_OSX = False
PLATFORM_MACOS = False
PLATFORM_LINUX = False
if sys.platform == 'win32': PLATFORM_WINDOWS = True
elif sys.platform == 'darwin': PLATFORM_OSX = True
elif sys.platform == 'darwin': PLATFORM_MACOS = True
elif sys.platform == 'linux': PLATFORM_LINUX = True
RUNNING_FROM_SOURCE = sys.argv[0].endswith( '.py' ) or sys.argv[0].endswith( '.pyw' )
RUNNING_FROM_OSX_APP = os.path.exists( os.path.join( BASE_DIR, 'running_from_app' ) )
RUNNING_FROM_MACOS_APP = os.path.exists( os.path.join( BASE_DIR, 'running_from_app' ) )
BIN_DIR = os.path.join( BASE_DIR, 'bin' )
HELP_DIR = os.path.join( BASE_DIR, 'help' )
@ -45,7 +45,7 @@ STATIC_DIR = os.path.join( BASE_DIR, 'static' )
DEFAULT_DB_DIR = os.path.join( BASE_DIR, 'db' )
if PLATFORM_OSX:
if PLATFORM_MACOS:
USERPATH_DB_DIR = os.path.join( os.path.expanduser( '~' ), 'Library', 'Hydrus' )
@ -67,7 +67,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 373
SOFTWARE_VERSION = 374
CLIENT_API_VERSION = 11
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -587,14 +587,7 @@ def GetNowFloat():
def GetNowPrecise():
if HC.PLATFORM_WINDOWS:
return time.clock()
else:
return time.time()
return time.perf_counter()
def GetSiblingProcessPorts( db_path, instance ):
@ -679,7 +672,7 @@ def GetSubprocessEnv():
changes_made = True
if ( HC.PLATFORM_LINUX or HC.PLATFORM_OSX ) and 'PATH' in env:
if ( HC.PLATFORM_LINUX or HC.PLATFORM_MACOS ) and 'PATH' in env:
# fix for pyinstaller, which drops this stuff for some reason and hence breaks ffmpeg

View File

@ -10,7 +10,7 @@ if HC.PLATFORM_LINUX:
SWFRENDER_PATH = os.path.join( HC.BIN_DIR, 'swfrender_linux' )
elif HC.PLATFORM_OSX:
elif HC.PLATFORM_MACOS:
SWFRENDER_PATH = os.path.join( HC.BIN_DIR, 'swfrender_osx' )

View File

@ -14,7 +14,7 @@ if HC.PLATFORM_LINUX:
upnpc_path = os.path.join( HC.BIN_DIR, 'upnpc_linux' )
elif HC.PLATFORM_OSX:
elif HC.PLATFORM_MACOS:
upnpc_path = os.path.join( HC.BIN_DIR, 'upnpc_osx' )

View File

@ -296,7 +296,7 @@ def GetDefaultLaunchPath():
return 'windows is called directly'
elif HC.PLATFORM_OSX:
elif HC.PLATFORM_MACOS:
return 'open "%path%"'
@ -347,16 +347,20 @@ def GetTempDir( dir = None ):
def SetEnvTempDir( path ):
if not os.path.exists( path ):
raise Exception( 'The given temp directory, "{}", does not exist!'.format( path ) )
if not os.path.isdir( path ):
if os.path.exists( path ) and not os.path.isdir( path ):
raise Exception( 'The given temp directory, "{}", does not seem to be a directory!'.format( path ) )
try:
MakeSureDirectoryExists( path )
except Exception as e:
raise Exception( 'Could not create the temp dir: {}'.format( e ) )
if not DirectoryIsWritable( path ):
raise Exception( 'The given temp directory, "{}", does not seem to be writable-to!'.format( path ) )
@ -443,7 +447,7 @@ def LaunchDirectory( path ):
else:
if HC.PLATFORM_OSX:
if HC.PLATFORM_MACOS:
cmd = [ 'open', path ]
@ -807,7 +811,7 @@ def OpenFileLocation( path ):
cmd = [ 'explorer', '/select,', path ]
elif HC.PLATFORM_OSX:
elif HC.PLATFORM_MACOS:
cmd = [ 'open', '-R', path ]
@ -848,6 +852,17 @@ def PathIsFree( path ):
try:
stat_result = os.stat( path )
current_bits = stat_result.st_mode
if not current_bits & stat.S_IWRITE:
# read-only file, cannot do the rename check
return True
os.rename( path, path ) # rename a path to itself
return True

View File

@ -16,7 +16,7 @@ class HydrusRequest( Request ):
Request.__init__( self, *args, **kwargs )
self.start_time = time.clock()
self.start_time = HydrusData.GetNowPrecise()
self.parsed_request_args = None
self.hydrus_response_context = None
self.hydrus_account = None
@ -44,7 +44,7 @@ class HydrusRequestLogging( HydrusRequest ):
status_text = '200'
message = str( host.port ) + ' ' + str( self.method, 'utf-8' ) + ' ' + str( self.path, 'utf-8' ) + ' ' + status_text + ' in ' + HydrusData.TimeDeltaToPrettyTimeDelta( time.clock() - self.start_time )
message = str( host.port ) + ' ' + str( self.method, 'utf-8' ) + ' ' + str( self.path, 'utf-8' ) + ' ' + status_text + ' in ' + HydrusData.TimeDeltaToPrettyTimeDelta( HydrusData.GetNowPrecise() - self.start_time )
HydrusData.Print( message )

View File

@ -18,7 +18,7 @@ import time
FFMPEG_MISSING_ERROR_PUBBED = False
FFMPEG_NO_CONTENT_ERROR_PUBBED = False
if HC.PLATFORM_LINUX or HC.PLATFORM_OSX:
if HC.PLATFORM_LINUX or HC.PLATFORM_MACOS:
FFMPEG_PATH = os.path.join( HC.BIN_DIR, 'ffmpeg' )

View File

@ -4,16 +4,6 @@ import os
from . import HydrusConstants as HC
# if local linux OS has newer font config, let's redirect with env var before we get qt in
if HC.PLATFORM_LINUX and HC.RUNNING_FROM_FROZEN_BUILD:
SYS_FONT_PATH = '/etc/fonts'
if os.path.exists( SYS_FONT_PATH ):
os.environ[ 'FONTCONFIG_PATH' ] = SYS_FONT_PATH
# If not explicitely set, prefer PySide2 instead of the qtpy default which is PyQt5
# It is important that this runs on startup *before* anything is imported from qtpy.
# Since test.py, client.py and client.pyw all import this module first before any other Qt related ones, this requirement is satisfied.
@ -87,6 +77,26 @@ def MonkeyPatchMissingMethods():
QC.QSize.toTuple = QSizeToTuple
QC.QSizeF.toTuple = QSizeToTuple
QG.QColor.toTuple = QColorToTuple
def MonkeyPatchGetSaveFileName( original_function ):
def new_function( *args, **kwargs ):
if 'selectedFilter' in kwargs:
kwargs[ 'initialFilter' ] = kwargs[ 'selectedFilter' ]
del kwargs[ 'selectedFilter' ]
return original_function( *args, **kwargs )
return new_function
QW.QFileDialog.getSaveFileName = MonkeyPatchGetSaveFileName( QW.QFileDialog.getSaveFileName )
class HBoxLayout( QW.QHBoxLayout ):
@ -511,6 +521,11 @@ class TabWidgetWithDnD( QW.QTabWidget ):
return
if HC.PLATFORM_MACOS:
return
global_pos = self.mapToGlobal( e.pos() )
pos_in_tab = self._tab_bar.mapFromGlobal( global_pos )
@ -577,22 +592,10 @@ class TabWidgetWithDnD( QW.QTabWidget ):
tab_index = self._tab_bar.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.setCurrentIndex( tab_index )
self.setCurrentIndex( tab_index )
if event.mimeData().formats():
@ -760,6 +763,21 @@ class TabWidgetWithDnD( QW.QTabWidget ):
self.setCurrentIndex( self.indexOf( source_page ) )
else:
if source_page_index > 1:
neighbour_page = source_notebook.widget( source_page_index - 1 )
page_key = neighbour_page.GetPageKey()
else:
page_key = source_notebook.GetPageKey()
HG.client_controller.gui.ShowPage( page_key )
self.pageDragAndDropped.emit( source_page, source_tab_bar )
@ -806,7 +824,7 @@ def SplitVertically( splitter, w1, w2, hpos ):
def SplitHorizontally( splitter, w1, w2, vpos ):
splitter.setOrientation( QC.Qt.Vertical )
if w1.parentWidget() != splitter:
splitter.addWiget( w1 )
@ -1259,7 +1277,13 @@ def GetSystemColour( colour ):
return QG.QPalette().color( colour )
def Center( window ):
def CenterOnWindow( parent, window ):
parent_window = parent.window()
window.move( parent_window.mapToGlobal( parent_window.rect().center() ) - window.rect().center() )
def CenterOnScreen( window ):
window.move( QW.QApplication.desktop().availableGeometry().center() - window.rect().center() )
@ -1566,10 +1590,11 @@ 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 ) )
layout = QW.QVBoxLayout()
layout = QW.QVBoxLayout( self )
self.setWindowTitle( 'About ' + about_info.name )
@ -1825,9 +1850,16 @@ class EllipsizedLabel( QW.QLabel ):
self._ellipsize_end = ellipsize_end
def minimumSizeHint( self ):
if self._ellipsize_end:
self.setWordWrap( True )
return self.sizeHint()
else:
return QW.QLabel.minimumSizeHint( self )
@ -1836,36 +1868,27 @@ class EllipsizedLabel( QW.QLabel ):
QW.QLabel.setText( self, text )
self.update()
def sizeHint( self ):
size_hint = QW.QLabel.sizeHint( self )
if hasattr( self, '_wrap_width' ) and self._wrap_width is not None:
text_lines = self.text().split( '\n' )
if self._ellipsize_end:
for text_line in text_lines:
line_width = self.fontMetrics().size( QC.Qt.TextSingleLine, text_line ).width()
if line_width > self._wrap_width:
size_hint.setWidth( self._wrap_width )
break
num_lines = self.text().count( '\n' ) + 1
elif size_hint.width() < line_width:
line_width = self.fontMetrics().lineWidth()
line_height = self.fontMetrics().lineSpacing()
size_hint.setWidth( line_width )
size_hint = QC.QSize( 3 * line_width, num_lines * line_height )
else:
size_hint = QW.QLabel.sizeHint( self )
return size_hint
def paintEvent( self, event ):
if not self._ellipsize_end:
@ -1873,46 +1896,52 @@ class EllipsizedLabel( QW.QLabel ):
QW.QLabel.paintEvent( self, event )
return
painter = QG.QPainter( self )
fontMetrics = painter.fontMetrics()
text_lines = self.text().split( '\n' )
if hasattr( self, '_wrap_width' ) and self._wrap_width is not None:
for text_line in text_lines:
if self.width() < self._wrap_width and fontMetrics.boundingRect( text_line ).width() > self.width():
self.resize( self._wrap_width, self.height() )
break
line_spacing = fontMetrics.lineSpacing()
y = 0
current_y = 0
done = False
my_width = self.width()
for text_line in text_lines:
elided_line = fontMetrics.elidedText( text_line, QC.Qt.ElideRight, my_width )
x = 0
width = my_width
height = line_spacing
flags = self.alignment()
painter.drawText( x, current_y, width, height, flags, elided_line )
# old hacky line that doesn't support alignment flags
#painter.drawText( QC.QPoint( 0, current_y + fontMetrics.ascent() ), elided_line )
current_y += line_spacing
# old code that did multiline wrap width stuff
'''
text_layout = QG.QTextLayout( text_line, painter.font() )
text_layout.beginLayout()
while True:
line = text_layout.createLine()
if not line.isValid(): break
line.setLineWidth( self.width() )
next_line_y = y + line_spacing
if self.height() >= next_line_y + line_spacing:
@ -1920,7 +1949,7 @@ class EllipsizedLabel( QW.QLabel ):
line.draw( painter, QC.QPoint( 0, y ) )
y = next_line_y
else:
last_line = text_line[ line.textStart(): ]
@ -1930,12 +1959,15 @@ class EllipsizedLabel( QW.QLabel ):
painter.drawText( QC.QPoint( 0, y + fontMetrics.ascent() ), elided_last_line )
done = True
break
text_layout.endLayout()
if done: break
'''
@ -2270,9 +2302,9 @@ class WidgetEventFilter ( QC.QObject ):
# Once somehow this got called with no _parent_widget set - which is probably fixed now but leaving the check just in case, wew
# Might be worth debugging this later if it still occurs - the only way I found to reproduce it is to run the help > debug > initialize server command
if not hasattr( self, '_parent_widget') or not isValid( self._parent_widget ): return False
type = event.type()
event_killed = False
if type == QC.QEvent.KeyPress:
@ -2498,6 +2530,7 @@ class CollectComboCtrl( QW.QComboBox ):
for (sort_by_type, namespaces) in sort_by:
text_and_data_tuples.update( namespaces )
text_and_data_tuples = list( [ ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ] )
@ -2517,8 +2550,11 @@ class CollectComboCtrl( QW.QComboBox ):
self._cached_text = ''
CallAfter( self.SetCollectByValue, media_collect )
if media_collect.DoesACollect():
CallAfter( self.SetCollectByValue, media_collect )
def paintEvent( self, e ):

View File

@ -54,7 +54,7 @@ try:
db_dir = HC.DEFAULT_DB_DIR
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_OSX_APP:
if not HydrusPaths.DirectoryIsWritable( db_dir ) or HC.RUNNING_FROM_MACOS_APP:
db_dir = HC.USERPATH_DB_DIR
@ -127,19 +127,38 @@ except Exception as e:
import traceback
import os
print( traceback.format_exc() )
error_trace = traceback.format_exc()
print( error_trace )
if 'db_dir' in locals() and os.path.exists( db_dir ):
dest_path = os.path.join( db_dir, 'crash.log' )
emergency_dir = db_dir
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
else:
emergency_dir = os.path.expanduser( '~' )
possible_desktop = os.path.join( emergency_dir, 'Desktop' )
if os.path.exists( possible_desktop ) and os.path.isdir( possible_desktop ):
f.write( traceback.format_exc() )
emergency_dir = possible_desktop
print( 'Critical boot error occurred! Details written to crash.log!' )
dest_path = os.path.join( emergency_dir, 'hydrus_crash.log' )
with open( dest_path, 'w', encoding = 'utf-8' ) as f:
f.write( error_trace )
print( 'Critical boot error occurred! Details written to hydrus_crash.log in either db dir or user dir!' )
import sys
sys.exit( 1 )
controller = None