hydrus/hydrus/client/gui/QtPorting.py

2786 lines
74 KiB
Python

#This file is licensed under the Do What the Fuck You Want To Public License aka WTFPL
import os
# If not explicitely set, prefer PySide2 instead of the qtpy default which is PyQt5
# It is important that this runs on startup *before* anything is imported from qtpy.
# Since test.py, client.py and client.pyw all import this module first before any other Qt related ones, this requirement is satisfied.
if not 'QT_API' in os.environ:
try:
import PySide2
os.environ[ 'QT_API' ] = 'pyside2'
except ImportError as e:
pass
#
import qtpy
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
import math
import typing
from collections import defaultdict
if qtpy.PYQT5:
import sip # pylint: disable=E0401
def isValid( obj ):
if isinstance( obj, sip.simplewrapper ):
return not sip.isdeleted( obj )
return True
elif qtpy.PYSIDE2:
import shiboken2
isValid = shiboken2.isValid
else:
raise RuntimeError( 'You need either PySide2 or PyQt5' )
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
def MonkeyPatchMissingMethods():
if qtpy.PYQT5:
def MonkeyPatchGetSaveFileName( original_function ):
def new_function( *args, **kwargs ):
if 'selectedFilter' in kwargs:
kwargs[ 'initialFilter' ] = kwargs[ 'selectedFilter' ]
del kwargs[ 'selectedFilter' ]
return original_function( *args, **kwargs )
return new_function
QW.QFileDialog.getSaveFileName = MonkeyPatchGetSaveFileName( QW.QFileDialog.getSaveFileName )
class HBoxLayout( QW.QHBoxLayout ):
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.pos() )
if event.button() == QC.Qt.LeftButton:
self._last_clicked_tab_index = index
self._last_clicked_global_pos = event.globalPos()
QW.QTabBar.mousePressEvent( self, event )
def mouseReleaseEvent( self, event ):
index = self.tabAt( event.pos() )
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.pos() )
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.pos() )
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.pos() ) ) ):
QW.QTabWidget.mouseMoveEvent( self, e )
if e.buttons() != QC.Qt.LeftButton:
return
my_mouse_pos = e.pos()
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.globalPos() == 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 ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
return QW.QTabWidget.dragEnterEvent( self, e )
if 'application/hydrus-tab' in e.mimeData().formats():
e.accept()
else:
e.ignore()
def dragMoveEvent( self, event ):
#if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.pos() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event )
screen_pos = self.mapToGlobal( event.pos() )
tab_pos = self._tab_bar.mapFromGlobal( screen_pos )
tab_index = self._tab_bar.tabAt( tab_pos )
if tab_index != -1:
shift_down = event.keyboardModifiers() & 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 ):
#if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ): 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 ):
if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
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.pos() )
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 )
dropped_on_left_edge = left_edge_rect.contains( e.pos() )
dropped_on_right_edge = right_edge_rect.contains( e.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.keyboardModifiers() & QC.Qt.ShiftModifier
follow_dropped_page = not shift_down
new_options = HG.client_controller.new_options
if new_options.GetBoolean( 'reverse_page_shift_drag_behaviour' ):
follow_dropped_page = not follow_dropped_page
if follow_dropped_page:
self.setCurrentIndex( self.indexOf( source_page ) )
else:
if source_page_index > 1:
neighbour_page = source_notebook.widget( source_page_index - 1 )
page_key = neighbour_page.GetPageKey()
else:
page_key = source_notebook.GetPageKey()
HG.client_controller.gui.ShowPage( page_key )
self.pageDragAndDropped.emit( source_page, source_tab_bar )
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 ] )
def MakeQLabelWithAlignment( label, parent, align ):
res = QW.QLabel( label, parent )
res.setAlignment( align )
return res
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, opacity_factor ):
new_image = QG.QImage( image.width(), image.height(), QG.QImage.Format_RGBA8888 )
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 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 )
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 ) )
class BusyCursor:
def __enter__( self ):
QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
def __exit__( self, exc_type, exc_val, exc_tb ):
QW.QApplication.restoreOverrideCursor()
def GetBackgroundColour( widget ):
return widget.palette().color( QG.QPalette.Window )
CallAfterEventType = QC.QEvent.Type( QC.QEvent.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, CheckListBox ):
return widget.item( idx ).data( 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 GetSystemColour( colour ):
return QG.QPalette().color( colour )
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 MakeQSpinBox( parent = None, initial = None, min = None, max = None, width = None ):
spinbox = QW.QSpinBox( parent )
if min is not None: spinbox.setMinimum( min )
if max is not None: spinbox.setMaximum( max )
if initial is not None: spinbox.setValue( initial )
if width is not None: spinbox.setMinimumWidth( width )
return spinbox
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 SetForegroundColour( 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( '#{} {{ color: {} }}'.format( object_name, colour.name()) )
elif isinstance( colour, tuple ):
colour = QG.QColor( *colour )
widget.setStyleSheet( '#{} {{ color: {} }}'.format( object_name, colour.name() ) )
else:
widget.setStyleSheet( '#{} {{ 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() )
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 )
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 CheckListBox( QW.QListWidget ):
checkListBoxChanged = QC.Signal( int )
rightClicked = QC.Signal()
def __init__( self, parent = None ):
QW.QListWidget.__init__( self, parent )
self.itemClicked.connect( self._ItemCheckStateChanged )
self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
def Check(self, index, state = True):
item = self.item( index )
item.setFlags( item.flags() | QC.Qt.ItemIsUserCheckable )
if state:
item.setCheckState( QC.Qt.Checked )
else:
item.setCheckState( QC.Qt.Unchecked )
def IsChecked(self, index):
return self.item( index ).checkState() == QC.Qt.Checked
def GetCheckedItems(self):
indices = []
for i in range( self.count() ):
if self.item( i ).checkState() == QC.Qt.Checked: indices.append( i )
return indices
def GetSelections( self ):
indices = []
for i in range( self.count() ):
if self.item( i ).isSelected(): indices.append( i )
return indices
def SetCheckedItems( self, items ):
for i in range( self.count() ):
if i in items:
self.item( i ).setCheckState( QC.Qt.Checked )
else:
self.item( i ).setCheckState( QC.Qt.Unchecked )
def Append( self, str, client_data ):
item = QW.QListWidgetItem()
item.setFlags( item.flags() | QC.Qt.ItemIsUserCheckable )
item.setCheckState( QC.Qt.Unchecked )
item.setText( str )
item.setData( QC.Qt.UserRole, client_data )
self.addItem( item )
def _ItemCheckStateChanged( self, item ):
self.checkListBoxChanged.emit( self.row( item ) )
def GetChecked( self ):
result = [ self.item( index ).data( QC.Qt.UserRole ) for index in self.GetCheckedItems() ]
return result
def SetCheckedData( self, datas ):
for index in range( self.count() ):
data = self.item( index ).data( QC.Qt.UserRole )
check_it = data in datas
self.Check( index, check_it )
def mousePressEvent( self, event ):
if event.button() == QC.Qt.RightButton:
self.rightClicked.emit()
else:
QW.QListWidget.mousePressEvent( self, event )
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 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 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.Close:
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_CLOSE', 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_CLOSE( self, callback ):
self._AddCallback( 'EVT_CLOSE', 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 )
# wew lad
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
class CheckBoxDelegate(QW.QStyledItemDelegate):
def __init__(self, parent=None):
super( CheckBoxDelegate, self ).__init__(parent)
def createEditor( self, parent, op, idx ):
self.editor = QW.QCheckBox( parent )
class CollectComboCtrl( QW.QComboBox ):
itemChanged = QC.Signal()
def __init__( self, parent, media_collect ):
QW.QComboBox.__init__( self, parent )
self.view().pressed.connect( self._HandleItemPressed )
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
self.setItemDelegate( CheckBoxDelegate() )
self.setModel( QG.QStandardItemModel( self ) )
text_and_data_tuples = set()
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
namespaces = media_sort.GetNamespaces()
text_and_data_tuples.update( namespaces )
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
for ratings_service in ratings_services:
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
for (text, data) in text_and_data_tuples:
self.Append( text, data )
# Trick to display custom text
self._cached_text = ''
if media_collect.DoesACollect():
CallAfter( self.SetCollectByValue, media_collect )
def paintEvent( self, e ):
painter = QW.QStylePainter( self )
painter.setPen( self.palette().color( QG.QPalette.Text ) )
opt = QW.QStyleOptionComboBox()
self.initStyleOption( opt )
opt.currentText = self._cached_text
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
def GetValues( self ):
namespaces = [ ]
rating_service_keys = [ ]
for index in self.GetCheckedItems():
(collect_type, collect_data) = self.itemData( index, QC.Qt.UserRole )
if collect_type == 'namespace':
namespaces.append( collect_data )
elif collect_type == 'rating':
rating_service_keys.append( collect_data )
collect_strings = self.GetCheckedStrings()
if len( collect_strings ) > 0:
description = 'collect by ' + '-'.join( collect_strings )
else:
description = 'no collections'
return ( namespaces, rating_service_keys, description )
def hidePopup(self):
if not self.view().underMouse():
QW.QComboBox.hidePopup( self )
def SetValue( self, text ):
self._cached_text = text
self.setCurrentText( text )
def SetCollectByValue( self, media_collect ):
try:
indices_to_check = []
for index in range( self.count() ):
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
if p1 or p2:
indices_to_check.append( index )
self.SetCheckedItems( indices_to_check )
self.itemChanged.emit()
except Exception as e:
HydrusData.ShowText( 'Failed to set a collect-by value!' )
HydrusData.ShowException( e )
def SetCheckedItems( self, indices_to_check ):
for idx in range( self.count() ):
item = self.model().item( idx )
if idx in indices_to_check:
item.setCheckState( QC.Qt.Checked )
else:
item.setCheckState( QC.Qt.Unchecked )
def GetCheckedItems( self ):
indices = []
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked: indices.append( idx )
return indices
def GetCheckedStrings( self ):
strings = [ ]
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked: strings.append( item.text() )
return strings
def Append( self, str, data ):
self.addItem( str, userData = data )
item = self.model().item( self.count() - 1, 0 )
item.setCheckState( QC.Qt.Unchecked )
def _HandleItemPressed( self, index ):
item = self.model().itemFromIndex( index )
if item.checkState() == QC.Qt.Checked:
item.setCheckState( QC.Qt.Unchecked )
else:
item.setCheckState( QC.Qt.Checked )
self.SetValue( self._cached_text )
self.itemChanged.emit()