hydrus/include/ClientGUIListBoxes.py

3099 lines
92 KiB
Python

import ClientCaches
import ClientConstants as CC
import ClientData
import ClientGUICommon
import ClientGUIMenus
import ClientGUIShortcuts
import ClientSearch
import ClientSerialisable
import ClientTags
import collections
import HydrusConstants as HC
import HydrusData
import HydrusExceptions
import HydrusGlobals as HG
import HydrusSerialisable
import HydrusTags
import os
import wx
( ListBoxEvent, EVT_LIST_BOX ) = wx.lib.newevent.NewCommandEvent()
class AddEditDeleteListBox( wx.Panel ):
def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_callable, edit_callable ):
self._data_to_pretty_callable = data_to_pretty_callable
self._add_callable = add_callable
self._edit_callable = edit_callable
wx.Panel.__init__( self, parent )
self._listbox = wx.ListBox( self, style = wx.LB_EXTENDED )
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
self._delete_button = ClientGUICommon.BetterButton( self, 'delete', self._Delete )
self._enabled_only_on_selection_buttons = []
self._permitted_object_types = []
#
vbox = wx.BoxSizer( wx.VERTICAL )
self._buttons_hbox = wx.BoxSizer( wx.HORIZONTAL )
self._buttons_hbox.Add( self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
self._buttons_hbox.Add( self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
self._buttons_hbox.Add( self._delete_button, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.Add( self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.Add( self._buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
#
( width, height ) = ClientGUICommon.ConvertTextToPixels( self._listbox, ( 20, height_num_chars ) )
self._listbox.SetInitialSize( ( width, height ) )
#
self._ShowHideButtons()
self._listbox.Bind( wx.EVT_LISTBOX, self.EventSelection )
self._listbox.Bind( wx.EVT_LISTBOX_DCLICK, self.EventEdit )
def _Add( self ):
( result, data ) = self._add_callable()
if result:
self._AddData( data )
def _AddAllDefaults( self, defaults_callable ):
defaults = defaults_callable()
for default in defaults:
self._AddData( default )
def _AddData( self, data ):
self._SetNoneDupeName( data )
pretty_data = self._data_to_pretty_callable( data )
self._listbox.Append( pretty_data, data )
def _AddSomeDefaults( self, defaults_callable ):
defaults = defaults_callable()
selected = False
choice_tuples = [ ( self._data_to_pretty_callable( default ), default, selected ) for default in defaults ]
import ClientGUITopLevelWindows
import ClientGUIScrolledPanelsEdit
with ClientGUITopLevelWindows.DialogEdit( self, 'select the defaults to add' ) as dlg:
panel = ClientGUIScrolledPanelsEdit.EditChooseMultiple( dlg, choice_tuples )
dlg.SetPanel( panel )
if dlg.ShowModal() == wx.ID_OK:
defaults_to_add = panel.GetValue()
for default in defaults_to_add:
self._AddData( default )
def _Delete( self ):
indices = list( self._listbox.GetSelections() )
if len( indices ) == 0:
return
indices.sort( reverse = True )
import ClientGUIDialogs
with ClientGUIDialogs.DialogYesNo( self, 'Remove all selected?' ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
for i in indices:
self._listbox.Delete( i )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _Edit( self ):
for i in range( self._listbox.GetCount() ):
if not self._listbox.IsSelected( i ):
continue
data = self._listbox.GetClientData( i )
( result, new_data ) = self._edit_callable( data )
if result:
self._listbox.Delete( i )
self._SetNoneDupeName( new_data )
pretty_new_data = self._data_to_pretty_callable( new_data )
self._listbox.Insert( pretty_new_data, i, new_data )
else:
break
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _Duplicate( self ):
dupe_data = self._GetExportObject()
if dupe_data is not None:
dupe_data = dupe_data.Duplicate()
self._ImportObject( dupe_data )
def _ExportToClipboard( self ):
export_object = self._GetExportObject()
if export_object is not None:
json = export_object.DumpToString()
HG.client_controller.pub( 'clipboard', 'text', json )
def _ExportToPng( self ):
export_object = self._GetExportObject()
if export_object is not None:
import ClientGUITopLevelWindows
import ClientGUISerialisable
with ClientGUITopLevelWindows.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object )
dlg.SetPanel( panel )
dlg.ShowModal()
def _ExportToPngs( self ):
export_object = self._GetExportObject()
if export_object is None:
return
if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
self._ExportToPng()
return
import ClientGUITopLevelWindows
import ClientGUISerialisable
with ClientGUITopLevelWindows.DialogNullipotent( self, 'export to pngs' ) as dlg:
panel = ClientGUISerialisable.PngsExportPanel( dlg, export_object )
dlg.SetPanel( panel )
dlg.ShowModal()
def _GetExportObject( self ):
to_export = HydrusSerialisable.SerialisableList()
for obj in self.GetData( only_selected = True ):
to_export.append( obj )
if len( to_export ) == 0:
return None
elif len( to_export ) == 1:
return to_export[0]
else:
return to_export
def _ImportFromClipboard( self ):
raw_text = HG.client_controller.GetClipboardText()
try:
obj = HydrusSerialisable.CreateFromString( raw_text )
self._ImportObject( obj )
except Exception as e:
wx.MessageBox( 'I could not understand what was in the clipboard' )
def _ImportFromPng( self ):
with wx.FileDialog( self, 'select the png or pngs with the encoded data', style = wx.FD_OPEN | wx.FD_MULTIPLE, wildcard = 'PNG (*.png)|*.png' ) as dlg:
if dlg.ShowModal() == wx.ID_OK:
for path in dlg.GetPaths():
path = HydrusData.ToUnicode( path )
try:
payload = ClientSerialisable.LoadFromPng( path )
except Exception as e:
wx.MessageBox( HydrusData.ToUnicode( e ) )
return
try:
obj = HydrusSerialisable.CreateFromNetworkString( payload )
self._ImportObject( obj )
except:
wx.MessageBox( 'I could not understand what was encoded in the png!' )
return
def _ImportObject( self, obj ):
bad_object_type_names = set()
if isinstance( obj, HydrusSerialisable.SerialisableList ):
for sub_obj in obj:
self._ImportObject( sub_obj )
else:
if isinstance( obj, self._permitted_object_types ):
self._AddData( obj )
else:
bad_object_type_names.add( HydrusData.GetTypeName( type( obj ) ) )
if len( bad_object_type_names ) > 0:
message = 'The imported objects included these types:'
message += os.linesep * 2
message += os.linesep.join( bad_object_type_names )
message += os.linesep * 2
message += 'Whereas this control only allows:'
message += os.linesep * 2
message += os.linesep.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
wx.MessageBox( message )
def _SetNoneDupeName( self, obj ):
pass
def _ShowHideButtons( self ):
if len( self._listbox.GetSelections() ) == 0:
self._edit_button.Disable()
self._delete_button.Disable()
for button in self._enabled_only_on_selection_buttons:
button.Disable()
else:
self._edit_button.Enable()
self._delete_button.Enable()
for button in self._enabled_only_on_selection_buttons:
button.Enable()
def AddDatas( self, datas ):
for data in datas:
self._AddData( data )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def AddDefaultsButton( self, defaults_callable ):
import_menu_items = []
all_call = HydrusData.Call( self._AddAllDefaults, defaults_callable )
some_call = HydrusData.Call( self._AddSomeDefaults, defaults_callable )
import_menu_items.append( ( 'normal', 'add them all', 'Load all the defaults.', all_call ) )
import_menu_items.append( ( 'normal', 'select from a list', 'Load some of the defaults.', some_call ) )
button = ClientGUICommon.MenuButton( self, 'add defaults', import_menu_items )
self._buttons_hbox.Add( button, CC.FLAGS_VCENTER )
def AddImportExportButtons( self, permitted_object_types ):
self._permitted_object_types = permitted_object_types
export_menu_items = []
export_menu_items.append( ( 'normal', 'to clipboard', 'Serialise the selected data and put it on your clipboard.', self._ExportToClipboard ) )
export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPng ) )
all_objs_are_named = False not in ( issubclass( o, HydrusSerialisable.SerialisableBaseNamed ) for o in self._permitted_object_types )
if all_objs_are_named:
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPngs ) )
import_menu_items = []
import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
import_menu_items.append( ( 'normal', 'from pngs', 'Load a data from an encoded png.', self._ImportFromPng ) )
button = ClientGUICommon.MenuButton( self, 'export', export_menu_items )
self._buttons_hbox.Add( button, CC.FLAGS_VCENTER )
self._enabled_only_on_selection_buttons.append( button )
button = ClientGUICommon.MenuButton( self, 'import', import_menu_items )
self._buttons_hbox.Add( button, CC.FLAGS_VCENTER )
button = ClientGUICommon.BetterButton( self, 'duplicate', self._Duplicate )
self._buttons_hbox.Add( button, CC.FLAGS_VCENTER )
self._enabled_only_on_selection_buttons.append( button )
self._ShowHideButtons()
def AddSeparator( self ):
self._buttons_hbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_PERPENDICULAR )
def Bind( self, event, handler ):
self._listbox.Bind( event, handler )
def EventEdit( self, event ):
self._Edit()
def EventSelection( self, event ):
self._ShowHideButtons()
event.Skip()
def GetCount( self ):
return self._listbox.GetCount()
def GetData( self, only_selected = False ):
datas = []
for i in range( self._listbox.GetCount() ):
if only_selected and not self._listbox.IsSelected( i ):
continue
data = self._listbox.GetClientData( i )
datas.append( data )
return datas
def GetValue( self ):
return self.GetData()
class AddEditDeleteListBoxUniqueNamedObjects( AddEditDeleteListBox ):
def _SetNoneDupeName( self, obj ):
disallowed_names = { o.GetName() for o in self.GetData() }
HydrusSerialisable.SetNonDupeName( obj, disallowed_names )
class QueueListBox( wx.Panel ):
def __init__( self, parent, height_num_chars, data_to_pretty_callable, add_callable = None, edit_callable = None ):
self._data_to_pretty_callable = data_to_pretty_callable
self._add_callable = add_callable
self._edit_callable = edit_callable
wx.Panel.__init__( self, parent )
self._listbox = wx.ListBox( self, style = wx.LB_EXTENDED )
self._up_button = ClientGUICommon.BetterButton( self, u'\u2191', self._Up )
self._delete_button = ClientGUICommon.BetterButton( self, 'X', self._Delete )
self._down_button = ClientGUICommon.BetterButton( self, u'\u2193', self._Down )
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
if self._add_callable is None:
self._add_button.Hide()
if self._edit_callable is None:
self._edit_button.Hide()
#
vbox = wx.BoxSizer( wx.VERTICAL )
buttons_vbox = wx.BoxSizer( wx.VERTICAL )
buttons_vbox.Add( self._up_button, CC.FLAGS_VCENTER )
buttons_vbox.Add( self._delete_button, CC.FLAGS_VCENTER )
buttons_vbox.Add( self._down_button, CC.FLAGS_VCENTER )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.Add( self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
hbox.Add( buttons_vbox, CC.FLAGS_VCENTER )
buttons_hbox = wx.BoxSizer( wx.HORIZONTAL )
buttons_hbox.Add( self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
buttons_hbox.Add( self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.Add( hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.Add( buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
#
( width, height ) = ClientGUICommon.ConvertTextToPixels( self._listbox, ( 20, height_num_chars ) )
self._listbox.SetInitialSize( ( width, height ) )
#
self._listbox.Bind( wx.EVT_LISTBOX, self.EventSelection )
self._listbox.Bind( wx.EVT_LISTBOX_DCLICK, self.EventEdit )
def _Add( self ):
( result, data ) = self._add_callable()
if result:
self._AddData( data )
def _AddData( self, data ):
pretty_data = self._data_to_pretty_callable( data )
self._listbox.Append( pretty_data, data )
def _Delete( self ):
indices = list( self._listbox.GetSelections() )
if len( indices ) == 0:
return
indices.sort( reverse = True )
import ClientGUIDialogs
with ClientGUIDialogs.DialogYesNo( self, 'Remove all selected?' ) as dlg_yn:
if dlg_yn.ShowModal() == wx.ID_YES:
for i in indices:
self._listbox.Delete( i )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _Down( self ):
indices = list( self._listbox.GetSelections() )
indices.sort( reverse = True )
for i in indices:
if i < self._listbox.GetCount() - 1:
if not self._listbox.IsSelected( i + 1 ): # is the one below not selected?
self._SwapRows( i, i + 1 )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _Edit( self ):
for i in range( self._listbox.GetCount() ):
if not self._listbox.IsSelected( i ):
continue
data = self._listbox.GetClientData( i )
( result, new_data ) = self._edit_callable( data )
if result:
self._listbox.Delete( i )
pretty_new_data = self._data_to_pretty_callable( new_data )
self._listbox.Insert( pretty_new_data, i, new_data )
else:
break
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _SwapRows( self, index_a, index_b ):
a_was_selected = self._listbox.IsSelected( index_a )
b_was_selected = self._listbox.IsSelected( index_b )
data_a = self._listbox.GetClientData( index_a )
data_b = self._listbox.GetClientData( index_b )
pretty_data_a = self._data_to_pretty_callable( data_a )
pretty_data_b = self._data_to_pretty_callable( data_b )
self._listbox.Delete( index_a )
self._listbox.Insert( pretty_data_b, index_a, data_b )
self._listbox.Delete( index_b )
self._listbox.Insert( pretty_data_a, index_b, data_a )
if b_was_selected:
self._listbox.Select( index_a )
if a_was_selected:
self._listbox.Select( index_b )
def _Up( self ):
indices = self._listbox.GetSelections()
for i in indices:
if i > 0:
if not self._listbox.IsSelected( i - 1 ): # is the one above not selected?
self._SwapRows( i, i - 1 )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def AddDatas( self, datas ):
for data in datas:
self._AddData( data )
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def Bind( self, event, handler ):
self._listbox.Bind( event, handler )
def EventEdit( self, event ):
self._Edit()
def EventSelection( self, event ):
if len( self._listbox.GetSelections() ) == 0:
self._up_button.Disable()
self._delete_button.Disable()
self._down_button.Disable()
self._edit_button.Disable()
else:
self._up_button.Enable()
self._delete_button.Enable()
self._down_button.Enable()
self._edit_button.Enable()
event.Skip()
def GetCount( self ):
return self._listbox.GetCount()
def GetData( self, only_selected = False ):
datas = []
for i in range( self._listbox.GetCount() ):
data = self._listbox.GetClientData( i )
datas.append( data )
return datas
def Pop( self ):
if self._listbox.GetCount() == 0:
return None
data = self._listbox.GetClientData( 0 )
self._listbox.Delete( 0 )
return data
class ListBox( wx.ScrolledWindow ):
TEXT_X_PADDING = 3
def __init__( self, parent, height_num_chars = 10 ):
wx.ScrolledWindow.__init__( self, parent, style = wx.VSCROLL | wx.BORDER_DOUBLE )
self._background_colour = wx.Colour( 255, 255, 255 )
self._terms = set()
self._ordered_terms = []
self._selected_terms = set()
self._terms_to_texts = {}
self._last_hit_index = None
self._last_view_start = None
self._dirty = True
self._client_bmp = wx.Bitmap( 20, 20, 24 )
dc = wx.MemoryDC( self._client_bmp )
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
( text_x, self._text_y ) = dc.GetTextExtent( 'abcdefghijklmnopqrstuvwxyz' )
self._num_rows_per_page = 0
self.SetScrollRate( 0, self._text_y )
( min_width, min_height ) = ClientGUICommon.ConvertTextToPixels( self, ( 16, height_num_chars ) )
self.SetMinClientSize( ( min_width, min_height ) )
self.Bind( wx.EVT_PAINT, self.EventPaint )
self.Bind( wx.EVT_SIZE, self.EventResize )
self.Bind( wx.EVT_ERASE_BACKGROUND, self.EventEraseBackground )
self.Bind( wx.EVT_LEFT_DOWN, self.EventMouseSelect )
self.Bind( wx.EVT_LEFT_DCLICK, self.EventDClick )
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
def __len__( self ):
return len( self._ordered_terms )
def _Activate( self ):
pass
def _DeleteActivate( self ):
pass
def _AppendTerm( self, term ):
was_selected_before = term in self._selected_terms
if term in self._terms:
self._RemoveTerm( term )
self._terms.add( term )
self._ordered_terms.append( term )
self._terms_to_texts[ term ] = self._GetTextFromTerm( term )
if was_selected_before:
self._selected_terms.add( term )
def _Clear( self ):
self._terms = set()
self._ordered_terms = []
self._selected_terms = set()
self._terms_to_texts = {}
self._last_hit_index = None
self._last_view_start = None
self._dirty = True
def _DataHasChanged( self ):
self._SetVirtualSize()
self._SetDirty()
wx.QueueEvent( self.GetEventHandler(), ListBoxEvent( -1 ) )
def _Deselect( self, index ):
term = self._GetTerm( index )
self._selected_terms.discard( term )
def _DeselectAll( self ):
self._selected_terms = set()
def _GetIndexUnderMouse( self, mouse_event ):
( xUnit, yUnit ) = self.GetScrollPixelsPerUnit()
( x_scroll, y_scroll ) = self.GetViewStart()
y_offset = y_scroll * yUnit
y = mouse_event.GetY() + y_offset
row_index = ( y / self._text_y )
if row_index >= len( self._ordered_terms ):
return None
return row_index
def _GetSelectedIncludeExcludePredicates( self ):
include_predicates = []
exclude_predicates = []
for term in self._selected_terms:
if isinstance( term, ClientSearch.Predicate ):
predicate_type = term.GetType()
if predicate_type in ( HC.PREDICATE_TYPE_TAG, HC.PREDICATE_TYPE_NAMESPACE, HC.PREDICATE_TYPE_WILDCARD ):
value = term.GetValue()
include_predicates.append( ClientSearch.Predicate( predicate_type, value ) )
exclude_predicates.append( ClientSearch.Predicate( predicate_type, value, False ) )
else:
include_predicates.append( term )
else:
s = term
include_predicates.append( ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, term ) )
exclude_predicates.append( ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, term, False ) )
return ( include_predicates, exclude_predicates )
def _GetTerm( self, index ):
if index < 0 or index > len( self._ordered_terms ) - 1:
raise HydrusExceptions.DataMissing( 'No term for index ' + str( index ) )
return self._ordered_terms[ index ]
def _GetTextColour( self, term ):
return ( 0, 111, 250 )
def _GetSafeHitIndex( self, hit_index, direction = None ):
if hit_index is not None:
if hit_index == -1 or hit_index > len( self._ordered_terms ):
hit_index = len( self._ordered_terms ) - 1
elif hit_index == len( self._ordered_terms ) or hit_index < -1:
hit_index = 0
return hit_index
def _GetSimplifiedTextFromTerm( self, term ):
return self._GetTextFromTerm( term )
def _GetTextFromTerm( self, term ):
raise NotImplementedError()
def _HandleClick( self, event ):
hit_index = self._GetIndexUnderMouse( event )
shift = event.ShiftDown()
ctrl = event.CmdDown()
self._Hit( shift, ctrl, hit_index )
def _Hit( self, shift, ctrl, hit_index ):
hit_index = self._GetSafeHitIndex( hit_index )
to_select = set()
to_deselect = set()
deselect_all = False
if shift:
if hit_index is not None:
if self._last_hit_index is not None:
lower = min( hit_index, self._last_hit_index )
upper = max( hit_index, self._last_hit_index )
to_select = range( lower, upper + 1 )
else:
to_select.add( hit_index )
elif ctrl:
if hit_index is not None:
if self._IsSelected( hit_index ):
to_deselect.add( hit_index )
else:
to_select.add( hit_index )
else:
if hit_index is None:
deselect_all = True
else:
if not self._IsSelected( hit_index ):
deselect_all = True
to_select.add( hit_index )
if deselect_all:
self._DeselectAll()
for index in to_select:
self._Select( index )
for index in to_deselect:
self._Deselect( index )
self._last_hit_index = hit_index
if self._last_hit_index is not None:
y = self._text_y * self._last_hit_index
( start_x, start_y ) = self.GetViewStart()
( x_unit, y_unit ) = self.GetScrollPixelsPerUnit()
( width, height ) = self.GetClientSize()
if y < start_y * y_unit:
y_to_scroll_to = y / y_unit
#self.Scroll( -1, y_to_scroll_to )
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to ) )
elif y > ( start_y * y_unit ) + height - self._text_y:
y_to_scroll_to = ( y - height ) / y_unit
#self.Scroll( -1, y_to_scroll_to + 2 )
wx.QueueEvent( self.GetEventHandler(), wx.ScrollWinEvent( wx.wxEVT_SCROLLWIN_THUMBRELEASE, pos = y_to_scroll_to + 2 ) )
self._SetDirty()
def _IsSelected( self, index ):
try:
term = self._GetTerm( index )
except HydrusExceptions.DataMissing:
return False
return term in self._selected_terms
def _Redraw( self, dc ):
( xUnit, yUnit ) = self.GetScrollPixelsPerUnit()
( x_scroll, y_scroll ) = self.GetViewStart()
self._last_view_start = self.GetViewStart()
y_offset = y_scroll * yUnit
( my_width, my_height ) = self.GetClientSize()
first_visible_index = y_offset / self._text_y
last_visible_index = ( y_offset + my_height ) / self._text_y
if ( y_offset + my_height ) % self._text_y != 0:
last_visible_index += 1
last_visible_index = min( last_visible_index, len( self._ordered_terms ) - 1 )
dc.SetFont( wx.SystemSettings.GetFont( wx.SYS_DEFAULT_GUI_FONT ) )
dc.SetBackground( wx.Brush( self._background_colour ) )
dc.Clear()
for ( i, current_index ) in enumerate( range( first_visible_index, last_visible_index + 1 ) ):
term = self._GetTerm( current_index )
text = self._terms_to_texts[ term ]
( r, g, b ) = self._GetTextColour( term )
text_colour = wx.Colour( r, g, b )
if term in self._selected_terms:
dc.SetBrush( wx.Brush( text_colour ) )
dc.SetPen( wx.TRANSPARENT_PEN )
dc.DrawRectangle( 0, i * self._text_y, my_width, self._text_y )
text_colour = self._background_colour
dc.SetTextForeground( text_colour )
( x, y ) = ( self.TEXT_X_PADDING, i * self._text_y )
dc.DrawText( text, x, y )
self._dirty = False
def _RefreshTexts( self ):
self._terms_to_texts = { term : self._GetTextFromTerm( term ) for term in self._terms }
self._SetDirty()
def _RemoveSelectedTerms( self ):
for term in list( self._selected_terms ):
self._RemoveTerm( term )
def _RemoveTerm( self, term ):
if term in self._terms:
self._terms.discard( term )
self._ordered_terms.remove( term )
self._selected_terms.discard( term )
del self._terms_to_texts[ term ]
def _Select( self, index ):
term = self._GetTerm( index )
self._selected_terms.add( term )
def _SelectAll( self ):
self._selected_terms = set( self._terms )
def _SetDirty( self ):
self._dirty = True
self.Refresh()
def _SetVirtualSize( self ):
( my_x, my_y ) = self.GetClientSize()
ideal_virtual_size = ( my_x, max( self._text_y * len( self._ordered_terms ), my_y ) )
if ideal_virtual_size != self.GetVirtualSize():
self.SetVirtualSize( ideal_virtual_size )
def _SortByText( self ):
def lexicographic_key( term ):
return self._terms_to_texts[ term ]
self._ordered_terms.sort( key = lexicographic_key )
def EventCharHook( self, event ):
shift = event.ShiftDown()
ctrl = event.CmdDown()
key_code = event.GetKeyCode()
if ClientGUICommon.WindowHasFocus( self ) and key_code in CC.DELETE_KEYS:
self._DeleteActivate()
elif key_code in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER ):
self._Activate()
else:
if ctrl and key_code in ( ord( 'A' ), ord( 'a' ) ):
self._SelectAll()
self._SetDirty()
else:
hit_index = None
if len( self._ordered_terms ) > 1:
roll_up = False
roll_down = False
if key_code in ( wx.WXK_HOME, wx.WXK_NUMPAD_HOME ):
hit_index = 0
elif key_code in ( wx.WXK_END, wx.WXK_NUMPAD_END ):
hit_index = len( self._ordered_terms ) - 1
roll_up = True
elif self._last_hit_index is not None:
if key_code in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ):
hit_index = self._last_hit_index - 1
roll_up = True
elif key_code in ( wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN ):
hit_index = self._last_hit_index + 1
roll_down = True
elif key_code in ( wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ):
hit_index = max( 0, self._last_hit_index - self._num_rows_per_page )
roll_up = True
elif key_code in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN ):
hit_index = min( len( self._ordered_terms ) - 1, self._last_hit_index + self._num_rows_per_page )
roll_down = True
if hit_index is None:
event.Skip()
else:
if roll_up:
hit_index = self._GetSafeHitIndex( hit_index, -1 )
if roll_down:
hit_index = self._GetSafeHitIndex( hit_index, 1 )
self._Hit( shift, ctrl, hit_index )
def EventDClick( self, event ):
self._Activate()
def EventEraseBackground( self, event ): pass
def EventMouseSelect( self, event ):
self._HandleClick( event )
event.Skip()
def EventPaint( self, event ):
( my_x, my_y ) = self.GetClientSize()
if ( my_x, my_y ) != self._client_bmp.GetSize():
self._client_bmp = wx.Bitmap( my_x, my_y, 24 )
self._dirty = True
dc = wx.BufferedPaintDC( self, self._client_bmp )
if self._dirty or self._last_view_start != self.GetViewStart():
self._Redraw( dc )
def EventResize( self, event ):
( my_x, my_y ) = self.GetClientSize()
self._num_rows_per_page = my_y / self._text_y
self._SetVirtualSize()
self._SetDirty()
def GetClientData( self, index = None ):
if index is None:
return set( self._terms )
else:
return self._GetTerm( index )
def GetIdealHeight( self ):
return self._text_y * len( self._ordered_terms ) + 20
def MoveSelectionDown( self ):
if len( self._ordered_terms ) > 1 and self._last_hit_index is not None:
self._Hit( False, False, self._last_hit_index + 1 )
def MoveSelectionUp( self ):
if len( self._ordered_terms ) > 1 and self._last_hit_index is not None:
self._Hit( False, False, self._last_hit_index - 1 )
class ListBoxTags( ListBox ):
has_counts = False
can_spawn_new_windows = True
def __init__( self, *args, **kwargs ):
ListBox.__init__( self, *args, **kwargs )
self._get_current_predicates_callable = None
self._UpdateBackgroundColour()
self.Bind( wx.EVT_RIGHT_DOWN, self.EventMouseRightClick )
self.Bind( wx.EVT_MIDDLE_DOWN, self.EventMouseMiddleClick )
HG.client_controller.sub( self, 'SiblingsHaveChanged', 'notify_new_siblings_gui' )
HG.client_controller.sub( self, '_UpdateBackgroundColour', 'notify_new_colourset' )
def _GetNamespaceColours( self ):
return HC.options[ 'namespace_colours' ]
def _GetAllTagsForClipboard( self, with_counts = False ):
texts = list( self._terms_to_texts.values() )
texts.sort()
return texts
def _GetNamespaceFromTerm( self, term ):
raise NotImplementedError()
def _GetTextColour( self, term ):
namespace_colours = self._GetNamespaceColours()
namespace = self._GetNamespaceFromTerm( term )
if namespace in namespace_colours:
( r, g, b ) = namespace_colours[ namespace ]
else:
( r, g, b ) = namespace_colours[ None ]
return ( r, g, b )
def _NewSearchPage( self ):
predicates = []
for term in self._selected_terms:
if isinstance( term, ClientSearch.Predicate ):
predicates.append( term )
else:
predicates.append( ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, term ) )
predicates = HG.client_controller.GetGUI().FlushOutPredicates( predicates )
if len( predicates ) > 0:
s = [ predicate.GetUnicode() for predicate in predicates ]
s.sort()
page_name = ', '.join( s )
HG.client_controller.pub( 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY, initial_predicates = predicates, page_name = page_name )
def _ProcessMenuCopyEvent( self, command ):
if command in ( 'copy_terms', 'copy_sub_terms' ):
texts = []
for term in self._selected_terms:
if isinstance( term, ClientSearch.Predicate ):
text = term.GetUnicode( with_count = False )
else:
text = HydrusData.ToUnicode( term )
if command == 'copy_sub_terms':
( namespace_gumpf, text ) = HydrusTags.SplitTag( text )
texts.append( text )
texts.sort()
text = os.linesep.join( texts )
elif command == 'copy_all_tags':
text = os.linesep.join( self._GetAllTagsForClipboard( with_counts = False ) )
elif command == 'copy_all_tags_with_counts':
text = os.linesep.join( self._GetAllTagsForClipboard( with_counts = True ) )
HG.client_controller.pub( 'clipboard', 'text', text )
def _ProcessMenuPredicateEvent( self, command ):
pass
def _ProcessMenuTagEvent( self, command ):
import ClientGUIDialogsManage
if command == 'censorship':
( tag, ) = self._selected_terms
with ClientGUIDialogsManage.DialogManageTagCensorship( self, tag ) as dlg:
dlg.ShowModal()
elif command == 'parent':
with ClientGUIDialogsManage.DialogManageTagParents( self, self._selected_terms ) as dlg:
dlg.ShowModal()
elif command == 'sibling':
with ClientGUIDialogsManage.DialogManageTagSiblings( self, self._selected_terms ) as dlg:
dlg.ShowModal()
def _UpdateBackgroundColour( self ):
new_options = HG.client_controller.new_options
self._background_colour = new_options.GetColour( CC.COLOUR_TAGS_BOX )
self.Refresh()
def EventMouseMiddleClick( self, event ):
self._HandleClick( event )
if self.can_spawn_new_windows:
self._NewSearchPage()
def EventMouseRightClick( self, event ):
self._HandleClick( event )
if len( self._ordered_terms ) > 0:
menu = wx.Menu()
if len( self._selected_terms ) > 0:
if len( self._selected_terms ) == 1:
( term, ) = self._selected_terms
if isinstance( term, ClientSearch.Predicate ):
if term.GetType() == HC.PREDICATE_TYPE_TAG:
selection_string = '"' + term.GetValue() + '"'
else:
selection_string = '"' + term.GetUnicode( with_count = False ) + '"'
else:
selection_string = '"' + HydrusData.ToUnicode( term ) + '"'
else:
selection_string = 'selected'
if self._get_current_predicates_callable is not None:
current_predicates = self._get_current_predicates_callable()
( include_predicates, exclude_predicates ) = self._GetSelectedIncludeExcludePredicates()
if current_predicates is not None:
if True in ( include_predicate in current_predicates for include_predicate in include_predicates ):
ClientGUIMenus.AppendMenuItem( self, menu, 'discard ' + selection_string + ' from current search', 'Remove the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_include_predicates' )
if True in ( include_predicate not in current_predicates for include_predicate in include_predicates ):
ClientGUIMenus.AppendMenuItem( self, menu, 'require ' + selection_string + ' for current search', 'Add the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'add_include_predicates' )
if True in ( exclude_predicate in current_predicates for exclude_predicate in exclude_predicates ):
ClientGUIMenus.AppendMenuItem( self, menu, 'permit ' + selection_string + ' for current search', 'Stop disallowing the selected predicates from the current search.', self._ProcessMenuPredicateEvent, 'remove_exclude_predicates' )
if True in ( exclude_predicate not in current_predicates for exclude_predicate in exclude_predicates ):
ClientGUIMenus.AppendMenuItem( self, menu, 'exclude ' + selection_string + ' from current search', 'Disallow the selected predicates for the current search.', self._ProcessMenuPredicateEvent, 'add_exclude_predicates' )
ClientGUIMenus.AppendSeparator( menu )
if self.can_spawn_new_windows:
ClientGUIMenus.AppendMenuItem( self, menu, 'open a new search page for ' + selection_string, 'Open a new search page starting with the selected predicates.', self._NewSearchPage )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( self, menu, 'copy ' + selection_string, 'Copy the selected predicates to your clipboard.', self._ProcessMenuCopyEvent, 'copy_terms' )
if len( self._selected_terms ) == 1:
( namespace, subtag ) = HydrusTags.SplitTag( selection_string )
if namespace != '':
sub_selection_string = '"' + subtag
ClientGUIMenus.AppendMenuItem( self, menu, 'copy ' + sub_selection_string, 'Copy the selected sub-predicates to your clipboard.', self._ProcessMenuCopyEvent, 'copy_sub_terms' )
else:
ClientGUIMenus.AppendMenuItem( self, menu, 'copy selected subtags', 'Copy the selected sub-predicates to your clipboard.', self._ProcessMenuCopyEvent, 'copy_sub_terms' )
ClientGUIMenus.AppendSeparator( menu )
if len( self._ordered_terms ) > len( self._selected_terms ):
ClientGUIMenus.AppendMenuItem( self, menu, 'copy all tags', 'Copy all the predicates in this list to your clipboard.', self._ProcessMenuCopyEvent, 'copy_all_tags' )
if self.has_counts:
ClientGUIMenus.AppendMenuItem( self, menu, 'copy all tags with counts', 'Copy all the predicates in this list, with their counts, to your clipboard.', self._ProcessMenuCopyEvent, 'copy_all_tags_with_counts' )
if self.can_spawn_new_windows and len( self._selected_terms ) > 0:
term_types = [ type( term ) for term in self._selected_terms ]
if str in term_types or unicode in term_types:
ClientGUIMenus.AppendSeparator( menu )
if len( self._selected_terms ) == 1:
( tag, ) = self._selected_terms
text = tag
else:
text = 'selection'
if len( self._selected_terms ) == 1:
ClientGUIMenus.AppendMenuItem( self, menu, 'censor ' + text, 'Hide this tag from view in future.', self._ProcessMenuTagEvent, 'censorship' )
ClientGUIMenus.AppendMenuItem( self, menu, 'add parents to ' + text, 'Add a parent to this tag.', self._ProcessMenuTagEvent, 'parent' )
ClientGUIMenus.AppendMenuItem( self, menu, 'add siblings to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )
HG.client_controller.PopupMenu( self, menu )
event.Skip()
def GetSelectedTags( self ):
return set( self._selected_terms )
def SiblingsHaveChanged( self ):
pass
class ListBoxTagsPredicates( ListBoxTags ):
has_counts = True
def _GetWithParentIndices( self, index ):
indices = [ index ]
index += 1
while index < len( self._ordered_terms ):
term = self._GetTerm( index )
if term.GetType() == HC.PREDICATE_TYPE_PARENT:
indices.append( index )
else:
break
index += 1
return indices
def _Deselect( self, index ):
to_deselect = self._GetWithParentIndices( index )
for index in to_deselect:
ListBoxTags._Deselect( self, index )
def _GetAllTagsForClipboard( self, with_counts = False ):
return [ term.GetUnicode( with_counts ) for term in self._terms ]
def _GetNamespaceFromTerm( self, term ):
predicate = term
namespace = predicate.GetNamespace()
return namespace
def _GetSafeHitIndex( self, hit_index, direction = None ):
hit_index = ListBox._GetSafeHitIndex( self, hit_index )
if direction is not None and hit_index is not None:
hit_term = self._GetTerm( hit_index )
while hit_term.GetType() == HC.PREDICATE_TYPE_PARENT:
hit_index += direction
if hit_index >= len( self._ordered_terms ):
hit_index = 0
hit_term = self._GetTerm( hit_index )
return hit_index
def _GetSimplifiedTextFromTerm( self, term ):
predicate = term
return predicate.GetUnicode( with_counts = False )
def _GetTextFromTerm( self, term ):
predicate = term
return predicate.GetUnicode()
def _HasPredicate( self, predicate ):
return predicate in self._terms
def _Hit( self, shift, ctrl, hit_index ):
hit_index = self._GetSafeHitIndex( hit_index )
if hit_index is not None and hit_index > 0:
# this realigns the hit index in the up direction, so if user clicks on parent, they get the upper child
while self._GetTerm( hit_index ).GetType() == HC.PREDICATE_TYPE_PARENT:
hit_index -= 1
ListBoxTags._Hit( self, shift, ctrl, hit_index )
def _Select( self, index ):
to_select = self._GetWithParentIndices( index )
for index in to_select:
ListBoxTags._Select( self, index )
def GetPredicates( self ):
return set( self._terms )
class ListBoxTagsActiveSearchPredicates( ListBoxTagsPredicates ):
has_counts = False
def __init__( self, parent, page_key, initial_predicates = None ):
if initial_predicates is None:
initial_predicates = []
ListBoxTagsPredicates.__init__( self, parent, height_num_chars = 6 )
self._page_key = page_key
self._get_current_predicates_callable = self.GetPredicates
if len( initial_predicates ) > 0:
for predicate in initial_predicates:
self._AppendTerm( predicate )
self._DataHasChanged()
HG.client_controller.sub( self, 'EnterPredicates', 'enter_predicates' )
def _Activate( self ):
if len( self._selected_terms ) > 0:
self._EnterPredicates( set( self._selected_terms ) )
def _DeleteActivate( self ):
self._Activate()
def _EnterPredicates( self, predicates, permit_add = True, permit_remove = True ):
if len( predicates ) == 0:
return
predicates_to_be_added = set()
predicates_to_be_removed = set()
for predicate in predicates:
predicate = predicate.GetCountlessCopy()
if self._HasPredicate( predicate ):
if permit_remove:
predicates_to_be_removed.add( predicate )
else:
if permit_add:
predicates_to_be_added.add( predicate )
inverse_predicate = predicate.GetInverseCopy()
if self._HasPredicate( inverse_predicate ):
predicates_to_be_removed.add( inverse_predicate )
for predicate in predicates_to_be_added:
self._AppendTerm( predicate )
for predicate in predicates_to_be_removed:
self._RemoveTerm( predicate )
self._SortByText()
self._DataHasChanged()
HG.client_controller.pub( 'refresh_query', self._page_key )
def _GetTextFromTerm( self, term ):
predicate = term
return predicate.GetUnicode( render_for_user = True )
def _ProcessMenuPredicateEvent( self, command ):
( include_predicates, exclude_predicates ) = self._GetSelectedIncludeExcludePredicates()
if command == 'add_include_predicates':
self._EnterPredicates( include_predicates, permit_remove = False )
elif command == 'remove_include_predicates':
self._EnterPredicates( include_predicates, permit_add = False )
elif command == 'add_exclude_predicates':
self._EnterPredicates( exclude_predicates, permit_remove = False )
elif command == 'remove_exclude_predicates':
self._EnterPredicates( exclude_predicates, permit_add = False )
def EnterPredicates( self, page_key, predicates, permit_add = True, permit_remove = True ):
if page_key == self._page_key:
self._EnterPredicates( predicates, permit_add = permit_add, permit_remove = permit_remove )
class ListBoxTagsAC( ListBoxTagsPredicates ):
def __init__( self, parent, callable, service_key, **kwargs ):
ListBoxTagsPredicates.__init__( self, parent, **kwargs )
self._callable = callable
self._service_key = service_key
self._predicates = {}
def _Activate( self ):
predicates = [ term for term in self._selected_terms if term.GetType() != HC.PREDICATE_TYPE_PARENT ]
predicates = HG.client_controller.GetGUI().FlushOutPredicates( predicates )
if len( predicates ) > 0:
self._callable( predicates )
def SetPredicates( self, predicates ):
# need to do a clever compare, since normal predicate compare doesn't take count into account
they_are_the_same = True
if len( predicates ) == len( self._predicates ):
for index in range( len( predicates ) ):
p_1 = predicates[ index ]
p_2 = self._predicates[ index ]
if p_1 != p_2 or p_1.GetCount() != p_2.GetCount():
they_are_the_same = False
break
else:
they_are_the_same = False
if not they_are_the_same:
# important to make own copy, as same object originals can be altered (e.g. set non-inclusive) in cache, and we need to notice that change just above
self._predicates = [ predicate.GetCopy() for predicate in predicates ]
self._Clear()
for predicate in predicates:
self._AppendTerm( predicate )
self._DataHasChanged()
if len( predicates ) > 0:
hit_index = 0
if len( predicates ) > 1:
if HG.client_controller.new_options.GetBoolean( 'ac_select_first_with_count' ):
for ( index, predicate ) in enumerate( predicates ):
if predicate.GetCount() != 0:
hit_index = index
break
self._Hit( False, False, hit_index )
def SetTagService( self, service_key ):
self._service_key = service_key
class ListBoxTagsACRead( ListBoxTagsAC ):
def _GetTextFromTerm( self, term ):
predicate = term
return predicate.GetUnicode( render_for_user = True )
class ListBoxTagsACWrite( ListBoxTagsAC ):
def _GetTextFromTerm( self, term ):
predicate = term
return predicate.GetUnicode( sibling_service_key = self._service_key )
class ListBoxTagsCensorship( ListBoxTags ):
def __init__( self, parent, removed_callable = None ):
ListBoxTags.__init__( self, parent )
self._removed_callable = removed_callable
def _Activate( self ):
if len( self._selected_terms ) > 0:
tags = set( self._selected_terms )
for tag in tags:
self._RemoveTerm( tag )
if self._removed_callable is not None:
self._removed_callable( tags )
self._ordered_terms.sort()
self._DataHasChanged()
def _GetNamespaceFromTerm( self, term ):
tag = term
if tag == ':':
return None
else:
( namespace, subtag ) = HydrusTags.SplitTag( tag )
return namespace
def _GetTextFromTerm( self, term ):
tag_slice = term
return ClientTags.ConvertTagSliceToString( tag_slice )
def AddTags( self, tags ):
for tag in tags:
self._AppendTerm( tag )
self._ordered_terms.sort()
self._DataHasChanged()
def EnterTags( self, tags ):
for tag in tags:
if tag in self._terms:
self._RemoveTerm( tag )
else:
self._AppendTerm( tag )
self._ordered_terms.sort()
self._DataHasChanged()
def GetTags( self ):
return list( self._ordered_terms )
def RemoveTags( self, tags ):
for tag in tags:
self._RemoveTerm( tag )
self._ordered_terms.sort()
self._DataHasChanged()
def SetTags( self, tags ):
self._Clear()
self.AddTags( tags )
class ListBoxTagsColourOptions( ListBoxTags ):
PROTECTED_TERMS = ( None, '' )
can_spawn_new_windows = False
def __init__( self, parent, initial_namespace_colours ):
ListBoxTags.__init__( self, parent )
for ( namespace, colour ) in initial_namespace_colours.items():
colour = tuple( colour ) # tuple to convert from list, for oooold users who have list colours
self._AppendTerm( ( namespace, colour ) )
self._SortByText()
self._DataHasChanged()
def _Activate( self ):
namespaces = [ namespace for ( namespace, colour ) in self._selected_terms ]
if len( namespaces ) > 0:
import ClientGUIDialogs
with ClientGUIDialogs.DialogYesNo( self, 'Delete all selected colours?' ) as dlg:
if dlg.ShowModal() == wx.ID_YES:
self._RemoveNamespaces( namespaces )
def _DeleteActivate( self ):
self._Activate()
def _GetTextFromTerm( self, term ):
( namespace, colour ) = term
if namespace is None:
namespace_string = 'default namespace:tag'
elif namespace == '':
namespace_string = 'unnamespaced tag'
else:
namespace_string = namespace + ':tag'
return namespace_string
def _GetNamespaceColours( self ):
return dict( self._terms )
def _GetNamespaceFromTerm( self, term ):
( namespace, colour ) = term
return namespace
def _RemoveNamespaces( self, namespaces ):
namespaces = [ namespace for namespace in namespaces if namespace not in self.PROTECTED_TERMS ]
removees = [ ( existing_namespace, existing_colour ) for ( existing_namespace, existing_colour ) in self._terms if existing_namespace in namespaces ]
for removee in removees:
self._RemoveTerm( removee )
self._DataHasChanged()
def SetNamespaceColour( self, namespace, colour ):
( r, g, b, a ) = colour.Get()
colour_tuple = ( r, g, b )
for ( existing_namespace, existing_colour ) in self._terms:
if existing_namespace == namespace:
self._RemoveTerm( ( existing_namespace, existing_colour ) )
break
self._AppendTerm( ( namespace, colour_tuple ) )
self._SortByText()
self._DataHasChanged()
def GetNamespaceColours( self ):
return self._GetNamespaceColours()
def GetSelectedNamespaceColours( self ):
namespace_colours = dict( self._selected_terms )
return namespace_colours
class ListBoxTagsStrings( ListBoxTags ):
def __init__( self, parent, service_key = None, show_sibling_text = True, sort_tags = True ):
ListBoxTags.__init__( self, parent )
if service_key is not None:
service_key = CC.COMBINED_TAG_SERVICE_KEY
self._service_key = service_key
self._show_sibling_text = show_sibling_text
self._sort_tags = sort_tags
def _GetNamespaceFromTerm( self, term ):
tag = term
( namespace, subtag ) = HydrusTags.SplitTag( tag )
return namespace
def _GetSimplifiedTextFromTerm( self, term ):
tag = term
return HydrusData.ToUnicode( tag )
def _GetTextFromTerm( self, term ):
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
tag = term
tag_string = ClientTags.RenderTag( tag, True )
if self._show_sibling_text:
sibling = siblings_manager.GetSibling( self._service_key, tag )
if sibling is not None:
tag_string += ' (will display as ' + ClientTags.RenderTag( sibling, True ) + ')'
return tag_string
def _RecalcTags( self ):
self._RefreshTexts()
if self._sort_tags:
self._SortByText()
self._DataHasChanged()
def GetTags( self ):
return set( self._terms )
def SetTags( self, tags ):
self._Clear()
for tag in tags:
self._AppendTerm( tag )
self._RecalcTags()
def SiblingsHaveChanged( self ):
self._RecalcTags()
class ListBoxTagsStringsAddRemove( ListBoxTagsStrings ):
def __init__( self, parent, service_key = None, removed_callable = None, show_sibling_text = True ):
ListBoxTagsStrings.__init__( self, parent, service_key = service_key, show_sibling_text = show_sibling_text )
self._removed_callable = removed_callable
def _Activate( self ):
if len( self._selected_terms ) > 0:
tags = set( self._selected_terms )
self._RemoveTags( tags )
def _RemoveTags( self, tags ):
for tag in tags:
self._RemoveTerm( tag )
self._RecalcTags()
if self._removed_callable is not None:
self._removed_callable( tags )
def AddTags( self, tags ):
for tag in tags:
self._AppendTerm( tag )
self._RecalcTags()
def Clear( self ):
self._Clear()
self._RecalcTags()
def EnterTags( self, tags ):
removed = set()
for tag in tags:
if tag in self._terms:
self._RemoveTerm( tag )
removed.add( tag )
else:
self._AppendTerm( tag )
self._RecalcTags()
if len( removed ) > 0 and self._removed_callable is not None:
self._removed_callable( removed )
def EventCharHook( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in CC.DELETE_KEYS:
self._Activate()
else:
event.Skip()
def RemoveTags( self, tags ):
self._RemoveTags( tags )
class ListBoxTagsSelection( ListBoxTags ):
render_for_user = True
has_counts = True
def __init__( self, parent, include_counts = True, collapse_siblings = False ):
ListBoxTags.__init__( self, parent, height_num_chars = 12 )
self._sort = HC.options[ 'default_tag_sort' ]
if not include_counts and self._sort in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_DESC, CC.SORT_BY_INCIDENCE_NAMESPACE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
self._sort = CC.SORT_BY_LEXICOGRAPHIC_ASC
self._last_media = set()
self._tag_service_key = CC.COMBINED_TAG_SERVICE_KEY
self._include_counts = include_counts
self._collapse_siblings = collapse_siblings
self._current_tags_to_count = collections.Counter()
self._deleted_tags_to_count = collections.Counter()
self._pending_tags_to_count = collections.Counter()
self._petitioned_tags_to_count = collections.Counter()
self._show_current = True
self._show_deleted = False
self._show_pending = True
self._show_petitioned = True
def _GetAllTagsForClipboard( self, with_counts = False ):
if with_counts:
return [ self._terms_to_texts[ term ] for term in self._ordered_terms ]
else:
return self._ordered_terms
def _GetNamespaceFromTerm( self, term ):
tag = term
( namespace, subtag ) = HydrusTags.SplitTag( tag )
return namespace
def _GetSimplifiedTextFromTerm( self, term ):
tag = term
return HydrusData.ToUnicode( tag )
def _GetTextFromTerm( self, term ):
tag = term
tag_string = ClientTags.RenderTag( tag, self.render_for_user )
if self._include_counts:
if self._show_current and tag in self._current_tags_to_count: tag_string += ' (' + HydrusData.ToHumanInt( self._current_tags_to_count[ tag ] ) + ')'
if self._show_pending and tag in self._pending_tags_to_count: tag_string += ' (+' + HydrusData.ToHumanInt( self._pending_tags_to_count[ tag ] ) + ')'
if self._show_petitioned and tag in self._petitioned_tags_to_count: tag_string += ' (-' + HydrusData.ToHumanInt( self._petitioned_tags_to_count[ tag ] ) + ')'
if self._show_deleted and tag in self._deleted_tags_to_count: tag_string += ' (X' + HydrusData.ToHumanInt( self._deleted_tags_to_count[ tag ] ) + ')'
else:
if self._show_pending and tag in self._pending_tags_to_count: tag_string += ' (+)'
if self._show_petitioned and tag in self._petitioned_tags_to_count: tag_string += ' (-)'
if self._show_deleted and tag in self._deleted_tags_to_count: tag_string += ' (X)'
if not self._collapse_siblings:
siblings_manager = HG.client_controller.GetManager( 'tag_siblings' )
sibling = siblings_manager.GetSibling( self._tag_service_key, tag )
if sibling is not None:
sibling = ClientTags.RenderTag( sibling, self.render_for_user )
tag_string += ' (will display as ' + sibling + ')'
return tag_string
def _RecalcStrings( self, limit_to_these_tags = None ):
previous_selected_terms = set( self._selected_terms )
if limit_to_these_tags is None:
self._Clear()
nonzero_tags = set()
if self._show_current: nonzero_tags.update( ( tag for ( tag, count ) in self._current_tags_to_count.items() if count > 0 ) )
if self._show_deleted: nonzero_tags.update( ( tag for ( tag, count ) in self._deleted_tags_to_count.items() if count > 0 ) )
if self._show_pending: nonzero_tags.update( ( tag for ( tag, count ) in self._pending_tags_to_count.items() if count > 0 ) )
if self._show_petitioned: nonzero_tags.update( ( tag for ( tag, count ) in self._petitioned_tags_to_count.items() if count > 0 ) )
for tag in nonzero_tags:
self._AppendTerm( tag )
else:
if not isinstance( limit_to_these_tags, set ):
limit_to_these_tags = set( limit_to_these_tags )
for tag in limit_to_these_tags:
self._RemoveTerm( tag )
nonzero_tags = set()
if self._show_current: nonzero_tags.update( ( tag for ( tag, count ) in self._current_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
if self._show_deleted: nonzero_tags.update( ( tag for ( tag, count ) in self._deleted_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
if self._show_pending: nonzero_tags.update( ( tag for ( tag, count ) in self._pending_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
if self._show_petitioned: nonzero_tags.update( ( tag for ( tag, count ) in self._petitioned_tags_to_count.items() if count > 0 and tag in limit_to_these_tags ) )
for tag in nonzero_tags:
self._AppendTerm( tag )
for term in previous_selected_terms:
if term in self._terms:
self._selected_terms.add( term )
self._SortTags()
def _SortTags( self ):
def lexicographic_key( term ):
tag = self._terms_to_texts[ term ]
( namespace, subtag ) = HydrusTags.SplitTag( tag )
comparable_subtag = HydrusTags.ConvertTagToSortable( subtag )
# 'cat' < 'character:rei'
# 'page:3' < 'page:20'
# '1' < 'series:eva'
# note that 'test' < ( 1, '' ) but u'test' > ( 1, '' ) wew
if namespace == '':
return ( comparable_subtag, comparable_subtag )
else:
return ( namespace, comparable_subtag )
def incidence_key( term ):
return tags_to_count[ term ]
def namespace_key( term ):
tag = term
( namespace, subtag ) = HydrusTags.SplitTag( tag )
if namespace == '':
namespace = '{' # '{' is above 'z' in ascii, so this works for most situations
return namespace
def namespace_lexicographic_key( term ):
tag = term
# '{' is above 'z' in ascii, so this works for most situations
( namespace, subtag ) = HydrusTags.SplitTag( tag )
if namespace == '':
return ( '{', HydrusTags.ConvertTagToSortable( subtag ) )
else:
return ( namespace, HydrusTags.ConvertTagToSortable( subtag ) )
if self._sort in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_DESC, CC.SORT_BY_INCIDENCE_NAMESPACE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
tags_to_count = collections.Counter()
if self._show_current: tags_to_count.update( self._current_tags_to_count )
if self._show_deleted: tags_to_count.update( self._deleted_tags_to_count )
if self._show_pending: tags_to_count.update( self._pending_tags_to_count )
if self._show_petitioned: tags_to_count.update( self._petitioned_tags_to_count )
# let's establish a-z here for equal incidence values later
if self._sort in ( CC.SORT_BY_INCIDENCE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_ASC ):
self._ordered_terms.sort( key = lexicographic_key, reverse = True )
reverse = False
elif self._sort in ( CC.SORT_BY_INCIDENCE_DESC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
self._ordered_terms.sort( key = lexicographic_key )
reverse = True
self._ordered_terms.sort( key = incidence_key, reverse = reverse )
if self._sort in ( CC.SORT_BY_INCIDENCE_NAMESPACE_ASC, CC.SORT_BY_INCIDENCE_NAMESPACE_DESC ):
# python list sort is stable, so lets now sort again
if self._sort == CC.SORT_BY_INCIDENCE_NAMESPACE_ASC:
reverse = True
elif self._sort == CC.SORT_BY_INCIDENCE_NAMESPACE_DESC:
reverse = False
self._ordered_terms.sort( key = namespace_key, reverse = reverse )
else:
if self._sort in ( CC.SORT_BY_LEXICOGRAPHIC_DESC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC ):
reverse = True
elif self._sort in ( CC.SORT_BY_LEXICOGRAPHIC_ASC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC ):
reverse = False
if self._sort in ( CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_ASC, CC.SORT_BY_LEXICOGRAPHIC_NAMESPACE_DESC ):
key = namespace_lexicographic_key
elif self._sort in ( CC.SORT_BY_LEXICOGRAPHIC_ASC, CC.SORT_BY_LEXICOGRAPHIC_DESC ):
key = lexicographic_key
self._ordered_terms.sort( key = key, reverse = reverse )
self._DataHasChanged()
def ChangeTagService( self, service_key ):
self._tag_service_key = service_key
self.SetTagsByMedia( self._last_media, force_reload = True )
def SetSort( self, sort ):
self._sort = sort
self._SortTags()
def SetShow( self, show_type, value ):
if show_type == 'current': self._show_current = value
elif show_type == 'deleted': self._show_deleted = value
elif show_type == 'pending': self._show_pending = value
elif show_type == 'petitioned': self._show_petitioned = value
self._RecalcStrings()
def IncrementTagsByMedia( self, media ):
media = set( media )
media = media.difference( self._last_media )
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientData.GetMediasTagCount( media, tag_service_key = self._tag_service_key, collapse_siblings = self._collapse_siblings )
self._current_tags_to_count.update( current_tags_to_count )
self._deleted_tags_to_count.update( deleted_tags_to_count )
self._pending_tags_to_count.update( pending_tags_to_count )
self._petitioned_tags_to_count.update( petitioned_tags_to_count )
tags_changed = set()
if self._show_current: tags_changed.update( current_tags_to_count.keys() )
if self._show_deleted: tags_changed.update( deleted_tags_to_count.keys() )
if self._show_pending: tags_changed.update( pending_tags_to_count.keys() )
if self._show_petitioned: tags_changed.update( petitioned_tags_to_count.keys() )
if len( tags_changed ) > 0:
self._RecalcStrings( tags_changed )
self._last_media.update( media )
def SetTagsByMedia( self, media, force_reload = False ):
media = set( media )
if len( media ) < len( self._last_media ) / 10: # if we are dropping to a much smaller selection (e.g. 5000 -> 1), we should just recalculate from scratch
force_reload = True
if force_reload:
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientData.GetMediasTagCount( media, tag_service_key = self._tag_service_key, collapse_siblings = self._collapse_siblings )
self._current_tags_to_count = current_tags_to_count
self._deleted_tags_to_count = deleted_tags_to_count
self._pending_tags_to_count = pending_tags_to_count
self._petitioned_tags_to_count = petitioned_tags_to_count
self._RecalcStrings()
else:
removees = self._last_media.difference( media )
adds = media.difference( self._last_media )
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientData.GetMediasTagCount( removees, tag_service_key = self._tag_service_key, collapse_siblings = self._collapse_siblings )
self._current_tags_to_count.subtract( current_tags_to_count )
self._deleted_tags_to_count.subtract( deleted_tags_to_count )
self._pending_tags_to_count.subtract( pending_tags_to_count )
self._petitioned_tags_to_count.subtract( petitioned_tags_to_count )
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = ClientData.GetMediasTagCount( adds, tag_service_key = self._tag_service_key, collapse_siblings = self._collapse_siblings )
self._current_tags_to_count.update( current_tags_to_count )
self._deleted_tags_to_count.update( deleted_tags_to_count )
self._pending_tags_to_count.update( pending_tags_to_count )
self._petitioned_tags_to_count.update( petitioned_tags_to_count )
for counter in ( self._current_tags_to_count, self._deleted_tags_to_count, self._pending_tags_to_count, self._petitioned_tags_to_count ):
tags = counter.keys()
for tag in tags:
if counter[ tag ] == 0:
del counter[ tag ]
if len( removees ) == 0:
tags_changed = set()
if self._show_current: tags_changed.update( current_tags_to_count.keys() )
if self._show_deleted: tags_changed.update( deleted_tags_to_count.keys() )
if self._show_pending: tags_changed.update( pending_tags_to_count.keys() )
if self._show_petitioned: tags_changed.update( petitioned_tags_to_count.keys() )
if len( tags_changed ) > 0:
self._RecalcStrings( tags_changed )
else:
self._RecalcStrings()
self._last_media = media
self._DataHasChanged()
def SiblingsHaveChanged( self ):
self.SetTagsByMedia( self._last_media, force_reload = True )
class ListBoxTagsSelectionHoverFrame( ListBoxTagsSelection ):
def __init__( self, parent, canvas_key ):
ListBoxTagsSelection.__init__( self, parent, include_counts = False, collapse_siblings = True )
self._canvas_key = canvas_key
def _Activate( self ):
HG.client_controller.pub( 'canvas_manage_tags', self._canvas_key )
class ListBoxTagsSelectionManagementPanel( ListBoxTagsSelection ):
def __init__( self, parent, page_key, predicates_callable = None ):
ListBoxTagsSelection.__init__( self, parent, include_counts = True, collapse_siblings = True )
self._page_key = page_key
self._get_current_predicates_callable = predicates_callable
HG.client_controller.sub( self, 'IncrementTagsByMediaPubsub', 'increment_tags_selection' )
HG.client_controller.sub( self, 'SetTagsByMediaPubsub', 'new_tags_selection' )
HG.client_controller.sub( self, 'ChangeTagServicePubsub', 'change_tag_service' )
def _Activate( self ):
predicates = [ ClientSearch.Predicate( HC.PREDICATE_TYPE_TAG, term ) for term in self._selected_terms ]
if len( predicates ) > 0:
HG.client_controller.pub( 'enter_predicates', self._page_key, predicates )
def _ProcessMenuPredicateEvent( self, command ):
( include_predicates, exclude_predicates ) = self._GetSelectedIncludeExcludePredicates()
if command == 'add_include_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, include_predicates, permit_remove = False )
elif command == 'remove_include_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, include_predicates, permit_add = False )
elif command == 'add_exclude_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, exclude_predicates, permit_remove = False )
elif command == 'remove_exclude_predicates':
HG.client_controller.pub( 'enter_predicates', self._page_key, exclude_predicates, permit_add = False )
def ChangeTagServicePubsub( self, page_key, service_key ):
if page_key == self._page_key:
self.ChangeTagService( service_key )
def IncrementTagsByMediaPubsub( self, page_key, media ):
if page_key == self._page_key:
self.IncrementTagsByMedia( media )
def SetTagsByMediaPubsub( self, page_key, media, force_reload = False ):
if page_key == self._page_key:
self.SetTagsByMedia( media, force_reload = force_reload )
class ListBoxTagsSelectionTagsDialog( ListBoxTagsSelection ):
render_for_user = False
def __init__( self, parent, add_func, delete_func ):
ListBoxTagsSelection.__init__( self, parent, include_counts = True, collapse_siblings = False )
self._add_func = add_func
self._delete_func = delete_func
def _Activate( self ):
if len( self._selected_terms ) > 0:
self._add_func( set( self._selected_terms ) )
def _DeleteActivate( self ):
if len( self._selected_terms ) > 0:
self._delete_func( set( self._selected_terms ) )