import os import re import typing from qtpy import QtCore as QC, QtWidgets as QW from qtpy import QtWidgets as QW from qtpy import QtGui as QG from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusGlobals as HG from hydrus.client import ClientApplicationCommand as CAC from hydrus.client import ClientConstants as CC from hydrus.client import ClientPaths from hydrus.client.gui import ClientGUICore as CGC from hydrus.client.gui import ClientGUIFunctions from hydrus.client.gui import ClientGUIMenus from hydrus.client.gui import ClientGUIShortcuts from hydrus.client.gui import QtPorting as QP from hydrus.client.gui.widgets import ClientGUIColourPicker def AddGridboxStretchSpacer( layout: QW.QGridLayout ): layout.addItem( QW.QSpacerItem( 10, 10, QW.QSizePolicy.Expanding, QW.QSizePolicy.Fixed ) ) def WrapInGrid( parent, rows, expand_text = False, add_stretch_at_end = True ): gridbox = QP.GridLayout( cols = 2 ) if expand_text: gridbox.setColumnStretch( 0, 1 ) text_flags = CC.FLAGS_EXPAND_BOTH_WAYS control_flags = CC.FLAGS_CENTER_PERPENDICULAR sizer_flags = CC.FLAGS_CENTER_PERPENDICULAR else: gridbox.setColumnStretch( 1, 1 ) text_flags = CC.FLAGS_ON_LEFT control_flags = CC.FLAGS_NONE sizer_flags = CC.FLAGS_EXPAND_SIZER_BOTH_WAYS for ( text, control ) in rows: if isinstance( text, BetterStaticText ): st = text else: st = BetterStaticText( parent, text ) possible_tooltip_widget = None if isinstance( control, QW.QLayout ): cflags = sizer_flags if control.count() > 0: possible_widget_item = control.itemAt( 0 ) if isinstance( possible_widget_item, QW.QWidgetItem ): possible_tooltip_widget = possible_widget_item.widget() else: cflags = control_flags possible_tooltip_widget = control if possible_tooltip_widget is not None and isinstance( possible_tooltip_widget, QW.QWidget ) and possible_tooltip_widget.toolTip() != '': st.setToolTip( possible_tooltip_widget.toolTip() ) QP.AddToLayout( gridbox, st, text_flags ) QP.AddToLayout( gridbox, control, cflags ) if add_stretch_at_end: gridbox.setRowStretch( gridbox.rowCount(), 1 ) return gridbox def WrapInText( control, parent, text, object_name = None ): hbox = QP.HBoxLayout() st = BetterStaticText( parent, text ) if object_name is not None: st.setObjectName( object_name ) QP.AddToLayout( hbox, st, CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, control, CC.FLAGS_EXPAND_BOTH_WAYS ) return hbox class ShortcutAwareToolTipMixin( object ): def __init__( self, tt_callable ): self._tt_callable = tt_callable self._tt = '' self._simple_shortcut_command = None if ClientGUIShortcuts.shortcuts_manager_initialised(): ClientGUIShortcuts.shortcuts_manager().shortcutsChanged.connect( self.RefreshToolTip ) def _RefreshToolTip( self ): tt = self._tt if self._simple_shortcut_command is not None: tt += os.linesep * 2 tt += '----------' names_to_shortcuts = ClientGUIShortcuts.shortcuts_manager().GetNamesToShortcuts( self._simple_shortcut_command ) if len( names_to_shortcuts ) > 0: names = sorted( names_to_shortcuts.keys() ) for name in names: shortcuts = names_to_shortcuts[ name ] shortcut_strings = sorted( ( shortcut.ToString() for shortcut in shortcuts ) ) if name in ClientGUIShortcuts.shortcut_names_to_pretty_names: pretty_name = ClientGUIShortcuts.shortcut_names_to_pretty_names[ name ] else: pretty_name = name tt += os.linesep * 2 tt += ', '.join( shortcut_strings ) tt += os.linesep tt += '({}->{})'.format( pretty_name, CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) else: tt += os.linesep * 2 tt += 'no shortcuts set' tt += os.linesep tt += '({})'.format( CAC.simple_enum_to_str_lookup[ self._simple_shortcut_command ] ) self._tt_callable( tt ) def RefreshToolTip( self ): if ClientGUIShortcuts.shortcuts_manager_initialised(): self._RefreshToolTip() def SetToolTipWithShortcuts( self, tt: str, simple_shortcut_command: int ): self._tt = tt self._simple_shortcut_command = simple_shortcut_command self._RefreshToolTip() class BetterBitmapButton( ShortcutAwareToolTipMixin, QW.QPushButton ): def __init__( self, parent, bitmap, func, *args, **kwargs ): QW.QPushButton.__init__( self, parent ) self.setIcon( QG.QIcon( bitmap ) ) self.setIconSize( bitmap.size() ) self.setSizePolicy( QW.QSizePolicy.Maximum, QW.QSizePolicy.Maximum ) ShortcutAwareToolTipMixin.__init__( self, self.setToolTip ) self._func = func self._args = args self._kwargs = kwargs self.clicked.connect( self.EventButton ) def EventButton( self ): self._func( *self._args, **self._kwargs ) class BetterButton( ShortcutAwareToolTipMixin, QW.QPushButton ): def __init__( self, parent, label, func, *args, **kwargs ): QW.QPushButton.__init__( self, parent ) ShortcutAwareToolTipMixin.__init__( self, self.setToolTip ) self.setText( label ) self._func = func self._args = args self._kwargs = kwargs self._yes_no_text = None self.clicked.connect( self.EventButton ) def EventButton( self ): if self._yes_no_text is not None: from hydrus.client.gui import ClientGUIDialogsQuick result = ClientGUIDialogsQuick.GetYesNo( self, message = self._yes_no_text ) if result != QW.QDialog.Accepted: return self._func( *self._args, **self._kwargs ) def SetYesNoText( self, text: str ): # this should probably be setyesnotextfactory, but WHATEVER for now self._yes_no_text = text def setText( self, label ): button_label = ClientGUIFunctions.EscapeMnemonics( label ) QW.QPushButton.setText( self, button_label ) class BetterCheckBoxList( QW.QListWidget ): checkBoxListChanged = QC.Signal() rightClicked = QC.Signal() def __init__( self, parent: QW.QWidget ): QW.QListWidget.__init__( self, parent ) self.itemClicked.connect( self._ItemCheckStateChanged ) self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection ) def _ItemCheckStateChanged( self, item ): self.checkBoxListChanged.emit() def Append( self, text, data, starts_checked = False ): item = QW.QListWidgetItem() item.setFlags( item.flags() | QC.Qt.ItemIsUserCheckable ) qt_state = QC.Qt.Checked if starts_checked else QC.Qt.Unchecked item.setCheckState( qt_state ) item.setText( text ) item.setData( QC.Qt.UserRole, data ) self.addItem( item ) self._ItemCheckStateChanged( item ) def Check( self, index: int, value: bool = True ): qt_state = QC.Qt.Checked if value else QC.Qt.Unchecked item = self.item( index ) item.setCheckState( qt_state ) self._ItemCheckStateChanged( item ) def Flip( self, index: int ): self.Check( index, not self.IsChecked( index ) ) def GetData( self, index: int ): return self.item( index ).data( QC.Qt.UserRole ) def GetCheckedIndices( self ) -> typing.List[ int ]: checked_indices = [ i for i in range( self.count() ) if self.IsChecked( i ) ] return checked_indices def GetSelectedIndices( self ): selected_indices = [ i for i in range( self.count() ) if self.IsSelected( i ) ] return selected_indices def GetValue( self ): result = [ self.GetData( index ) for index in self.GetCheckedIndices() ] return result def IsChecked( self, index: int ) -> bool: return self.item( index ).checkState() == QC.Qt.Checked def IsSelected( self, index: int ) -> bool: return self.item( index ).isSelected() def SetValue( self, datas: typing.Collection ): for index in range( self.count() ): data = self.GetData( 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 BetterChoice( QW.QComboBox ): def __init__( self, *args, **kwargs ): QW.QComboBox.__init__( self, *args, **kwargs ) self.setMaxVisibleItems( 32 ) def addItem( self, display_string, client_data ): QW.QComboBox.addItem( self, display_string, client_data ) if self.count() == 1: self.setCurrentIndex( 0 ) def GetValue( self ): selection = self.currentIndex() if selection != -1: return self.itemData( selection, QC.Qt.UserRole ) elif self.count() > 0: return self.itemData( 0, QC.Qt.UserRole ) else: return None def SetValue( self, data ): for i in range( self.count() ): if data == self.itemData( i, QC.Qt.UserRole ): self.setCurrentIndex( i ) return if self.count() > 0: self.setCurrentIndex( 0 ) class BetterNotebook( QW.QTabWidget ): def _ShiftSelection( self, delta ): existing_selection = self.currentIndex() if existing_selection != -1: new_selection = ( existing_selection + delta ) % self.count() if new_selection != existing_selection: self.setCurrentIndex( new_selection ) def DeleteAllPages( self ): while self.count() > 0: page = self.widget( 0 ) self.removeTab( 0 ) page.deleteLater() def GetPages( self ): return [ self.widget( i ) for i in range( self.count() ) ] def SelectLeft( self ): self._ShiftSelection( -1 ) def SelectPage( self, page ): for i in range( self.count() ): if self.widget( i ) == page: self.setCurrentIndex( i ) return def SelectRight( self ): self._ShiftSelection( 1 ) class BetterSpinBox( QW.QSpinBox ): def __init__( self, parent: QW.QWidget, initial = None, min = None, max = None, width = None ): QW.QSpinBox.__init__( self, parent ) if min is not None: self.setMinimum( min ) if max is not None: self.setMaximum( max ) if initial is not None: self.setValue( initial ) if width is not None: self.setMinimumWidth( width ) class ButtonWithMenuArrow( QW.QToolButton ): def __init__( self, parent: QW.QWidget, action: QW.QAction ): QW.QToolButton.__init__( self, parent ) self.setPopupMode( QW.QToolButton.MenuButtonPopup ) self.setToolButtonStyle( QC.Qt.ToolButtonTextOnly ) self.setDefaultAction( action ) self._menu = QW.QMenu( self ) self._menu.installEventFilter( self ) self.setMenu( self._menu ) self._menu.aboutToShow.connect( self._ClearAndPopulateMenu ) def _ClearAndPopulateMenu( self ): self._menu.clear() self._PopulateMenu( self._menu ) def _PopulateMenu( self, menu ): raise NotImplementedError() def eventFilter( self, watched, event ): if event.type() == QC.QEvent.Show and watched == self._menu: pos = QG.QCursor.pos() self._menu.move( pos ) return True return False class BetterRadioBox( QP.RadioBox ): def __init__( self, *args, **kwargs ): self._indices_to_data = { i : data for ( i, ( s, data ) ) in enumerate( kwargs[ 'choices' ] ) } kwargs[ 'choices' ] = [ s for ( s, data ) in kwargs[ 'choices' ] ] QP.RadioBox.__init__( self, *args, **kwargs ) def GetValue( self ): index = self.GetCurrentIndex() return self._indices_to_data[ index ] def SetValue( self, data ): for ( i, d ) in self._indices_to_data.items(): if d == data: self.Select( i ) return class BetterStaticText( QP.EllipsizedLabel ): def __init__( self, parent, label = None, tooltip_label = False, **kwargs ): ellipsize_end = 'ellipsize_end' in kwargs and kwargs[ 'ellipsize_end' ] QP.EllipsizedLabel.__init__( self, parent, ellipsize_end = ellipsize_end ) # otherwise by default html in 'this is a
parsing step' stuff renders fully lmaoooo self.setTextFormat( QC.Qt.PlainText ) self._tooltip_label = tooltip_label if 'ellipsize_end' in kwargs and kwargs[ 'ellipsize_end' ]: self._tooltip_label = True self._last_set_text = '' # we want a separate copy since the one we'll send to the st will be wrapped and have additional '\n's self._wrap_width = None if label is not None: self.setText( label ) def clear( self ): self._last_set_text = '' QP.EllipsizedLabel.clear( self ) def setText( self, text ): # this doesn't need mnemonic escape _unless_ a buddy is set, wew lad if text != self._last_set_text: self._last_set_text = text QP.EllipsizedLabel.setText( self, text ) if self._tooltip_label: self.setToolTip( text ) class BetterHyperLink( BetterStaticText ): def __init__( self, parent, label, url ): BetterStaticText.__init__( self, parent, label ) self._url = url self.setToolTip( self._url ) self.setTextFormat( QC.Qt.RichText ) self.setTextInteractionFlags( QC.Qt.LinksAccessibleByMouse | QC.Qt.LinksAccessibleByKeyboard ) self._colours = { 'link_color' : QG.QColor( 0, 0, 255 ) } self.setObjectName( 'HydrusHyperlink' ) # need this polish to load the QSS property and update self._colours self.style().polish( self ) self.setText( '{}'.format( self._colours[ 'link_color' ].name(), url, label ) ) self.linkActivated.connect( self.Activated ) def Activated( self ): ClientPaths.LaunchURLInWebBrowser( self._url ) def get_link_color( self ): return self._colours[ 'link_color' ] def set_link_color( self, colour ): self._colours[ 'link_color' ] = colour link_color = QC.Property( QG.QColor, get_link_color, set_link_color ) class BufferedWindow( QW.QWidget ): def __init__( self, *args, **kwargs ): QW.QWidget.__init__( self, *args ) if 'size' in kwargs: size = kwargs[ 'size' ] if isinstance( size, QC.QSize ): self.setFixedSize( kwargs[ 'size' ] ) def _Draw( self, painter ): raise NotImplementedError() def paintEvent( self, event ): painter = QG.QPainter( self ) self._Draw( painter ) class BufferedWindowIcon( BufferedWindow ): def __init__( self, parent, pixmap: QG.QPixmap, click_callable = None ): device_independant_size = pixmap.size() / pixmap.devicePixelRatio() BufferedWindow.__init__( self, parent, size = device_independant_size ) self._pixmap = pixmap self._click_callable = click_callable def _Draw( self, painter ): background_colour = QP.GetBackgroundColour( self.parentWidget() ) painter.setBackground( QG.QBrush( background_colour ) ) painter.eraseRect( painter.viewport() ) painter.setRenderHint( QG.QPainter.SmoothPixmapTransform, True ) # makes any scaling here due to jank thumbs look good if isinstance( self._pixmap, QG.QImage ): painter.drawImage( self.rect(), self._pixmap ) else: painter.drawPixmap( self.rect(), self._pixmap ) def mousePressEvent( self, event ): if self._click_callable is None: return BufferedWindow.mousePressEvent( self, event ) else: self._click_callable() class BusyCursor( object ): def __enter__( self ): QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor ) def __exit__( self, exc_type, exc_val, exc_tb ): QW.QApplication.restoreOverrideCursor() class CheckboxManager( object ): def GetCurrentValue( self ): raise NotImplementedError() def Invert( self ): raise NotImplementedError() class CheckboxManagerBoolean( CheckboxManager ): def __init__( self, obj, name ): CheckboxManager.__init__( self ) self._obj = obj self._name = name def GetCurrentValue( self ): if not self._obj: return False return getattr( self._obj, self._name ) def Invert( self ): if not self._obj: return value = getattr( self._obj, self._name ) setattr( self._obj, self._name, not value ) class CheckboxManagerCalls( CheckboxManager ): def __init__( self, invert_call, value_call ): CheckboxManager.__init__( self ) self._invert_call = invert_call self._value_call = value_call def GetCurrentValue( self ): return self._value_call() def Invert( self ): self._invert_call() class CheckboxManagerOptions( CheckboxManager ): def __init__( self, boolean_name ): CheckboxManager.__init__( self ) self._boolean_name = boolean_name def GetCurrentValue( self ): new_options = HG.client_controller.new_options return new_options.GetBoolean( self._boolean_name ) def Invert( self ): new_options = HG.client_controller.new_options new_options.InvertBoolean( self._boolean_name ) if self._boolean_name == 'advanced_mode': HG.client_controller.pub( 'notify_advanced_mode' ) HG.client_controller.pub( 'checkbox_manager_inverted' ) HG.client_controller.pub( 'notify_new_menu_option' ) class AlphaColourControl( QW.QWidget ): def __init__( self, parent ): QW.QWidget.__init__( self, parent ) self._colour_picker = ClientGUIColourPicker.ColourPickerButton( self ) self._alpha_selector = BetterSpinBox( self, min=0, max=255 ) hbox = QP.HBoxLayout( spacing = 5 ) QP.AddToLayout( hbox, self._colour_picker, CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, BetterStaticText(self,'alpha:'), CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, self._alpha_selector, CC.FLAGS_CENTER_PERPENDICULAR ) hbox.addStretch( 1 ) self.setLayout( hbox ) def GetValue( self ): colour = self._colour_picker.GetColour() a = self._alpha_selector.value() colour.setAlpha( a ) return colour def SetValue( self, colour: QG.QColor ): picker_colour = QG.QColor( colour.rgb() ) self._colour_picker.SetColour( picker_colour ) self._alpha_selector.setValue( colour.alpha() ) class ExportPatternButton( BetterButton ): def __init__( self, parent ): BetterButton.__init__( self, parent, 'pattern shortcuts', self._Hit ) def _Hit( self ): menu = QW.QMenu() ClientGUIMenus.AppendMenuLabel( menu, 'click on a phrase to copy to clipboard' ) ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( menu, 'unique numerical file id - {file_id}', 'copy "{file_id}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{file_id}' ) ClientGUIMenus.AppendMenuItem( menu, 'the file\'s hash - {hash}', 'copy "{hash}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{hash}' ) ClientGUIMenus.AppendMenuItem( menu, 'all the file\'s tags - {tags}', 'copy "{tags}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{tags}' ) ClientGUIMenus.AppendMenuItem( menu, 'all the file\'s non-namespaced tags - {nn tags}', 'copy "{nn tags}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{nn tags}' ) ClientGUIMenus.AppendMenuItem( menu, 'file order - {#}', 'copy "{#}" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '{#}' ) ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( menu, 'all instances of a particular namespace - [\u2026]', 'copy "[\u2026]" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '[\u2026]' ) ClientGUIMenus.AppendSeparator( menu ) ClientGUIMenus.AppendMenuItem( menu, 'a particular tag, if the file has it - (\u2026)', 'copy "(\u2026)" to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(\u2026)' ) CGC.core().PopupMenu( self, menu ) class Gauge( QW.QProgressBar ): def __init__( self, *args, **kwargs ): QW.QProgressBar.__init__( self, *args, **kwargs ) self._actual_value = None self._actual_range = None self._is_pulsing = False self.SetRange( 1 ) self.SetValue( 0 ) def GetValueRange( self ): if self._actual_range is None: range = self.maximum() else: range = self._actual_range return ( self._actual_value, range ) def SetRange( self, range ): if range is None or range == 0: self.Pulse() else: if self._is_pulsing: self.StopPulsing() if range > 1000: self._actual_range = range range = 1000 else: self._actual_range = None if range != self.maximum(): QW.QProgressBar.setMaximum( self, range ) def SetValue( self, value ): self._actual_value = value if not self._is_pulsing: if value is None: self.Pulse() else: if self._actual_range is not None: value = min( int( 1000 * ( value / self._actual_range ) ), 1000 ) value = min( value, self.maximum() ) if value != self.value(): QW.QProgressBar.setValue( self, value ) def StopPulsing( self ): self._is_pulsing = False self.SetRange( 1 ) self.SetValue( 0 ) def Pulse( self ): # pulse looked stupid, was turning on too much, should improve it later #self.setMaximum( 0 ) #self.setMinimum( 0 ) self.SetRange( 1 ) self.SetValue( 0 ) self._is_pulsing = True class ListBook( QW.QWidget ): def __init__( self, *args, **kwargs ): QW.QWidget.__init__( self, *args, **kwargs ) self._keys_to_active_pages = {} self._keys_to_proto_pages = {} self._list_box = QW.QListWidget( self ) self._list_box.setSelectionMode( QW.QListWidget.SingleSelection ) self._empty_panel = QW.QWidget( self ) self._current_key = None self._current_panel = self._empty_panel self._panel_sizer = QP.VBoxLayout() QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) hbox = QP.HBoxLayout( margin = 0 ) QP.AddToLayout( hbox, self._list_box, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( hbox, self._panel_sizer, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) self._list_box.itemSelectionChanged.connect( self.EventSelection ) self.setLayout( hbox ) def _ActivatePage( self, key ): ( classname, args, kwargs ) = self._keys_to_proto_pages[ key ] page = classname( *args, **kwargs ) page.setVisible( False ) QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) self._keys_to_active_pages[ key ] = page del self._keys_to_proto_pages[ key ] def _GetIndex( self, key ): for i in range( self._list_box.count() ): i_key = self._list_box.item( i ).data( QC.Qt.UserRole ) if i_key == key: return i return -1 def _Select( self, selection ): if selection == -1: self._current_key = None else: self._current_key = self._list_box.item( selection ).data( QC.Qt.UserRole ) self._current_panel.setVisible( False ) self._list_box.blockSignals( True ) QP.ListWidgetSetSelection( self._list_box, selection ) self._list_box.blockSignals( False ) if selection == -1: self._current_panel = self._empty_panel else: if self._current_key in self._keys_to_proto_pages: self._ActivatePage( self._current_key ) self._current_panel = self._keys_to_active_pages[ self._current_key ] self._current_panel.show() self.update() def AddPage( self, display_name, key, page, select = False ): if self._GetIndex( key ) != -1: raise HydrusExceptions.NameException( 'That entry already exists!' ) if not isinstance( page, tuple ): page.setVisible( False ) QP.AddToLayout( self._panel_sizer, page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) # Could call QListWidget.sortItems() here instead of doing it manually current_display_names = QP.ListWidgetGetStrings( self._list_box ) insertion_index = len( current_display_names ) for ( i, current_display_name ) in enumerate( current_display_names ): if current_display_name > display_name: insertion_index = i break item = QW.QListWidgetItem() item.setText( display_name ) item.setData( QC.Qt.UserRole, key ) self._list_box.insertItem( insertion_index, item ) self._keys_to_active_pages[ key ] = page if self._list_box.count() == 1: self._Select( 0 ) elif select: index = self._GetIndex( key ) self._Select( index ) def AddPageArgs( self, display_name, key, classname, args, kwargs ): if self._GetIndex( key ) != -1: raise HydrusExceptions.NameException( 'That entry already exists!' ) # Could call QListWidget.sortItems() here instead of doing it manually current_display_names = QP.ListWidgetGetStrings( self._list_box ) insertion_index = len( current_display_names ) for ( i, current_display_name ) in enumerate( current_display_names ): if current_display_name > display_name: insertion_index = i break item = QW.QListWidgetItem() item.setText( display_name ) item.setData( QC.Qt.UserRole, key ) self._list_box.insertItem( insertion_index, item ) self._keys_to_proto_pages[ key ] = ( classname, args, kwargs ) if self._list_box.count() == 1: self._Select( 0 ) def DeleteAllPages( self ): self._panel_sizer.removeWidget( self._empty_panel ) QP.ClearLayout( self._panel_sizer, delete_widgets=True ) QP.AddToLayout( self._panel_sizer, self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS ) self._current_key = None self._current_panel = self._empty_panel self._keys_to_active_pages = {} self._keys_to_proto_pages = {} self._list_box.clear() def DeleteCurrentPage( self ): selection = QP.ListWidgetGetSelection( self._list_box ) if selection != -1: key_to_delete = self._current_key page_to_delete = self._current_panel next_selection = selection + 1 previous_selection = selection - 1 if next_selection < self._list_box.count(): self._Select( next_selection ) elif previous_selection >= 0: self._Select( previous_selection ) else: self._Select( -1 ) self._panel_sizer.removeWidget( page_to_delete ) page_to_delete.deleteLater() del self._keys_to_active_pages[ key_to_delete ] QP.ListWidgetDelete( self._list_box, selection ) def EventSelection( self ): selection = QP.ListWidgetGetSelection( self._list_box ) if selection != self._GetIndex( self._current_key ): self._Select( selection ) def GetCurrentKey( self ): return self._current_key def GetCurrentPage( self ): if self._current_panel == self._empty_panel: return None else: return self._current_panel def GetActivePages( self ): return list(self._keys_to_active_pages.values()) def GetPage( self, key ): if key in self._keys_to_proto_pages: self._ActivatePage( key ) if key in self._keys_to_active_pages: return self._keys_to_active_pages[ key ] raise Exception( 'That page not found!' ) def GetPageCount( self ): return len( self._keys_to_active_pages ) + len( self._keys_to_proto_pages ) def KeyExists( self, key ): return key in self._keys_to_active_pages or key in self._keys_to_proto_pages def Select( self, key ): index = self._GetIndex( key ) if index != -1 and index != QP.ListWidgetGetSelection( self._list_box ) : self._Select( index ) def SelectDown( self ): current_selection = QP.ListWidgetGetSelection( self._list_box ) if current_selection != -1: num_entries = self._list_box.count() if current_selection == num_entries - 1: selection = 0 else: selection = current_selection + 1 if selection != current_selection: self._Select( selection ) def SelectPage( self, page_to_select ): for ( key, page ) in list(self._keys_to_active_pages.items()): if page == page_to_select: self._Select( self._GetIndex( key ) ) return def SelectUp( self ): current_selection = QP.ListWidgetGetSelection( self._list_box ) if current_selection != -1: num_entries = self._list_box.count() if current_selection == 0: selection = num_entries - 1 else: selection = current_selection - 1 if selection != current_selection: self._Select( selection ) class NoneableSpinCtrl( QW.QWidget ): valueChanged = QC.Signal() def __init__( self, parent, message = '', none_phrase = 'no limit', min = 0, max = 1000000, unit = None, multiplier = 1, num_dimensions = 1 ): QW.QWidget.__init__( self, parent ) self._unit = unit self._multiplier = multiplier self._num_dimensions = num_dimensions self._checkbox = QW.QCheckBox( self ) self._checkbox.stateChanged.connect( self.EventCheckBox ) self._checkbox.setText( none_phrase ) self._one = BetterSpinBox( self, min=min, max=max ) width = ClientGUIFunctions.ConvertTextToPixelWidth( self._one, len( str( max ) ) + 5 ) self._one.setMaximumWidth( width ) if num_dimensions == 2: self._two = BetterSpinBox( self, initial=0, min=min, max=max ) self._two.valueChanged.connect( self._HandleValueChanged ) width = ClientGUIFunctions.ConvertTextToPixelWidth( self._two, len( str( max ) ) + 5 ) self._two.setMinimumWidth( width ) hbox = QP.HBoxLayout( margin = 0 ) if len( message ) > 0: QP.AddToLayout( hbox, BetterStaticText(self,message+': '), CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, self._one, CC.FLAGS_CENTER_PERPENDICULAR ) if self._num_dimensions == 2: QP.AddToLayout( hbox, BetterStaticText(self,'x'), CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, self._two, CC.FLAGS_CENTER_PERPENDICULAR ) if self._unit is not None: QP.AddToLayout( hbox, BetterStaticText(self,self._unit), CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, self._checkbox, CC.FLAGS_CENTER_PERPENDICULAR ) hbox.addStretch( 1 ) self.setLayout( hbox ) self._one.valueChanged.connect( self._HandleValueChanged ) self._checkbox.stateChanged.connect( self._HandleValueChanged ) def _HandleValueChanged( self, val ): self.valueChanged.emit() def EventCheckBox( self, state ): if self._checkbox.isChecked(): self._one.setEnabled( False ) if self._num_dimensions == 2: self._two.setEnabled( False ) else: self._one.setEnabled( True ) if self._num_dimensions == 2: self._two.setEnabled( True ) def GetValue( self ): if self._checkbox.isChecked(): return None else: if self._num_dimensions == 2: return ( self._one.value() * self._multiplier, self._two.value() * self._multiplier ) else: return self._one.value() * self._multiplier def setToolTip( self, text ): QW.QWidget.setToolTip( self, text ) for c in self.children(): if isinstance( c, QW.QWidget ): c.setToolTip( text ) def SetValue( self, value ): if value is None: self._checkbox.setChecked( True ) self._one.setEnabled( False ) if self._num_dimensions == 2: self._two.setEnabled( False ) else: self._checkbox.setChecked( False ) if self._num_dimensions == 2: self._two.setEnabled( True ) ( value, y ) = value self._two.setValue( y // self._multiplier ) self._one.setEnabled( True ) self._one.setValue( value // self._multiplier ) class NoneableTextCtrl( QW.QWidget ): valueChanged = QC.Signal() def __init__( self, parent, message = '', none_phrase = 'none' ): QW.QWidget.__init__( self, parent ) self._checkbox = QW.QCheckBox( self ) self._checkbox.stateChanged.connect( self.EventCheckBox ) self._checkbox.setText( none_phrase ) self._text = QW.QLineEdit( self ) hbox = QP.HBoxLayout( margin = 0 ) if len( message ) > 0: QP.AddToLayout( hbox, BetterStaticText(self,message+': '), CC.FLAGS_CENTER_PERPENDICULAR ) QP.AddToLayout( hbox, self._text, CC.FLAGS_EXPAND_BOTH_WAYS ) QP.AddToLayout( hbox, self._checkbox, CC.FLAGS_CENTER_PERPENDICULAR ) self.setLayout( hbox ) self._checkbox.stateChanged.connect( self._HandleValueChanged ) self._text.textChanged.connect( self._HandleValueChanged ) def _HandleValueChanged( self, val ): self.valueChanged.emit() def EventCheckBox( self, state ): if self._checkbox.isChecked(): self._text.setEnabled( False ) else: self._text.setEnabled( True ) def GetValue( self ): if self._checkbox.isChecked(): return None else: return self._text.text() def setPlaceholderText( self, text: str ): self._text.setPlaceholderText( text ) def setToolTip( self, text ): QW.QWidget.setToolTip( self, text ) for c in self.children(): if isinstance( c, QW.QWidget ): c.setToolTip( text ) def SetValue( self, value ): if value is None: self._checkbox.setChecked( True ) self._text.setEnabled( False ) else: self._checkbox.setChecked( False ) self._text.setEnabled( True ) self._text.setText( value ) class OnOffButton( QW.QPushButton ): valueChanged = QC.Signal( bool ) def __init__( self, parent, on_label, off_label = None, start_on = True ): if start_on: label = on_label else: label = off_label QW.QPushButton.__init__( self, parent ) QW.QPushButton.setText( self, label ) self.setObjectName( 'HydrusOnOffButton' ) self._on_label = on_label if off_label is None: self._off_label = on_label else: self._off_label = off_label self.setProperty( 'hydrus_on', start_on ) self.clicked.connect( self.Flip ) def _SetValue( self, value ): self.setProperty( 'hydrus_on', value ) if value: self.setText( self._on_label ) else: self.setText( self._off_label ) self.valueChanged.emit( value ) self.style().polish( self ) def Flip( self ): new_value = not self.property( 'hydrus_on' ) self._SetValue( new_value ) def IsOn( self ): return self.property( 'hydrus_on' ) def SetOnOff( self, value ): self._SetValue( value ) class RegexButton( BetterButton ): def __init__( self, parent ): BetterButton.__init__( self, parent, 'regex shortcuts', self._ShowMenu ) def _ShowMenu( self ): menu = QW.QMenu() ClientGUIMenus.AppendMenuLabel( menu, 'click on a phrase to copy it to the clipboard' ) ClientGUIMenus.AppendSeparator( menu ) submenu = QW.QMenu( menu ) ClientGUIMenus.AppendMenuItem( submenu, r'whitespace character - \s', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'\s' ) ClientGUIMenus.AppendMenuItem( submenu, r'number character - \d', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'\d' ) ClientGUIMenus.AppendMenuItem( submenu, r'alphanumeric or backspace character - \w', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'\w' ) ClientGUIMenus.AppendMenuItem( submenu, r'any character - .', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'.' ) ClientGUIMenus.AppendMenuItem( submenu, r'backslash character - \\', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'\\' ) ClientGUIMenus.AppendMenuItem( submenu, r'beginning of line - ^', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'^' ) ClientGUIMenus.AppendMenuItem( submenu, r'end of line - $', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'$' ) ClientGUIMenus.AppendMenuItem( submenu, 'any of these - [\u2026]', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '[\u2026]' ) ClientGUIMenus.AppendMenuItem( submenu, 'anything other than these - [^\u2026]', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '[^\u2026]' ) ClientGUIMenus.AppendSeparator( submenu ) ClientGUIMenus.AppendMenuItem( submenu, r'0 or more matches, consuming as many as possible - *', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'*' ) ClientGUIMenus.AppendMenuItem( submenu, r'1 or more matches, consuming as many as possible - +', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'+' ) ClientGUIMenus.AppendMenuItem( submenu, r'0 or 1 matches, preferring 1 - ?', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'?' ) ClientGUIMenus.AppendMenuItem( submenu, r'0 or more matches, consuming as few as possible - *?', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'*?' ) ClientGUIMenus.AppendMenuItem( submenu, r'1 or more matches, consuming as few as possible - +?', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'+?' ) ClientGUIMenus.AppendMenuItem( submenu, r'0 or 1 matches, preferring 0 - ??', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'??' ) ClientGUIMenus.AppendMenuItem( submenu, r'exactly m matches - {m}', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'{m}' ) ClientGUIMenus.AppendMenuItem( submenu, r'm to n matches, consuming as many as possible - {m,n}', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'{m,n}' ) ClientGUIMenus.AppendMenuItem( submenu, r'm to n matches, consuming as few as possible - {m,n}?', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'{m,n}?' ) ClientGUIMenus.AppendSeparator( submenu ) ClientGUIMenus.AppendMenuItem( submenu, 'the next characters are: (non-consuming) - (?=\u2026)', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(?=\u2026)' ) ClientGUIMenus.AppendMenuItem( submenu, 'the next characters are not: (non-consuming) - (?!\u2026)', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(?!\u2026)' ) ClientGUIMenus.AppendMenuItem( submenu, 'the previous characters are: (non-consuming) - (?<=\u2026)', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(?<=\u2026)' ) ClientGUIMenus.AppendMenuItem( submenu, 'the previous characters are not: (non-consuming) - (? 74 - [1-9]+\d*', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', r'[1-9]+\d*' ) ClientGUIMenus.AppendMenuItem( submenu, r'filename - (?<=' + re.escape( os.path.sep ) + r')[^' + re.escape( os.path.sep ) + r']*?(?=\..*$)', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(?<=' + re.escape( os.path.sep ) + r')[^' + re.escape( os.path.sep ) + r']*?(?=\..*$)' ) ClientGUIMenus.AppendMenu( menu, submenu, 'regex components' ) submenu = QW.QMenu( menu ) ClientGUIMenus.AppendMenuItem( submenu, 'manage favourites', 'manage some custom favourite phrases', self._ManageFavourites ) ClientGUIMenus.AppendSeparator( submenu ) for ( regex_phrase, description ) in HC.options[ 'regex_favourites' ]: ClientGUIMenus.AppendMenuItem( submenu, description, 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', regex_phrase ) ClientGUIMenus.AppendMenu( menu, submenu, 'favourites' ) CGC.core().PopupMenu( self, menu ) def _ManageFavourites( self ): regex_favourites = HC.options[ 'regex_favourites' ] from hydrus.client.gui import ClientGUITopLevelWindowsPanels from hydrus.client.gui import ClientGUIScrolledPanelsEdit with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'manage regex favourites' ) as dlg: panel = ClientGUIScrolledPanelsEdit.EditRegexFavourites( dlg, regex_favourites ) dlg.SetPanel( panel ) if dlg.exec() == QW.QDialog.Accepted: regex_favourites = panel.GetValue() HC.options[ 'regex_favourites' ] = regex_favourites HG.client_controller.Write( 'save_options', HC.options ) class StaticBox( QW.QFrame ): def __init__( self, parent, title ): QW.QFrame.__init__( self, parent ) self.setFrameStyle( QW.QFrame.Box | QW.QFrame.Raised ) self._spacer = QW.QSpacerItem( 0, 0, QW.QSizePolicy.Minimum, QW.QSizePolicy.MinimumExpanding ) self._sizer = QP.VBoxLayout() normal_font = self.font() normal_font_size = normal_font.pointSize() normal_font_family = normal_font.family() title_font = QG.QFont( normal_font_family, int( normal_font_size ), QG.QFont.Bold ) self._title_st = BetterStaticText( self, label = title ) self._title_st.setFont( title_font ) QP.AddToLayout( self._sizer, self._title_st, CC.FLAGS_CENTER ) self.setLayout( self._sizer ) self.layout().addSpacerItem( self._spacer ) def Add( self, widget, flags = None ): self.layout().removeItem( self._spacer ) QP.AddToLayout( self._sizer, widget, flags ) self.layout().addSpacerItem( self._spacer ) def SetTitle( self, title ): self._title_st.setText( title ) class RadioBox( StaticBox ): def __init__( self, parent, title, choice_pairs, initial_index = None ): StaticBox.__init__( self, parent, title ) self._indices_to_radio_buttons = {} self._radio_buttons_to_data = {} for ( index, ( text, data ) ) in enumerate( choice_pairs ): radio_button = QW.QRadioButton( text, self ) self.Add( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR ) self._indices_to_radio_buttons[ index ] = radio_button self._radio_buttons_to_data[ radio_button ] = data if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].setChecked( True ) def GetSelectedClientData( self ): for radio_button in list(self._radio_buttons_to_data.keys()): if radio_button.isDown(): return self._radio_buttons_to_data[ radio_button] def SetSelection( self, index ): self._indices_to_radio_buttons[ index ].setChecked( True ) def SetString( self, index, text ): self._indices_to_radio_buttons[ index ].setText( text ) class TextCatchEnterEventFilter( QC.QObject ): def __init__( self, parent, callable, *args, **kwargs ): QC.QObject.__init__( self, parent ) self._callable = HydrusData.Call( callable, *args, **kwargs ) def eventFilter( self, watched, event ): if event.type() == QC.QEvent.KeyPress and event.key() in ( QC.Qt.Key_Enter, QC.Qt.Key_Return ): self._callable() event.accept() return True return False class TextAndGauge( QW.QWidget ): def __init__( self, parent ): QW.QWidget.__init__( self, parent ) self._st = BetterStaticText( self ) self._gauge = Gauge( self ) vbox = QP.VBoxLayout( margin = 0 ) QP.AddToLayout( vbox, self._st, CC.FLAGS_EXPAND_PERPENDICULAR ) QP.AddToLayout( vbox, self._gauge, CC.FLAGS_EXPAND_PERPENDICULAR ) self.setLayout( vbox ) def SetText( self, text ): if not self or not QP.isValid( self ): return self._st.setText( text ) def SetValue( self, text, value, range ): if not self or not QP.isValid( self ): return self._st.setText( text ) self._gauge.SetRange( range ) self._gauge.SetValue( value )