2724 lines
73 KiB
Python
2724 lines
73 KiB
Python
#This file is licensed under the Do What the Fuck You Want To Public License aka WTFPL
|
|
|
|
import os
|
|
|
|
from . import HydrusConstants as HC
|
|
|
|
# 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:
|
|
|
|
pass
|
|
|
|
|
|
from . import HydrusData
|
|
from . import HydrusGlobals as HG
|
|
from . import ClientConstants as CC
|
|
|
|
#
|
|
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
|
|
|
|
if qtpy.PYQT5:
|
|
|
|
import sip
|
|
|
|
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' )
|
|
|
|
def MonkeyPatchMissingMethods():
|
|
|
|
if qtpy.PYQT5:
|
|
|
|
def QPointToTuple( self ):
|
|
|
|
return ( self.x(), self.y() )
|
|
|
|
def QSizeToTuple( self ):
|
|
|
|
return ( self.width(), self.height() )
|
|
|
|
def QColorToTuple( self ):
|
|
|
|
return ( self.red(), self.green(), self.blue(), self.alpha() )
|
|
|
|
QC.QPoint.toTuple = QPointToTuple
|
|
QC.QPointF.toTuple = QPointToTuple
|
|
QC.QSize.toTuple = QSizeToTuple
|
|
QC.QSizeF.toTuple = QSizeToTuple
|
|
QG.QColor.toTuple = QColorToTuple
|
|
|
|
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 ).isVisible(): 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()
|
|
|
|
path = QW.QFileDialog.getExistingDirectory( self, '', existing_path )
|
|
|
|
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 ):
|
|
|
|
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
|
|
|
|
|
|
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 self._save_mode:
|
|
|
|
if self._wildcard:
|
|
|
|
path = QW.QFileDialog.getSaveFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard )[0]
|
|
|
|
else:
|
|
|
|
path = QW.QFileDialog.getSaveFileName( self, '', existing_path )[0]
|
|
|
|
|
|
else:
|
|
|
|
if self._wildcard:
|
|
|
|
path = QW.QFileDialog.getOpenFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard )[0]
|
|
|
|
else:
|
|
|
|
path = QW.QFileDialog.getOpenFileName( self, '', existing_path )[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 ):
|
|
|
|
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
|
|
|
|
|
|
def AddSupplementaryTabBarDropTarget( self, drop_target ):
|
|
|
|
self._supplementary_drop_target = drop_target
|
|
|
|
|
|
def clearLastClickedTabIndex( self ):
|
|
|
|
self._last_clicked_tab_index = -1
|
|
|
|
|
|
def mouseMoveEvent( self, e ):
|
|
|
|
e.ignore()
|
|
|
|
|
|
def mousePressEvent( self, event ):
|
|
|
|
if event.button() == QC.Qt.LeftButton:
|
|
|
|
self._last_clicked_tab_index = self.tabAt( event.pos() )
|
|
|
|
|
|
QW.QTabBar.mousePressEvent( 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 lastClickedTabIndex( self ):
|
|
|
|
return self._last_clicked_tab_index
|
|
|
|
|
|
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 ):
|
|
|
|
# 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 = self._tab_bar.lastClickedTabIndex()
|
|
|
|
if clicked_tab_index == -1:
|
|
|
|
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_tab_bar.lastClickedTabIndex()
|
|
|
|
source_tab_bar.clearLastClickedTabIndex()
|
|
|
|
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, w1, w2, hpos ):
|
|
|
|
splitter.setOrientation( QC.Qt.Horizontal )
|
|
|
|
if w1.parentWidget() != splitter:
|
|
|
|
splitter.addWiget( w1 )
|
|
|
|
w1.setVisible( True )
|
|
|
|
if w2.parentWidget() != splitter:
|
|
|
|
splitter.addWiget( 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, w1, w2, vpos ):
|
|
|
|
splitter.setOrientation( QC.Qt.Vertical )
|
|
|
|
if w1.parentWidget() != splitter:
|
|
|
|
splitter.addWiget( w1 )
|
|
|
|
w1.setVisible( True )
|
|
|
|
if w2.parentWidget() != splitter:
|
|
|
|
splitter.addWiget( 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, sizePolicy = 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
|
|
|
|
if isinstance( item, QW.QWidget ):
|
|
|
|
if sizePolicy is not None:
|
|
|
|
item.setSizePolicy( sizePolicy[0], sizePolicy[1] )
|
|
|
|
expand_both_ways = flag in ( CC.FLAGS_EXPAND_BOTH_WAYS, CC.FLAGS_EXPAND_BOTH_WAYS_POLITE, CC.FLAGS_EXPAND_BOTH_WAYS_SHY )
|
|
zero_border = False
|
|
|
|
# This is kind of a mess right now, adjustments might be needed
|
|
# Left all the original wx definitions of the flags as comments for future reference
|
|
if flag is None or flag == CC.FLAGS_NONE:
|
|
pass
|
|
elif flag == CC.FLAGS_SMALL_INDENT:
|
|
pass
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 2 )
|
|
elif flag == CC.FLAGS_BIG_INDENT:
|
|
pass
|
|
#item.setContentsMargins( 10, 10, 10, 10 )
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 10 )
|
|
elif flag == CC.FLAGS_CENTER:
|
|
layout.setAlignment( item, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter )
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 2 )
|
|
elif flag == CC.FLAGS_EXPAND_PERPENDICULAR:
|
|
|
|
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 )
|
|
|
|
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Expand()
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
elif flag == CC.FLAGS_EXPAND_DEPTH_ONLY:
|
|
#if isinstance( item, QW.QWidget ): item.setSizePolicy( item.sizePolicy().verticalPolicy(), QW.QSizePolicy.Expanding )
|
|
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ): layout.setStretchFactor( item, 5 )
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 5 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
elif flag == CC.FLAGS_SIZER_CENTER:
|
|
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ): layout.setStretchFactor( item, 5 )
|
|
layout.setAlignment( item, QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
|
|
zero_border = True
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
#wx.SizerFlags( 5 ).Center()
|
|
elif flag == CC.FLAGS_EXPAND_SIZER_PERPENDICULAR:
|
|
zero_border = True
|
|
#wx.SizerFlags( 0 ).Expand()
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
elif flag == CC.FLAGS_EXPAND_SIZER_BOTH_WAYS:
|
|
zero_border = True
|
|
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ):
|
|
|
|
layout.setStretchFactor( item, 5 )
|
|
|
|
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
|
|
#wx.SizerFlags( 5 ).Expand()
|
|
elif flag == CC.FLAGS_EXPAND_SIZER_DEPTH_ONLY:
|
|
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ): layout.setStretchFactor( item, 5 )
|
|
layout.setAlignment( item, QC.Qt.AlignVCenter )
|
|
zero_border = True
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
#wx.SizerFlags( 5 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
elif flag == CC.FLAGS_BUTTON_SIZER:
|
|
item.insertStretch( 0, 10 )
|
|
layout.setAlignment( item, QC.Qt.AlignRight )
|
|
zero_border = True
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
#wx.SizerFlags( 0 ).Align( wx.ALIGN_RIGHT )
|
|
elif flag == CC.FLAGS_LONE_BUTTON:
|
|
layout.setAlignment( item, QC.Qt.AlignRight )
|
|
zero_border = True
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_RIGHT )
|
|
elif flag == CC.FLAGS_VCENTER:
|
|
layout.setAlignment( item, QC.Qt.AlignVCenter )
|
|
zero_border = True
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
elif flag == CC.FLAGS_SIZER_VCENTER:
|
|
layout.setAlignment( item, QC.Qt.AlignVCenter )
|
|
zero_border = True
|
|
#item.setContentsMargins( 0, 0, 0, 0 )
|
|
#wx.SizerFlags( 0 ).Align( wx.ALIGN_CENTRE_VERTICAL )
|
|
elif flag == CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY:
|
|
if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ): layout.setStretchFactor( item, 5 )
|
|
layout.setAlignment( item, QC.Qt.AlignVCenter )
|
|
zero_border = True
|
|
#item.setContentsMargins( 2, 2, 2, 2 )
|
|
#wx.SizerFlags( 5 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
|
|
if expand_both_ways:
|
|
|
|
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 == CC.FLAGS_EXPAND_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 EscapeMnemonics( str ):
|
|
|
|
return str.replace( "&", "&&" )
|
|
|
|
|
|
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 DrawText( painter, x, y, text ):
|
|
|
|
boundingRect = painter.fontMetrics().size( QC.Qt.TextSingleLine, text )
|
|
|
|
painter.drawText( QC.QRectF( x, y, boundingRect.width(), boundingRect.height() ), text )
|
|
|
|
|
|
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 ) )
|
|
|
|
|
|
def AdjustColour( colour, percent ):
|
|
|
|
percent = percent / 100
|
|
|
|
return QG.QColor( colour.red() + colour.red() * percent, colour.green() + colour.green() * percent, colour.blue() + colour.blue() * percent, colour.alpha() )
|
|
|
|
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 )
|
|
|
|
|
|
class CallAfterEvent( QC.QEvent ):
|
|
|
|
def __init__( self, fn, *args, **kwargs ):
|
|
|
|
QC.QEvent.__init__( self, QC.QEvent.User )
|
|
|
|
self._fn = fn
|
|
self._args = args
|
|
self._kwargs = kwargs
|
|
|
|
def Execute( self ):
|
|
|
|
self._fn( *self._args, **self._kwargs )
|
|
|
|
|
|
class CallAfterEventFilter( QC.QObject ):
|
|
|
|
def __init__( self, parent = None ):
|
|
|
|
QC.QObject.__init__( self, parent )
|
|
|
|
|
|
def eventFilter( self, watched, event ):
|
|
|
|
if event.type() == QC.QEvent.User and isinstance( event, CallAfterEvent ):
|
|
|
|
event.Execute()
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def CallAfter( fn, *args, **kwargs ):
|
|
|
|
QW.QApplication.instance().postEvent( QW.QApplication.instance().call_after_catcher, CallAfterEvent( fn, *args, **kwargs ) )
|
|
|
|
|
|
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 ListWidgetGetStringSelection( widget ):
|
|
|
|
for i in range( widget.count() ):
|
|
|
|
if widget.item( i ).isSelected(): return widget.item( i ).text()
|
|
|
|
return None
|
|
|
|
|
|
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.mapToGlobal( parent_window.rect().center() ) - window.rect().center() )
|
|
|
|
def CenterOnScreen( window ):
|
|
|
|
window.move( QW.QApplication.desktop().availableGeometry().center() - window.rect().center() )
|
|
|
|
def WarningHandler( msg_type, context, str ):
|
|
|
|
if msg_type == QC.QtWarningMsg:
|
|
|
|
print( str )
|
|
|
|
|
|
def TupleToQColor( tup):
|
|
|
|
return QG.QColor( *tup )
|
|
|
|
|
|
def TupleToQPoint( tup ):
|
|
|
|
if isinstance( tup, QC.QPoint ):
|
|
|
|
raise ValueError( 'Unnecessary use of TupleToQPoint' )
|
|
|
|
else:
|
|
|
|
return QC.QPoint( tup[ 0 ], tup[ 1 ] )
|
|
|
|
|
|
def TupleToQSize( tup ):
|
|
|
|
if isinstance( tup, QC.QSize ):
|
|
|
|
raise ValueError( 'Unnecessary use of TupleToQSize' )
|
|
|
|
else:
|
|
|
|
return QC.QSize( tup[0], tup[1] )
|
|
|
|
|
|
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 ListWidgetIndexForString( widget, string ):
|
|
|
|
for i in range( widget.count() ):
|
|
|
|
if widget.item( i ).text() == string: return i
|
|
|
|
return -1
|
|
|
|
|
|
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 ]
|
|
|
|
for idx in idxs:
|
|
|
|
if idx != -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 ):
|
|
|
|
widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, TupleToQColor( 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 ):
|
|
|
|
widget.setStyleSheet( '#{} {{ color: {} }}'.format( object_name, TupleToQColor( 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 ):
|
|
|
|
self._labels[index].setText( text )
|
|
|
|
|
|
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, key, text = None ):
|
|
|
|
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.postEvent( QW.QApplication.focusWidget(), ev1 )
|
|
QW.QApplication.postEvent( QW.QApplication.focusWidget(), 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 = [ GetClientData( self, index ) for index in self.GetCheckedItems() ]
|
|
|
|
return result
|
|
|
|
|
|
def SetCheckedData( self, datas ):
|
|
|
|
for index in range( self.count() ):
|
|
|
|
data = GetClientData( self, index )
|
|
|
|
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 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 ):
|
|
|
|
QW.QLabel.setText( self, 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 )
|
|
|
|
|
|
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, defaultFile = None, wildcard = None ):
|
|
|
|
QW.QFileDialog.__init__( self, parent )
|
|
|
|
if message is not None: self.setWindowTitle( message )
|
|
|
|
self.setAcceptMode( acceptMode )
|
|
|
|
self.setFileMode( fileMode )
|
|
|
|
if defaultFile: self.setDirectory( defaultFile )
|
|
|
|
if wildcard: self.setNameFilter( wildcard )
|
|
|
|
|
|
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_MOTION', '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.FocusOut:
|
|
|
|
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KILL_FOCUS', event )
|
|
|
|
elif type == QC.QEvent.FocusIn:
|
|
|
|
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SET_FOCUS', event )
|
|
|
|
elif type == QC.QEvent.MouseMove:
|
|
|
|
event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOTION', event )
|
|
|
|
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: 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_KILL_FOCUS( self, callback ):
|
|
|
|
self._AddCallback( 'EVT_KILL_FOCUS', 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_MOTION( self, callback ):
|
|
|
|
self._AddCallback( 'EVT_MOTION', 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_SET_FOCUS( self, callback ):
|
|
|
|
self._AddCallback( 'EVT_SET_FOCUS', 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 )
|
|
|
|
if QW.QApplication.style().metaObject().className() == "QFusionStyle":
|
|
|
|
self.setItemDelegate( CheckBoxDelegate() )
|
|
|
|
|
|
self.setModel( QG.QStandardItemModel( self ) )
|
|
|
|
text_and_data_tuples = set()
|
|
|
|
sort_by = HC.options[ 'sort_by' ]
|
|
|
|
for (sort_by_type, namespaces) in sort_by:
|
|
|
|
text_and_data_tuples.update( namespaces )
|
|
|
|
|
|
text_and_data_tuples = list( [ ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ] )
|
|
|
|
text_and_data_tuples.sort()
|
|
|
|
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) = GetClientData( self, index )
|
|
|
|
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 ) = GetClientData( self, index )
|
|
|
|
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 )
|
|
|
|
if len( indices_to_check ) > 0:
|
|
|
|
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()
|