hydrus/hydrus/client/gui/QtPorting.py

2380 lines
64 KiB
Python

#This file is licensed under the Do What the Fuck You Want To Public License aka WTFPL
import os
import qtpy
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
import math
from collections import defaultdict
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import QtInit
isValid = QtInit.isValid
def registerEventType():
if QtInit.WE_ARE_PYSIDE:
return QC.QEvent.Type( QC.QEvent.registerEventType() )
else:
return QC.QEvent.registerEventType()
class HBoxLayout( QW.QHBoxLayout ):
def __init__( self, margin = 2, spacing = 2 ):
QW.QHBoxLayout.__init__( self )
self.setMargin( margin )
self.setSpacing( spacing )
def setMargin( self, val ):
self.setContentsMargins( val, val, val, val )
class VBoxLayout( QW.QVBoxLayout ):
def __init__( self, margin = 2, spacing = 2 ):
QW.QVBoxLayout.__init__( self )
self.setMargin( margin )
self.setSpacing( spacing )
def setMargin( self, val ):
self.setContentsMargins( val, val, val, val )
class LabelledSlider( QW.QWidget ):
def __init__( self, parent = None ):
QW.QWidget.__init__( self, parent )
self.setLayout( VBoxLayout( spacing = 2 ) )
top_layout = HBoxLayout( spacing = 2 )
self._min_label = QW.QLabel()
self._max_label = QW.QLabel()
self._value_label = QW.QLabel()
self._slider = QW.QSlider()
self._slider.setOrientation( QC.Qt.Horizontal )
self._slider.setTickInterval( 1 )
self._slider.setTickPosition( QW.QSlider.TicksBothSides )
top_layout.addWidget( self._min_label )
top_layout.addWidget( self._slider )
top_layout.addWidget( self._max_label )
self.layout().addLayout( top_layout )
self.layout().addWidget( self._value_label )
self._value_label.setAlignment( QC.Qt.AlignVCenter | QC.Qt.AlignHCenter )
self.layout().setAlignment( self._value_label, QC.Qt.AlignHCenter )
self._slider.valueChanged.connect( self._UpdateLabels )
self._UpdateLabels()
def _UpdateLabels( self ):
self._min_label.setText( str( self._slider.minimum() ) )
self._max_label.setText( str( self._slider.maximum() ) )
self._value_label.setText( str( self._slider.value() ) )
def GetValue( self ):
return self._slider.value()
def SetRange( self, min, max ):
self._slider.setRange( min, max )
self._UpdateLabels()
def SetValue( self, value ):
self._slider.setValue( value )
self._UpdateLabels()
def SplitterVisibleCount( splitter ):
count = 0
for i in range( splitter.count() ):
if splitter.widget( i ).isVisibleTo( splitter ): count += 1
return count
class DirPickerCtrl( QW.QWidget ):
dirPickerChanged = QC.Signal()
def __init__( self, parent ):
QW.QWidget.__init__( self, parent )
layout = HBoxLayout( spacing = 2 )
self._path_edit = QW.QLineEdit( self )
self._button = QW.QPushButton( 'browse', self )
self._button.clicked.connect( self._Browse )
self._path_edit.textEdited.connect( self._TextEdited )
layout.addWidget( self._path_edit )
layout.addWidget( self._button )
self.setLayout( layout )
def SetPath( self, path ):
self._path_edit.setText( path )
def GetPath( self ):
return self._path_edit.text()
def _Browse( self ):
existing_path = self._path_edit.text()
if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
options = QW.QFileDialog.Options( QW.QFileDialog.DontUseNativeDialog )
else:
options = QW.QFileDialog.Options()
path = QW.QFileDialog.getExistingDirectory( self, '', existing_path, options = options )
if path == '':
return
path = os.path.normpath( path )
self._path_edit.setText( path )
if os.path.exists( path ):
self.dirPickerChanged.emit()
def _TextEdited( self, text ):
if os.path.exists( text ):
self.dirPickerChanged.emit()
class FilePickerCtrl( QW.QWidget ):
filePickerChanged = QC.Signal()
def __init__( self, parent = None, wildcard = None, starting_directory = None ):
QW.QWidget.__init__( self, parent )
layout = HBoxLayout( spacing = 2 )
self._path_edit = QW.QLineEdit( self )
self._button = QW.QPushButton( 'browse', self )
self._button.clicked.connect( self._Browse )
self._path_edit.textEdited.connect( self._TextEdited )
layout.addWidget( self._path_edit )
layout.addWidget( self._button )
self.setLayout( layout )
self._save_mode = False
self._wildcard = wildcard
self._starting_directory = starting_directory
def SetPath( self, path ):
self._path_edit.setText( path )
def GetPath( self ):
return self._path_edit.text()
def SetSaveMode( self, save_mode ):
self._save_mode = save_mode
def _Browse( self ):
existing_path = self._path_edit.text()
if existing_path == '' and self._starting_directory is not None:
existing_path = self._starting_directory
if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
options = QW.QFileDialog.Options( QW.QFileDialog.DontUseNativeDialog )
else:
options = QW.QFileDialog.Options()
if self._save_mode:
if self._wildcard:
path = QW.QFileDialog.getSaveFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard, options = options )[0]
else:
path = QW.QFileDialog.getSaveFileName( self, '', existing_path, options = options )[0]
else:
if self._wildcard:
path = QW.QFileDialog.getOpenFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard, options = options )[0]
else:
path = QW.QFileDialog.getOpenFileName( self, '', existing_path, options = options )[0]
if path == '':
return
path = os.path.normpath( path )
self._path_edit.setText( path )
if self._save_mode or os.path.exists( path ):
self.filePickerChanged.emit()
def _TextEdited( self, text ):
if self._save_mode or os.path.exists( text ):
self.filePickerChanged.emit()
class TabBar( QW.QTabBar ):
tabDoubleLeftClicked = QC.Signal( int )
tabMiddleClicked = QC.Signal( int )
tabSpaceDoubleLeftClicked = QC.Signal()
tabSpaceDoubleMiddleClicked = QC.Signal()
def __init__( self, parent = None ):
QW.QTabBar.__init__( self, parent )
self.setMouseTracking( True )
self.setAcceptDrops( True )
self._supplementary_drop_target = None
self._last_clicked_tab_index = -1
self._last_clicked_global_pos = None
def AddSupplementaryTabBarDropTarget( self, drop_target ):
self._supplementary_drop_target = drop_target
def clearLastClickedTabInfo( self ):
self._last_clicked_tab_index = -1
self._last_clicked_global_pos = None
def event( self, event ):
return QW.QTabBar.event( self, event )
def mouseMoveEvent( self, e ):
e.ignore()
def mousePressEvent( self, event ):
index = self.tabAt( event.position().toPoint() )
if event.button() == QC.Qt.LeftButton:
self._last_clicked_tab_index = index
self._last_clicked_global_pos = event.globalPosition().toPoint()
QW.QTabBar.mousePressEvent( self, event )
def mouseReleaseEvent( self, event ):
index = self.tabAt( event.position().toPoint() )
if event.button() == QC.Qt.MiddleButton:
if index != -1:
self.tabMiddleClicked.emit( index )
return
QW.QTabBar.mouseReleaseEvent( self, event )
def mouseDoubleClickEvent( self, event ):
index = self.tabAt( event.position().toPoint() )
if event.button() == QC.Qt.LeftButton:
if index == -1:
self.tabSpaceDoubleLeftClicked.emit()
else:
self.tabDoubleLeftClicked.emit( index )
return
elif event.button() == QC.Qt.MiddleButton:
if index == -1:
self.tabSpaceDoubleMiddleClicked.emit()
else:
self.tabMiddleClicked.emit( index )
return
QW.QTabBar.mouseDoubleClickEvent( self, event )
def dragEnterEvent(self, event):
if 'application/hydrus-tab' in event.mimeData().formats():
event.ignore()
else:
event.accept()
def dragMoveEvent( self, event ):
if 'application/hydrus-tab' not in event.mimeData().formats():
tab_index = self.tabAt( event.position().toPoint() )
if tab_index != -1:
self.parentWidget().setCurrentIndex( tab_index )
else:
event.ignore()
def lastClickedTabInfo( self ):
return ( self._last_clicked_tab_index, self._last_clicked_global_pos )
def dropEvent( self, event ):
if self._supplementary_drop_target:
self._supplementary_drop_target.eventFilter( self, event )
else:
event.ignore()
# A heavily extended/tweaked version of https://forum.qt.io/topic/67542/drag-tabs-between-qtabwidgets/
class TabWidgetWithDnD( QW.QTabWidget ):
pageDragAndDropped = QC.Signal( QW.QWidget, QW.QWidget )
def __init__( self, parent = None ):
QW.QTabWidget.__init__( self, parent )
self.setTabBar( TabBar( self ) )
self.setAcceptDrops( True )
self._tab_bar = self.tabBar()
self._supplementary_drop_target = None
def _LayoutPagesHelper( self ):
current_index = self.currentIndex()
for i in range( self.count() ):
self.setCurrentIndex( i )
if isinstance( self.widget( i ), TabWidgetWithDnD ):
self.widget( i )._LayoutPagesHelper()
self.setCurrentIndex( current_index )
def LayoutPages( self ):
# hydev adds: I no longer call this, as I moved splitter setting to a thing called per page when page is first visibly shown
# leaving it here for now in case I need it again
# Momentarily switch to each page, then back, forcing a layout update.
# If this is not done, the splitters on the hidden pages won't resize their widgets properly when we restore
# splitter sizes after this, since they would never became visible.
# We first have to climb up the widget hierarchy and go down recursively from the root tab widget,
# since it's not enough to make a page visible if its a nested page: all of its ancestor pages have to be visible too.
# This shouldn't be visible to users since we switch back immediately.
# There is probably a proper way to do this...
highest_ancestor_of_same_type = self
parent = self.parentWidget()
while parent is not None:
if isinstance( parent, TabWidgetWithDnD ):
highest_ancestor_of_same_type = parent
parent = parent.parentWidget()
highest_ancestor_of_same_type._LayoutPagesHelper() # This does the actual recursive descent and making pages visible
# This is a hack that adds an additional drop target to the tab bar. The added drop target will get drop events from the tab bar.
# Used to make the case of files/media droppend onto tabs work.
def AddSupplementaryTabBarDropTarget( self, drop_target ):
self._supplementary_drop_target = drop_target
self.tabBar().AddSupplementaryTabBarDropTarget( drop_target )
def mouseMoveEvent( self, e ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ):
QW.QTabWidget.mouseMoveEvent( self, e )
if e.buttons() != QC.Qt.LeftButton:
return
my_mouse_pos = e.position().toPoint()
global_mouse_pos = self.mapToGlobal( my_mouse_pos )
tab_bar_mouse_pos = self._tab_bar.mapFromGlobal( global_mouse_pos )
if not self._tab_bar.rect().contains( tab_bar_mouse_pos ):
return
if not isinstance( self._tab_bar, TabBar ):
return
( clicked_tab_index, clicked_global_pos ) = self._tab_bar.lastClickedTabInfo()
if clicked_tab_index == -1:
return
if e.globalPosition().toPoint() == clicked_global_pos:
# don't start a drag until movement
return
tab_rect = self._tab_bar.tabRect( clicked_tab_index )
pixmap = QG.QPixmap( tab_rect.size() )
self._tab_bar.render( pixmap, QC.QPoint(), QG.QRegion( tab_rect ) )
mimeData = QC.QMimeData()
mimeData.setData( 'application/hydrus-tab', b'' )
drag = QG.QDrag( self._tab_bar )
drag.setMimeData( mimeData )
drag.setPixmap( pixmap )
cursor = QG.QCursor( QC.Qt.OpenHandCursor )
drag.setHotSpot( QC.QPoint( 0, 0 ) )
# this puts the tab pixmap exactly where we picked it up, but it looks bad
# drag.setHotSpot( tab_bar_mouse_pos - tab_rect.topLeft() )
drag.setDragCursor( cursor.pixmap(), QC.Qt.MoveAction )
drag.exec_( QC.Qt.MoveAction )
def dragEnterEvent( self, e: QG.QDragEnterEvent ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ):
return QW.QTabWidget.dragEnterEvent( self, e )
if 'application/hydrus-tab' in e.mimeData().formats():
e.accept()
else:
e.ignore()
def dragMoveEvent( self, event: QG.QDragMoveEvent ):
#if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.position().toPoint() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event )
screen_pos = self.mapToGlobal( event.position().toPoint() )
tab_pos = self._tab_bar.mapFromGlobal( screen_pos )
tab_index = self._tab_bar.tabAt( tab_pos )
if tab_index != -1:
shift_down = event.modifiers() & QC.Qt.ShiftModifier
self.setCurrentIndex( tab_index )
if 'application/hydrus-tab' not in event.mimeData().formats():
event.reject()
#return QW.QTabWidget.dragMoveEvent( self, event )
def dragLeaveEvent( self, e: QG.QDragLeaveEvent ):
#if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ): return QW.QTabWidget.dragLeaveEvent( self, e )
e.accept()
def addTab(self, widget, *args, **kwargs ):
if isinstance( widget, TabWidgetWithDnD ):
widget.AddSupplementaryTabBarDropTarget( self._supplementary_drop_target )
QW.QTabWidget.addTab( self, widget, *args, **kwargs )
def insertTab(self, index, widget, *args, **kwargs):
if isinstance( widget, TabWidgetWithDnD ):
widget.AddSupplementaryTabBarDropTarget( self._supplementary_drop_target )
QW.QTabWidget.insertTab( self, index, widget, *args, **kwargs )
def dropEvent( self, e: QG.QDropEvent ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.position().toPoint() ) ) ):
return QW.QTabWidget.dropEvent( self, e )
if 'application/hydrus-tab' not in e.mimeData().formats(): #Page dnd has no associated mime data
e.ignore()
return
w = self
source_tab_bar = e.source()
if not isinstance( source_tab_bar, TabBar ):
return
( source_page_index, source_page_click_global_pos ) = source_tab_bar.lastClickedTabInfo()
source_tab_bar.clearLastClickedTabInfo()
source_notebook = source_tab_bar.parentWidget()
source_page = source_notebook.widget( source_page_index )
source_name = source_tab_bar.tabText( source_page_index )
while w is not None:
if source_page == w:
# you cannot drop a page of pages inside itself
return
w = w.parentWidget()
e.setDropAction( QC.Qt.MoveAction )
e.accept()
counter = self.count()
screen_pos = self.mapToGlobal( e.position().toPoint() )
tab_pos = self.tabBar().mapFromGlobal( screen_pos )
dropped_on_tab_index = self.tabBar().tabAt( tab_pos )
if source_notebook == self and dropped_on_tab_index == source_page_index:
return # if we drop on ourself, make no action, even on the right edge
dropped_on_left_edge = False
dropped_on_right_edge = False
if dropped_on_tab_index != -1:
EDGE_PADDING = 15
tab_rect = self.tabBar().tabRect( dropped_on_tab_index )
edge_size = QC.QSize( EDGE_PADDING, tab_rect.height() )
left_edge_rect = QC.QRect( tab_rect.topLeft(), edge_size )
right_edge_rect = QC.QRect( tab_rect.topRight() - QC.QPoint( EDGE_PADDING, 0 ), edge_size )
drop_pos = e.position().toPoint()
dropped_on_left_edge = left_edge_rect.contains( drop_pos )
dropped_on_right_edge = right_edge_rect.contains( drop_pos )
if counter == 0:
self.addTab( source_page, source_name )
else:
if dropped_on_tab_index == -1:
insert_index = counter
else:
insert_index = dropped_on_tab_index
if dropped_on_right_edge:
insert_index += 1
if self == source_notebook:
if insert_index == source_page_index + 1 and not dropped_on_left_edge:
pass # in this special case, moving it confidently one to the right, we will disobey the normal rules and indeed move one to the right, rather than no-op
elif insert_index > source_page_index:
# we are inserting to our right, which needs a shift since we will be removing ourselves from the list
insert_index -= 1
if source_notebook == self and insert_index == source_page_index:
return # if we mean to insert on ourself, make no action
self.insertTab( insert_index, source_page, source_name )
shift_down = e.modifiers() & 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( 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()
CallAfter( HG.client_controller.gui.ShowPage, page_key )
self.pageDragAndDropped.emit( source_page, source_tab_bar )
def DeleteAllNotebookPages( notebook ):
while notebook.count() > 0:
tab = notebook.widget( 0 )
notebook.removeTab( 0 )
tab.deleteLater()
def SplitVertically( splitter: QW.QSplitter, w1, w2, hpos ):
splitter.setOrientation( QC.Qt.Horizontal )
if w1.parentWidget() != splitter:
splitter.addWidget( w1 )
w1.setVisible( True )
if w2.parentWidget() != splitter:
splitter.addWidget( w2 )
w2.setVisible( True )
total_sum = sum( splitter.sizes() )
if hpos < 0:
splitter.setSizes( [ total_sum + hpos, -hpos ] )
elif hpos > 0:
splitter.setSizes( [ hpos, total_sum - hpos ] )
def SplitHorizontally( splitter: QW.QSplitter, w1, w2, vpos ):
splitter.setOrientation( QC.Qt.Vertical )
if w1.parentWidget() != splitter:
splitter.addWidget( w1 )
w1.setVisible( True )
if w2.parentWidget() != splitter:
splitter.addWidget( w2 )
w2.setVisible( True )
total_sum = sum( splitter.sizes() )
if vpos < 0:
splitter.setSizes( [ total_sum + vpos, -vpos ] )
elif vpos > 0:
splitter.setSizes( [ vpos, total_sum - vpos ] )
class GridLayout( QW.QGridLayout ):
def __init__( self, cols = 1, spacing = 2 ):
QW.QGridLayout.__init__( self )
self._col_count = cols
self.setMargin( 2 )
self.setSpacing( spacing )
def GetFixedColumnCount( self ):
return self._col_count
def setMargin( self, val ):
self.setContentsMargins( val, val, val, val )
def AddToLayout( layout, item, flag = None, alignment = None ):
if isinstance( layout, GridLayout ):
cols = layout.GetFixedColumnCount()
count = layout.count()
row = math.floor( count / cols )
col = count % cols
if isinstance( item, QW.QLayout ):
layout.addLayout( item, row, col )
elif isinstance( item, QW.QWidget ):
layout.addWidget( item, row, col )
elif isinstance( item, tuple ):
spacer = QW.QPushButton()#QW.QSpacerItem( 0, 0, QW.QSizePolicy.Expanding, QW.QSizePolicy.Fixed )
layout.addWidget( spacer, row, col )
spacer.setVisible(False)
return
else:
if isinstance( item, QW.QLayout ):
layout.addLayout( item )
if alignment is not None:
layout.setAlignment( item, alignment )
elif isinstance( item, QW.QWidget ):
layout.addWidget( item )
if alignment is not None:
layout.setAlignment( item, alignment )
elif isinstance( item, tuple ):
layout.addStretch( 1 )
return
zero_border = False
if flag is None or flag == CC.FLAGS_NONE:
pass
elif flag in ( CC.FLAGS_CENTER, CC.FLAGS_ON_LEFT, CC.FLAGS_ON_RIGHT, CC.FLAGS_CENTER_PERPENDICULAR, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH ):
if flag == CC.FLAGS_CENTER:
alignment = QC.Qt.AlignVCenter | QC.Qt.AlignHCenter
if flag == CC.FLAGS_ON_LEFT:
alignment = QC.Qt.AlignLeft | QC.Qt.AlignVCenter
elif flag == CC.FLAGS_ON_RIGHT:
alignment = QC.Qt.AlignRight | QC.Qt.AlignVCenter
elif flag in ( CC.FLAGS_CENTER_PERPENDICULAR, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH ):
if isinstance( layout, QW.QHBoxLayout ):
alignment = QC.Qt.AlignVCenter
else:
alignment = QC.Qt.AlignHCenter
layout.setAlignment( item, alignment )
if flag == CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH:
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ):
layout.setStretchFactor( item, 5 )
if isinstance( item, QW.QLayout ):
zero_border = True
elif flag in ( CC.FLAGS_EXPAND_PERPENDICULAR, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ):
if flag == CC.FLAGS_EXPAND_SIZER_PERPENDICULAR:
zero_border = True
if isinstance( item, QW.QWidget ):
if isinstance( layout, QW.QHBoxLayout ):
h_policy = QW.QSizePolicy.Fixed
v_policy = QW.QSizePolicy.Expanding
else:
h_policy = QW.QSizePolicy.Expanding
v_policy = QW.QSizePolicy.Fixed
item.setSizePolicy( h_policy, v_policy )
elif flag in ( CC.FLAGS_EXPAND_BOTH_WAYS, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS, CC.FLAGS_EXPAND_BOTH_WAYS_POLITE, CC.FLAGS_EXPAND_BOTH_WAYS_SHY ):
if flag == CC.FLAGS_EXPAND_SIZER_BOTH_WAYS:
zero_border = True
if isinstance( item, QW.QWidget ):
item.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding )
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ):
if flag in ( CC.FLAGS_EXPAND_BOTH_WAYS, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ):
stretch_factor = 5
elif flag == CC.FLAGS_EXPAND_BOTH_WAYS_POLITE:
stretch_factor = 3
elif flag == CC.FLAGS_EXPAND_BOTH_WAYS_SHY:
stretch_factor = 1
layout.setStretchFactor( item, stretch_factor )
if zero_border:
margin = 0
if isinstance( item, QW.QFrame ):
margin = item.frameWidth()
item.setContentsMargins( margin, margin, margin, margin )
def ScrollAreaVisibleRect( scroll_area ):
if not scroll_area.widget(): return QC.QRect( 0, 0, 0, 0 )
rect = scroll_area.widget().visibleRegion().boundingRect()
# Do not allow it to be smaller than the scroll area's viewport size:
if rect.width() < scroll_area.viewport().width():
rect.setWidth( scroll_area.viewport().width() )
if rect.height() < scroll_area.viewport().height():
rect.setHeight( scroll_area.viewport().height() )
return rect
def AdjustOpacity( image: QG.QImage, opacity_factor ):
new_image = QG.QImage( image.width(), image.height(), QG.QImage.Format_RGBA8888 )
new_image.setDevicePixelRatio( image.devicePixelRatio() )
new_image.fill( QC.Qt.transparent )
painter = QG.QPainter( new_image )
painter.setOpacity( opacity_factor )
painter.drawImage( 0, 0, image )
return new_image
def ToKeySequence( modifiers, key ):
if QtInit.WE_ARE_QT5:
if isinstance( modifiers, QC.Qt.KeyboardModifiers ):
seq_str = ''
for modifier in [ QC.Qt.ShiftModifier, QC.Qt.ControlModifier, QC.Qt.AltModifier, QC.Qt.MetaModifier, QC.Qt.KeypadModifier, QC.Qt.GroupSwitchModifier ]:
if modifiers & modifier: seq_str += QG.QKeySequence( modifier ).toString()
seq_str += QG.QKeySequence( key ).toString()
return QG.QKeySequence( seq_str )
else:
return QG.QKeySequence( key + modifiers )
else:
return QG.QKeySequence( QC.QKeyCombination( modifiers, key ) ) # pylint: disable=E1101
def AddShortcut( widget, modifier, key, callable, *args ):
shortcut = QW.QShortcut( widget )
shortcut.setKey( ToKeySequence( modifier, key ) )
shortcut.setContext( QC.Qt.WidgetWithChildrenShortcut )
shortcut.activated.connect( lambda: callable( *args ) )
def GetBackgroundColour( widget ):
return widget.palette().color( QG.QPalette.Window )
CallAfterEventType = registerEventType()
class CallAfterEvent( QC.QEvent ):
def __init__( self, fn, *args, **kwargs ):
QC.QEvent.__init__( self, CallAfterEventType )
self._fn = fn
self._args = args
self._kwargs = kwargs
def Execute( self ):
if self._fn is not None:
self._fn( *self._args, **self._kwargs )
class CallAfterEventCatcher( QC.QObject ):
def __init__( self, parent ):
QC.QObject.__init__( self, parent )
self.installEventFilter( self )
def eventFilter( self, watched, event ):
if event.type() == CallAfterEventType and isinstance( event, CallAfterEvent ):
if HG.profile_mode:
summary = 'Profiling CallAfter Event: {}'.format( event._fn )
HydrusData.Profile( summary, 'event.Execute()', globals(), locals(), min_duration_ms = HG.callto_profile_min_job_time_ms )
else:
event.Execute()
event.accept()
return True
return False
def CallAfter( fn, *args, **kwargs ):
QW.QApplication.instance().postEvent( QW.QApplication.instance().call_after_catcher, CallAfterEvent( fn, *args, **kwargs ) )
QW.QApplication.instance().eventDispatcher().wakeUp()
def ClearLayout( layout, delete_widgets = False ):
while layout.count() > 0:
item = layout.itemAt( 0 )
if delete_widgets:
if item.widget():
item.widget().deleteLater()
elif item.layout():
ClearLayout( item.layout(), delete_widgets = True )
item.layout().deleteLater()
else:
spacer = item.layout().spacerItem()
del spacer
layout.removeItem( item )
def GetClientData( widget, idx ):
if isinstance( widget, QW.QComboBox ):
return widget.itemData( idx, QC.Qt.UserRole )
elif isinstance( widget, QW.QTreeWidget ):
return widget.topLevelItem( idx ).data( 0, QC.Qt.UserRole )
elif isinstance( widget, QW.QListWidget ):
return widget.item( idx ).data( QC.Qt.UserRole )
else:
raise ValueError( 'Unknown widget class in GetClientData' )
def Unsplit( splitter, widget ):
if widget.parentWidget() == splitter:
widget.setVisible( False )
def CenterOnWindow( parent, window ):
parent_window = parent.window()
window.move( parent_window.frameGeometry().center() - window.rect().center() )
def ListWidgetDelete( widget, idx ):
if isinstance( idx, QC.QModelIndex ):
idx = idx.row()
if idx != -1:
item = widget.takeItem( idx )
del item
def ListWidgetGetSelection( widget ):
for i in range( widget.count() ):
if widget.item( i ).isSelected(): return i
return -1
def ListWidgetGetStrings( widget ):
strings = []
for i in range( widget.count() ):
strings.append( widget.item( i ).text() )
return strings
def ListWidgetIsSelected( widget, idx ):
if idx == -1: return False
return widget.item( idx ).isSelected()
def ListWidgetSetSelection( widget, idxs ):
widget.clearSelection()
if not isinstance( idxs, list ):
idxs = [ idxs ]
count = widget.count()
for idx in idxs:
if 0 <= idx <= count -1:
widget.item( idx ).setSelected( True )
def SetInitialSize( widget, size ):
if hasattr( widget, 'SetInitialSize' ):
widget.SetInitialSize( size )
return
if isinstance( size, tuple ):
size = QC.QSize( size[0], size[1] )
if size.width() >= 0: widget.setMinimumWidth( size.width() )
if size.height() >= 0: widget.setMinimumHeight( size.height() )
def SetBackgroundColour( widget, colour ):
widget.setAutoFillBackground( True )
object_name = widget.objectName()
if not object_name:
object_name = str( id( widget ) )
widget.setObjectName( object_name )
if isinstance( colour, QG.QColor ):
widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, colour.name()) )
elif isinstance( colour, tuple ):
colour = QG.QColor( *colour )
widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, colour.name() ) )
else:
widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, QG.QColor( colour ).name() ) )
def SetStringSelection( combobox, string ):
index = combobox.findText( string )
if index != -1:
combobox.setCurrentIndex( index )
def SetClientSize( widget, size ):
if isinstance( size, tuple ):
size = QC.QSize( size[ 0 ], size[ 1 ] )
if size.width() < 0: size.setWidth( widget.width() )
if size.height() < 0: size.setHeight( widget.height() )
widget.resize( size )
def SetMinClientSize( widget, size ):
if isinstance( size, tuple ):
size = QC.QSize( size[0], size[1] )
if size.width() >= 0: widget.setMinimumWidth( size.width() )
if size.height() >= 0: widget.setMinimumHeight( size.height() )
def WheelEventIsSynthesised( event: QG.QWheelEvent ):
if QtInit.WE_ARE_QT5:
return event.source() == QC.Qt.MouseEventSynthesizedBySystem
elif QtInit.WE_ARE_QT6:
return event.pointerType() != QG.QPointingDevice.PointerType.Generic
else:
return False
class StatusBar( QW.QStatusBar ):
def __init__( self, status_widths ):
QW.QStatusBar.__init__( self )
self._labels = []
for w in status_widths:
label = QW.QLabel()
self._labels.append( label )
if w < 0:
self.addWidget( label, -1 * w )
else:
label.setFixedWidth( w )
self.addWidget( label )
def SetStatusText( self, text, index, tooltip = None ):
if tooltip is None:
tooltip = text
cell = self._labels[ index ]
if cell.text() != text:
cell.setText( text )
if cell.toolTip() != tooltip:
cell.setToolTip( tooltip )
class AboutDialogInfo:
def __init__( self ):
self.name = ''
self.version = ''
self.description = ''
self.license = ''
self.developers = []
self.website = ''
def SetName( self, name ):
self.name = name
def SetVersion( self, version ):
self.version = version
def SetDescription( self, description ):
self.description = description
def SetLicense( self, license ):
self.license = license
def SetDevelopers( self, developers_list ):
self.developers = developers_list
def SetWebSite( self, url ):
self.website = url
class UIActionSimulator:
def __init__( self ):
pass
def Char( self, widget, key, text = None ):
if widget is None:
widget = QW.QApplication.focusWidget()
ev1 = QG.QKeyEvent( QC.QEvent.KeyPress, key, QC.Qt.NoModifier, text = text )
ev2 = QG.QKeyEvent( QC.QEvent.KeyRelease, key, QC.Qt.NoModifier, text = text )
QW.QApplication.instance().postEvent( widget, ev1 )
QW.QApplication.instance().postEvent( widget, ev2 )
# TODO: rewrite this to be on my newer panel system so this can resize for lads on small screens etc..
class AboutBox( QW.QDialog ):
def __init__( self, parent, about_info ):
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( self )
self.setWindowTitle( 'About ' + about_info.name )
icon_label = QW.QLabel( self )
name_label = QW.QLabel( about_info.name, self )
version_label = QW.QLabel( about_info.version, self )
tabwidget = QW.QTabWidget( self )
desc_panel = QW.QWidget( self )
desc_label = QW.QLabel( about_info.description, self )
url_label = QW.QLabel( '<a href="{0}">{0}</a>'.format( about_info.website ), self )
credits = QW.QTextEdit( self )
license = QW.QTextEdit( self )
close_button = QW.QPushButton( 'close', self )
icon_label.setPixmap( HG.client_controller.frame_icon_pixmap )
layout.addWidget( icon_label, alignment = QC.Qt.AlignHCenter )
name_label_font = name_label.font()
name_label_font.setBold( True )
name_label.setFont( name_label_font )
layout.addWidget( name_label, alignment = QC.Qt.AlignHCenter )
layout.addWidget( version_label, alignment = QC.Qt.AlignHCenter )
layout.addWidget( tabwidget, alignment = QC.Qt.AlignHCenter )
tabwidget.addTab( desc_panel, 'Description' )
tabwidget.addTab( credits, 'Credits' )
tabwidget.addTab( license, 'License' )
tabwidget.setCurrentIndex( 0 )
credits.setPlainText( 'Created by ' + ', '.join(about_info.developers) )
credits.setReadOnly( True )
credits.setAlignment( QC.Qt.AlignHCenter )
license.setPlainText( about_info.license )
license.setReadOnly( True )
desc_layout = QW.QVBoxLayout()
desc_layout.addWidget( desc_label, alignment = QC.Qt.AlignHCenter )
desc_label.setWordWrap( True )
desc_label.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
desc_layout.addWidget( url_label, alignment = QC.Qt.AlignHCenter )
url_label.setTextFormat( QC.Qt.RichText )
url_label.setTextInteractionFlags( QC.Qt.TextBrowserInteraction )
url_label.setOpenExternalLinks( True )
desc_panel.setLayout( desc_layout )
layout.addWidget( close_button, alignment = QC.Qt.AlignRight )
close_button.clicked.connect( self.accept )
self.setLayout( layout )
self.exec_()
class RadioBox( QW.QFrame ):
radioBoxChanged = QC.Signal()
def __init__( self, parent = None, choices = [], vertical = False ):
QW.QFrame.__init__( self, parent )
self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised )
if vertical:
self.setLayout( VBoxLayout() )
else:
self.setLayout( HBoxLayout() )
self._choices = []
for choice in choices:
radiobutton = QW.QRadioButton( choice, self )
self._choices.append( radiobutton )
radiobutton.clicked.connect( self.radioBoxChanged )
self.layout().addWidget( radiobutton )
if vertical and len( self._choices ):
self._choices[0].setChecked( True )
elif len( self._choices ):
self._choices[-1].setChecked( True )
def _GetCurrentChoiceWidget( self ):
for choice in self._choices:
if choice.isChecked():
return choice
return None
def GetCurrentIndex( self ):
for i in range( len( self._choices ) ):
if self._choices[ i ].isChecked(): return i
return -1
def SetStringSelection( self, str ):
for i in range( len( self._choices ) ):
if self._choices[ i ].text() == str:
self._choices[ i ].setChecked( True )
return
def GetStringSelection( self ):
for i in range( len( self._choices ) ):
if self._choices[ i ].isChecked(): return self._choices[ i ].text()
return None
def setFocus( self, reason ):
item = self._GetCurrentChoiceWidget()
if item is not None:
item.setFocus( reason )
else:
QW.QFrame.setFocus( self, reason )
def SetValue( self, data ):
pass
def Select( self, idx ):
self._choices[ idx ].setChecked( True )
# Adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-elidedlabel-example.html
class EllipsizedLabel( QW.QLabel ):
def __init__( self, parent = None, ellipsize_end = False ):
QW.QLabel.__init__( self, parent )
self._ellipsize_end = ellipsize_end
def minimumSizeHint( self ):
if self._ellipsize_end:
return self.sizeHint()
else:
return QW.QLabel.minimumSizeHint( self )
def setText( self, text ):
try:
QW.QLabel.setText( self, text )
except ValueError:
QW.QLabel.setText( self, repr( text ) )
self.update()
def sizeHint( self ):
if self._ellipsize_end:
num_lines = self.text().count( '\n' ) + 1
line_width = self.fontMetrics().lineWidth()
line_height = self.fontMetrics().lineSpacing()
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:
QW.QLabel.paintEvent( self, event )
return
painter = QG.QPainter( self )
fontMetrics = painter.fontMetrics()
text_lines = self.text().split( '\n' )
line_spacing = fontMetrics.lineSpacing()
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:
line.draw( painter, QC.QPoint( 0, y ) )
y = next_line_y
else:
last_line = text_line[ line.textStart(): ]
elided_last_line = fontMetrics.elidedText( last_line, QC.Qt.ElideRight, self.width() )
painter.drawText( QC.QPoint( 0, y + fontMetrics.ascent() ), elided_last_line )
done = True
break
text_layout.endLayout()
if done: break
'''
class Dialog( QW.QDialog ):
def __init__( self, parent = None, **kwargs ):
title = None
if 'title' in kwargs:
title = kwargs['title']
del kwargs['title']
QW.QDialog.__init__( self, parent, **kwargs )
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
if title is not None:
self.setWindowTitle( title )
self._closed_by_user = False
def closeEvent( self, event ):
if event.spontaneous():
self._closed_by_user = True
QW.QDialog.closeEvent( self, event )
# True if the dialog was closed by the user clicking on the X on the titlebar (so neither reject nor accept was chosen - the dialog result is still reject in this case though)
def WasCancelled( self ):
return self._closed_by_user
def SetCancelled( self, closed ):
self._closed_by_user = closed
def __enter__( self ):
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
if isValid( self ):
self.deleteLater()
class PasswordEntryDialog( Dialog ):
def __init__( self, parent, message, caption ):
Dialog.__init__( self, parent )
self.setWindowTitle( caption )
self._ok_button = QW.QPushButton( 'OK', self )
self._ok_button.clicked.connect( self.accept )
self._cancel_button = QW.QPushButton( 'Cancel', self )
self._cancel_button.clicked.connect( self.reject )
self._password = QW.QLineEdit( self )
self._password.setEchoMode( QW.QLineEdit.Password )
self.setLayout( QW.QVBoxLayout() )
entry_layout = QW.QHBoxLayout()
entry_layout.addWidget( QW.QLabel( message, self ) )
entry_layout.addWidget( self._password )
button_layout = QW.QHBoxLayout()
button_layout.addStretch( 1 )
button_layout.addWidget( self._cancel_button )
button_layout.addWidget( self._ok_button )
self.layout().addLayout( entry_layout )
self.layout().addLayout( button_layout )
def GetValue( self ):
return self._password.text()
class DirDialog( QW.QFileDialog ):
def __init__( self, parent = None, message = None ):
QW.QFileDialog.__init__( self, parent )
if message is not None: self.setWindowTitle( message )
self.setAcceptMode( QW.QFileDialog.AcceptOpen )
self.setFileMode( QW.QFileDialog.Directory )
self.setOption( QW.QFileDialog.ShowDirsOnly, True )
if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
self.setOption( QW.QFileDialog.DontUseNativeDialog, True )
def __enter__( self ):
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
self.deleteLater()
def _GetSelectedFiles( self ):
return [ os.path.normpath( path ) for path in self.selectedFiles() ]
def GetPath(self):
sel = self._GetSelectedFiles()
if len( sel ) > 0:
return sel[0]
return None
class FileDialog( QW.QFileDialog ):
def __init__( self, parent = None, message = None, acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFile, default_filename = None, default_directory = None, wildcard = None, defaultSuffix = None ):
QW.QFileDialog.__init__( self, parent )
if message is not None:
self.setWindowTitle( message )
self.setAcceptMode( acceptMode )
self.setFileMode( fileMode )
if default_directory is not None:
self.setDirectory( default_directory )
if defaultSuffix is not None:
self.setDefaultSuffix( defaultSuffix )
if default_filename is not None:
self.selectFile( default_filename )
if wildcard:
self.setNameFilter( wildcard )
if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
self.setOption( QW.QFileDialog.DontUseNativeDialog, True )
def __enter__( self ):
return self
def __exit__( self, exc_type, exc_val, exc_tb ):
self.deleteLater()
def _GetSelectedFiles( self ):
return [ os.path.normpath( path ) for path in self.selectedFiles() ]
def GetPath( self ):
sel = self._GetSelectedFiles()
if len( sel ) > 0:
return sel[ 0 ]
return None
def GetPaths( self ):
return self._GetSelectedFiles()
# A QTreeWidget where if an item is (un)checked, all its children are also (un)checked, recursively
class TreeWidgetWithInheritedCheckState( QW.QTreeWidget ):
def __init__( self, *args, **kwargs ):
QW.QTreeWidget.__init__( self, *args, **kwargs )
self.itemClicked.connect( self._HandleItemClickedForCheckStateUpdate )
def _HandleItemClickedForCheckStateUpdate( self, item, column ):
self._UpdateCheckState( item, item.checkState( 0 ) )
def _UpdateCheckState( self, item, check_state ):
# this is an int, should be a checkstate
item.setCheckState( 0, check_state )
for i in range( item.childCount() ):
self._UpdateCheckState( item.child( i ), check_state )
class ColourPickerCtrl( QW.QPushButton ):
def __init__( self, parent = None ):
QW.QPushButton.__init__( self, parent )
self._colour = QG.QColor( 0, 0, 0, 0 )
self.clicked.connect( self._ChooseColour )
self._highlighted = False
def SetColour( self, colour ):
self._colour = colour
self._UpdatePixmap()
def _UpdatePixmap( self ):
px = QG.QPixmap( self.contentsRect().height(), self.contentsRect().height() )
painter = QG.QPainter( px )
colour = self._colour
if self._highlighted:
colour = self._colour.lighter( 125 ) # 25% lighter
painter.fillRect( px.rect(), QG.QBrush( colour ) )
painter.end()
self.setIcon( QG.QIcon( px ) )
self.setIconSize( px.size() )
self.setFlat( True )
self.setFixedSize( px.size() )
def enterEvent( self, event ):
self._highlighted = True
self._UpdatePixmap()
def leaveEvent( self, event ):
self._highlighted = False
self._UpdatePixmap()
def GetColour( self ):
return self._colour
def _ChooseColour( self ):
new_colour = QW.QColorDialog.getColor( initial = self._colour )
if new_colour.isValid():
self.SetColour( new_colour )
def ListsToTuples( l ): # Since lists are not hashable, we need to (recursively) convert lists to tuples in data that is to be added to BetterListCtrl
if isinstance( l, list ) or isinstance( l, tuple ):
return tuple( map( ListsToTuples, l ) )
else:
return l
class WidgetEventFilter ( QC.QObject ):
_mouse_tracking_required = { 'EVT_MOUSE_EVENTS' }
_strong_focus_required = { 'EVT_KEY_DOWN' }
def __init__( self, parent_widget ):
self._parent_widget = parent_widget
QC.QObject.__init__( self, parent_widget )
parent_widget.installEventFilter( self )
self._callback_map = defaultdict( list )
self._user_moved_window = False # There is no EVT_MOVE_END in Qt so some trickery is required.
def _ExecuteCallbacks( self, event_name, event ):
if not event_name in self._callback_map: return
event_killed = False
for callback in self._callback_map[ event_name ]:
if not callback( event ): event_killed = True
return event_killed
def eventFilter( self, watched, event ):
# 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:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KEY_DOWN', event )
elif type == QC.QEvent.WindowStateChange:
if isValid( self._parent_widget ):
if self._parent_widget.isMinimized() or (event.oldState() & QC.Qt.WindowMinimized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_ICONIZE', event )
if self._parent_widget.isMaximized() or (event.oldState() & QC.Qt.WindowMaximized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MAXIMIZE', event )
elif type == QC.QEvent.MouseMove:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonDblClick:
if event.button() == QC.Qt.LeftButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DCLICK', event )
elif event.button() == QC.Qt.RightButton:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DCLICK', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonPress:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DOWN', event )
if event.buttons() & QC.Qt.MiddleButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MIDDLE_DOWN', event )
if event.buttons() & QC.Qt.RightButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DOWN', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.MouseButtonRelease:
if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_UP', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Wheel:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSEWHEEL', event )
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
elif type == QC.QEvent.Scroll:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SCROLLWIN', event )
elif type == QC.QEvent.Move:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE', event )
if isValid( self._parent_widget ) and self._parent_widget.isVisible():
self._user_moved_window = True
elif type == QC.QEvent.Resize:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SIZE', event )
elif type == QC.QEvent.NonClientAreaMouseButtonPress:
self._user_moved_window = False
elif type == QC.QEvent.NonClientAreaMouseButtonRelease:
if self._user_moved_window:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE_END', event )
self._user_moved_window = False
if event_killed:
event.accept()
return True
return False
def _AddCallback( self, evt_name, callback ):
if evt_name in self._mouse_tracking_required:
self._parent_widget.setMouseTracking( True )
if evt_name in self._strong_focus_required:
self._parent_widget.setFocusPolicy( QC.Qt.StrongFocus )
self._callback_map[ evt_name ].append( callback )
def EVT_ICONIZE( self, callback ):
self._AddCallback( 'EVT_ICONIZE', callback )
def EVT_KEY_DOWN( self, callback ):
self._AddCallback( 'EVT_KEY_DOWN', callback )
def EVT_LEFT_DCLICK( self, callback ):
self._AddCallback( 'EVT_LEFT_DCLICK', callback )
def EVT_RIGHT_DCLICK( self, callback ):
self._AddCallback( 'EVT_RIGHT_DCLICK', callback )
def EVT_LEFT_DOWN( self, callback ):
self._AddCallback( 'EVT_LEFT_DOWN', callback )
def EVT_LEFT_UP( self, callback ):
self._AddCallback( 'EVT_LEFT_UP', callback )
def EVT_MAXIMIZE( self, callback ):
self._AddCallback( 'EVT_MAXIMIZE', callback )
def EVT_MIDDLE_DOWN( self, callback ):
self._AddCallback( 'EVT_MIDDLE_DOWN', callback )
def EVT_MOUSE_EVENTS( self, callback ):
self._AddCallback( 'EVT_MOUSE_EVENTS', callback )
def EVT_MOUSEWHEEL( self, callback ):
self._AddCallback( 'EVT_MOUSEWHEEL', callback )
def EVT_MOVE( self, callback ):
self._AddCallback( 'EVT_MOVE', callback )
def EVT_MOVE_END( self, callback ):
self._AddCallback( 'EVT_MOVE_END', callback )
def EVT_RIGHT_DOWN( self, callback ):
self._AddCallback( 'EVT_RIGHT_DOWN', callback )
def EVT_SCROLLWIN( self, callback ):
self._AddCallback( 'EVT_SCROLLWIN', callback )
def EVT_SIZE( self, callback ):
self._AddCallback( 'EVT_SIZE', callback )