722 lines
21 KiB
Python
722 lines
21 KiB
Python
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
from qtpy import QtGui as QG
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusGlobals as HG
|
|
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIShortcuts
|
|
from hydrus.client.gui import QtPorting as QP
|
|
|
|
CHILD_POSITION_PADDING = 24
|
|
FUZZY_PADDING = 10
|
|
|
|
def GetSafePosition( position: QC.QPoint, frame_key ):
|
|
|
|
# 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 with frame key "{}" that wanted to display at "{}" was rescued from apparent off-screen to the new location at "{}".'.format( frame_key, 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 with frame key "{}" that wanted to display at "{}" could not be rescued from off-screen! Please let hydrus dev know!'.format( frame_key, position )
|
|
|
|
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 = ClientGUIFunctions.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 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(), frame_key )
|
|
|
|
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 = ClientGUIFunctions.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 = ClientGUIFunctions.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, frame_key )
|
|
|
|
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 = ClientGUIFunctions.GetDisplaySize( tlw )
|
|
display_pos = ClientGUIFunctions.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 _DoClose( self, value ):
|
|
|
|
return
|
|
|
|
|
|
def _SaveOKPosition( self ):
|
|
|
|
pass
|
|
|
|
|
|
def _TestValidityAndPresentVetoMessage( self, value ):
|
|
|
|
return True
|
|
|
|
|
|
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._TestValidityAndPresentVetoMessage( value ):
|
|
|
|
return False
|
|
|
|
|
|
if not self._UserIsOKToClose( value ):
|
|
|
|
return False
|
|
|
|
|
|
if value == QW.QDialog.Rejected:
|
|
|
|
self.SetCancelled( True )
|
|
|
|
elif value == QW.QDialog.Accepted:
|
|
|
|
self._SaveOKPosition()
|
|
|
|
|
|
self._DoClose( value )
|
|
|
|
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 _UserIsOKToClose( self, value ):
|
|
|
|
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 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_MOVE( self.EventMove )
|
|
|
|
HG.client_controller.ResetIdleTimer()
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
pass
|
|
|
|
|
|
def closeEvent( self, event ):
|
|
|
|
self.CleanBeforeDestroy()
|
|
|
|
|
|
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 )
|
|
|
|
HG.client_controller.ResetIdleTimer()
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
pass
|
|
|
|
|
|
def closeEvent( self, event ):
|
|
|
|
self.CleanBeforeDestroy()
|
|
|
|
|
|
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_MAXIMIZE( self.EventSizeAndPositionChanged )
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
MainFrame.CleanBeforeDestroy( self )
|
|
|
|
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
|
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
|
|
|
|
|
|
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, 'save frame size and position: {}'.format( self._frame_key ), 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_MAXIMIZE( self.EventSizeAndPositionChanged )
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
MainFrame.CleanBeforeDestroy( self )
|
|
|
|
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
|
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
|
|
|
|
|
|
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, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
|
|
|
|
return True # was: event.ignore()
|
|
|
|
|