hydrus/hydrus/client/gui/widgets/ClientGUICommon.py

1952 lines
56 KiB
Python

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 <hr> 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( '<a style="text-decoration:none; color:{};" href="{}">{}</a>'.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) - (?<!\u2026)', 'copy this phrase to the clipboard', HG.client_controller.pub, 'clipboard', 'text', '(?<!\u2026)' )
ClientGUIMenus.AppendSeparator( submenu )
ClientGUIMenus.AppendMenuItem( submenu, r'0074 -> 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 )