hydrus/hydrus/client/gui/ClientGUITopLevelWindows.py

1022 lines
28 KiB
Python

from . import ClientCaches
from . import ClientConstants as CC
from . import ClientGUIFunctions
from . import ClientGUIMenus
from . import ClientGUIShortcuts
from . import HydrusConstants as HC
from . import HydrusData
from . import HydrusExceptions
from . import HydrusGlobals as HG
import os
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from . import QtPorting as QP
from . import QtPorting as QP
CHILD_POSITION_PADDING = 24
FUZZY_PADDING = 10
def GetDisplayPosition( window ):
return QW.QApplication.desktop().availableGeometry( window ).topLeft()
def GetDisplaySize( window ):
return QW.QApplication.desktop().availableGeometry( window ).size()
def GetSafePosition( position: QC.QPoint ):
# some window managers size the windows just off screen to cut off borders
# so choose a test position that's a little more lenient
fuzzy_point = QC.QPoint( FUZZY_PADDING, FUZZY_PADDING )
test_position = position + fuzzy_point
screen = QW.QApplication.screenAt( test_position )
if screen is None:
try:
first_display = QW.QApplication.screens()[0]
rescue_position = first_display.availableGeometry().topLeft() + fuzzy_point
rescue_screen = QW.QApplication.screenAt( rescue_position )
if rescue_screen == first_display:
message = 'A window that wanted to display at "{}" was rescued from apparent off-screen to the new location at "{}".'.format( position, rescue_position )
return ( rescue_position, message )
except Exception as e:
# user is using IceMongo Linux, a Free Libre Open Source derivation of WeasleBlue Linux, with the iJ4 5-D inverted Window Managing system, which has a holographically virtualised desktop system
HydrusData.PrintException( e )
message = 'A window that wanted to display at "{}" could not be rescued from off-screen! Please let hydrus dev know!'
return ( None, message )
else:
return ( position, None )
def GetSafeSize( tlw: QW.QWidget, min_size: QC.QSize, gravity ) -> QC.QSize:
min_width = min_size.width()
min_height = min_size.height()
frame_padding = tlw.frameGeometry().size() - tlw.size()
parent = tlw.parentWidget()
if parent is None:
width = min_width
height = min_height
else:
parent_window = parent.window()
# when we initialise, we might not have a frame yet because we haven't done show() yet
# so borrow main gui's
if frame_padding.isEmpty():
main_gui = HG.client_controller.gui
if main_gui is not None and QP.isValid( main_gui ) and not main_gui.isFullScreen():
frame_padding = main_gui.frameGeometry().size() - main_gui.size()
if parent_window.isFullScreen():
parent_available_size = parent_window.size()
else:
parent_frame_size = parent_window.frameGeometry().size()
parent_available_size = parent_frame_size - frame_padding
parent_available_width = parent_available_size.width()
parent_available_height = parent_available_size.height()
( width_gravity, height_gravity ) = gravity
if width_gravity == -1:
width = min_width
else:
max_width = parent_available_width - ( 2 * CHILD_POSITION_PADDING )
width = int( width_gravity * max_width )
if height_gravity == -1:
height = min_height
else:
max_height = parent_available_height - ( 2 * CHILD_POSITION_PADDING )
height = int( height_gravity * max_height )
display_size = GetDisplaySize( tlw )
display_available_size = display_size - frame_padding
width = min( display_available_size.width() - 2 * CHILD_POSITION_PADDING, width )
height = min( display_available_size.height() - 2 * CHILD_POSITION_PADDING, height )
return QC.QSize( width, height )
def ExpandTLWIfPossible( tlw: QW.QWidget, frame_key, desired_size_delta: QC.QSize ):
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 )
if not tlw.isMaximized() and not tlw.isFullScreen():
current_size = tlw.size()
current_width = current_size.width()
current_height = current_size.height()
desired_delta_width = desired_size_delta.width()
desired_delta_height = desired_size_delta.height()
desired_width = current_width
if desired_delta_width > 0:
desired_width = current_width + desired_delta_width + FUZZY_PADDING
desired_height = current_height
if desired_delta_height > 0:
desired_height = current_height + desired_delta_height + FUZZY_PADDING
desired_size = QC.QSize( desired_width, desired_height )
new_size = GetSafeSize( tlw, desired_size, default_gravity )
if new_size.width() > current_width or new_size.height() > current_height:
tlw.resize( new_size )
#tlw.setMinimumSize( tlw.sizeHint() )
SlideOffScreenTLWUpAndLeft( tlw )
def GetMouseScreen():
return QW.QApplication.screenAt( QG.QCursor.pos() )
def MouseIsOnMyDisplay( window ):
window_handle = window.window().windowHandle()
if window_handle is None:
return False
window_screen = window_handle.screen()
mouse_screen = GetMouseScreen()
return mouse_screen is window_screen
def SaveTLWSizeAndPosition( tlw: QW.QWidget, 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 )
maximised = tlw.isMaximized()
fullscreen = tlw.isFullScreen()
if not ( maximised or fullscreen ):
( safe_position, position_message ) = GetSafePosition( tlw.pos() )
if safe_position is not None:
last_size = ( tlw.size().width(), tlw.size().height() )
last_position = ( safe_position.x(), safe_position.y() )
new_options.SetFrameLocation( frame_key, remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen )
def SetInitialTLWSizeAndPosition( tlw: QW.QWidget, frame_key ):
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 )
parent = tlw.parentWidget()
if parent is None:
parent_window = None
else:
parent_window = parent.window()
if remember_size and last_size is not None:
( width, height ) = last_size
new_size = QC.QSize( width, height )
else:
new_size = GetSafeSize( tlw, tlw.sizeHint(), default_gravity )
tlw.resize( new_size )
min_width = min( 240, new_size.width() )
min_height = min( 240, new_size.height() )
tlw.setMinimumSize( QC.QSize( min_width, min_height ) )
#
child_position_point = QC.QPoint( CHILD_POSITION_PADDING, CHILD_POSITION_PADDING )
desired_position = child_position_point
we_care_about_off_screen_messages = True
slide_up_and_left = False
if remember_position and last_position is not None:
( x, y ) = last_position
desired_position = QC.QPoint( x, y )
elif default_position == 'topleft':
if parent_window is None:
we_care_about_off_screen_messages = False
screen = GetMouseScreen()
if screen is not None:
desired_position = screen.availableGeometry().topLeft() + QC.QPoint( CHILD_POSITION_PADDING, CHILD_POSITION_PADDING )
else:
parent_tlw = parent_window.window()
desired_position = parent_tlw.pos() + QC.QPoint( CHILD_POSITION_PADDING, CHILD_POSITION_PADDING )
slide_up_and_left = True
elif default_position == 'center':
if parent_window is None:
we_care_about_off_screen_messages = False
screen = GetMouseScreen()
if screen is not None:
desired_position = screen.availableGeometry().center()
else:
desired_position = parent_window.frameGeometry().center() - tlw.rect().center()
( safe_position, position_message ) = GetSafePosition( desired_position )
if we_care_about_off_screen_messages and position_message is not None:
HydrusData.ShowText( position_message )
if safe_position is not None:
tlw.move( safe_position )
if slide_up_and_left:
SlideOffScreenTLWUpAndLeft( 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:
tlw.showMaximized()
if fullscreen and not HC.PLATFORM_MACOS:
tlw.showFullScreen()
def SlideOffScreenTLWUpAndLeft( tlw ):
tlw_frame_rect = tlw.frameGeometry()
tlw_top_left = tlw_frame_rect.topLeft()
tlw_bottom_right = tlw_frame_rect.bottomRight()
tlw_right = tlw_bottom_right.x()
tlw_bottom = tlw_bottom_right.y()
display_size = GetDisplaySize( tlw )
display_pos = GetDisplayPosition( tlw )
display_right = display_pos.x() + display_size.width() - CHILD_POSITION_PADDING
display_bottom = display_pos.y() + display_size.height() - CHILD_POSITION_PADDING
move_x = tlw_right > display_right
move_y = tlw_bottom > display_bottom
if move_x or move_y:
delta_x = min( display_right - tlw_right, 0 )
delta_y = min( display_bottom - tlw_bottom, 0 )
delta_point = QC.QPoint( delta_x, delta_y )
safe_pos = tlw_top_left + delta_point
tlw.move( safe_pos )
class NewDialog( QP.Dialog ):
def __init__( self, parent, title, do_not_activate = False ):
QP.Dialog.__init__( self, parent )
if do_not_activate:
self.setAttribute( QC.Qt.WA_ShowWithoutActivating )
self.setWindowTitle( title )
self._last_move_pub = 0.0
self._new_options = HG.client_controller.new_options
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
HG.client_controller.ResetIdleTimer()
self._widget_event_filter = QP.WidgetEventFilter( self )
def moveEvent( self, event ):
if HydrusData.TimeHasPassedFloat( self._last_move_pub + 0.1 ):
HG.client_controller.pub( 'top_level_window_move_event' )
self._last_move_pub = HydrusData.GetNowPrecise()
event.ignore()
def _CanCancel( self ):
return True
def _CanOK( self ):
return True
def _ReadyToClose( self, value ):
return True
def _SaveOKPosition( self ):
pass
def _TryEndModal( self, value ):
if not self.isModal(): # in some rare cases (including spammy AutoHotkey, looks like), this can be fired before the dialog can clean itself up
return False
if not self._ReadyToClose( value ):
return False
if value == QW.QDialog.Rejected:
if not self._CanCancel():
return False
self.SetCancelled( True )
if value == QW.QDialog.Accepted:
if not self._CanOK():
return False
self._SaveOKPosition()
self.CleanBeforeDestroy()
try:
self.done( value )
except Exception as e:
HydrusData.ShowText( 'This dialog seems to have been unable to close for some reason. I am printing the stack to the log. The dialog may have already closed, or may attempt to close now. Please inform hydrus dev of this situation. I recommend you restart the client if you can. If the UI is locked, you will have to kill it via task manager.' )
HydrusData.PrintException( e )
import traceback
HydrusData.DebugPrint( ''.join( traceback.format_stack() ) )
try:
self.close()
except:
HydrusData.ShowText( 'The dialog would not close on command.' )
try:
self.deleteLater()
except:
HydrusData.ShowText( 'The dialog would not destroy on command.' )
return True
def CleanBeforeDestroy( self ):
pass
def DoOK( self ):
self._TryEndModal( QW.QDialog.Accepted )
def closeEvent( self, event ):
if not self or not QP.isValid( self ):
return
was_ended = self._TryEndModal( QW.QDialog.Rejected )
if was_ended:
event.accept()
else:
event.ignore()
def EventDialogButtonApply( self ):
if not self or not QP.isValid( self ):
return
event_object = self.sender()
if event_object is not None:
tlw = event_object.window()
if tlw != self:
return
self._TryEndModal( QW.QDialog.Accepted )
def EventDialogButtonCancel( self ):
if not self or not QP.isValid( self ):
return
event_object = self.sender()
if event_object is not None:
tlw = event_object.window()
if tlw != self:
return
self._TryEndModal( QW.QDialog.Rejected )
def keyPressEvent( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
current_focus = QW.QApplication.focusWidget()
event_from_us = current_focus is not None and ClientGUIFunctions.IsQtAncestor( current_focus, self )
if event_from_us and key == QC.Qt.Key_Escape:
self._TryEndModal( QW.QDialog.Rejected )
else:
QP.Dialog.keyPressEvent( self, event )
class DialogThatResizes( NewDialog ):
def __init__( self, parent, title, frame_key, do_not_activate = False ):
self._frame_key = frame_key
NewDialog.__init__( self, parent, title, do_not_activate = do_not_activate )
def _SaveOKPosition( self ):
SaveTLWSizeAndPosition( self, self._frame_key )
class DialogThatTakesScrollablePanel( DialogThatResizes ):
def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = False, do_not_activate = False ):
self._panel = None
self._hide_buttons = hide_buttons
DialogThatResizes.__init__( self, parent, title, frame_key, do_not_activate = do_not_activate )
self._InitialiseButtons()
def _CanCancel( self ):
return self._panel.CanCancel()
def _CanOK( self ):
return self._panel.CanOK()
def _GetButtonBox( self ):
raise NotImplementedError()
def _InitialiseButtons( self ):
raise NotImplementedError()
def CleanBeforeDestroy( self ):
DialogThatResizes.CleanBeforeDestroy( self )
if hasattr( self._panel, 'CleanBeforeDestroy' ):
self._panel.CleanBeforeDestroy()
def SetPanel( self, panel ):
self._panel = panel
if hasattr( self._panel, 'okSignal'): self._panel.okSignal.connect( self.DoOK )
buttonbox = self._GetButtonBox()
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._panel, CC.FLAGS_EXPAND_BOTH_WAYS )
if buttonbox is not None:
QP.AddToLayout( vbox, buttonbox, CC.FLAGS_BUTTON_SIZER )
self.setLayout( vbox )
SetInitialTLWSizeAndPosition( self, self._frame_key )
class DialogNullipotent( DialogThatTakesScrollablePanel ):
def _GetButtonBox( self ):
buttonbox = QP.HBoxLayout()
QP.AddToLayout( buttonbox, self._close )
return buttonbox
def _InitialiseButtons( self ):
self._close = QW.QPushButton( 'close', self )
self._close.clicked.connect( self.DoOK )
if self._hide_buttons:
self._close.setVisible( False )
def _ReadyToClose( self, value ):
try:
self._panel.TryToClose()
return True
except HydrusExceptions.VetoException as e:
message = str( e )
if len( message ) > 0:
QW.QMessageBox.critical( self, 'Error', message )
return False
class DialogApplyCancel( DialogThatTakesScrollablePanel ):
def _GetButtonBox( self ):
buttonbox = QP.HBoxLayout()
QP.AddToLayout( buttonbox, self._apply )
QP.AddToLayout( buttonbox, self._cancel )
return buttonbox
def _InitialiseButtons( self ):
self._apply = QW.QPushButton( 'apply', self )
self._apply.setObjectName( 'HydrusAccept' )
self._apply.clicked.connect( self.EventDialogButtonApply )
self._cancel = QW.QPushButton( 'cancel', self )
self._cancel.setObjectName( 'HydrusCancel' )
self._cancel.clicked.connect( self.EventDialogButtonCancel )
if self._hide_buttons:
self._apply.setVisible( False )
self._cancel.setVisible( False )
class DialogEdit( DialogApplyCancel ):
def __init__( self, parent, title, frame_key = 'regular_dialog', hide_buttons = False ):
DialogApplyCancel.__init__( self, parent, title, frame_key = frame_key, hide_buttons = hide_buttons )
def _ReadyToClose( self, value ):
if value != QW.QDialog.Accepted:
return True
try:
value = self._panel.GetValue()
return True
except HydrusExceptions.VetoException as e:
message = str( e )
if len( message ) > 0:
QW.QMessageBox.critical( self, 'Error', message )
return False
class DialogManage( DialogApplyCancel ):
def _ReadyToClose( self, value ):
if value != QW.QDialog.Accepted:
return True
try:
self._panel.CommitChanges()
return True
except HydrusExceptions.VetoException as e:
message = str( e )
if len( message ) > 0:
QW.QMessageBox.critical( self, 'Error', message )
return False
class DialogCustomButtonQuestion( DialogThatTakesScrollablePanel ):
def __init__( self, parent, title, frame_key = 'regular_center_dialog' ):
DialogThatTakesScrollablePanel.__init__( self, parent, title, frame_key = frame_key )
def _GetButtonBox( self ):
return None
def _InitialiseButtons( self ):
pass
class Frame( QW.QWidget ):
def __init__( self, parent, title ):
QW.QWidget.__init__( self, parent )
self.setWindowTitle( title )
self.setWindowFlags( QC.Qt.Window )
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
self.setAttribute( QC.Qt.WA_DeleteOnClose )
self._new_options = HG.client_controller.new_options
self._last_move_pub = 0.0
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_CLOSE( self.EventAboutToClose )
self._widget_event_filter.EVT_MOVE( self.EventMove )
HG.client_controller.ResetIdleTimer()
def CleanBeforeDestroy( self ):
pass
def EventAboutToClose( self, event ):
self.CleanBeforeDestroy()
return True # was: event.ignore()
def EventMove( self, event ):
if HydrusData.TimeHasPassedFloat( self._last_move_pub + 0.1 ):
HG.client_controller.pub( 'top_level_window_move_event' )
self._last_move_pub = HydrusData.GetNowPrecise()
return True # was: event.ignore()
class MainFrame( QW.QMainWindow ):
def __init__( self, parent, title ):
QW.QMainWindow.__init__( self, parent )
self.setWindowTitle( title )
self._new_options = HG.client_controller.new_options
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_CLOSE( self.EventAboutToClose )
HG.client_controller.ResetIdleTimer()
def CleanBeforeDestroy( self ):
pass
def EventAboutToClose( self, event ):
self.CleanBeforeDestroy()
return True # was: event.ignore()
class FrameThatResizes( Frame ):
def __init__( self, parent, title, frame_key ):
self._frame_key = frame_key
Frame.__init__( self, parent, title )
self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_CLOSE( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_MAXIMIZE( self.EventSizeAndPositionChanged )
def EventSizeAndPositionChanged( self, event ):
# 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()
class FrameThatResizesWithHovers( FrameThatResizes ): pass
class MainFrameThatResizes( MainFrame ):
def __init__( self, parent, title, frame_key ):
self._frame_key = frame_key
MainFrame.__init__( self, parent, title )
self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_CLOSE( self.EventSizeAndPositionChanged )
self._widget_event_filter.EVT_MAXIMIZE( self.EventSizeAndPositionChanged )
def EventSizeAndPositionChanged( self, event ):
# 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()
class FrameThatTakesScrollablePanel( FrameThatResizes ):
def __init__( self, parent, title, frame_key = 'regular_dialog' ):
self._panel = None
FrameThatResizes.__init__( self, parent, title, frame_key )
self._ok = QW.QPushButton( 'close', self )
self._ok.clicked.connect( self.close )
def CleanBeforeDestroy( self ):
FrameThatResizes.CleanBeforeDestroy( self )
if hasattr( self._panel, 'CleanBeforeDestroy' ):
self._panel.CleanBeforeDestroy()
def keyPressEvent( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key == QC.Qt.Key_Escape:
self.close()
else:
event.ignore()
def GetPanel( self ):
return self._panel
def SetPanel( self, panel ):
self._panel = panel
if hasattr( self._panel, 'okSignal' ):
self._panel.okSignal.connect( self.close )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._panel )
QP.AddToLayout( vbox, self._ok, CC.FLAGS_LONE_BUTTON )
self.setLayout( vbox )
SetInitialTLWSizeAndPosition( self, self._frame_key )
self.show()