hydrus/include/ClientGUIListCtrl.py

1472 lines
42 KiB
Python

import ClientConstants as CC
import ClientData
import ClientDragDrop
import ClientGUICommon
import ClientSerialisable
import ClientGUIShortcuts
import HydrusData
import HydrusExceptions
import HydrusGlobals as HG
import HydrusSerialisable
import os
import wx
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
( ListCtrlEvent, EVT_LIST_CTRL ) = wx.lib.newevent.NewCommandEvent()
# This used to be ColumnSorterMixin, but it was crashing on sort-click on clients with many pages open
# I've disabled it for now because it was still catching people. The transition to BetterListCtrl will nuke the whole thing eventually.
class SaneListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
def __init__( self, parent, height, columns, delete_key_callback = None, activation_callback = None ):
num_columns = len( columns )
wx.ListCtrl.__init__( self, parent, style = wx.LC_REPORT )
ListCtrlAutoWidthMixin.__init__( self )
self.itemDataMap = {}
self._data_indices_to_sort_indices = {}
self._data_indices_to_sort_indices_dirty = False
self._next_data_index = 0
resize_column = 1
for ( i, ( name, width ) ) in enumerate( columns ):
self.InsertColumn( i, name, width = width )
if width == -1:
resize_column = i + 1
self.setResizeColumn( resize_column )
self.SetMinSize( ( -1, height ) )
self._delete_key_callback = delete_key_callback
self._activation_callback = activation_callback
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
self.Bind( wx.EVT_LIST_ITEM_ACTIVATED, self.EventItemActivated )
self.Bind( wx.EVT_LIST_COL_BEGIN_DRAG, self.EventBeginColDrag )
def _GetIndexFromDataIndex( self, data_index ):
if self._data_indices_to_sort_indices_dirty:
self._data_indices_to_sort_indices = { self.GetItemData( index ) : index for index in range( self.GetItemCount() ) }
self._data_indices_to_sort_indices_dirty = False
try:
return self._data_indices_to_sort_indices[ data_index ]
except KeyError:
raise HydrusExceptions.DataMissing( 'Data not found!' )
def Append( self, display_tuple, sort_tuple ):
index = wx.ListCtrl.Append( self, display_tuple )
data_index = self._next_data_index
self.SetItemData( index, data_index )
self.itemDataMap[ data_index ] = list( sort_tuple )
self._data_indices_to_sort_indices[ data_index ] = index
self._next_data_index += 1
def DeleteItem( self, *args, **kwargs ):
wx.ListCtrl.DeleteItem( self, *args, **kwargs )
self._data_indices_to_sort_indices_dirty = True
def EventBeginColDrag( self, event ):
# resizeCol is not zero-indexed
if event.GetColumn() == self._resizeCol - 1:
last_column = self.GetColumnCount()
if self._resizeCol != last_column:
self.setResizeColumn( last_column )
else:
event.Veto()
return
event.Skip()
def EventItemActivated( self, event ):
if self._activation_callback is not None:
self._activation_callback()
else:
event.Skip()
def EventKeyDown( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in CC.DELETE_KEYS:
if self._delete_key_callback is not None:
self._delete_key_callback()
elif key in ( ord( 'A' ), ord( 'a' ) ) and modifier == wx.ACCEL_CTRL:
self.SelectAll()
else:
event.Skip()
def GetAllSelected( self ):
indices = []
i = self.GetFirstSelected()
while i != -1:
indices.append( i )
i = self.GetNextSelected( i )
return indices
def GetClientData( self, index = None ):
if index is None:
data_indicies = [ self.GetItemData( index ) for index in range( self.GetItemCount() ) ]
datas = [ tuple( self.itemDataMap[ data_index ] ) for data_index in data_indicies ]
return datas
else:
data_index = self.GetItemData( index )
return tuple( self.itemDataMap[ data_index ] )
def GetIndexFromClientData( self, data, column_index = None ):
for index in range( self.GetItemCount() ):
client_data = self.GetClientData( index )
if column_index is None:
comparison_data = client_data
else:
comparison_data = client_data[ column_index ]
if comparison_data == data:
return index
raise HydrusExceptions.DataMissing( 'Data not found!' )
def GetSecondarySortValues( self, col, key1, key2 ):
# This overrides the ColumnSortedMixin. Just spam the whole tuple back.
return ( self.itemDataMap[ key1 ], self.itemDataMap[ key2 ] )
def GetListCtrl( self ):
return self
def GetSelectedClientData( self ):
indices = self.GetAllSelected()
results = []
for index in indices:
results.append( self.GetClientData( index ) )
return results
def HasClientData( self, data, column_index = None ):
try:
index = self.GetIndexFromClientData( data, column_index )
return True
except HydrusExceptions.DataMissing:
return False
def OnSortOrderChanged( self ):
self._data_indices_to_sort_indices_dirty = True
def RemoveAllSelected( self ):
indices = self.GetAllSelected()
self.RemoveIndices( indices )
def RemoveIndices( self, indices ):
indices.sort()
indices.reverse() # so we don't screw with the indices of deletees below
for index in indices:
self.DeleteItem( index )
def SelectAll( self ):
currently_selected = set( self.GetAllSelected() )
currently_not_selected = [ index for index in range( self.GetItemCount() ) if index not in currently_selected ]
for index in currently_not_selected:
self.Select( index )
def UpdateRow( self, index, display_tuple, sort_tuple ):
column = 0
for value in display_tuple:
self.SetItem( index, column, value )
column += 1
data_index = self.GetItemData( index )
self.itemDataMap[ data_index ] = list( sort_tuple )
class SaneListCtrlForSingleObject( SaneListCtrl ):
def __init__( self, *args, **kwargs ):
# this could one day just take column parameters that the user can pick
# it could just take obj in append or whatever and generate column tuples off that
self._data_indices_to_objects = {}
self._objects_to_data_indices = {}
SaneListCtrl.__init__( self, *args, **kwargs )
def Append( self, display_tuple, sort_tuple, obj ):
self._data_indices_to_objects[ self._next_data_index ] = obj
self._objects_to_data_indices[ obj ] = self._next_data_index
SaneListCtrl.Append( self, display_tuple, sort_tuple )
def GetIndexFromObject( self, obj ):
try:
data_index = self._objects_to_data_indices[ obj ]
index = self._GetIndexFromDataIndex( data_index )
return index
except KeyError:
raise HydrusExceptions.DataMissing( 'Data not found!' )
def GetObject( self, index ):
data_index = self.GetItemData( index )
return self._data_indices_to_objects[ data_index ]
def GetObjects( self, only_selected = False ):
if only_selected:
indicies = self.GetAllSelected()
else:
indicies = range( self.GetItemCount() )
data_indicies = [ self.GetItemData( index ) for index in indicies ]
datas = [ self._data_indices_to_objects[ data_index ] for data_index in data_indicies ]
return datas
def HasObject( self, obj ):
try:
index = self.GetIndexFromObject( obj )
return True
except HydrusExceptions.DataMissing:
return False
def SetNonDupeName( self, obj ):
# when column population is handled here, we can tuck this into normal append/update calls internally
current_names = { o.GetName() for o in self.GetObjects() if o is not obj }
HydrusSerialisable.SetNonDupeName( obj, current_names )
def UpdateRow( self, index, display_tuple, sort_tuple, obj ):
SaneListCtrl.UpdateRow( self, index, display_tuple, sort_tuple )
data_index = self.GetItemData( index )
self._data_indices_to_objects[ data_index ] = obj
self._objects_to_data_indices[ obj ] = data_index
class SaneListCtrlPanel( wx.Panel ):
def __init__( self, parent ):
wx.Panel.__init__( self, parent )
self._vbox = wx.BoxSizer( wx.VERTICAL )
self._buttonbox = wx.BoxSizer( wx.HORIZONTAL )
self._listctrl = None
self._button_infos = []
def _SomeSelected( self ):
return self._listctrl.GetSelectedItemCount() > 0
def _UpdateButtons( self ):
for ( button, enabled_check_func ) in self._button_infos:
if enabled_check_func():
button.Enable()
else:
button.Disable()
def AddButton( self, label, clicked_func, enabled_only_on_selection = False, enabled_check_func = None ):
button = ClientGUICommon.BetterButton( self, label, clicked_func )
self._buttonbox.Add( button, CC.FLAGS_VCENTER )
if enabled_only_on_selection:
enabled_check_func = self._SomeSelected
if enabled_check_func is not None:
self._button_infos.append( ( button, enabled_check_func ) )
def AddWindow( self, window ):
self._buttonbox.Add( window, CC.FLAGS_VCENTER )
def EventContentChanged( self, event ):
if not self._listctrl:
return
self._UpdateButtons()
event.Skip()
def EventSelectionChanged( self, event ):
if not self._listctrl:
return
self._UpdateButtons()
event.Skip()
def SetListCtrl( self, listctrl ):
self._listctrl = listctrl
self._vbox.Add( self._listctrl, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._vbox.Add( self._buttonbox, CC.FLAGS_BUTTON_SIZER )
self.SetSizer( self._vbox )
self._listctrl.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_INSERT_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )
class BetterListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
def __init__( self, parent, name, height_num_chars, sizing_column_initial_width_num_chars, columns, data_to_tuples_func, delete_key_callback = None, activation_callback = None, style = None ):
if style is None:
style = wx.LC_REPORT
else:
style = wx.LC_REPORT | style
wx.ListCtrl.__init__( self, parent, style = style )
ListCtrlAutoWidthMixin.__init__( self )
self._data_to_tuples_func = data_to_tuples_func
self._menu_callable = None
self._sort_column = 0
self._sort_asc = True
# eventually have it look up 'name' in some options somewhere and see previous height, width, and column selection
# this thing should deal with missing entries but also have some filtered defaults for subs listctrl, which will have a bunch of possible columns
self._indices_to_data_info = {}
self._data_to_indices = {}
total_width = ClientGUICommon.ConvertTextToPixelWidth( self, sizing_column_initial_width_num_chars )
resize_column = 1
for ( i, ( name, width_num_chars ) ) in enumerate( columns ):
if width_num_chars == -1:
width = -1
resize_column = i + 1
else:
width = ClientGUICommon.ConvertTextToPixelWidth( self, width_num_chars )
total_width += width
self.InsertColumn( i, name, width = width )
self.setResizeColumn( resize_column )
self.SetInitialSize( ( total_width, -1 ) )
self.GrowShrinkColumnsHeight( height_num_chars )
self._delete_key_callback = delete_key_callback
self._activation_callback = activation_callback
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
self.Bind( wx.EVT_LIST_ITEM_ACTIVATED, self.EventItemActivated )
self.Bind( wx.EVT_LIST_COL_BEGIN_DRAG, self.EventBeginColDrag )
self.Bind( wx.EVT_LIST_COL_CLICK, self.EventColumnClick )
def _AddDataInfo( self, data_info ):
( data, display_tuple, sort_tuple ) = data_info
index = self.Append( display_tuple )
self._indices_to_data_info[ index ] = data_info
self._data_to_indices[ data ] = index
def _GetDisplayAndSortTuples( self, data ):
( display_tuple, sort_tuple ) = self._data_to_tuples_func( data )
better_sort = []
for item in sort_tuple:
if isinstance( item, ( str, unicode ) ):
item = item.lower()
better_sort.append( item )
sort_tuple = tuple( better_sort )
return ( display_tuple, sort_tuple )
def _GetSelected( self ):
indices = []
i = self.GetFirstSelected()
while i != -1:
indices.append( i )
i = self.GetNextSelected( i )
return indices
def _RecalculateIndicesAfterDelete( self ):
indices_and_data_info = list( self._indices_to_data_info.items() )
indices_and_data_info.sort()
self._indices_to_data_info = {}
self._data_to_indices = {}
for ( index, ( old_index, data_info ) ) in enumerate( indices_and_data_info ):
( data, display_tuple, sort_tuple ) = data_info
self._data_to_indices[ data ] = index
self._indices_to_data_info[ index ] = data_info
def _ShowMenu( self ):
try:
menu = self._menu_callable()
except HydrusExceptions.DataMissing:
return
HG.client_controller.PopupMenu( self, menu )
def _SortDataInfo( self ):
data_infos = list( self._indices_to_data_info.values() )
def sort_key( data_info ):
( data, display_tuple, sort_tuple ) = data_info
return ( sort_tuple[ self._sort_column ], sort_tuple ) # add the sort tuple to get secondary sorting
data_infos.sort( key = sort_key, reverse = not self._sort_asc )
return data_infos
def _SortAndRefreshRows( self ):
selected_data_quick = set( self.GetData( only_selected = True ) )
selected_indices = self._GetSelected()
for selected_index in selected_indices:
self.Select( selected_index, False )
sorted_data_info = self._SortDataInfo()
self._indices_to_data_info = {}
self._data_to_indices = {}
for ( index, data_info ) in enumerate( sorted_data_info ):
self._indices_to_data_info[ index ] = data_info
( data, display_tuple, sort_tuple ) = data_info
self._data_to_indices[ data ] = index
self._UpdateRow( index, display_tuple )
if data in selected_data_quick:
self.Select( index )
def _UpdateRow( self, index, display_tuple ):
for ( column_index, value ) in enumerate( display_tuple ):
existing_value = self.GetItem( index, column_index )
if existing_value != value:
self.SetItem( index, column_index, value )
def AddDatas( self, datas ):
for data in datas:
( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data )
self._AddDataInfo( ( data, display_tuple, sort_tuple ) )
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
def AddMenuCallable( self, menu_callable ):
self._menu_callable = menu_callable
self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )
def DeleteDatas( self, datas ):
deletees = [ ( self._data_to_indices[ data ], data ) for data in datas ]
deletees.sort( reverse = True )
# I am not sure, but I think if subsequent deleteitems occur in the same event, the event processing of the first is forced!!
# this means that button checking and so on occurs for n-1 times on an invalid indices structure in this thing before correcting itself in the last one
# if a button update then tests selected data against the invalid index and a selection is on the i+1 or whatever but just got bumped up into invalid area, we are exception city
# this doesn't normally affect us because mostly we _are_ deleting selections when we do deletes, but 'try to link url stuff' auto thing hit this
# I obviously don't want to recalc all indices for every delete
# so I wrote a catch in getdata to skip the missing error, and now I'm moving the data deletion to a second loop, which seems to help
for ( index, data ) in deletees:
self.DeleteItem( index )
for ( index, data ) in deletees:
del self._data_to_indices[ data ]
del self._indices_to_data_info[ index ]
self._RecalculateIndicesAfterDelete()
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
def DeleteSelected( self ):
indices = self._GetSelected()
indices.sort( reverse = True )
for index in indices:
( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ index ]
self.DeleteItem( index )
del self._data_to_indices[ data ]
del self._indices_to_data_info[ index ]
self._RecalculateIndicesAfterDelete()
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
def EventBeginColDrag( self, event ):
# resizeCol is not zero-indexed
if event.GetColumn() == self._resizeCol - 1:
last_column = self.GetColumnCount()
if self._resizeCol != last_column:
self.setResizeColumn( last_column )
else:
event.Veto()
return
event.Skip()
def EventColumnClick( self, event ):
col = event.GetColumn()
if col == self._sort_column:
self._sort_asc = not self._sort_asc
else:
self._sort_column = col
self._sort_asc = True
self._SortAndRefreshRows()
def EventItemActivated( self, event ):
if self._activation_callback is not None:
self._activation_callback()
else:
event.Skip()
def EventKeyDown( self, event ):
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
if key in CC.DELETE_KEYS:
if self._delete_key_callback is not None:
self._delete_key_callback()
elif key in ( ord( 'A' ), ord( 'a' ) ) and modifier == wx.ACCEL_CTRL:
self.SelectAll()
else:
event.Skip()
def EventShowMenu( self, event ):
wx.CallAfter( self._ShowMenu )
event.Skip() # let the right click event go through before doing menu, in case selection should happen
def GetData( self, only_selected = False ):
if only_selected:
indices = self._GetSelected()
else:
indices = self._indices_to_data_info.keys()
result = []
for index in indices:
# this can get fired while indices are invalid, wew
if index not in self._indices_to_data_info:
continue
( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ index ]
result.append( data )
return result
def HasData( self, data ):
return data in self._data_to_indices
def HasSelected( self ):
return self.GetSelectedItemCount() > 0
def SelectAll( self ):
currently_selected = set( self._GetSelected() )
currently_not_selected = [ index for index in range( self.GetItemCount() ) if index not in currently_selected ]
for index in currently_not_selected:
self.Select( index )
def SelectDatas( self, datas ):
for data in datas:
if data in self._data_to_indices:
index = self._data_to_indices[ data ]
self.Select( index )
def SelectNone( self ):
currently_selected = set( self._GetSelected() )
for index in currently_selected:
self.Select( index, False )
def SetData( self, datas ):
existing_datas = set( self._data_to_indices.keys() )
# useful to preserve order here sometimes (e.g. export file path generation order)
datas_to_add = [ data for data in datas if data not in existing_datas ]
datas_to_update = [ data for data in datas if data in existing_datas ]
datas_to_delete = existing_datas.difference( datas )
if len( datas_to_delete ) > 0:
self.DeleteDatas( datas_to_delete )
if len( datas_to_update ) > 0:
self.UpdateDatas( datas_to_update )
if len( datas_to_add ) > 0:
self.AddDatas( datas_to_add )
self._SortAndRefreshRows()
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
def GrowShrinkColumnsHeight( self, ideal_rows ):
# +2 for the header row and * 1.25 for magic rough text-to-rowheight conversion
existing_min_width = self.GetMinClientSize()[0]
( width_gumpf, ideal_client_height ) = ClientGUICommon.ConvertTextToPixels( self, ( 20, int( ( ideal_rows + 2 ) * 1.25 ) ) )
self.SetMinClientSize( ( existing_min_width, ideal_client_height ) )
def Sort( self, col = None, asc = None ):
if col is not None:
self._sort_column = col
if asc is not None:
self._sort_asc = asc
self._SortAndRefreshRows()
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
def UpdateDatas( self, datas = None ):
if datas is None:
# keep it sorted here, which is sometimes useful
indices_and_datas = [ ( index, data ) for ( data, index ) in self._data_to_indices.items() ]
indices_and_datas.sort()
datas = [ data for ( index, data ) in indices_and_datas ]
sort_data_has_changed = False
for data in datas:
( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data )
data_info = ( data, display_tuple, sort_tuple )
index = self._data_to_indices[ data ]
existing_data_info = self._indices_to_data_info[ index ]
if data_info != existing_data_info:
if not sort_data_has_changed:
( existing_data, existing_display_tuple, existing_sort_tuple ) = existing_data_info
if sort_tuple[ self._sort_column ] != existing_sort_tuple[ self._sort_column ]: # this does not govern secondary sorts, but let's not spam sorts m8
sort_data_has_changed = True
self._indices_to_data_info[ index ] = data_info
self._UpdateRow( index, display_tuple )
wx.QueueEvent( self.GetEventHandler(), ListCtrlEvent( -1 ) )
return sort_data_has_changed
class BetterListCtrlPanel( wx.Panel ):
def __init__( self, parent ):
wx.Panel.__init__( self, parent )
self._vbox = wx.BoxSizer( wx.VERTICAL )
self._buttonbox = wx.BoxSizer( wx.HORIZONTAL )
self._listctrl = None
self._permitted_object_types = []
self._import_add_callable = lambda x: None
self._button_infos = []
def _AddAllDefaults( self, defaults_callable, add_callable ):
defaults = defaults_callable()
for default in defaults:
add_callable( default )
self._listctrl.Sort()
def _AddButton( self, button, enabled_only_on_selection = False, enabled_check_func = None ):
self._buttonbox.Add( button, CC.FLAGS_VCENTER )
if enabled_only_on_selection:
enabled_check_func = self._HasSelected
if enabled_check_func is not None:
self._button_infos.append( ( button, enabled_check_func ) )
def _AddSomeDefaults( self, defaults_callable, add_callable ):
defaults = defaults_callable()
selected = False
choice_tuples = [ ( default.GetName(), 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:
add_callable( default )
self._listctrl.Sort()
def _Duplicate( self ):
dupe_data = self._GetExportObject()
if dupe_data is not None:
dupe_data = dupe_data.Duplicate()
self._ImportObject( dupe_data )
self._listctrl.Sort()
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._listctrl.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 _HasSelected( self ):
return self._listctrl.HasSelected()
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' )
self._listctrl.Sort()
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:
paths = dlg.GetPaths()
self._ImportPngs( paths )
self._listctrl.Sort()
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._import_add_callable( 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 _ImportPngs( self, paths ):
for path in paths:
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 file!' )
return
def _UpdateButtons( self ):
for ( button, enabled_check_func ) in self._button_infos:
if enabled_check_func():
button.Enable()
else:
button.Disable()
def AddButton( self, label, clicked_func, enabled_only_on_selection = False, enabled_check_func = None ):
button = ClientGUICommon.BetterButton( self, label, clicked_func )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_check_func = enabled_check_func )
self._UpdateButtons()
def AddDefaultsButton( self, defaults_callable, add_callable ):
import_menu_items = []
all_call = HydrusData.Call( self._AddAllDefaults, defaults_callable, add_callable )
some_call = HydrusData.Call( self._AddSomeDefaults, defaults_callable, add_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 ) )
self.AddMenuButton( 'add defaults', import_menu_items )
def AddImportExportButtons( self, permitted_object_types, import_add_callable ):
self._permitted_object_types = permitted_object_types
self._import_add_callable = import_add_callable
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 (note you can also drag and drop pngs onto this list)', 'Load a data from an encoded png.', self._ImportFromPng ) )
self.AddMenuButton( 'export', export_menu_items, enabled_only_on_selection = True )
self.AddMenuButton( 'import', import_menu_items )
self.AddButton( 'duplicate', self._Duplicate, enabled_only_on_selection = True )
self.SetDropTarget( ClientDragDrop.FileDropTarget( self, filenames_callable = self.ImportFromDragDrop ) )
def AddMenuButton( self, label, menu_items, enabled_only_on_selection = False, enabled_check_func = None ):
button = ClientGUICommon.MenuButton( self, label, menu_items )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_check_func = enabled_check_func )
self._UpdateButtons()
def AddSeparator( self ):
self._buttonbox.Add( ( 20, 20 ), CC.FLAGS_EXPAND_PERPENDICULAR )
def AddWindow( self, window ):
self._buttonbox.Add( window, CC.FLAGS_VCENTER )
def EventContentChanged( self, event ):
if not self._listctrl:
return
self._UpdateButtons()
event.Skip()
def EventSelectionChanged( self, event ):
if not self._listctrl:
return
self._UpdateButtons()
event.Skip()
def ImportFromDragDrop( self, paths ):
import ClientGUIDialogs
message = 'Try to import the ' + HydrusData.ToHumanInt( len( paths ) ) + ' dropped files to this list? I am expecting png files.'
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
if dlg.ShowModal() == wx.ID_YES:
self._ImportPngs( paths )
self._listctrl.Sort()
def NewButtonRow( self ):
self._buttonbox = wx.BoxSizer( wx.HORIZONTAL )
self._vbox.Add( self._buttonbox, CC.FLAGS_BUTTON_SIZER )
def SetListCtrl( self, listctrl ):
self._listctrl = listctrl
self._vbox.Add( self._listctrl, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._vbox.Add( self._buttonbox, CC.FLAGS_BUTTON_SIZER )
self.SetSizer( self._vbox )
self._listctrl.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelectionChanged )
self._listctrl.Bind( wx.EVT_LIST_INSERT_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )
def UpdateButtons( self ):
self._UpdateButtons()