text: #This file is licensed under the Do What the Fuck You Want To Public License aka WTFPL
  
  import os
  
  # If not explicitely set, prefer PySide2 instead of the qtpy default which is PyQt5
  # It is important that this runs on startup *before* anything is imported from qtpy.
  # Since test.py, client.py and client.pyw all import this module first before any other Qt related ones, this requirement is satisfied.
  if not 'QT_API' in os.environ:
      
      try:
  
          import PySide2
  
          os.environ[ 'QT_API' ] = 'pyside2'
          
      except ImportError as e:
          
          pass
          
  
  # 
  import qtpy
  from qtpy import QtCore as QC
  from qtpy import QtWidgets as QW
  from qtpy import QtGui as QG
  
  import math
  import typing
  
  from collections import defaultdict
      
  if qtpy.PYQT5:
      
      import sip # pylint: disable=E0401
      
      def isValid( obj ):
          
          if isinstance( obj, sip.simplewrapper ):
          
              return not sip.isdeleted( obj )
              
          
          return True
          
      
  elif qtpy.PYSIDE2:
      
      import shiboken2
      
      isValid = shiboken2.isValid
      
  else:
      
      raise RuntimeError( 'You need either PySide2 or PyQt5' )
      
  
  from hydrus.core import HydrusConstants as HC
  from hydrus.core import HydrusData
  from hydrus.core import HydrusGlobals as HG
  
  from hydrus.client import ClientConstants as CC
  
  def MonkeyPatchMissingMethods():
      
      if qtpy.PYQT5:
          
          def MonkeyPatchGetSaveFileName( original_function ):
              
              def new_function( *args, **kwargs ):
                  
                  if 'selectedFilter' in kwargs:
                      
                      kwargs[ 'initialFilter' ] = kwargs[ 'selectedFilter' ]
                      del kwargs[ 'selectedFilter' ]
                      
                      return original_function( *args, **kwargs )
                      
                  
              
              return new_function
              
          
          QW.QFileDialog.getSaveFileName = MonkeyPatchGetSaveFileName( QW.QFileDialog.getSaveFileName )
          
  
  
  class HBoxLayout( QW.QHBoxLayout ):
      
      def __init__( self, margin = 2, spacing = 2 ):
          
          QW.QHBoxLayout.__init__( self )
          
          self.setMargin( margin )
          self.setSpacing( spacing )
          
      
      def setMargin( self, val ):
          
          self.setContentsMargins( val, val, val, val )
          
  
  class VBoxLayout( QW.QVBoxLayout ):
  
      def __init__( self, margin = 2, spacing = 2 ):
          
          QW.QVBoxLayout.__init__( self )
          
          self.setMargin( margin )
          self.setSpacing( spacing )
          
  
      def setMargin( self, val ):
          
          self.setContentsMargins( val, val, val, val )
          
      
  class LabelledSlider( QW.QWidget ):
      
      def __init__( self, parent = None ):
          
          QW.QWidget.__init__( self, parent )
          
          self.setLayout( VBoxLayout( spacing = 2 ) )
          
          top_layout = HBoxLayout( spacing = 2 )
          
          self._min_label = QW.QLabel()
          self._max_label = QW.QLabel()
          self._value_label = QW.QLabel()
          self._slider = QW.QSlider()
          self._slider.setOrientation( QC.Qt.Horizontal )
          self._slider.setTickInterval( 1 )
          self._slider.setTickPosition( QW.QSlider.TicksBothSides )
          
          top_layout.addWidget( self._min_label )
          top_layout.addWidget( self._slider )
          top_layout.addWidget( self._max_label )
          
          self.layout().addLayout( top_layout )
          self.layout().addWidget( self._value_label )
          self._value_label.setAlignment( QC.Qt.AlignVCenter | QC.Qt.AlignHCenter )
          self.layout().setAlignment( self._value_label, QC.Qt.AlignHCenter )
          
          self._slider.valueChanged.connect( self._UpdateLabels )
          
          self._UpdateLabels()
          
      def _UpdateLabels( self ):
          
          self._min_label.setText( str( self._slider.minimum() ) )
          self._max_label.setText( str( self._slider.maximum() ) )
          self._value_label.setText( str( self._slider.value() ) )
          
      def GetValue( self ):
          
          return self._slider.value()
      
      def SetRange( self, min, max ):
          
          self._slider.setRange( min, max )
          
          self._UpdateLabels()
          
      def SetValue( self, value ):
          
          self._slider.setValue( value )
          
          self._UpdateLabels()
          
  
  def SplitterVisibleCount( splitter ):
      
      count = 0
      
      for i in range( splitter.count() ):
          
          if splitter.widget( i ).isVisibleTo( splitter ): count += 1
          
      return count
  
  
  class DirPickerCtrl( QW.QWidget ):
  
      dirPickerChanged = QC.Signal()
      
      def __init__( self, parent ):
          
          QW.QWidget.__init__( self, parent )
          
          layout = HBoxLayout( spacing = 2 )
          
          self._path_edit = QW.QLineEdit( self )
          
          self._button = QW.QPushButton( 'browse', self )
          
          self._button.clicked.connect( self._Browse )
          
          self._path_edit.textEdited.connect( self._TextEdited )
          
          layout.addWidget( self._path_edit )
          layout.addWidget( self._button )
          
          self.setLayout( layout )
          
      
      def SetPath( self, path ):
          
          self._path_edit.setText( path )
          
      
      def GetPath( self ):
          
          return self._path_edit.text()
          
      
      def _Browse( self ):
          
          existing_path = self._path_edit.text()
          
          if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
              
              options = QW.QFileDialog.Options( QW.QFileDialog.DontUseNativeDialog )
              
          else:
              
              options = QW.QFileDialog.Options()
              
          
          path = QW.QFileDialog.getExistingDirectory( self, '', existing_path, options = options )
          
          if path == '':
              
              return
              
          
          path = os.path.normpath( path )
          
          self._path_edit.setText( path )
          
          if os.path.exists( path ):
              
              self.dirPickerChanged.emit()
              
          
      
      def _TextEdited( self, text ):
          
          if os.path.exists( text ):
              
              self.dirPickerChanged.emit()
              
          
  
  class FilePickerCtrl( QW.QWidget ):
      
      filePickerChanged = QC.Signal()
  
      def __init__( self, parent = None, wildcard = None, starting_directory = None ):
          
          QW.QWidget.__init__( self, parent )
  
          layout = HBoxLayout( spacing = 2 )
  
          self._path_edit = QW.QLineEdit( self )
  
          self._button = QW.QPushButton( 'browse', self )
  
          self._button.clicked.connect( self._Browse )
  
          self._path_edit.textEdited.connect( self._TextEdited )
  
          layout.addWidget( self._path_edit )
          layout.addWidget( self._button )
  
          self.setLayout( layout )
          
          self._save_mode = False
          
          self._wildcard = wildcard
          
          self._starting_directory = starting_directory
          
  
      def SetPath( self, path ):
          
          self._path_edit.setText( path )
          
  
      def GetPath( self ):
          
          return self._path_edit.text()
          
      
      def SetSaveMode( self, save_mode ):
          
          self._save_mode = save_mode
          
  
      def _Browse( self ):
          
          existing_path = self._path_edit.text()
          
          if existing_path == '' and self._starting_directory is not None:
              
              existing_path = self._starting_directory
              
          
          if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
              
              options = QW.QFileDialog.Options( QW.QFileDialog.DontUseNativeDialog )
              
          else:
              
              options = QW.QFileDialog.Options()
              
          
          if self._save_mode:
              
              if self._wildcard:
                  
                  path = QW.QFileDialog.getSaveFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard, options = options )[0]
                  
              else:
                  
                  path = QW.QFileDialog.getSaveFileName( self, '', existing_path, options = options )[0]
                  
              
          else:
              
              if self._wildcard:
                  
                  path = QW.QFileDialog.getOpenFileName( self, '', existing_path, filter = self._wildcard, selectedFilter = self._wildcard, options = options )[0]
                  
              else:
                  
                  path = QW.QFileDialog.getOpenFileName( self, '', existing_path, options = options )[0]
                  
              
          
          if path == '':
              
              return
              
          
          path = os.path.normpath( path )
          
          self._path_edit.setText( path )
          
          if self._save_mode or os.path.exists( path ):
              
              self.filePickerChanged.emit()
              
          
  
      def _TextEdited( self, text ):
          
          if self._save_mode or os.path.exists( text ):
              
              self.filePickerChanged.emit()
              
          
  
  class TabBar( QW.QTabBar ):
      
      tabDoubleLeftClicked = QC.Signal( int )
      tabMiddleClicked = QC.Signal( int )
      
      tabSpaceDoubleLeftClicked = QC.Signal()
      tabSpaceDoubleMiddleClicked = QC.Signal()
      
      def __init__( self, parent = None ):
          
          QW.QTabBar.__init__( self, parent )
          
          self.setMouseTracking( True )
          self.setAcceptDrops( True )
          self._supplementary_drop_target = None
          
          self._last_clicked_tab_index = -1
          self._last_clicked_global_pos = None
          
      
      def AddSupplementaryTabBarDropTarget( self, drop_target ):
          
          self._supplementary_drop_target = drop_target
          
      
      def clearLastClickedTabInfo( self ):
          
          self._last_clicked_tab_index = -1
          
          self._last_clicked_global_pos = None
          
      
      def event( self, event ):
          
          return QW.QTabBar.event( self, event )
          
      
      def mouseMoveEvent( self, e ):
          
          e.ignore()
          
      
      def mousePressEvent( self, event ):
          
          index = self.tabAt( event.pos() )
          
          if event.button() == QC.Qt.LeftButton:
              
              self._last_clicked_tab_index = index
              
              self._last_clicked_global_pos = event.globalPos()
              
          
          QW.QTabBar.mousePressEvent( self, event )
          
      
      def mouseReleaseEvent( self, event ):
          
          index = self.tabAt( event.pos() )
          
          if event.button() == QC.Qt.MiddleButton:
              
              if index != -1:
                  
                  self.tabMiddleClicked.emit( index )
                  
                  return
                  
              
          
          QW.QTabBar.mouseReleaseEvent( self, event )
          
      
      def mouseDoubleClickEvent( self, event ):
          
          index = self.tabAt( event.pos() )
          
          if event.button() == QC.Qt.LeftButton:
              
              if index == -1:
                  
                  self.tabSpaceDoubleLeftClicked.emit()
                  
              else:
                  
                  self.tabDoubleLeftClicked.emit( index )
                  
              
              return
              
          elif event.button() == QC.Qt.MiddleButton:
              
              if index == -1:
                  
                  self.tabSpaceDoubleMiddleClicked.emit()
                  
              else:
                  
                  self.tabMiddleClicked.emit( index )
                  
              
              return
              
          
          QW.QTabBar.mouseDoubleClickEvent( self, event )
          
      
      def dragEnterEvent(self, event):
  
          if 'application/hydrus-tab' in event.mimeData().formats():
              
              event.ignore()
              
          else:
              
              event.accept()
              
          
      
      def dragMoveEvent( self, event ):
          
          if 'application/hydrus-tab' not in event.mimeData().formats():
              
              tab_index = self.tabAt( event.pos() )
              
              if tab_index != -1:
                  
                  self.parentWidget().setCurrentIndex( tab_index )
                  
              
          else:
              
              event.ignore()
              
          
      
      def lastClickedTabInfo( self ):
          
          return ( self._last_clicked_tab_index, self._last_clicked_global_pos )
          
      
      def dropEvent( self, event ):
          
          if self._supplementary_drop_target:
              
              self._supplementary_drop_target.eventFilter( self, event )
              
          else:
              
              event.ignore()
              
          
  
  # A heavily extended/tweaked version of https://forum.qt.io/topic/67542/drag-tabs-between-qtabwidgets/
  class TabWidgetWithDnD( QW.QTabWidget ):
      
      pageDragAndDropped = QC.Signal( QW.QWidget, QW.QWidget )
      
      def __init__( self, parent = None ):
          
          QW.QTabWidget.__init__( self, parent )
          
          self.setTabBar( TabBar( self ) )
          
          self.setAcceptDrops( True )
          
          self._tab_bar = self.tabBar()
          
          self._supplementary_drop_target = None
          
      
      def _LayoutPagesHelper( self ):
          
          current_index = self.currentIndex()
  
          for i in range( self.count() ):
  
              self.setCurrentIndex( i )
  
              if isinstance( self.widget( i ), TabWidgetWithDnD ):
                  
                  self.widget( i )._LayoutPagesHelper()
                  
              
          
          self.setCurrentIndex( current_index )
          
      
      def LayoutPages( self ):
          
          # hydev adds: I no longer call this, as I moved splitter setting to a thing called per page when page is first visibly shown
          # leaving it here for now in case I need it again
          
          # Momentarily switch to each page, then back, forcing a layout update.
          # If this is not done, the splitters on the hidden pages won't resize their widgets properly when we restore
          # splitter sizes after this, since they would never became visible.
          # We first have to climb up the widget hierarchy and go down recursively from the root tab widget,
          # since it's not enough to make a page visible if its a nested page: all of its ancestor pages have to be visible too.
          # This shouldn't be visible to users since we switch back immediately.
          # There is probably a proper way to do this...
  
          highest_ancestor_of_same_type = self
  
          parent = self.parentWidget()
  
          while parent is not None:
  
              if isinstance( parent, TabWidgetWithDnD ):
                  
                  highest_ancestor_of_same_type = parent
                  
  
              parent = parent.parentWidget()
              
          
          highest_ancestor_of_same_type._LayoutPagesHelper() # This does the actual recursive descent and making pages visible
          
      
      # This is a hack that adds an additional drop target to the tab bar. The added drop target will get drop events from the tab bar.
      # Used to make the case of files/media droppend onto tabs work.
      def AddSupplementaryTabBarDropTarget( self, drop_target ):
          
          self._supplementary_drop_target = drop_target
          self.tabBar().AddSupplementaryTabBarDropTarget( drop_target )
          
      
      def mouseMoveEvent( self, e ):
          
          if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
              
              QW.QTabWidget.mouseMoveEvent( self, e )
              
          
          if e.buttons() != QC.Qt.LeftButton:
              
              return
              
          
          my_mouse_pos = e.pos()
          global_mouse_pos = self.mapToGlobal( my_mouse_pos )
          tab_bar_mouse_pos = self._tab_bar.mapFromGlobal( global_mouse_pos )
          
          if not self._tab_bar.rect().contains( tab_bar_mouse_pos ):
              
              return
              
          
          if not isinstance( self._tab_bar, TabBar ):
              
              return
              
          
          ( clicked_tab_index, clicked_global_pos ) = self._tab_bar.lastClickedTabInfo()
          
          if clicked_tab_index == -1:
              
              return
              
          
          if e.globalPos() == clicked_global_pos:
              
              # don't start a drag until movement
              
              return
              
          
          tab_rect = self._tab_bar.tabRect( clicked_tab_index )
          
          pixmap = QG.QPixmap( tab_rect.size() )
          self._tab_bar.render( pixmap, QC.QPoint(), QG.QRegion( tab_rect ) )
          
          mimeData = QC.QMimeData()
          
          mimeData.setData( 'application/hydrus-tab', b'' )
          
          drag = QG.QDrag( self._tab_bar )
          
          drag.setMimeData( mimeData )
          
          drag.setPixmap( pixmap )
          
          cursor = QG.QCursor( QC.Qt.OpenHandCursor )
          
          drag.setHotSpot( QC.QPoint( 0, 0 ) )
          
          # this puts the tab pixmap exactly where we picked it up, but it looks bad
          # drag.setHotSpot( tab_bar_mouse_pos - tab_rect.topLeft() )
          
          drag.setDragCursor( cursor.pixmap(), QC.Qt.MoveAction )
          
          drag.exec_( QC.Qt.MoveAction )
          
  
      def dragEnterEvent( self, e ):
          
          if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
              
              return QW.QTabWidget.dragEnterEvent( self, e )
              
          
          if 'application/hydrus-tab' in e.mimeData().formats():
              
              e.accept()
              
          else:
              
              e.ignore()
              
          
      
      def dragMoveEvent( self, event ):
          
          #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( event.pos() ) ) ): return QW.QTabWidget.dragMoveEvent( self, event )
          
          screen_pos = self.mapToGlobal( event.pos() )
          
          tab_pos = self._tab_bar.mapFromGlobal( screen_pos )
          
          tab_index = self._tab_bar.tabAt( tab_pos )
          
          if tab_index != -1:
              
              shift_down = event.keyboardModifiers() & QC.Qt.ShiftModifier
              
              self.setCurrentIndex( tab_index )
              
          
          if 'application/hydrus-tab' not in event.mimeData().formats():
              
              event.reject()
              
  
          #return QW.QTabWidget.dragMoveEvent( self, event )
          
  
      def dragLeaveEvent( self, e ):
          
          #if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ): return QW.QTabWidget.dragLeaveEvent( self, e )
          
          e.accept()
          
  
      def addTab(self, widget, *args, **kwargs ):
          
          if isinstance( widget, TabWidgetWithDnD ):
              
              widget.AddSupplementaryTabBarDropTarget( self._supplementary_drop_target )
              
          
          QW.QTabWidget.addTab( self, widget, *args, **kwargs )
          
      
      def insertTab(self, index, widget, *args, **kwargs):
  
          if isinstance( widget, TabWidgetWithDnD ):
              
              widget.AddSupplementaryTabBarDropTarget( self._supplementary_drop_target )
              
  
          QW.QTabWidget.insertTab( self, index, widget, *args, **kwargs )
          
      
      def dropEvent( self, e ):
          
          if self.currentWidget() and self.currentWidget().rect().contains( self.currentWidget().mapFromGlobal( self.mapToGlobal( e.pos() ) ) ):
              
              return QW.QTabWidget.dropEvent( self, e )
              
         
          if 'application/hydrus-tab' not in e.mimeData().formats(): #Page dnd has no associated mime data
              
              e.ignore()
              
              return
              
          
          w = self
          
          source_tab_bar = e.source()
          
          if not isinstance( source_tab_bar, TabBar ):
              
              return
              
          
          ( source_page_index, source_page_click_global_pos ) = source_tab_bar.lastClickedTabInfo()
          
          source_tab_bar.clearLastClickedTabInfo()
          
          source_notebook = source_tab_bar.parentWidget()
          source_page = source_notebook.widget( source_page_index )
          source_name = source_tab_bar.tabText( source_page_index )
          
          while w is not None:
              
              if source_page == w:
                  
                  # you cannot drop a page of pages inside itself
                  
                  return
                  
              
              w = w.parentWidget()
              
  
          e.setDropAction( QC.Qt.MoveAction )
          
          e.accept()
          
          counter = self.count()
          
          screen_pos = self.mapToGlobal( e.pos() )
          
          tab_pos = self.tabBar().mapFromGlobal( screen_pos )
          
          dropped_on_tab_index = self.tabBar().tabAt( tab_pos )
          
          if source_notebook == self and dropped_on_tab_index == source_page_index:
              
              return # if we drop on ourself, make no action, even on the right edge
              
          
          dropped_on_left_edge = False
          dropped_on_right_edge = False
          
          if dropped_on_tab_index != -1:
              
              EDGE_PADDING = 15
              
              tab_rect = self.tabBar().tabRect( dropped_on_tab_index )
              
              edge_size = QC.QSize( EDGE_PADDING, tab_rect.height() )
              
              left_edge_rect = QC.QRect( tab_rect.topLeft(), edge_size )
              right_edge_rect = QC.QRect( tab_rect.topRight() - QC.QPoint( EDGE_PADDING, 0 ), edge_size )
              
              dropped_on_left_edge = left_edge_rect.contains( e.pos() )
              dropped_on_right_edge = right_edge_rect.contains( e.pos() )
              
          
          if counter == 0:
              
              self.addTab( source_page, source_name )
              
          else:
              
              if dropped_on_tab_index == -1:
                  
                  insert_index = counter
                  
              else:
                  
                  insert_index = dropped_on_tab_index
                  
                  if dropped_on_right_edge:
                      
                      insert_index += 1
                      
                  
                  if self == source_notebook:
                      
                      if insert_index == source_page_index + 1 and not dropped_on_left_edge:
                          
                          pass # in this special case, moving it confidently one to the right, we will disobey the normal rules and indeed move one to the right, rather than no-op
                          
                      elif insert_index > source_page_index:
                          
                          # we are inserting to our right, which needs a shift since we will be removing ourselves from the list
                          
                          insert_index -= 1
                          
                      
                  
              
              if source_notebook == self and insert_index == source_page_index:
                  
                  return # if we mean to insert on ourself, make no action
                  
              
              self.insertTab( insert_index, source_page, source_name )
  
              shift_down = e.keyboardModifiers() & QC.Qt.ShiftModifier
  
              follow_dropped_page = not shift_down
  
              new_options = HG.client_controller.new_options
  
              if new_options.GetBoolean( 'reverse_page_shift_drag_behaviour' ):
                  
                  follow_dropped_page = not follow_dropped_page
                  
              
              if follow_dropped_page:
                  
                  self.setCurrentIndex( self.indexOf( source_page ) )
                  
              else:
                  
                  if source_page_index > 1:
                      
                      neighbour_page = source_notebook.widget( source_page_index - 1 )
                      
                      page_key = neighbour_page.GetPageKey()
                      
                  else:
                      
                      page_key = source_notebook.GetPageKey()
                      
                  
                  HG.client_controller.gui.ShowPage( page_key )
                  
              
          
          self.pageDragAndDropped.emit( source_page, source_tab_bar )
          
      
  def DeleteAllNotebookPages( notebook ):
      
      while notebook.count() > 0:
          
          tab = notebook.widget( 0 )
          
          notebook.removeTab( 0 )
          
          tab.deleteLater()
          
      
  def SplitVertically( splitter: QW.QSplitter, w1, w2, hpos ):
  
      splitter.setOrientation( QC.Qt.Horizontal )
  
      if w1.parentWidget() != splitter:
          
          splitter.addWidget( w1 )
  
      w1.setVisible( True )
  
      if w2.parentWidget() != splitter:
          
          splitter.addWidget( w2 )
  
      w2.setVisible( True )
  
      total_sum = sum( splitter.sizes() )
  
      if hpos < 0:
  
          splitter.setSizes( [ total_sum + hpos, -hpos ] )
  
      elif hpos > 0:
          
          splitter.setSizes( [ hpos, total_sum - hpos ] )
  
  
  def SplitHorizontally( splitter: QW.QSplitter, w1, w2, vpos ):
      
      splitter.setOrientation( QC.Qt.Vertical )
      
      if w1.parentWidget() != splitter:
          
          splitter.addWidget( w1 )
          
      w1.setVisible( True )
          
      if w2.parentWidget() != splitter:
          
          splitter.addWidget( w2 )
  
      w2.setVisible( True )
      
      total_sum = sum( splitter.sizes() )
      
      if vpos < 0:
          
          splitter.setSizes( [ total_sum + vpos, -vpos ] )
          
      elif vpos > 0:
          
          splitter.setSizes( [ vpos, total_sum - vpos ] )
  
  
  def MakeQLabelWithAlignment( label, parent, align ):
      
      res = QW.QLabel( label, parent )
  
      res.setAlignment( align )
  
      return res
  
  
  class GridLayout( QW.QGridLayout ):
      
      def __init__( self, cols = 1, spacing = 2 ):
          
          QW.QGridLayout.__init__( self )
          
          self._col_count = cols
          self.setMargin( 2 )
          self.setSpacing( spacing )
          
      def GetFixedColumnCount( self ):
          
          return self._col_count
  
      def setMargin( self, val ):
          
          self.setContentsMargins( val, val, val, val )
          
      
  def AddToLayout( layout, item, flag = None, alignment = None ):
  
      if isinstance( layout, GridLayout ):
          
          cols = layout.GetFixedColumnCount()
          
          count = layout.count()
          
          row = math.floor( count / cols )
          
          col = count % cols
          
          if isinstance( item, QW.QLayout ):
              
              layout.addLayout( item, row, col )
              
          elif isinstance( item, QW.QWidget ):
              
              layout.addWidget( item, row, col )
              
          elif isinstance( item, tuple ):
              
              spacer = QW.QPushButton()#QW.QSpacerItem( 0, 0, QW.QSizePolicy.Expanding, QW.QSizePolicy.Fixed )
              layout.addWidget( spacer, row, col )
              spacer.setVisible(False)
              
              return
              
          
      else:
  
          if isinstance( item, QW.QLayout ):
  
              layout.addLayout( item )
              
              if alignment is not None:
                  
                  layout.setAlignment( item, alignment )
  
          elif isinstance( item, QW.QWidget ):
  
              layout.addWidget( item )
              
              if alignment is not None:
                  
                  layout.setAlignment( item, alignment )
  
          elif isinstance( item, tuple ):
  
              layout.addStretch( 1 )
              
              return
          
      
      zero_border = False
      
      if flag is None or flag == CC.FLAGS_NONE:
          
          pass
          
      elif flag in ( CC.FLAGS_CENTER, CC.FLAGS_ON_LEFT, CC.FLAGS_ON_RIGHT, CC.FLAGS_CENTER_PERPENDICULAR, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH ):
          
          if flag == CC.FLAGS_CENTER:
              
              alignment = QC.Qt.AlignVCenter | QC.Qt.AlignHCenter
              
          if flag == CC.FLAGS_ON_LEFT:
              
              alignment = QC.Qt.AlignLeft | QC.Qt.AlignVCenter
              
          elif flag == CC.FLAGS_ON_RIGHT:
              
              alignment = QC.Qt.AlignRight | QC.Qt.AlignVCenter
              
          elif flag in ( CC.FLAGS_CENTER_PERPENDICULAR, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH ):
              
              if isinstance( layout, QW.QHBoxLayout ):
                  
                  alignment = QC.Qt.AlignVCenter
                  
              else:
                  
                  alignment = QC.Qt.AlignHCenter
                  
              
          
          layout.setAlignment( item, alignment )
          
          if flag == CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH:
              
              if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ):
                  
                  layout.setStretchFactor( item, 5 )
                  
              
          
          if isinstance( item, QW.QLayout ):
              
              zero_border = True
              
          
      elif flag in ( CC.FLAGS_EXPAND_PERPENDICULAR, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR ):
          
          if flag == CC.FLAGS_EXPAND_SIZER_PERPENDICULAR:
              
              zero_border = True
              
          
          if isinstance( item, QW.QWidget ):
              
              if isinstance( layout, QW.QHBoxLayout ):
                  
                  h_policy = QW.QSizePolicy.Fixed
                  v_policy = QW.QSizePolicy.Expanding
                  
              else:
                  
                  h_policy = QW.QSizePolicy.Expanding
                  v_policy = QW.QSizePolicy.Fixed
                  
              
              item.setSizePolicy( h_policy, v_policy )
              
          
      elif flag in ( CC.FLAGS_EXPAND_BOTH_WAYS, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS, CC.FLAGS_EXPAND_BOTH_WAYS_POLITE, CC.FLAGS_EXPAND_BOTH_WAYS_SHY ):
          
          if flag == CC.FLAGS_EXPAND_SIZER_BOTH_WAYS:
              
              zero_border = True
              
          
          if isinstance( item, QW.QWidget ):
              
              item.setSizePolicy( QW.QSizePolicy.Expanding, QW.QSizePolicy.Expanding )
              
          
          if isinstance( layout, QW.QVBoxLayout ) or isinstance( layout, QW.QHBoxLayout ):
              
              if flag in ( CC.FLAGS_EXPAND_BOTH_WAYS, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ):
                  
                  stretch_factor = 5
                  
              elif flag == CC.FLAGS_EXPAND_BOTH_WAYS_POLITE:
                  
                  stretch_factor = 3
                  
              elif flag == CC.FLAGS_EXPAND_BOTH_WAYS_SHY:
                  
                  stretch_factor = 1
                  
              
              layout.setStretchFactor( item, stretch_factor )
              
          
      
      if zero_border:
          
          margin = 0
          
          if isinstance( item, QW.QFrame ):
              
              margin = item.frameWidth()
              
          
          item.setContentsMargins( margin, margin, margin, margin )
          
  
  def ScrollAreaVisibleRect( scroll_area ):
      
      if not scroll_area.widget(): return QC.QRect( 0, 0, 0, 0 )
      
      rect = scroll_area.widget().visibleRegion().boundingRect()
  
      # Do not allow it to be smaller than the scroll area's viewport size:
      
      if rect.width() < scroll_area.viewport().width():
          
          rect.setWidth( scroll_area.viewport().width() )
  
      if rect.height() < scroll_area.viewport().height():
          
          rect.setHeight( scroll_area.viewport().height() )
          
      
      return rect
      
  def AdjustOpacity( image, opacity_factor ):
      
      new_image = QG.QImage( image.width(), image.height(), QG.QImage.Format_RGBA8888 )
      
      new_image.fill( QC.Qt.transparent )
      
      painter = QG.QPainter( new_image )
      
      painter.setOpacity( opacity_factor )
      
      painter.drawImage( 0, 0, image )
      
      return new_image
  
  def ToKeySequence( modifiers, key ):
      
      if isinstance( modifiers, QC.Qt.KeyboardModifiers ):
          
          seq_str = ''
          
          for modifier in [ QC.Qt.ShiftModifier, QC.Qt.ControlModifier, QC.Qt.AltModifier, QC.Qt.MetaModifier, QC.Qt.KeypadModifier, QC.Qt.GroupSwitchModifier ]:
              
              if modifiers & modifier: seq_str += QG.QKeySequence( modifier ).toString()
              
          seq_str += QG.QKeySequence( key ).toString()
              
          return QG.QKeySequence( seq_str )
              
      else: return QG.QKeySequence( key + modifiers )
  
  
  def AddShortcut( widget, modifier, key, callable, *args ):
      
      shortcut = QW.QShortcut( widget )
      
      shortcut.setKey( ToKeySequence( modifier, key ) )
      
      shortcut.setContext( QC.Qt.WidgetWithChildrenShortcut )
      
      shortcut.activated.connect( lambda: callable( *args ) )
      
  class BusyCursor:
      
      def __enter__( self ):
          
          QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
      
      def __exit__( self, exc_type, exc_val, exc_tb ):
          
          QW.QApplication.restoreOverrideCursor()
  
  
  def GetBackgroundColour( widget ):
      
      return widget.palette().color( QG.QPalette.Window )
  
  
  CallAfterEventType = QC.QEvent.Type( QC.QEvent.registerEventType() )
  
  class CallAfterEvent( QC.QEvent ):
      
      def __init__( self, fn, *args, **kwargs ):
          
          QC.QEvent.__init__( self, CallAfterEventType )
          
          self._fn = fn
          self._args = args
          self._kwargs = kwargs
          
      
      def Execute( self ):
          
          if self._fn is not None:
              
              self._fn( *self._args, **self._kwargs )
              
          
      
  class CallAfterEventCatcher( QC.QObject ):
      
      def __init__( self, parent ):
          
          QC.QObject.__init__( self, parent )
          
          self.installEventFilter( self )
          
      
      def eventFilter( self, watched, event ):
          
          if event.type() == CallAfterEventType and isinstance( event, CallAfterEvent ):
              
              if HG.callto_profile_mode:
                  
                  summary = 'Profiling CallAfter Event: {}'.format( event._fn )
                  
                  HydrusData.Profile( summary, 'event.Execute()', globals(), locals(), min_duration_ms = 3, show_summary = True )
                  
              else:
                  
                  event.Execute()
                  
              
              event.accept()
              
              return True
              
          
          return False
          
  
  def CallAfter( fn, *args, **kwargs ):
      
      QW.QApplication.instance().postEvent( QW.QApplication.instance().call_after_catcher, CallAfterEvent( fn, *args, **kwargs ) )
      
      QW.QApplication.instance().eventDispatcher().wakeUp()
      
  def ClearLayout( layout, delete_widgets = False ):
      
      while layout.count() > 0:
  
          item = layout.itemAt( 0 )
  
          if delete_widgets:
  
              if item.widget():
  
                  item.widget().deleteLater()
  
              elif item.layout():
  
                  ClearLayout( item.layout(), delete_widgets = True )
                  item.layout().deleteLater()
  
              else:
  
                  spacer = item.layout().spacerItem()
  
                  del spacer
  
          layout.removeItem( item )
          
      
  def GetClientData( widget, idx ):
      
      if isinstance( widget, QW.QComboBox ):
          
          return widget.itemData( idx, QC.Qt.UserRole )
      
      elif isinstance( widget, CheckListBox ):
          
          return widget.item( idx ).data( QC.Qt.UserRole )
      
      elif isinstance( widget, QW.QTreeWidget ):
          
          return widget.topLevelItem( idx ).data( 0, QC.Qt.UserRole )
      
      elif isinstance( widget, QW.QListWidget ):
          
          return widget.item( idx ).data( QC.Qt.UserRole )
      
      else:
          
          raise ValueError( 'Unknown widget class in GetClientData' )
  
  
  def Unsplit( splitter, widget ):
      
      if widget.parentWidget() == splitter:
          
          widget.setVisible( False )
          
      
  def GetSystemColour( colour ):
      
      return QG.QPalette().color( colour )
  
  def CenterOnWindow( parent, window ):
      
      parent_window = parent.window()
      
      window.move( parent_window.frameGeometry().center() - window.rect().center() )
  
  def ListWidgetDelete( widget, idx ):
      
      if isinstance( idx, QC.QModelIndex ):
          
          idx = idx.row()
      
      if idx != -1:
          
          item = widget.takeItem( idx )
          
          del item
  
  
  def ListWidgetGetSelection( widget ):
      
      for i in range( widget.count() ):
  
          if widget.item( i ).isSelected(): return i
  
      return -1
  
  
  def ListWidgetGetStrings( widget ):
      
      strings = []
      
      for i in range( widget.count() ):
          
          strings.append( widget.item( i ).text() )
          
      return strings
  
  
  def ListWidgetIsSelected( widget, idx ):
      
      if idx == -1: return False
      
      return widget.item( idx ).isSelected()
  
  
  def ListWidgetSetSelection( widget, idxs ):
      
      widget.clearSelection()
  
      if not isinstance( idxs, list ):
          
          idxs = [ idxs ]
          
      
      count = widget.count()
      
      for idx in idxs:
          
          if 0 <= idx <= count -1:
              
              widget.item( idx ).setSelected( True )
              
          
      
  def MakeQSpinBox( parent = None, initial = None, min = None, max = None, width = None ):
      
      spinbox = QW.QSpinBox( parent )
      
      if min is not None: spinbox.setMinimum( min )
      
      if max is not None: spinbox.setMaximum( max )
      
      if initial is not None: spinbox.setValue( initial )
      
      if width is not None: spinbox.setMinimumWidth( width )
  
      return spinbox
  
  
  def SetInitialSize( widget, size ):
      
      if hasattr( widget, 'SetInitialSize' ):
          
          widget.SetInitialSize( size )
          
          return
      
      if isinstance( size, tuple ):
          
          size = QC.QSize( size[0], size[1] )
          
      
      if size.width() >= 0: widget.setMinimumWidth( size.width() )
      if size.height() >= 0: widget.setMinimumHeight( size.height() )
  
  def SetBackgroundColour( widget, colour ):
  
      widget.setAutoFillBackground( True )
  
      object_name = widget.objectName()
  
      if not object_name:
          
          object_name = str( id( widget ) )
  
          widget.setObjectName( object_name )
      
      if isinstance( colour, QG.QColor ):
          
          widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, colour.name()) )
          
      elif isinstance( colour, tuple ):
          
          colour = QG.QColor( *colour )
          
          widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, colour.name() ) )
          
      else:
  
          widget.setStyleSheet( '#{} {{ background-color: {} }}'.format( object_name, QG.QColor( colour ).name() ) )
          
      
  def SetForegroundColour( widget, colour ):
      
      widget.setAutoFillBackground( True )
      
      object_name = widget.objectName()
  
      if not object_name:
          
          object_name = str( id( widget ) )
          
          widget.setObjectName( object_name )
          
  
      if isinstance( colour, QG.QColor ):
  
          widget.setStyleSheet( '#{} {{ color: {} }}'.format( object_name, colour.name()) )
          
      elif isinstance( colour, tuple ):
          
          colour = QG.QColor( *colour )
          
          widget.setStyleSheet( '#{} {{ color: {} }}'.format( object_name, colour.name() ) )
          
      else:
  
          widget.setStyleSheet( '#{} {{ color: {} }}'.format( object_name, QG.QColor( colour ).name() ) )
          
  
  def SetStringSelection( combobox, string ):
      
      index = combobox.findText( string )
      
      if index != -1:
          
          combobox.setCurrentIndex( index )
  
  
  def SetClientSize( widget, size ):
      
      if isinstance( size, tuple ):
  
          size = QC.QSize( size[ 0 ], size[ 1 ] )
  
      if size.width() < 0: size.setWidth( widget.width() )
      if size.height() < 0: size.setHeight( widget.height() )
  
      widget.resize( size )
  
  
  def SetMinClientSize( widget, size ):
      
      if isinstance( size, tuple ):
          
          size = QC.QSize( size[0], size[1] )
          
      
      if size.width() >= 0: widget.setMinimumWidth( size.width() )
      if size.height() >= 0: widget.setMinimumHeight( size.height() )
  
  
  class StatusBar( QW.QStatusBar ):
      
      def __init__( self, status_widths ):
          
          QW.QStatusBar.__init__( self )
          
          self._labels = []
          
          for w in status_widths:
              
              label = QW.QLabel()
              
              self._labels.append( label )
              
              if w < 0:
                  
                  self.addWidget( label, -1 * w )
                  
              else:
                  
                  label.setFixedWidth( w )
                  
                  self.addWidget( label )
                  
              
          
      
      def SetStatusText( self, text, index, tooltip = None ):
          
          if tooltip is None:
              
              tooltip = text
              
          
          cell = self._labels[ index ]
          
          if cell.text() != text:
              
              cell.setText( text )
              
          
          if cell.toolTip() != tooltip:
              
              cell.setToolTip( tooltip )
              
          
      
  class AboutDialogInfo:
      
      def __init__( self ):
          
          self.name = ''
          self.version = ''
          self.description = ''
          self.license = ''
          self.developers = []
          self.website = ''
  
      def SetName( self, name ):
          
          self.name = name
  
  
      def SetVersion( self, version ):
          
          self.version = version
  
  
      def SetDescription( self, description ):
          
          self.description = description
  
  
      def SetLicense( self, license ):
          
          self.license = license
  
  
      def SetDevelopers( self, developers_list ):
          
          self.developers = developers_list
  
  
      def SetWebSite( self, url ):
          
          self.website = url
  
  
  class UIActionSimulator:
      
      def __init__( self ):
          
          pass
          
      
      def Char( self, widget, key, text = None ):
          
          if widget is None:
              
              widget = QW.QApplication.focusWidget()
              
          
          ev1 = QG.QKeyEvent( QC.QEvent.KeyPress, key, QC.Qt.NoModifier, text = text )
          ev2 = QG.QKeyEvent( QC.QEvent.KeyRelease, key, QC.Qt.NoModifier, text = text )
          
          QW.QApplication.instance().postEvent( widget, ev1 )
          QW.QApplication.instance().postEvent( widget, ev2 )
          
  
  class AboutBox( QW.QDialog ):
      
      def __init__( self, parent, about_info ):
          
          QW.QDialog.__init__( self, parent )
          
          self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
          
          self.setAttribute( QC.Qt.WA_DeleteOnClose )
          
          self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
          
          layout = QW.QVBoxLayout( self )
          
          self.setWindowTitle( 'About ' + about_info.name )
          
          icon_label = QW.QLabel( self )
          name_label = QW.QLabel( about_info.name, self )
          version_label = QW.QLabel( about_info.version, self )
          tabwidget = QW.QTabWidget( self )
          desc_panel = QW.QWidget( self )
          desc_label = QW.QLabel( about_info.description, self )
          url_label = QW.QLabel( '<a href="{0}">{0}</a>'.format( about_info.website ), self )
          credits = QW.QTextEdit( self )
          license = QW.QTextEdit( self )
          close_button = QW.QPushButton( 'close', self )
          icon_label.setPixmap( HG.client_controller.frame_icon_pixmap )
          layout.addWidget( icon_label, alignment = QC.Qt.AlignHCenter )
          
          name_label_font = name_label.font()
          name_label_font.setBold( True )
          name_label.setFont( name_label_font )
          layout.addWidget( name_label, alignment = QC.Qt.AlignHCenter )
          
          layout.addWidget( version_label, alignment = QC.Qt.AlignHCenter )
          
          layout.addWidget( tabwidget, alignment =  QC.Qt.AlignHCenter )
          tabwidget.addTab( desc_panel, 'Description' )
          tabwidget.addTab( credits, 'Credits' )
          tabwidget.addTab( license, 'License' )
          tabwidget.setCurrentIndex( 0 )
          
          credits.setPlainText( 'Created by ' + ', '.join(about_info.developers) )
          credits.setReadOnly( True )
          credits.setAlignment( QC.Qt.AlignHCenter )
          
          license.setPlainText( about_info.license )
          license.setReadOnly( True )
          
          desc_layout = QW.QVBoxLayout()
          
          desc_layout.addWidget( desc_label, alignment = QC.Qt.AlignHCenter )
          desc_label.setWordWrap( True )
          desc_label.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
          desc_layout.addWidget( url_label, alignment = QC.Qt.AlignHCenter )
          url_label.setTextFormat( QC.Qt.RichText )
          url_label.setTextInteractionFlags( QC.Qt.TextBrowserInteraction )
          url_label.setOpenExternalLinks( True )
          desc_panel.setLayout( desc_layout )
          
          layout.addWidget( close_button, alignment = QC.Qt.AlignRight )
          close_button.clicked.connect( self.accept )
          
          self.setLayout( layout )
          
          self.exec_()
  
  
  class CheckListBox( QW.QListWidget ):
      
      checkListBoxChanged = QC.Signal( int )
      rightClicked = QC.Signal()
      
      def __init__( self, parent = None ):
          
          QW.QListWidget.__init__( self, parent )
          
          self.itemClicked.connect( self._ItemCheckStateChanged )
          
          self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
          
      
      def Check(self, index, state = True):
          
          item = self.item( index )
          
          item.setFlags( item.flags() | QC.Qt.ItemIsUserCheckable )
          
          if state:
              
              item.setCheckState( QC.Qt.Checked )
              
          else:
              
              item.setCheckState( QC.Qt.Unchecked )
          
          
      def IsChecked(self, index):
          
          return self.item( index ).checkState() == QC.Qt.Checked
      
      
      def GetCheckedItems(self):
          
          indices = []
          
          for i in range( self.count() ):
              
              if self.item( i ).checkState() == QC.Qt.Checked: indices.append( i )
          
          return indices
      
      
      def GetSelections( self ):
          
          indices = []
  
          for i in range( self.count() ):
  
              if self.item( i ).isSelected(): indices.append( i )
  
          return indices
      
      def SetCheckedItems( self, items ):
          
          for i in range( self.count() ):
              
              if i in items:
                  
                  self.item( i ).setCheckState( QC.Qt.Checked )
              
              else:
  
                  self.item( i ).setCheckState( QC.Qt.Unchecked )
                  
      
      def Append( self, str, client_data ):
          
          item = QW.QListWidgetItem()
  
          item.setFlags( item.flags() | QC.Qt.ItemIsUserCheckable )
  
          item.setCheckState( QC.Qt.Unchecked )
          
          item.setText( str )
          
          item.setData( QC.Qt.UserRole, client_data )
          
          self.addItem( item )
          
          
      def _ItemCheckStateChanged( self, item ):
          
          self.checkListBoxChanged.emit( self.row( item ) )
  
  
      def GetChecked( self ):
  
          result = [ self.item( index ).data( QC.Qt.UserRole ) for index in self.GetCheckedItems() ]
  
          return result
  
  
      def SetCheckedData( self, datas ):
  
          for index in range( self.count() ):
              
              data = self.item( index ).data( QC.Qt.UserRole )
              
              check_it = data in datas
              
              self.Check( index, check_it )
              
          
      
      def mousePressEvent( self, event ):
  
          if event.button() == QC.Qt.RightButton:
  
              self.rightClicked.emit()
              
          else:
  
              QW.QListWidget.mousePressEvent( self, event )
              
          
      
  class RadioBox( QW.QFrame ):
      
      radioBoxChanged = QC.Signal()
      
      def __init__( self, parent = None, choices = [], vertical = False ):
      
          QW.QFrame.__init__( self, parent )
          
          self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised )
          
          if vertical:
              
              self.setLayout( VBoxLayout() )
              
          else:
              
              self.setLayout( HBoxLayout() )
              
          
          self._choices = []
          
          for choice in choices:
              
              radiobutton = QW.QRadioButton( choice, self )
              
              self._choices.append( radiobutton )
              
              radiobutton.clicked.connect( self.radioBoxChanged )
              
              self.layout().addWidget( radiobutton )
              
          if vertical and len( self._choices ):
              
              self._choices[0].setChecked( True )
          
          elif len( self._choices ):
              
              self._choices[-1].setChecked( True )
          
          
      def GetCurrentIndex( self ):
          
          for i in range( len( self._choices ) ):
              
              if self._choices[ i ].isChecked(): return i
              
          return -1
      
      
      def SetStringSelection( self, str ):
  
          for i in range( len( self._choices ) ):
  
              if self._choices[ i ].text() == str:
                  
                  self._choices[ i ].setChecked( True )
                  
                  return
                  
              
          
      
      def GetStringSelection( self ):
  
          for i in range( len( self._choices ) ):
  
              if self._choices[ i ].isChecked(): return self._choices[ i ].text()
  
          return None
      
      def SetValue( self, data ):
          
          pass
          
      
      def Select( self, idx ):
      
          self._choices[ idx ].setChecked( True )
  
  
  # Adapted from https://doc.qt.io/qt-5/qtwidgets-widgets-elidedlabel-example.html
  class EllipsizedLabel( QW.QLabel ):
      
      def __init__( self, parent = None, ellipsize_end = False ):
          
          QW.QLabel.__init__( self, parent )
          
          self._ellipsize_end = ellipsize_end
          
      
      def minimumSizeHint( self ):
          
          if self._ellipsize_end:
              
              return self.sizeHint()
              
          else:
              
              return QW.QLabel.minimumSizeHint( self )
              
          
      
      def setText( self, text ):
          
          try:
              
              QW.QLabel.setText( self, text )
              
          except ValueError:
              
              QW.QLabel.setText( self, repr( text ) )
              
          
          self.update()
          
      
      def sizeHint( self ):
          
          if self._ellipsize_end:
              
              num_lines = self.text().count( '\n' ) + 1
              
              line_width = self.fontMetrics().lineWidth()
              line_height = self.fontMetrics().lineSpacing()
              
              size_hint = QC.QSize( 3 * line_width, num_lines * line_height )
              
          else:
              
              size_hint = QW.QLabel.sizeHint( self )
              
          
          return size_hint
          
      
      def paintEvent( self, event ):
  
          if not self._ellipsize_end:
              
              QW.QLabel.paintEvent( self, event )
              
              return
              
          
          painter = QG.QPainter( self )
  
          fontMetrics = painter.fontMetrics()
  
          text_lines = self.text().split( '\n' )
          
          line_spacing = fontMetrics.lineSpacing()
          
          current_y = 0
          
          done = False
          
          my_width = self.width()
          
          for text_line in text_lines:
              
              elided_line = fontMetrics.elidedText( text_line, QC.Qt.ElideRight, my_width )
              
              x = 0
              width = my_width
              height = line_spacing
              flags = self.alignment()
              
              painter.drawText( x, current_y, width, height, flags, elided_line )
              
              # old hacky line that doesn't support alignment flags
              #painter.drawText( QC.QPoint( 0, current_y + fontMetrics.ascent() ), elided_line )
              
              current_y += line_spacing
              
              # old code that did multiline wrap width stuff
              '''
              text_layout = QG.QTextLayout( text_line, painter.font() )
              
              text_layout.beginLayout()
              
              while True:
              
                  line = text_layout.createLine()
                  
                  if not line.isValid(): break
                  
                  line.setLineWidth( self.width() )
                  
                  next_line_y = y + line_spacing
              
                  if self.height() >= next_line_y + line_spacing:
              
                      line.draw( painter, QC.QPoint( 0, y ) )
                  
                      y = next_line_y
                      
                  else:
  
                      last_line = text_line[ line.textStart(): ]
                  
                      elided_last_line = fontMetrics.elidedText( last_line, QC.Qt.ElideRight, self.width() )
                  
                      painter.drawText( QC.QPoint( 0, y + fontMetrics.ascent() ), elided_last_line )
                      
                      done = True
                      
                      break
                      
                  
  
              text_layout.endLayout()
              
              if done: break
              '''
              
          
  
  class Dialog( QW.QDialog ):
      
      def __init__( self, parent = None, **kwargs ):
  
          title = None 
          
          if 'title' in kwargs:
              
              title = kwargs['title']
              
              del kwargs['title']
              
          
          QW.QDialog.__init__( self, parent, **kwargs )
          
          self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
          
          if title is not None:
              
              self.setWindowTitle( title )
              
          
          self._closed_by_user = False
          
      
      def closeEvent( self, event ):
          
          if event.spontaneous():
              
              self._closed_by_user = True
              
          
          QW.QDialog.closeEvent( self, event )
          
      
      # True if the dialog was closed by the user clicking on the X on the titlebar (so neither reject nor accept was chosen - the dialog result is still reject in this case though)    
      def WasCancelled( self ):
          
          return self._closed_by_user
          
      
      def SetCancelled( self, closed ):
          
          self._closed_by_user = closed
          
      
      def __enter__( self ):
          
          return self
          
      
      def __exit__( self, exc_type, exc_val, exc_tb ):
          
          if isValid( self ):
              
              self.deleteLater()
              
          
      
  class PasswordEntryDialog( Dialog ):
      
      def __init__( self, parent, message, caption ):
          
          Dialog.__init__( self, parent )
          
          self.setWindowTitle( caption )
          
          self._ok_button = QW.QPushButton( 'OK', self )
          self._ok_button.clicked.connect( self.accept )
          
          self._cancel_button = QW.QPushButton( 'Cancel', self )
          self._cancel_button.clicked.connect( self.reject )
          
          self._password = QW.QLineEdit( self )
          self._password.setEchoMode( QW.QLineEdit.Password )
          
          self.setLayout( QW.QVBoxLayout() )
          
          entry_layout = QW.QHBoxLayout()
          
          entry_layout.addWidget( QW.QLabel( message, self ) )
          entry_layout.addWidget( self._password )
          
          button_layout = QW.QHBoxLayout()
          
          button_layout.addStretch( 1 )
          button_layout.addWidget( self._cancel_button )
          button_layout.addWidget( self._ok_button )
          
          self.layout().addLayout( entry_layout )
          self.layout().addLayout( button_layout )
          
  
      def GetValue( self ):
          
          return self._password.text()
          
  
  class DirDialog( QW.QFileDialog ):
  
      def __init__( self, parent = None, message = None ):
          
          QW.QFileDialog.__init__( self, parent )
          
          if message is not None: self.setWindowTitle( message )
          
          self.setAcceptMode( QW.QFileDialog.AcceptOpen )
          
          self.setFileMode( QW.QFileDialog.Directory )
          
          self.setOption( QW.QFileDialog.ShowDirsOnly, True )
          
          if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
              
              self.setOption( QW.QFileDialog.DontUseNativeDialog, True )
              
          
      
      def __enter__( self ):
          
          return self
          
      
      def __exit__( self, exc_type, exc_val, exc_tb ):
          
          self.deleteLater()
          
      
      def _GetSelectedFiles( self ):
          
          return [ os.path.normpath( path ) for path in self.selectedFiles() ]
          
      
      def GetPath(self):
          
          sel = self._GetSelectedFiles()
          
          if len( sel ) > 0:
              
              return sel[0]
              
          
          return None
  
  
  class FileDialog( QW.QFileDialog ):
      
      def __init__( self, parent = None, message = None, acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFile, default_filename = None, default_directory = None, wildcard = None, defaultSuffix = None ):
          
          QW.QFileDialog.__init__( self, parent )
          
          if message is not None:
              
              self.setWindowTitle( message )
              
          
          self.setAcceptMode( acceptMode )
          
          self.setFileMode( fileMode )
          
          if default_directory is not None:
              
              self.setDirectory( default_directory )
              
          
          if defaultSuffix is not None:
              
              self.setDefaultSuffix( defaultSuffix )
              
          
          if default_filename is not None:
              
              self.selectFile( default_filename )
              
          
          if wildcard:
              
              self.setNameFilter( wildcard )
              
          
          if HG.client_controller.new_options.GetBoolean( 'use_qt_file_dialogs' ):
              
              self.setOption( QW.QFileDialog.DontUseNativeDialog, True )
              
          
  
      def __enter__( self ):
  
          return self
          
  
      def __exit__( self, exc_type, exc_val, exc_tb ):
  
          self.deleteLater()
          
  
      def _GetSelectedFiles( self ):
          
          return [ os.path.normpath( path ) for path in self.selectedFiles() ]
          
      
      def GetPath( self ):
  
          sel = self._GetSelectedFiles()
  
          if len( sel ) > 0:
              
              return sel[ 0 ]
              
  
          return None
      
      
      def GetPaths( self ):
          
          return self._GetSelectedFiles()
  
  
  # A QTreeWidget where if an item is (un)checked, all its children are also (un)checked, recursively
  class TreeWidgetWithInheritedCheckState( QW.QTreeWidget ):
      
      def __init__( self, *args, **kwargs ):
          
          QW.QTreeWidget.__init__( self, *args, **kwargs )
          
          self.itemClicked.connect( self._HandleItemClickedForCheckStateUpdate )
          
      
      def _HandleItemClickedForCheckStateUpdate( self, item, column ):
          
          self._UpdateCheckState( item, item.checkState( 0 ) )
          
      
      def _UpdateCheckState( self, item, check_state ):
          
          # this is an int, should be a checkstate
          item.setCheckState( 0, check_state )
          
          for i in range( item.childCount() ):
              
              self._UpdateCheckState( item.child( i ), check_state )
              
          
      
  class ColourPickerCtrl( QW.QPushButton ):
      
      def __init__( self, parent = None ):
          
          QW.QPushButton.__init__( self, parent )
          
          self._colour = QG.QColor( 0, 0, 0, 0 )
          
          self.clicked.connect( self._ChooseColour )
          
          self._highlighted = False
      
      
      def SetColour( self, colour ):
          
          self._colour = colour
  
          self._UpdatePixmap()
          
  
      def _UpdatePixmap( self ):
          
          px = QG.QPixmap( self.contentsRect().height(), self.contentsRect().height() )
          
          painter = QG.QPainter( px )
          
          colour = self._colour
          
          if self._highlighted:
              
              colour = self._colour.lighter( 125 ) # 25% lighter
              
          
          painter.fillRect( px.rect(), QG.QBrush( colour ) )
          
          painter.end()
          
          self.setIcon( QG.QIcon( px ) )
          
          self.setIconSize( px.size() )
          
          self.setFlat( True )
          
          self.setFixedSize( px.size() )
          
      
      def enterEvent( self, event ):
          
          self._highlighted = True
          
          self._UpdatePixmap()
          
      
      def leaveEvent( self, event ):
          
          self._highlighted = False
          
          self._UpdatePixmap()
          
      
      def GetColour( self ):
          
          return self._colour
          
      
      def _ChooseColour( self ):
          
          new_colour = QW.QColorDialog.getColor( initial = self._colour )
          
          if new_colour.isValid():
              
              self.SetColour( new_colour )
              
          
      
  def ListsToTuples( l ): # Since lists are not hashable, we need to (recursively) convert lists to tuples in data that is to be added to BetterListCtrl
      
      if isinstance( l, list ) or isinstance( l, tuple ):
  
          return tuple( map( ListsToTuples, l ) )
  
      else:
          
          return l
  
  
  class WidgetEventFilter ( QC.QObject ):
      
      _mouse_tracking_required = { 'EVT_MOUSE_EVENTS' }
  
      _strong_focus_required = { 'EVT_KEY_DOWN' }
  
      def __init__( self, parent_widget ):
  
          self._parent_widget = parent_widget
          
          QC.QObject.__init__( self, parent_widget )
          
          parent_widget.installEventFilter( self )
          
          self._callback_map = defaultdict( list )
          
          self._user_moved_window = False # There is no EVT_MOVE_END in Qt so some trickery is required.
  
      def _ExecuteCallbacks( self, event_name, event ):
          
          if not event_name in self._callback_map: return
          
          event_killed = False
          
          for callback in self._callback_map[ event_name ]:
              
              if not callback( event ): event_killed = True
              
          return event_killed
  
  
      def eventFilter( self, watched, event ):
          
          # Once somehow this got called with no _parent_widget set - which is probably fixed now but leaving the check just in case, wew
          # Might be worth debugging this later if it still occurs - the only way I found to reproduce it is to run the help > debug > initialize server command
          if not hasattr( self, '_parent_widget') or not isValid( self._parent_widget ): return False
          
          type = event.type()
          
          event_killed = False
          
          if type == QC.QEvent.KeyPress:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_KEY_DOWN', event )
              
          elif type == QC.QEvent.Close:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_CLOSE', event )
              
          elif type == QC.QEvent.WindowStateChange:
              
              if isValid( self._parent_widget ):
                  
                  if self._parent_widget.isMinimized() or (event.oldState() & QC.Qt.WindowMinimized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_ICONIZE', event )
              
                  if self._parent_widget.isMaximized() or (event.oldState() & QC.Qt.WindowMaximized): event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MAXIMIZE', event )
          
          elif type == QC.QEvent.MouseMove:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
              
          elif type == QC.QEvent.MouseButtonDblClick:
              
              if event.button() == QC.Qt.LeftButton:
              
                  event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DCLICK', event )
                  
              elif event.button() == QC.Qt.RightButton:
  
                  event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DCLICK', event )
  
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
              
          elif type == QC.QEvent.MouseButtonPress:
              
              if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_DOWN', event )
              
              if event.buttons() & QC.Qt.MiddleButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MIDDLE_DOWN', event )
              
              if event.buttons() & QC.Qt.RightButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_RIGHT_DOWN', event )
  
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
              
          elif type == QC.QEvent.MouseButtonRelease:
              
              if event.buttons() & QC.Qt.LeftButton: event_killed = event_killed or self._ExecuteCallbacks( 'EVT_LEFT_UP', event )
  
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
              
          elif type == QC.QEvent.Wheel:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSEWHEEL', event )
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOUSE_EVENTS', event )
          
          elif type == QC.QEvent.Scroll:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SCROLLWIN', event )
              
          elif type == QC.QEvent.Move:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE', event )
              
              if isValid( self._parent_widget ) and self._parent_widget.isVisible():
                  
                  self._user_moved_window = True
              
          elif type == QC.QEvent.Resize:
              
              event_killed = event_killed or self._ExecuteCallbacks( 'EVT_SIZE', event )
              
          elif type == QC.QEvent.NonClientAreaMouseButtonPress:
              
              self._user_moved_window = False
          
          elif type == QC.QEvent.NonClientAreaMouseButtonRelease:
              
              if self._user_moved_window:
                  
                  event_killed = event_killed or self._ExecuteCallbacks( 'EVT_MOVE_END', event )
                  
                  self._user_moved_window = False
                  
              
          
          if event_killed:
              
              event.accept()
              
              return True
              
          
          return False
  
      
      def _AddCallback( self, evt_name, callback ):
          
          if evt_name in self._mouse_tracking_required:
              
              self._parent_widget.setMouseTracking( True )
              
          if evt_name in self._strong_focus_required:
              
              self._parent_widget.setFocusPolicy( QC.Qt.StrongFocus )
              
          self._callback_map[ evt_name ].append( callback )
  
      def EVT_CLOSE( self, callback ):
          
          self._AddCallback( 'EVT_CLOSE', callback )
  
      def EVT_ICONIZE( self, callback ):
          
          self._AddCallback( 'EVT_ICONIZE', callback )
  
      def EVT_KEY_DOWN( self, callback ):
          
          self._AddCallback( 'EVT_KEY_DOWN', callback )
  
      def EVT_LEFT_DCLICK( self, callback ):
          
          self._AddCallback( 'EVT_LEFT_DCLICK', callback )
  
      def EVT_RIGHT_DCLICK( self, callback ):
  
          self._AddCallback( 'EVT_RIGHT_DCLICK', callback )
          
      def EVT_LEFT_DOWN( self, callback ):
          
          self._AddCallback( 'EVT_LEFT_DOWN', callback )
  
      def EVT_LEFT_UP( self, callback ):
          
          self._AddCallback( 'EVT_LEFT_UP', callback )
  
      def EVT_MAXIMIZE( self, callback ):
          
          self._AddCallback( 'EVT_MAXIMIZE', callback )
  
      def EVT_MIDDLE_DOWN( self, callback ):
          
          self._AddCallback( 'EVT_MIDDLE_DOWN', callback )
  
      def EVT_MOUSE_EVENTS( self, callback ):
          
          self._AddCallback( 'EVT_MOUSE_EVENTS', callback )
  
      def EVT_MOUSEWHEEL( self, callback ):
          
          self._AddCallback( 'EVT_MOUSEWHEEL', callback )
  
      def EVT_MOVE( self, callback ):
          
          self._AddCallback( 'EVT_MOVE', callback )
  
      def EVT_MOVE_END( self, callback ):
          
          self._AddCallback( 'EVT_MOVE_END', callback )
  
      def EVT_RIGHT_DOWN( self, callback ):
          
          self._AddCallback( 'EVT_RIGHT_DOWN', callback )
  
      def EVT_SCROLLWIN( self, callback ):
          
          self._AddCallback( 'EVT_SCROLLWIN', callback )
  
      def EVT_SIZE( self, callback ):
          
          self._AddCallback( 'EVT_SIZE', callback )
  
  # wew lad
  # https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
  class CheckBoxDelegate(QW.QStyledItemDelegate):
      
      def __init__(self, parent=None):
          
          super( CheckBoxDelegate, self ).__init__(parent)
          
  
      def createEditor( self, parent, op, idx ):
          
          self.editor = QW.QCheckBox( parent )
          
  
  class CollectComboCtrl( QW.QComboBox ):
      
      itemChanged = QC.Signal()
      
      def __init__( self, parent, media_collect ):
          
          QW.QComboBox.__init__( self, parent )
          
          self.view().pressed.connect( self._HandleItemPressed )
          
          # this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
          self.setItemDelegate( CheckBoxDelegate() )
          
          self.setModel( QG.QStandardItemModel( self ) )
          
          text_and_data_tuples = set()
          
          for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
              
              namespaces = media_sort.GetNamespaces()
              
              text_and_data_tuples.update( namespaces )
              
          
          text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
          
          ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
          
          for ratings_service in ratings_services:
              
              text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
              
  
          for (text, data) in text_and_data_tuples:
  
              self.Append( text, data )
              
          # Trick to display custom text
          
          self._cached_text = ''
          
          if media_collect.DoesACollect():
              
              CallAfter( self.SetCollectByValue, media_collect )
              
          
  
      def paintEvent( self, e ):
          
          painter = QW.QStylePainter( self )
          painter.setPen( self.palette().color( QG.QPalette.Text ) )
  
          opt = QW.QStyleOptionComboBox()
          self.initStyleOption( opt )
  
          opt.currentText = self._cached_text
  
          painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
  
          painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
          
      
      def GetValues( self ):
  
          namespaces = [ ]
          rating_service_keys = [ ]
  
          for index in self.GetCheckedItems():
  
              (collect_type, collect_data) = self.itemData( index, QC.Qt.UserRole )
  
              if collect_type == 'namespace':
  
                  namespaces.append( collect_data )
  
              elif collect_type == 'rating':
  
                  rating_service_keys.append( collect_data )
  
          collect_strings = self.GetCheckedStrings()
  
          if len( collect_strings ) > 0:
              
              description = 'collect by ' + '-'.join( collect_strings )
              
          else:
  
              description = 'no collections'
              
  
          return ( namespaces, rating_service_keys, description )
  
  
      def hidePopup(self):
  
          if not self.view().underMouse():
              
              QW.QComboBox.hidePopup( self )
              
              
      def SetValue( self, text ):
          
          self._cached_text = text
             
          self.setCurrentText( text )
          
          
      def SetCollectByValue( self, media_collect ):
  
          try:
  
              indices_to_check = []
  
              for index in range( self.count() ):
  
                  ( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
  
                  p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
                  p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
  
                  if p1 or p2:
                      
                      indices_to_check.append( index )
  
              self.SetCheckedItems( indices_to_check )
              
              self.itemChanged.emit()
  
          except Exception as e:
  
              HydrusData.ShowText( 'Failed to set a collect-by value!' )
  
              HydrusData.ShowException( e )
  
  
      def SetCheckedItems( self, indices_to_check ):
          
          for idx in range( self.count() ):
  
              item = self.model().item( idx )
              
              if idx in indices_to_check:
                  
                  item.setCheckState( QC.Qt.Checked )
                  
              else:
                  
                  item.setCheckState( QC.Qt.Unchecked )
                  
              
          
      
      def GetCheckedItems( self ):
          
          indices = []
          
          for idx in range( self.count() ):
  
              item = self.model().item( idx )
              
              if item.checkState() == QC.Qt.Checked: indices.append( idx )
              
          return indices
  
  
      def GetCheckedStrings( self ):
  
          strings = [ ]
  
          for idx in range( self.count() ):
  
              item = self.model().item( idx )
  
              if item.checkState() == QC.Qt.Checked: strings.append( item.text() )
  
          return strings
  
  
      def Append( self, str, data ):
          
          self.addItem( str, userData = data )
          
          item = self.model().item( self.count() - 1, 0 )
          
          item.setCheckState( QC.Qt.Unchecked )
  
  
      def _HandleItemPressed( self, index ):
          
          item = self.model().itemFromIndex( index )
          
          if item.checkState() == QC.Qt.Checked:
              
              item.setCheckState( QC.Qt.Unchecked )
              
          else:
              
              item.setCheckState( QC.Qt.Checked )
              
          self.SetValue( self._cached_text )
          self.itemChanged.emit()
  