2022-01-26 21:57:04 +00:00
import numpy
2021-07-14 20:42:19 +00:00
import typing
2020-04-22 21:00:35 +00:00
2019-11-14 03:56:30 +00:00
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
2020-04-22 21:00:35 +00:00
2021-10-27 21:12:33 +00:00
from hydrus . core import HydrusConstants as HC
2023-04-26 21:10:03 +00:00
from hydrus . core import HydrusData
2020-12-09 22:18:48 +00:00
from hydrus . core import HydrusText
2024-02-14 21:20:24 +00:00
from hydrus . client import ClientGlobals as CG
2022-08-01 00:45:12 +00:00
from hydrus . client . gui import QtInit
2020-04-22 21:00:35 +00:00
from hydrus . client . gui import QtPorting as QP
2024-01-03 21:21:53 +00:00
from hydrus . core . files . images import HydrusImageNormalisation
2019-06-26 21:27:18 +00:00
2020-02-26 22:28:52 +00:00
def ClientToScreen ( win : QW . QWidget , pos : QC . QPoint ) - > QC . QPoint :
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
tlw = win . window ( )
2019-11-14 03:56:30 +00:00
2021-12-01 22:12:16 +00:00
if ( win . isVisible ( ) and tlw . isVisible ( ) ) :
2019-06-26 21:27:18 +00:00
2019-11-14 03:56:30 +00:00
return win . mapToGlobal ( pos )
2019-06-26 21:27:18 +00:00
else :
2019-11-14 03:56:30 +00:00
return QC . QPoint ( 50 , 50 )
2019-06-26 21:27:18 +00:00
2020-05-20 21:36:02 +00:00
def ColourIsBright ( colour : QG . QColor ) :
it_is_bright = colour . valueF ( ) > 0.75
return it_is_bright
def ColourIsGreyish ( colour : QG . QColor ) :
it_is_greyish = colour . hsvSaturationF ( ) < 0.12
return it_is_greyish
2021-07-14 20:42:19 +00:00
# OK, so we now have a fixed block for width, which we sometimes want to calculate in both directions.
# by normalising our 'one character' width, the inverse calculation uses the same coefficient and we aren't losing so much in rounding
NUM_CHARS_FOR_WIDTH_CALCULATIONS = 32
MAGIC_TEXT_PADDING = 1.1
def GetOneCharacterPixelHeight ( window ) - > float :
return window . fontMetrics ( ) . height ( ) * MAGIC_TEXT_PADDING
def GetOneCharacterPixelWidth ( window ) - > float :
2020-07-15 20:52:09 +00:00
2021-07-14 20:42:19 +00:00
char_block_width = window . fontMetrics ( ) . boundingRect ( NUM_CHARS_FOR_WIDTH_CALCULATIONS * ' x ' ) . width ( ) * MAGIC_TEXT_PADDING
one_char_width = char_block_width / NUM_CHARS_FOR_WIDTH_CALCULATIONS
return one_char_width
def ConvertPixelsToTextWidth ( window , pixels , round_down = False ) - > int :
one_char_width = GetOneCharacterPixelWidth ( window )
2020-07-15 20:52:09 +00:00
2021-04-14 21:54:17 +00:00
if round_down :
2021-07-14 20:42:19 +00:00
return int ( pixels / / one_char_width )
2021-04-14 21:54:17 +00:00
else :
2021-07-14 20:42:19 +00:00
return round ( pixels / one_char_width )
2021-04-14 21:54:17 +00:00
2020-07-15 20:52:09 +00:00
2023-11-08 21:42:59 +00:00
def ConvertQtImageToNumPy ( qt_image : QG . QImage , strip_useless_alpha = True ) :
2022-01-26 21:57:04 +00:00
2023-08-23 20:43:26 +00:00
# _ _ _ _
# _ | | (_) _ | | | |
# _| |_| |__ _ ___ _| |_ ___ ___ | | _ | |__ ___ _ _ ____ ___
# (_ _) _ \| |/___) (_ _) _ \ / _ \| |_/ ) | _ \ / _ \| | | |/ ___)___)
# | |_| | | | |___ | | || |_| | |_| | _ ( | | | | |_| | |_| | | |___ |
# \__)_| |_|_(___/ \__)___/ \___/|_| \_) |_| |_|\___/|____/|_| (___/
#
# Ok so I don't know what is going on, but QImage 'ARGB32' bitmaps seem to actually be stored BGRA!!!
# if you tell them to convert to RGB888, they switch their bytes around to RGB, so this is probably some internal Qt gubbins
# The spec says they are 0xAARRGGBB, so I'm guessing it is swapped for some X-endian reason???
# unfortunately this messes with us a little, since we rip these bits and assume we are getting RGBA
# Ok, I figured it out, just convert to RGBA8888 (which supposedly _is_ endian ordered) and it sorts itself out
# I guess someone on different endian-ness won't get the right answer? I'd believe it if ARGB32 wasn't reversed
# if that is the case, we can inspect the 'PixelFormat' of the bmp and it'll say our endianness, and then I guess I reverse or something
# probably easier, whatever the case, to do that sort of clever channel swapping once we are in numpy
# another ultimate answer is probably to convert to rgb888 and rip the alpha too and recombine, or just ditch the alpha who cares
qt_image = qt_image . copy ( )
if qt_image . hasAlphaChannel ( ) :
if qt_image . format ( ) != QG . QImage . Format_RGBA8888 :
qt_image . convertTo ( QG . QImage . Format_RGBA8888 )
else :
if qt_image . format ( ) != QG . QImage . Format_RGB888 :
qt_image . convertTo ( QG . QImage . Format_RGB888 )
2022-01-26 21:57:04 +00:00
width = qt_image . width ( )
height = qt_image . height ( )
if qt_image . depth ( ) == 1 :
# this is probably super wrong, but whatever for now
depth = 1
else :
# 8, 24, 32 etc...
depth = qt_image . depth ( ) / / 8
data_bytearray = qt_image . bits ( )
2022-08-01 00:45:12 +00:00
if QtInit . WE_ARE_PYSIDE :
2022-01-26 21:57:04 +00:00
data_bytes = bytes ( data_bytearray )
2022-08-01 00:45:12 +00:00
elif QtInit . WE_ARE_PYQT :
2022-01-26 21:57:04 +00:00
data_bytes = data_bytearray . asstring ( height * width * depth )
2023-07-05 20:52:58 +00:00
if qt_image . bytesPerLine ( ) == width * depth :
numpy_image = numpy . fromstring ( data_bytes , dtype = ' uint8 ' ) . reshape ( ( height , width , depth ) )
else :
# ok bro, so in some cases a qt_image stores its lines with a bit of \x00 padding. you have a 990-pixel line that is 2970+2 bytes long
# apparently this is system memory storage limitations blah blah blah. it can also happen when you qt_image.copy(), so I guess it makes for pleasant memory layout little-endian something
# so far I have only encountered simple instances of this, with data up front and zero bytes at the end
# so let's just strip it lad
bytes_per_line = qt_image . bytesPerLine ( )
desired_bytes_per_line = width * depth
excess_bytes_to_trim = bytes_per_line - desired_bytes_per_line
numpy_padded = numpy . fromstring ( data_bytes , dtype = ' uint8 ' ) . reshape ( ( height , bytes_per_line ) )
numpy_image = numpy_padded [ : , : - excess_bytes_to_trim ] . reshape ( ( height , width , depth ) )
2022-01-26 21:57:04 +00:00
2023-11-08 21:42:59 +00:00
if strip_useless_alpha :
numpy_image = HydrusImageNormalisation . StripOutAnyUselessAlphaChannel ( numpy_image )
2022-01-26 21:57:04 +00:00
return numpy_image
2021-07-14 20:42:19 +00:00
def ConvertTextToPixels ( window , char_dimensions ) - > typing . Tuple [ int , int ] :
2019-06-26 21:27:18 +00:00
( char_cols , char_rows ) = char_dimensions
2021-07-14 20:42:19 +00:00
one_char_width = GetOneCharacterPixelWidth ( window )
one_char_height = GetOneCharacterPixelHeight ( window )
return ( round ( char_cols * one_char_width ) , round ( char_rows * one_char_height ) )
def ConvertTextToPixelWidth ( window , char_cols ) - > int :
2019-06-26 21:27:18 +00:00
2021-07-14 20:42:19 +00:00
one_char_width = GetOneCharacterPixelWidth ( window )
2019-06-26 21:27:18 +00:00
2021-07-14 20:42:19 +00:00
return round ( char_cols * one_char_width )
2019-06-26 21:27:18 +00:00
2023-08-02 21:11:08 +00:00
2020-02-26 22:28:52 +00:00
def DialogIsOpen ( ) :
tlws = QW . QApplication . topLevelWidgets ( )
for tlw in tlws :
if isinstance ( tlw , QP . Dialog ) and tlw . isModal ( ) :
return True
return False
2022-08-17 20:54:59 +00:00
2020-12-02 22:04:38 +00:00
def DrawText ( painter , x , y , text ) :
2022-08-17 20:54:59 +00:00
( size , text ) = GetTextSizeFromPainter ( painter , text )
painter . drawText ( QC . QRectF ( x , y , size . width ( ) , size . height ( ) ) , text )
2020-12-02 22:04:38 +00:00
2020-05-20 21:36:02 +00:00
def EscapeMnemonics ( s : str ) :
return s . replace ( " & " , " && " )
2022-08-17 20:54:59 +00:00
2020-05-20 21:36:02 +00:00
def GetDifferentLighterDarkerColour ( colour , intensity = 3 ) :
new_hue = colour . hsvHueF ( )
2020-04-22 21:00:35 +00:00
2020-05-20 21:36:02 +00:00
if new_hue == - 1 : # completely achromatic
new_hue = 0.5
else :
new_hue = ( new_hue + 0.33 ) % 1.0
new_saturation = colour . hsvSaturationF ( )
if ColourIsGreyish ( colour ) :
new_saturation = 0.2
new_colour = QG . QColor . fromHsvF ( new_hue , new_saturation , colour . valueF ( ) , colour . alphaF ( ) )
return GetLighterDarkerColour ( new_colour , intensity )
2020-04-22 21:00:35 +00:00
2020-04-29 21:44:12 +00:00
def GetDisplayPosition ( window ) :
2022-07-25 15:33:44 +00:00
return window . screen ( ) . availableGeometry ( ) . topLeft ( )
2020-04-29 21:44:12 +00:00
def GetDisplaySize ( window ) :
2022-07-25 15:33:44 +00:00
return window . screen ( ) . availableGeometry ( ) . size ( )
2020-04-29 21:44:12 +00:00
2020-05-20 21:36:02 +00:00
def GetLighterDarkerColour ( colour , intensity = 3 ) :
if intensity is None or intensity == 0 :
return colour
# darker/lighter works by multiplying value, so when it is closer to 0, lmao
breddy_darg_made = 0.25
if colour . value ( ) < breddy_darg_made :
colour = QG . QColor . fromHslF ( colour . hsvHueF ( ) , colour . hsvSaturationF ( ) , breddy_darg_made , colour . alphaF ( ) )
qt_intensity = 100 + ( 20 * intensity )
if ColourIsBright ( colour ) :
return colour . darker ( qt_intensity )
else :
return colour . lighter ( qt_intensity )
2024-02-21 21:09:02 +00:00
def GetMouseScreen ( ) - > typing . Optional [ QG . QScreen ] :
2020-04-29 21:44:12 +00:00
return QW . QApplication . screenAt ( QG . QCursor . pos ( ) )
2020-12-02 22:04:38 +00:00
def GetTextSizeFromPainter ( painter : QG . QPainter , text : str ) :
try :
text_size = painter . fontMetrics ( ) . size ( QC . Qt . TextSingleLine , text )
except ValueError :
from hydrus . client . metadata import ClientTags
if not ClientTags . have_shown_invalid_tag_warning :
from hydrus . core import HydrusData
HydrusData . ShowText ( ' Hey, I think hydrus stumbled across an invalid tag! Please run _database->check and repair->fix invalid tags_ immediately, or you may get errors! ' )
2020-12-09 22:18:48 +00:00
bad_text = repr ( text )
bad_text = HydrusText . ElideText ( bad_text , 24 )
HydrusData . ShowText ( ' The bad text was: {} ' . format ( bad_text ) )
2020-12-02 22:04:38 +00:00
ClientTags . have_shown_invalid_tag_warning = True
text = ' *****INVALID, UNDISPLAYABLE TAG, RUN DATABASE REPAIR NOW***** '
text_size = painter . fontMetrics ( ) . size ( QC . Qt . TextSingleLine , text )
return ( text_size , text )
2019-12-11 23:18:37 +00:00
def GetTLWParents ( widget ) :
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
widget_tlw = widget . window ( )
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
parent_tlws = [ ]
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
parent = widget_tlw . parentWidget ( )
2019-06-26 21:27:18 +00:00
while parent is not None :
2019-12-11 23:18:37 +00:00
parent_tlw = parent . window ( )
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
parent_tlws . append ( parent_tlw )
parent = parent_tlw . parentWidget ( )
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
return parent_tlws
2019-06-26 21:27:18 +00:00
2022-07-13 21:35:17 +00:00
def IsQtAncestor ( child : QW . QWidget , ancestor : QW . QWidget , through_tlws = False ) :
2019-06-26 21:27:18 +00:00
2022-10-12 20:18:22 +00:00
if child is None :
return False
2019-07-17 22:10:19 +00:00
if child == ancestor :
return True
2019-06-26 21:27:18 +00:00
parent = child
if through_tlws :
2023-06-28 20:29:14 +00:00
while parent is not None :
2019-06-26 21:27:18 +00:00
if parent == ancestor :
return True
2019-11-14 03:56:30 +00:00
parent = parent . parentWidget ( )
2019-06-26 21:27:18 +00:00
else :
2019-11-14 03:56:30 +00:00
# only works within window
return ancestor . isAncestorOf ( child )
2019-06-26 21:27:18 +00:00
return False
2020-04-29 21:44:12 +00:00
def MouseIsOnMyDisplay ( window ) :
window_handle = window . window ( ) . windowHandle ( )
if window_handle is None :
return False
window_screen = window_handle . screen ( )
mouse_screen = GetMouseScreen ( )
2024-02-21 21:09:02 +00:00
# something's busted!
if mouse_screen is None :
return True
2020-04-29 21:44:12 +00:00
return mouse_screen is window_screen
2021-12-22 22:31:23 +00:00
def MouseIsOverWidget ( win : QW . QWidget ) :
# note this is different from win.underMouse(), which in different situations seems to be more complicated than just a rect test
# I also had QWidget.underMouse() do flicker on the border edge between two lads next to each other. I guess there might be a frameGeometry vs geometry issue, but dunno. not like I test that here
global_mouse_pos = QG . QCursor . pos ( )
local_mouse_pos = win . mapFromGlobal ( global_mouse_pos )
return win . rect ( ) . contains ( local_mouse_pos )
2019-06-26 21:27:18 +00:00
def NotebookScreenToHitTest ( notebook , screen_position ) :
2019-12-05 05:29:32 +00:00
tab_pos = notebook . tabBar ( ) . mapFromGlobal ( screen_position )
2019-06-26 21:27:18 +00:00
2019-12-05 05:29:32 +00:00
return notebook . tabBar ( ) . tabAt ( tab_pos )
2019-06-26 21:27:18 +00:00
2023-04-26 21:10:03 +00:00
2019-06-26 21:27:18 +00:00
def SetBitmapButtonBitmap ( button , bitmap ) :
2019-11-14 03:56:30 +00:00
# old wx stuff, but still basically relevant
2019-06-26 21:27:18 +00:00
# the button's bitmap, retrieved via GetBitmap, is not the same as the one we gave it!
# hence testing bitmap vs that won't work to save time on an update loop, so we'll just save it here custom
2019-11-14 03:56:30 +00:00
# this isn't a big memory deal for our purposes since they are small and mostly if not all from the GlobalPixmaps library so shared anyway
2019-06-26 21:27:18 +00:00
if hasattr ( button , ' last_bitmap ' ) :
if button . last_bitmap == bitmap :
return
2019-11-14 03:56:30 +00:00
button . setIcon ( QG . QIcon ( bitmap ) )
button . setIconSize ( bitmap . size ( ) )
2019-06-26 21:27:18 +00:00
button . last_bitmap = bitmap
2021-06-23 21:11:38 +00:00
def SetFocusLater ( win : QW . QWidget ) :
2024-02-14 21:20:24 +00:00
CG . client_controller . CallAfterQtSafe ( win , ' set focus to a window ' , win . setFocus , QC . Qt . OtherFocusReason )
2021-06-23 21:11:38 +00:00
2019-12-11 23:18:37 +00:00
def TLWIsActive ( window ) :
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
return window . window ( ) == QW . QApplication . activeWindow ( )
2019-06-26 21:27:18 +00:00
2019-12-11 23:18:37 +00:00
def TLWOrChildIsActive ( win ) :
current_focus_tlw = QW . QApplication . activeWindow ( )
if current_focus_tlw is None :
return False
if current_focus_tlw == win :
return True
if win in GetTLWParents ( current_focus_tlw ) :
return True
return False
2023-08-02 21:11:08 +00:00
2021-10-27 21:12:33 +00:00
def UpdateAppDisplayName ( ) :
2024-02-14 21:20:24 +00:00
app_display_name = CG . client_controller . new_options . GetString ( ' app_display_name ' )
2021-10-27 21:12:33 +00:00
QW . QApplication . instance ( ) . setApplicationDisplayName ( ' {} {} ' . format ( app_display_name , HC . SOFTWARE_VERSION ) )
for tlw in QW . QApplication . topLevelWidgets ( ) :
window_title = tlw . windowTitle ( )
if window_title != ' ' :
tlw . setWindowTitle ( ' ' )
tlw . setWindowTitle ( window_title )
2023-08-02 21:11:08 +00:00
2019-12-11 23:18:37 +00:00
def WidgetOrAnyTLWChildHasFocus ( window ) :
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
active_window = QW . QApplication . activeWindow ( )
2019-11-14 03:56:30 +00:00
2019-11-20 23:10:46 +00:00
if window == active_window :
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
return True
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
widget = QW . QApplication . focusWidget ( )
2019-11-14 03:56:30 +00:00
2019-11-20 23:10:46 +00:00
if widget is None :
2019-12-11 23:18:37 +00:00
# take active window in lieu of focus, if it is unavailable
2019-11-20 23:10:46 +00:00
widget = active_window
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
while widget is not None :
2019-06-26 21:27:18 +00:00
2019-11-20 23:10:46 +00:00
if widget == window :
2019-06-26 21:27:18 +00:00
return True
2019-11-20 23:10:46 +00:00
widget = widget . parentWidget ( )
2019-06-26 21:27:18 +00:00
return False
2024-04-10 20:36:05 +00:00
def WrapToolTip ( s : str , max_line_length = 80 ) :
wrapped_lines = [ ]
for line in s . splitlines ( ) :
if len ( line ) == 0 :
wrapped_lines . append ( line )
continue
words = line . split ( ' ' )
words_of_current_line = [ ]
num_chars_in_current_line = 0
for word in words :
if num_chars_in_current_line + len ( words_of_current_line ) + len ( word ) > max_line_length and len ( words_of_current_line ) > 0 :
wrapped_lines . append ( ' ' . join ( words_of_current_line ) )
words_of_current_line = [ word ]
num_chars_in_current_line = len ( word )
else :
words_of_current_line . append ( word )
num_chars_in_current_line + = len ( word )
if len ( words_of_current_line ) > 0 :
wrapped_lines . append ( ' ' . join ( words_of_current_line ) )
wrapped_tt = ' \n ' . join ( wrapped_lines )
return wrapped_tt