#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.putenv( '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 ): path = QW.QFileDialog.getExistingDirectory( None, '', self._path_edit.text() ) 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 ): if self._save_mode: if self._wildcard: path = QW.QFileDialog.getSaveFileName( None, '', self._path_edit.text(), filter = self._wildcard, selectedFilter = self._wildcard )[0] else: path = QW.QFileDialog.getSaveFileName( None, '', self._path_edit.text() )[0] else: if self._wildcard: path = QW.QFileDialog.getOpenFileName( None, '', self._path_edit.text(), filter = self._wildcard, selectedFilter = self._wildcard )[0] else: path = QW.QFileDialog.getOpenFileName( None, '', self._path_edit.text() )[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 event.mimeData().formats(): event.accept() else: event.ignore() def dragMoveEvent(self, event): if event.mimeData().formats(): tab_index = self.tabAt( event.pos() ) if tab_index != -1: shift_down = event.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.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 if HC.PLATFORM_MACOS: return global_pos = self.mapToGlobal( e.pos() ) pos_in_tab = self._tab_bar.mapFromGlobal( global_pos ) if not self._tab_bar.rect().contains( pos_in_tab ): 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() drag = QG.QDrag( self._tab_bar ) drag.setMimeData( mimeData ) drag.setPixmap( pixmap ) cursor = QG.QCursor( QC.Qt.OpenHandCursor ) drag.setHotSpot( e.pos() - pos_in_tab ) 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 not 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 ) tab_index = self._tab_bar.tabAt( event.pos() ) if tab_index != -1: shift_down = event.keyboardModifiers() & QC.Qt.ShiftModifier self.setCurrentIndex( tab_index ) if 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 len( 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() dropped_on_tab_index = self.tabBar().tabAt( e.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] ) 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: pass #if isinstance( item, QW.QWidget ): item.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding ) #wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Expand() #item.setContentsMargins( 2, 2, 2, 2 ) elif flag == CC.FLAGS_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 ): layout.setStretchFactor( item, 5 ) #item.setContentsMargins( 2, 2, 2, 2 ) #wx.SizerFlags( 5 ).Border( wx.ALL, 2 ).Expand() 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_EXPAND_BOTH_WAYS_POLITE: 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, 3 ) #item.setContentsMargins( 2, 2, 2, 2 ) #wx.SizerFlags( 3 ).Border( wx.ALL, 2 ).Expand() elif flag == CC.FLAGS_EXPAND_BOTH_WAYS_SHY: if isinstance( item, QW.QWidget ): item.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding ) if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ): layout.setStretchFactor( item, 1 ) #item.setContentsMargins( 2, 2, 2, 2 ) #wx.SizerFlags( 1 ).Border( wx.ALL, 2 ).Expand() 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: 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 ) else: raise ValueError( 'Unknown legacy sizer flag' ) 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() ) def DestroyChildren( widget ): if not widget.layout(): return ClearLayout( widget.layout(), delete_widgets = True ) # This creates a new hidden widget, reparents the layout to it then deletes the widget. # Not the nicest solution but otherwise widget.setLayout( None ) refused to work... tmp = QW.QWidget() tmp.setVisible( False ) tmp.setLayout( widget.layout() ) tmp.deleteLater() widget.setLayout( None ) 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 GetEffectiveMinSize( widget ): return widget.sizeHint().toTuple() #widget.minimumSize().toTuple() 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( '{0}'.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.Plain ) 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): 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__( *args, **kwargs ) self.itemClicked.connect( self._UpdateCheckState ) def _HandleItemClickedForCheckStateUpdate( self, item, column ): self._UpdateCheckState( item, item.checkState() ) def _UpdateCheckState( self, item, check_state ): item.setCheckState( 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()