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
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-02-19 21:48:36 +00:00
if key in ClientGUIShortcuts . DELETE_KEYS :
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 ( )