hydrus/hydrus/client/gui/ClientGUIListCtrl.py

1084 lines
34 KiB
Python
Raw Normal View History

2017-11-29 21:48:23 +00:00
import os
2020-04-29 21:44:12 +00:00
import typing
2020-04-22 21:00:35 +00:00
2019-11-14 03:56:30 +00:00
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
2020-04-22 21:00:35 +00:00
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientSerialisable
from hydrus.client.gui import ClientGUIDragDrop
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
2017-12-06 22:06:56 +00:00
2019-01-09 22:59:03 +00:00
def SafeNoneInt( value ):
return -1 if value is None else value
def SafeNoneStr( value ):
return '' if value is None else value
2019-11-14 03:56:30 +00:00
class BetterListCtrl( QW.QTreeWidget ):
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
listCtrlChanged = QC.Signal()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
def __init__( self, parent, name, height_num_chars, sizing_column_initial_width_num_chars, columns, data_to_tuples_func, use_simple_delete = False, delete_key_callback = None, activation_callback = None, style = None ):
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
QW.QTreeWidget.__init__( self, parent )
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.setAlternatingRowColors( True )
self.setColumnCount( len(columns) )
self.setSortingEnabled( False ) # Keeping the custom sort implementation. It would be better to use Qt's native sorting in the future so sort indicators are displayed on the headers as expected.
self.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
self.setRootIsDecorated( False )
2017-08-02 21:32:54 +00:00
self._data_to_tuples_func = data_to_tuples_func
2018-10-17 21:00:09 +00:00
self._use_simple_delete = use_simple_delete
2018-09-19 21:54:51 +00:00
self._menu_callable = None
2017-08-02 21:32:54 +00:00
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 = {}
2019-11-14 03:56:30 +00:00
sizing_column_initial_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width()
total_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width()
2017-08-02 21:32:54 +00:00
resize_column = 1
for ( i, ( name, width_num_chars ) ) in enumerate( columns ):
if width_num_chars == -1:
width = -1
resize_column = i + 1
else:
2019-11-14 03:56:30 +00:00
width = self.fontMetrics().boundingRect( 'x' * width_num_chars ).width()
2017-08-02 21:32:54 +00:00
total_width += width
2019-11-14 03:56:30 +00:00
self.headerItem().setText( i, name )
2019-12-05 05:29:32 +00:00
2019-11-14 03:56:30 +00:00
self.setColumnWidth( i, width )
2017-08-02 21:32:54 +00:00
2019-12-05 05:29:32 +00:00
2019-11-14 03:56:30 +00:00
# Technically this is the previous behavior, but the two commented lines might work better in some cases (?)
self.header().setStretchLastSection( False )
self.header().setSectionResizeMode( resize_column - 1 , QW.QHeaderView.Stretch )
#self.setColumnWidth( resize_column - 1, sizing_column_initial_width )
#self.header().setStretchLastSection( True )
2017-08-02 21:32:54 +00:00
2020-05-27 21:27:52 +00:00
# hydev looked at this problem. the real answer I think will be to move to column size memory and let the last section resize
# start with decent values and then we can remember whatever the user ends up liking later. this will be simpler
2019-11-14 03:56:30 +00:00
self.setMinimumWidth( total_width )
2018-08-08 20:29:54 +00:00
self.GrowShrinkColumnsHeight( height_num_chars )
2017-08-02 21:32:54 +00:00
self._delete_key_callback = delete_key_callback
self._activation_callback = activation_callback
2019-11-14 03:56:30 +00:00
self._widget_event_filter = QP.WidgetEventFilter( self )
self._widget_event_filter.EVT_KEY_DOWN( self.EventKeyDown )
self.itemDoubleClicked.connect( self.EventItemActivated )
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.header().setSectionsClickable( True )
self.header().sectionClicked.connect( self.EventColumnClick )
2017-08-02 21:32:54 +00:00
2017-08-30 20:27:47 +00:00
def _AddDataInfo( self, data_info ):
2017-08-02 21:32:54 +00:00
( data, display_tuple, sort_tuple ) = data_info
2018-11-28 22:31:04 +00:00
if data in self._data_to_indices:
return
2019-11-14 03:56:30 +00:00
append_item = QW.QTreeWidgetItem()
for i in range( len( display_tuple ) ):
2019-12-05 05:29:32 +00:00
text = display_tuple[i]
if len( text ) > 0:
text = text.splitlines()[0]
append_item.setText( i, text )
append_item.setToolTip( i, text )
2019-11-14 03:56:30 +00:00
2019-12-05 05:29:32 +00:00
2019-11-14 03:56:30 +00:00
self.addTopLevelItem( append_item )
index = self.topLevelItemCount() - 1
2017-08-02 21:32:54 +00:00
self._indices_to_data_info[ index ] = data_info
self._data_to_indices[ data ] = index
2018-06-27 19:27:05 +00:00
def _GetDisplayAndSortTuples( self, data ):
( display_tuple, sort_tuple ) = self._data_to_tuples_func( data )
better_sort = []
for item in sort_tuple:
2019-01-09 22:59:03 +00:00
if isinstance( item, str ):
2018-06-27 19:27:05 +00:00
2019-01-09 22:59:03 +00:00
item = HydrusData.HumanTextSortKey( item )
2018-06-27 19:27:05 +00:00
better_sort.append( item )
sort_tuple = tuple( better_sort )
return ( display_tuple, sort_tuple )
2019-11-14 03:56:30 +00:00
def _GetSelected( self ):
2017-08-02 21:32:54 +00:00
indices = []
2019-11-14 03:56:30 +00:00
for i in range( self.topLevelItemCount() ):
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
if self.topLevelItem( i ).isSelected(): indices.append( i )
2017-08-02 21:32:54 +00:00
return indices
2017-08-09 21:33:51 +00:00
def _RecalculateIndicesAfterDelete( self ):
2020-05-13 19:03:16 +00:00
indices_and_data_info = sorted( self._indices_to_data_info.items() )
2017-08-09 21:33:51 +00:00
self._indices_to_data_info = {}
self._data_to_indices = {}
2017-08-30 20:27:47 +00:00
for ( index, ( old_index, data_info ) ) in enumerate( indices_and_data_info ):
2017-08-09 21:33:51 +00:00
( data, display_tuple, sort_tuple ) = data_info
self._data_to_indices[ data ] = index
self._indices_to_data_info[ index ] = data_info
2018-09-19 21:54:51 +00:00
def _ShowMenu( self ):
try:
menu = self._menu_callable()
except HydrusExceptions.DataMissing:
return
2020-03-04 22:12:53 +00:00
CGC.core().PopupMenu( self, menu )
2018-09-19 21:54:51 +00:00
2017-08-02 21:32:54 +00:00
def _SortDataInfo( self ):
data_infos = list( self._indices_to_data_info.values() )
def sort_key( data_info ):
( data, display_tuple, sort_tuple ) = data_info
2017-08-09 21:33:51 +00:00
return ( sort_tuple[ self._sort_column ], sort_tuple ) # add the sort tuple to get secondary sorting
2017-08-02 21:32:54 +00:00
data_infos.sort( key = sort_key, reverse = not self._sort_asc )
return data_infos
2017-08-09 21:33:51 +00:00
def _SortAndRefreshRows( self ):
2017-08-02 21:32:54 +00:00
2017-09-20 19:47:31 +00:00
selected_data_quick = set( self.GetData( only_selected = True ) )
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.clearSelection()
2017-08-02 21:32:54 +00:00
sorted_data_info = self._SortDataInfo()
self._indices_to_data_info = {}
self._data_to_indices = {}
2017-08-30 20:27:47 +00:00
for ( index, data_info ) in enumerate( sorted_data_info ):
self._indices_to_data_info[ index ] = data_info
2017-08-02 21:32:54 +00:00
( data, display_tuple, sort_tuple ) = data_info
2017-08-30 20:27:47 +00:00
self._data_to_indices[ data ] = index
2017-08-02 21:32:54 +00:00
2017-08-30 20:27:47 +00:00
self._UpdateRow( index, display_tuple )
2017-09-20 19:47:31 +00:00
if data in selected_data_quick:
2017-08-30 20:27:47 +00:00
2019-11-14 03:56:30 +00:00
self.topLevelItem( index ).setSelected( True )
2017-08-30 20:27:47 +00:00
2017-08-02 21:32:54 +00:00
2017-08-30 20:27:47 +00:00
def _UpdateRow( self, index, display_tuple ):
2017-08-02 21:32:54 +00:00
2017-08-30 20:27:47 +00:00
for ( column_index, value ) in enumerate( display_tuple ):
2019-12-05 05:29:32 +00:00
if len( value ) > 0:
value = value.splitlines()[0]
tree_widget_item = self.topLevelItem( index )
existing_value = tree_widget_item.text( column_index )
2018-05-23 21:05:06 +00:00
if existing_value != value:
2019-12-05 05:29:32 +00:00
tree_widget_item.setText( column_index, value )
tree_widget_item.setToolTip( column_index, value )
2018-05-23 21:05:06 +00:00
2017-08-30 20:27:47 +00:00
2017-08-02 21:32:54 +00:00
2017-08-30 20:27:47 +00:00
2020-04-29 21:44:12 +00:00
def AddDatas( self, datas: typing.Iterable[ object ] ):
2017-08-30 20:27:47 +00:00
for data in datas:
2018-06-27 19:27:05 +00:00
( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data )
2017-08-30 20:27:47 +00:00
self._AddDataInfo( ( data, display_tuple, sort_tuple ) )
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2018-04-11 22:30:40 +00:00
2017-08-02 21:32:54 +00:00
2018-09-19 21:54:51 +00:00
def AddMenuCallable( self, menu_callable ):
self._menu_callable = menu_callable
2019-11-14 03:56:30 +00:00
self.setContextMenuPolicy( QC.Qt.CustomContextMenu )
self.customContextMenuRequested.connect( self.EventShowMenu )
2018-09-19 21:54:51 +00:00
2020-04-29 21:44:12 +00:00
def DeleteDatas( self, datas: typing.Iterable[ object ] ):
2017-08-02 21:32:54 +00:00
deletees = [ ( self._data_to_indices[ data ], data ) for data in datas ]
deletees.sort( reverse = True )
2019-11-14 03:56:30 +00:00
# The below comment is most probably obsolote (from before the Qt port), but keeping it just in case it is not and also as an explanation.
#
2018-08-29 20:20:41 +00:00
# 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
2017-08-02 21:32:54 +00:00
for ( index, data ) in deletees:
2019-11-14 03:56:30 +00:00
self.takeTopLevelItem( index )
2017-08-02 21:32:54 +00:00
2018-08-29 20:20:41 +00:00
for ( index, data ) in deletees:
2017-08-02 21:32:54 +00:00
del self._data_to_indices[ data ]
del self._indices_to_data_info[ index ]
2017-08-09 21:33:51 +00:00
self._RecalculateIndicesAfterDelete()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2017-08-30 20:27:47 +00:00
2017-08-02 21:32:54 +00:00
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 ]
2019-11-14 03:56:30 +00:00
item = self.takeTopLevelItem( index )
del item
2017-08-02 21:32:54 +00:00
del self._data_to_indices[ data ]
del self._indices_to_data_info[ index ]
2017-08-09 21:33:51 +00:00
self._RecalculateIndicesAfterDelete()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2018-04-11 22:30:40 +00:00
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
def EventColumnClick( self, col ):
2019-12-11 23:18:37 +00:00
2017-08-02 21:32:54 +00:00
if col == self._sort_column:
self._sort_asc = not self._sort_asc
else:
self._sort_column = col
self._sort_asc = True
2017-08-09 21:33:51 +00:00
self._SortAndRefreshRows()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
def EventItemActivated( self, item, column ):
2017-08-02 21:32:54 +00:00
if self._activation_callback is not None:
self._activation_callback()
def EventKeyDown( self, event ):
2018-05-16 20:09:50 +00:00
( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
2017-08-02 21:32:54 +00:00
2020-05-27 21:27:52 +00:00
if key in ClientGUIShortcuts.DELETE_KEYS_QT:
2017-08-02 21:32:54 +00:00
2018-10-17 21:00:09 +00:00
self.ProcessDeleteAction()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
elif key in ( ord( 'A' ), ord( 'a' ) ) and modifier == QC.Qt.ControlModifier:
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.selectAll()
2017-08-02 21:32:54 +00:00
else:
2019-11-14 03:56:30 +00:00
return True # was: event.ignore()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
def EventShowMenu( self ):
2018-09-19 21:54:51 +00:00
2019-11-14 03:56:30 +00:00
QP.CallAfter( self._ShowMenu )
2018-09-19 21:54:51 +00:00
2017-08-02 21:32:54 +00:00
def GetData( self, only_selected = False ):
if only_selected:
indices = self._GetSelected()
else:
2019-01-09 22:59:03 +00:00
indices = list(self._indices_to_data_info.keys())
2017-08-02 21:32:54 +00:00
2017-09-20 19:47:31 +00:00
result = []
2017-08-02 21:32:54 +00:00
for index in indices:
2018-08-29 20:20:41 +00:00
# this can get fired while indices are invalid, wew
if index not in self._indices_to_data_info:
continue
2017-08-02 21:32:54 +00:00
( data, display_tuple, sort_tuple ) = self._indices_to_data_info[ index ]
2017-09-20 19:47:31 +00:00
result.append( data )
2017-08-02 21:32:54 +00:00
return result
2018-10-17 21:00:09 +00:00
def GrowShrinkColumnsHeight( self, ideal_rows ):
# +2 for the header row and * 1.25 for magic rough text-to-rowheight conversion
2019-11-14 03:56:30 +00:00
existing_min_width = self.minimumWidth()
2018-10-17 21:00:09 +00:00
2019-06-26 21:27:18 +00:00
( width_gumpf, ideal_client_height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 20, int( ( ideal_rows + 2 ) * 1.25 ) ) )
2018-10-17 21:00:09 +00:00
2019-11-14 03:56:30 +00:00
QP.SetMinClientSize( self, ( existing_min_width, ideal_client_height ) )
2018-10-17 21:00:09 +00:00
2020-04-29 21:44:12 +00:00
def HasData( self, data: object ):
2017-08-02 21:32:54 +00:00
return data in self._data_to_indices
2019-01-30 22:14:54 +00:00
def HasOneSelected( self ):
2019-11-14 03:56:30 +00:00
return len( self.selectedItems() ) == 1
2019-01-30 22:14:54 +00:00
2017-08-02 21:32:54 +00:00
def HasSelected( self ):
2019-11-14 03:56:30 +00:00
return len( self.selectedItems() ) > 0
2017-08-02 21:32:54 +00:00
2018-10-17 21:00:09 +00:00
def ProcessDeleteAction( self ):
if self._use_simple_delete:
self.ShowDeleteSelectedDialog()
elif self._delete_key_callback is not None:
self._delete_key_callback()
2020-04-29 21:44:12 +00:00
def SelectDatas( self, datas: typing.Iterable[ object ] ):
2017-11-15 22:35:49 +00:00
for data in datas:
if data in self._data_to_indices:
index = self._data_to_indices[ data ]
2019-11-14 03:56:30 +00:00
self.topLevelItem( index ).setSelected( True )
2017-11-15 22:35:49 +00:00
2020-04-29 21:44:12 +00:00
def SetData( self, datas: typing.Iterable[ object ] ):
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
existing_datas = set( self._data_to_indices.keys() )
2017-08-02 21:32:54 +00:00
2018-05-30 20:13:21 +00:00
# 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 ]
2017-08-09 21:33:51 +00:00
datas_to_delete = existing_datas.difference( datas )
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
if len( datas_to_delete ) > 0:
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
self.DeleteDatas( datas_to_delete )
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
if len( datas_to_update ) > 0:
self.UpdateDatas( datas_to_update )
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
if len( datas_to_add ) > 0:
2017-08-30 20:27:47 +00:00
self.AddDatas( datas_to_add )
2017-08-09 21:33:51 +00:00
2017-12-13 22:33:07 +00:00
self._SortAndRefreshRows()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2018-04-11 22:30:40 +00:00
2017-08-02 21:32:54 +00:00
2018-10-17 21:00:09 +00:00
def ShowDeleteSelectedDialog( self ):
2018-08-08 20:29:54 +00:00
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUIDialogsQuick
2018-08-08 20:29:54 +00:00
2019-09-05 00:05:32 +00:00
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
2019-11-14 03:56:30 +00:00
if result == QW.QDialog.Accepted:
2018-10-17 21:00:09 +00:00
2019-09-05 00:05:32 +00:00
self.DeleteSelected()
2018-10-17 21:00:09 +00:00
2018-08-08 20:29:54 +00:00
2017-08-02 21:32:54 +00:00
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
2017-08-09 21:33:51 +00:00
self._SortAndRefreshRows()
2017-08-02 21:32:54 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2018-04-11 22:30:40 +00:00
2017-08-02 21:32:54 +00:00
2020-04-29 21:44:12 +00:00
def UpdateDatas( self, datas: typing.Optional[ typing.Iterable[ object ] ] = None ):
2017-09-06 20:18:20 +00:00
if datas is None:
2018-05-30 20:13:21 +00:00
# keep it sorted here, which is sometimes useful
2020-05-13 19:03:16 +00:00
indices_and_datas = sorted( ( ( index, data ) for ( data, index ) in self._data_to_indices.items() ) )
2018-05-30 20:13:21 +00:00
datas = [ data for ( index, data ) in indices_and_datas ]
2017-09-06 20:18:20 +00:00
2017-08-09 21:33:51 +00:00
2018-05-23 21:05:06 +00:00
sort_data_has_changed = False
2017-08-09 21:33:51 +00:00
for data in datas:
2018-06-27 19:27:05 +00:00
( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( data )
2017-08-09 21:33:51 +00:00
data_info = ( data, display_tuple, sort_tuple )
index = self._data_to_indices[ data ]
2018-05-23 21:05:06 +00:00
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
2017-08-09 21:33:51 +00:00
self._indices_to_data_info[ index ] = data_info
2017-08-30 20:27:47 +00:00
self._UpdateRow( index, display_tuple )
2017-08-09 21:33:51 +00:00
2019-11-14 03:56:30 +00:00
self.listCtrlChanged.emit()
2018-04-11 22:30:40 +00:00
2018-05-23 21:05:06 +00:00
return sort_data_has_changed
2019-11-14 03:56:30 +00:00
2020-04-29 21:44:12 +00:00
def SetNonDupeName( self, obj: object ):
2019-11-14 03:56:30 +00:00
current_names = { o.GetName() for o in self.GetData() if o is not obj }
HydrusSerialisable.SetNonDupeName( obj, current_names )
2018-05-23 21:05:06 +00:00
2019-11-14 03:56:30 +00:00
2020-04-29 21:44:12 +00:00
def ReplaceData( self, old_data: object, new_data: object ):
2019-11-14 03:56:30 +00:00
new_data = QP.ListsToTuples( new_data )
data_index = self._data_to_indices[ old_data ]
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
( display_tuple, sort_tuple ) = self._GetDisplayAndSortTuples( new_data )
data_info = ( new_data, display_tuple, sort_tuple )
self._indices_to_data_info[ data_index ] = data_info
del self._data_to_indices[ old_data ]
self._data_to_indices[ new_data ] = data_index
self._UpdateRow( data_index, display_tuple )
2020-04-16 00:09:42 +00:00
2019-11-14 03:56:30 +00:00
class BetterListCtrlPanel( QW.QWidget ):
2017-09-27 21:52:54 +00:00
def __init__( self, parent ):
2019-11-14 03:56:30 +00:00
QW.QWidget.__init__( self, parent )
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
self._vbox = QP.VBoxLayout()
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
self._buttonbox = QP.HBoxLayout()
2017-09-27 21:52:54 +00:00
self._listctrl = None
2017-11-29 21:48:23 +00:00
self._permitted_object_types = []
self._import_add_callable = lambda x: None
2020-04-16 00:09:42 +00:00
self._custom_get_callable = None
2017-11-29 21:48:23 +00:00
2017-09-27 21:52:54 +00:00
self._button_infos = []
2018-02-21 21:59:37 +00:00
def _AddAllDefaults( self, defaults_callable, add_callable ):
defaults = defaults_callable()
for default in defaults:
add_callable( default )
2018-04-25 22:07:52 +00:00
self._listctrl.Sort()
2018-02-21 21:59:37 +00:00
2019-01-30 22:14:54 +00:00
def _AddButton( self, button, enabled_only_on_selection = False, enabled_only_on_single_selection = False, enabled_check_func = None ):
2017-11-08 22:07:12 +00:00
2019-11-14 03:56:30 +00:00
QP.AddToLayout( self._buttonbox, button, CC.FLAGS_VCENTER )
2017-11-08 22:07:12 +00:00
if enabled_only_on_selection:
enabled_check_func = self._HasSelected
2019-01-30 22:14:54 +00:00
if enabled_only_on_single_selection:
enabled_check_func = self._HasOneSelected
2017-11-08 22:07:12 +00:00
if enabled_check_func is not None:
self._button_infos.append( ( button, enabled_check_func ) )
2018-02-21 21:59:37 +00:00
def _AddSomeDefaults( self, defaults_callable, add_callable ):
defaults = defaults_callable()
selected = False
choice_tuples = [ ( default.GetName(), default, selected ) for default in defaults ]
2020-04-29 21:44:12 +00:00
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
2018-02-21 21:59:37 +00:00
2020-04-29 21:44:12 +00:00
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'select the defaults to add' ) as dlg:
2018-08-08 20:29:54 +00:00
panel = ClientGUIScrolledPanelsEdit.EditChooseMultiple( dlg, choice_tuples )
dlg.SetPanel( panel )
2018-02-21 21:59:37 +00:00
2019-11-14 03:56:30 +00:00
if dlg.exec() == QW.QDialog.Accepted:
2018-02-21 21:59:37 +00:00
2018-08-08 20:29:54 +00:00
defaults_to_add = panel.GetValue()
2018-02-21 21:59:37 +00:00
for default in defaults_to_add:
add_callable( default )
2018-04-25 22:07:52 +00:00
self._listctrl.Sort()
2018-02-21 21:59:37 +00:00
2017-11-29 21:48:23 +00:00
def _Duplicate( self ):
dupe_data = self._GetExportObject()
if dupe_data is not None:
dupe_data = dupe_data.Duplicate()
self._ImportObject( dupe_data )
2018-08-08 20:29:54 +00:00
self._listctrl.Sort()
2017-11-29 21:48:23 +00:00
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:
2020-04-29 21:44:12 +00:00
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUISerialisable
2017-11-29 21:48:23 +00:00
2020-04-29 21:44:12 +00:00
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
2017-11-29 21:48:23 +00:00
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object )
dlg.SetPanel( panel )
2019-11-14 03:56:30 +00:00
dlg.exec()
2017-11-29 21:48:23 +00:00
2018-02-07 23:40:33 +00:00
def _ExportToPngs( self ):
export_object = self._GetExportObject()
if export_object is None:
return
if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
self._ExportToPng()
return
2020-04-29 21:44:12 +00:00
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUISerialisable
2018-02-07 23:40:33 +00:00
2020-04-29 21:44:12 +00:00
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to pngs' ) as dlg:
2018-02-07 23:40:33 +00:00
panel = ClientGUISerialisable.PngsExportPanel( dlg, export_object )
dlg.SetPanel( panel )
2019-11-14 03:56:30 +00:00
dlg.exec()
2018-02-07 23:40:33 +00:00
2017-11-29 21:48:23 +00:00
def _GetExportObject( self ):
2020-04-16 00:09:42 +00:00
if self._custom_get_callable is None:
2017-11-29 21:48:23 +00:00
2020-04-16 00:09:42 +00:00
to_export = HydrusSerialisable.SerialisableList()
for obj in self._listctrl.GetData( only_selected = True ):
to_export.append( obj )
else:
to_export = [ self._custom_get_callable() ]
2017-11-29 21:48:23 +00:00
if len( to_export ) == 0:
return None
elif len( to_export ) == 1:
return to_export[0]
else:
return to_export
2017-09-27 21:52:54 +00:00
def _HasSelected( self ):
return self._listctrl.HasSelected()
2019-01-30 22:14:54 +00:00
def _HasOneSelected( self ):
return self._listctrl.HasOneSelected()
2017-11-29 21:48:23 +00:00
def _ImportFromClipboard( self ):
2019-06-19 22:08:48 +00:00
try:
raw_text = HG.client_controller.GetClipboardText()
except HydrusExceptions.DataMissing as e:
2019-11-14 03:56:30 +00:00
QW.QMessageBox.critical( self, 'Error', str(e) )
2019-06-19 22:08:48 +00:00
return
2017-12-13 22:33:07 +00:00
try:
2017-11-29 21:48:23 +00:00
2017-12-13 22:33:07 +00:00
obj = HydrusSerialisable.CreateFromString( raw_text )
2017-11-29 21:48:23 +00:00
2017-12-13 22:33:07 +00:00
self._ImportObject( obj )
2017-11-29 21:48:23 +00:00
2017-12-13 22:33:07 +00:00
except Exception as e:
2017-11-29 21:48:23 +00:00
2019-11-14 03:56:30 +00:00
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was in the clipboard' )
2017-11-29 21:48:23 +00:00
2018-04-25 22:07:52 +00:00
self._listctrl.Sort()
2017-11-29 21:48:23 +00:00
def _ImportFromPng( self ):
2019-11-14 03:56:30 +00:00
with QP.FileDialog( self, 'select the png or pngs with the encoded data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'PNG (*.png)|*.png' ) as dlg:
2017-11-29 21:48:23 +00:00
2019-11-14 03:56:30 +00:00
if dlg.exec() == QW.QDialog.Accepted:
2017-11-29 21:48:23 +00:00
2018-06-20 20:20:22 +00:00
paths = dlg.GetPaths()
self._ImportPngs( paths )
2017-11-29 21:48:23 +00:00
2018-04-25 22:07:52 +00:00
self._listctrl.Sort()
2017-11-29 21:48:23 +00:00
def _ImportObject( self, obj ):
2018-06-20 20:20:22 +00:00
bad_object_type_names = set()
2017-11-29 21:48:23 +00:00
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:
2018-06-20 20:20:22 +00:00
bad_object_type_names.add( HydrusData.GetTypeName( type( obj ) ) )
2017-11-29 21:48:23 +00:00
2018-06-20 20:20:22 +00:00
if len( bad_object_type_names ) > 0:
2017-11-29 21:48:23 +00:00
message = 'The imported objects included these types:'
message += os.linesep * 2
2018-06-20 20:20:22 +00:00
message += os.linesep.join( bad_object_type_names )
2017-11-29 21:48:23 +00:00
message += os.linesep * 2
message += 'Whereas this control only allows:'
message += os.linesep * 2
2018-06-20 20:20:22 +00:00
message += os.linesep.join( ( HydrusData.GetTypeName( o ) for o in self._permitted_object_types ) )
2017-11-29 21:48:23 +00:00
2019-11-14 03:56:30 +00:00
QW.QMessageBox.critical( self, 'Error', message )
2017-11-29 21:48:23 +00:00
2018-06-20 20:20:22 +00:00
def _ImportPngs( self, paths ):
for path in paths:
try:
payload = ClientSerialisable.LoadFromPng( path )
except Exception as e:
2019-11-14 03:56:30 +00:00
QW.QMessageBox.critical( self, 'Error', str(e) )
2018-06-20 20:20:22 +00:00
return
try:
2019-01-09 22:59:03 +00:00
obj = HydrusSerialisable.CreateFromNetworkBytes( payload )
2018-06-20 20:20:22 +00:00
self._ImportObject( obj )
except:
2019-11-14 03:56:30 +00:00
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in the file!' )
2018-06-20 20:20:22 +00:00
return
2017-09-27 21:52:54 +00:00
def _UpdateButtons( self ):
for ( button, enabled_check_func ) in self._button_infos:
if enabled_check_func():
2019-11-14 03:56:30 +00:00
button.setEnabled( True )
2017-09-27 21:52:54 +00:00
else:
2019-11-14 03:56:30 +00:00
button.setEnabled( False )
2017-09-27 21:52:54 +00:00
2020-01-02 03:05:35 +00:00
def AddBitmapButton( self, bitmap, clicked_func, tooltip = None, enabled_only_on_selection = False, enabled_only_on_single_selection = False, enabled_check_func = None ):
button = ClientGUICommon.BetterBitmapButton( self, bitmap, clicked_func )
if tooltip is not None:
button.setToolTip( tooltip )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_only_on_single_selection = enabled_only_on_single_selection, enabled_check_func = enabled_check_func )
self._UpdateButtons()
2019-01-30 22:14:54 +00:00
def AddButton( self, label, clicked_func, enabled_only_on_selection = False, enabled_only_on_single_selection = False, enabled_check_func = None ):
2017-09-27 21:52:54 +00:00
button = ClientGUICommon.BetterButton( self, label, clicked_func )
2019-01-30 22:14:54 +00:00
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_only_on_single_selection = enabled_only_on_single_selection, enabled_check_func = enabled_check_func )
2017-09-27 21:52:54 +00:00
2017-11-15 22:35:49 +00:00
self._UpdateButtons()
2017-11-08 22:07:12 +00:00
2018-02-21 21:59:37 +00:00
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 )
2020-01-22 21:04:43 +00:00
def AddDeleteButton( self, enabled_check_func = None ):
2018-10-17 21:00:09 +00:00
2020-01-22 21:04:43 +00:00
if enabled_check_func is None:
enabled_only_on_selection = True
else:
enabled_only_on_selection = False
self.AddButton( 'delete', self._listctrl.ProcessDeleteAction, enabled_check_func = enabled_check_func, enabled_only_on_selection = enabled_only_on_selection )
2018-10-17 21:00:09 +00:00
2020-04-16 00:09:42 +00:00
def AddImportExportButtons( self, permitted_object_types, import_add_callable, custom_get_callable = None ):
2017-11-29 21:48:23 +00:00
self._permitted_object_types = permitted_object_types
self._import_add_callable = import_add_callable
2020-04-16 00:09:42 +00:00
self._custom_get_callable = custom_get_callable
2017-11-29 21:48:23 +00:00
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 ) )
2020-04-16 00:09:42 +00:00
if self._custom_get_callable is None:
2018-02-07 23:40:33 +00:00
2020-04-16 00:09:42 +00:00
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 ) )
2018-02-07 23:40:33 +00:00
2017-11-29 21:48:23 +00:00
import_menu_items = []
import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
2018-06-20 20:20:22 +00:00
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 ) )
2017-11-29 21:48:23 +00:00
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 )
2019-11-14 03:56:30 +00:00
self.setAcceptDrops( True )
2020-03-11 21:52:11 +00:00
self.installEventFilter( ClientGUIDragDrop.FileDropTarget( self, filenames_callable = self.ImportFromDragDrop ) )
2018-06-20 20:20:22 +00:00
2017-11-29 21:48:23 +00:00
2017-11-08 22:07:12 +00:00
def AddMenuButton( self, label, menu_items, enabled_only_on_selection = False, enabled_check_func = None ):
2017-09-27 21:52:54 +00:00
2017-11-08 22:07:12 +00:00
button = ClientGUICommon.MenuButton( self, label, menu_items )
self._AddButton( button, enabled_only_on_selection = enabled_only_on_selection, enabled_check_func = enabled_check_func )
2017-11-15 22:35:49 +00:00
self._UpdateButtons()
2017-11-08 22:07:12 +00:00
def AddSeparator( self ):
2019-11-14 03:56:30 +00:00
self._buttonbox.insertStretch( -1, 1 )
2017-09-27 21:52:54 +00:00
def AddWindow( self, window ):
2019-11-14 03:56:30 +00:00
QP.AddToLayout( self._buttonbox, window, CC.FLAGS_VCENTER )
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
def EventContentChanged( self, parent, first, last ):
2017-09-27 21:52:54 +00:00
2018-01-17 22:52:10 +00:00
if not self._listctrl:
return
2017-09-27 21:52:54 +00:00
self._UpdateButtons()
2019-11-14 03:56:30 +00:00
def EventSelectionChanged( self ):
2017-09-27 21:52:54 +00:00
2018-01-17 22:52:10 +00:00
if not self._listctrl:
return
2017-09-27 21:52:54 +00:00
self._UpdateButtons()
2018-06-20 20:20:22 +00:00
def ImportFromDragDrop( self, paths ):
2020-04-22 21:00:35 +00:00
from hydrus.client.gui import ClientGUIDialogsQuick
2018-06-20 20:20:22 +00:00
2018-07-04 20:48:28 +00:00
message = 'Try to import the ' + HydrusData.ToHumanInt( len( paths ) ) + ' dropped files to this list? I am expecting png files.'
2018-06-20 20:20:22 +00:00
2019-09-05 00:05:32 +00:00
result = ClientGUIDialogsQuick.GetYesNo( self, message )
2019-11-14 03:56:30 +00:00
if result == QW.QDialog.Accepted:
2018-06-20 20:20:22 +00:00
2019-09-05 00:05:32 +00:00
self._ImportPngs( paths )
self._listctrl.Sort()
2018-06-20 20:20:22 +00:00
2017-12-20 22:55:48 +00:00
def NewButtonRow( self ):
2019-11-14 03:56:30 +00:00
self._buttonbox = QP.HBoxLayout()
2017-12-20 22:55:48 +00:00
2019-11-14 03:56:30 +00:00
QP.AddToLayout( self._vbox, self._buttonbox, CC.FLAGS_BUTTON_SIZER )
2017-12-20 22:55:48 +00:00
2017-09-27 21:52:54 +00:00
def SetListCtrl( self, listctrl ):
self._listctrl = listctrl
2019-11-14 03:56:30 +00:00
QP.AddToLayout( self._vbox, self._listctrl, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( self._vbox, self._buttonbox, CC.FLAGS_BUTTON_SIZER )
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
self.setLayout( self._vbox )
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
self._listctrl.itemSelectionChanged.connect( self.EventSelectionChanged )
2017-09-27 21:52:54 +00:00
2019-11-14 03:56:30 +00:00
self._listctrl.model().rowsInserted.connect( self.EventContentChanged )
self._listctrl.model().rowsRemoved.connect( self.EventContentChanged )
2017-09-27 21:52:54 +00:00
2018-05-16 20:09:50 +00:00
def UpdateButtons( self ):
self._UpdateButtons()