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
2020-07-29 20:52:44 +00:00
2020-04-22 21:00:35 +00:00
from hydrus . client import ClientConstants as CC
from hydrus . client import ClientSerialisable
from hydrus . client . gui import ClientGUIDragDrop
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
2020-07-15 20:52:09 +00:00
from hydrus . client . gui . lists import ClientGUIListConstants as CGLC
from hydrus . client . gui . lists import ClientGUIListStatus
2021-03-17 21:59:28 +00:00
from hydrus . client . gui . widgets import ClientGUICommon
from hydrus . client . gui . widgets import ClientGUIMenuButton
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
2020-07-15 20:52:09 +00:00
columnListContentsChanged = QC . Signal ( )
columnListStatusChanged = QC . Signal ( )
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
def __init__ ( self , parent , column_list_type , height_num_chars , data_to_tuples_func , use_simple_delete = False , delete_key_callback = None , activation_callback = None , style = None , column_types_to_name_overrides = 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
2021-01-20 22:22:03 +00:00
self . _have_shown_a_column_data_error = False
2020-11-04 23:23:07 +00:00
self . _creation_time = HydrusData . GetNow ( )
2020-07-15 20:52:09 +00:00
self . _column_list_type = column_list_type
self . _column_list_status : ClientGUIListStatus . ColumnListStatus = HG . client_controller . column_list_manager . GetStatus ( self . _column_list_type )
2020-11-04 23:23:07 +00:00
self . _original_column_list_status = self . _column_list_status
2020-07-15 20:52:09 +00:00
2019-11-14 03:56:30 +00:00
self . setAlternatingRowColors ( True )
2020-07-15 20:52:09 +00:00
self . setColumnCount ( self . _column_list_status . GetColumnCount ( ) )
2019-11-14 03:56:30 +00:00
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
2020-07-15 20:52:09 +00:00
self . _initial_height_num_chars = height_num_chars
self . _forced_height_num_chars = None
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
2020-07-15 20:52:09 +00:00
( self . _sort_column_type , self . _sort_asc ) = self . _column_list_status . GetSort ( )
2017-08-02 21:32:54 +00:00
self . _indices_to_data_info = { }
self . _data_to_indices = { }
2020-07-15 20:52:09 +00:00
# old way
'''
#sizing_column_initial_width = self.fontMetrics().boundingRect( 'x' * sizing_column_initial_width_num_chars ).width()
2019-11-14 03:56:30 +00:00
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 )
2020-07-15 20:52:09 +00:00
'''
main_tlw = HG . client_controller . GetMainTLW ( )
2020-07-22 20:59:16 +00:00
# if last section is set too low, for instance 3, the column seems unable to ever shrink from initial (expanded to fill space) size
2020-07-15 20:52:09 +00:00
# _ _ ___ _ _ __ __ ___
# ( \/\/ )( _)( \/\/ ) ( ) ( ) ( \
# \ / ) _) \ / )(__ /__\ ) ) )
# \/\/ (___) \/\/ (____)(_)(_)(___/
#
2020-11-04 23:23:07 +00:00
# I think this is because of mismatch between set size and min size! So ensuring we never set smaller than that initially should fix this???!?
2020-07-15 20:52:09 +00:00
MIN_SECTION_SIZE_CHARS = 3
MIN_LAST_SECTION_SIZE_CHARS = 10
2020-11-04 23:23:07 +00:00
self . _min_section_width = ClientGUIFunctions . ConvertTextToPixelWidth ( main_tlw , MIN_SECTION_SIZE_CHARS )
2020-07-15 20:52:09 +00:00
2020-11-04 23:23:07 +00:00
self . header ( ) . setMinimumSectionSize ( self . _min_section_width )
2020-07-15 20:52:09 +00:00
last_column_index = self . _column_list_status . GetColumnCount ( ) - 1
for ( i , column_type ) in enumerate ( self . _column_list_status . GetColumnTypes ( ) ) :
self . headerItem ( ) . setData ( i , QC . Qt . UserRole , column_type )
if column_types_to_name_overrides is not None and column_type in column_types_to_name_overrides :
name = column_types_to_name_overrides [ column_type ]
else :
name = CGLC . column_list_column_name_lookup [ self . _column_list_type ] [ column_type ]
self . headerItem ( ) . setText ( i , name )
self . headerItem ( ) . setToolTip ( i , name )
if i == last_column_index :
2020-11-04 23:23:07 +00:00
width_chars = MIN_SECTION_SIZE_CHARS
2020-07-15 20:52:09 +00:00
else :
width_chars = self . _column_list_status . GetColumnWidth ( column_type )
2020-11-04 23:23:07 +00:00
width_chars = max ( width_chars , MIN_SECTION_SIZE_CHARS )
2020-07-15 20:52:09 +00:00
# ok this is a pain in the neck issue, but fontmetrics changes afte widget init. I guess font gets styled on top afterwards
# this means that if I use this window's fontmetrics here, in init, then it is different later on, and we get creeping growing columns lmao
# several other places in the client are likely affected in different ways by this also!
width_pixels = ClientGUIFunctions . ConvertTextToPixelWidth ( main_tlw , width_chars )
self . setColumnWidth ( i , width_pixels )
2018-08-08 20:29:54 +00:00
2020-07-15 20:52:09 +00:00
self . header ( ) . setStretchLastSection ( True )
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
2020-07-15 20:52:09 +00:00
self . header ( ) . setSectionsMovable ( False ) # can only turn this on when we move from data/sort tuples
# self.header().setFirstSectionMovable( True ) # same
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
2020-07-15 20:52:09 +00:00
#self.header().sectionMoved.connect( self._DoStatusChanged ) # same
self . header ( ) . sectionResized . connect ( self . _SectionsResized )
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
2020-07-15 20:52:09 +00:00
def _SectionsResized ( self , logical_index , old_size , new_size ) :
self . _DoStatusChanged ( )
2020-11-04 23:23:07 +00:00
self . updateGeometry ( )
2020-07-15 20:52:09 +00:00
def _DoStatusChanged ( self ) :
self . _column_list_status = self . _GenerateCurrentStatus ( )
HG . client_controller . column_list_manager . SaveStatus ( self . _column_list_status )
def _GenerateCurrentStatus ( self ) - > ClientGUIListStatus . ColumnListStatus :
status = ClientGUIListStatus . ColumnListStatus ( )
status . SetColumnListType ( self . _column_list_type )
main_tlw = HG . client_controller . GetMainTLW ( )
columns = [ ]
header = self . header ( )
2020-07-22 20:59:16 +00:00
num_columns = header . count ( )
last_column_index = num_columns - 1
2021-04-14 21:54:17 +00:00
# ok, the big pain in the ass situation here is getting a precise last column size that is reproduced on next dialog launch
# ultimately, with fuzzy sizing, style padding, scrollbars appearing, and other weirdness, the more precisely we try to define it, the more we will get dialogs that grow/shrink by a pixel each time
# *therefore*, the actual solution here is to move to snapping with a decent snap distance. the user loses size setting precision, but we'll snap back to a decent size every time, compensating for fuzz
LAST_COLUMN_SNAP_DISTANCE_CHARS = 5
2020-07-22 20:59:16 +00:00
for visual_index in range ( num_columns ) :
2020-07-15 20:52:09 +00:00
logical_index = header . logicalIndex ( visual_index )
column_type = self . headerItem ( ) . data ( logical_index , QC . Qt . UserRole )
width_pixels = header . sectionSize ( logical_index )
shown = not header . isSectionHidden ( logical_index )
2021-04-14 21:54:17 +00:00
if visual_index == last_column_index :
2020-07-22 20:59:16 +00:00
2021-04-14 21:54:17 +00:00
if self . verticalScrollBar ( ) . isVisible ( ) :
width_pixels + = max ( 0 , min ( self . verticalScrollBar ( ) . width ( ) , 20 ) )
2020-07-22 20:59:16 +00:00
2020-07-15 20:52:09 +00:00
width_chars = ClientGUIFunctions . ConvertPixelsToTextWidth ( main_tlw , width_pixels )
2021-04-14 21:54:17 +00:00
if visual_index == last_column_index :
# here's the snap magic
width_chars = round ( width_chars / / LAST_COLUMN_SNAP_DISTANCE_CHARS ) * LAST_COLUMN_SNAP_DISTANCE_CHARS
2020-07-15 20:52:09 +00:00
columns . append ( ( column_type , width_chars , shown ) )
status . SetColumns ( columns )
status . SetSort ( self . _sort_column_type , self . _sort_asc )
return status
2018-06-27 19:27:05 +00:00
def _GetDisplayAndSortTuples ( self , data ) :
2021-01-20 22:22:03 +00:00
try :
( display_tuple , sort_tuple ) = self . _data_to_tuples_func ( data )
except Exception as e :
if not self . _have_shown_a_column_data_error :
HydrusData . ShowText ( ' A multi-column list was unable to generate text or sort data for one or more rows! Please send hydrus dev the traceback! ' )
HydrusData . ShowException ( e )
self . _have_shown_a_column_data_error = True
error_display_tuple = [ ' unable to display ' for i in range ( self . _column_list_status . GetColumnCount ( ) ) ]
return ( error_display_tuple , None )
2018-06-27 19:27:05 +00:00
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
2021-04-14 21:54:17 +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 ) :
2020-07-15 20:52:09 +00:00
sort_column_index = self . _column_list_status . GetColumnIndexFromType ( self . _sort_column_type )
2017-08-02 21:32:54 +00:00
data_infos = list ( self . _indices_to_data_info . values ( ) )
2021-01-20 22:22:03 +00:00
data_infos_good = [ ( data , display_tuple , sort_tuple ) for ( data , display_tuple , sort_tuple ) in data_infos if sort_tuple is not None ]
data_infos_bad = [ ( data , display_tuple , sort_tuple ) for ( data , display_tuple , sort_tuple ) in data_infos if sort_tuple is None ]
2017-08-02 21:32:54 +00:00
def sort_key ( data_info ) :
( data , display_tuple , sort_tuple ) = data_info
2020-07-15 20:52:09 +00:00
return ( sort_tuple [ sort_column_index ] , sort_tuple ) # add the sort tuple to get secondary sorting
2017-08-02 21:32:54 +00:00
2021-01-20 22:22:03 +00:00
try :
data_infos_good . sort ( key = sort_key , reverse = not self . _sort_asc )
except Exception as e :
HydrusData . ShowText ( ' A multi-column list failed to sort! Please send hydrus dev the traceback! ' )
HydrusData . ShowException ( e )
data_infos_bad . extend ( data_infos_good )
data_infos = data_infos_bad
2017-08-02 21:32:54 +00:00
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
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . 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
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . 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
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . 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
2020-07-15 20:52:09 +00:00
sort_column_type = self . _column_list_status . GetColumnTypeFromIndex ( col )
if sort_column_type == self . _sort_column_type :
2017-08-02 21:32:54 +00:00
self . _sort_asc = not self . _sort_asc
else :
2020-07-15 20:52:09 +00:00
self . _sort_column_type = sort_column_type
2017-08-02 21:32:54 +00:00
self . _sort_asc = True
2017-08-09 21:33:51 +00:00
self . _SortAndRefreshRows ( )
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
self . _DoStatusChanged ( )
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
2020-07-15 20:52:09 +00:00
def ForceHeight ( self , rows ) :
self . _forced_height_num_chars = rows
self . updateGeometry ( )
# +2 for the header row and * 1.25 for magic rough text-to-rowheight conversion
#existing_min_width = self.minimumWidth()
#( width_gumpf, ideal_client_height ) = ClientGUIFunctions.ConvertTextToPixels( self, ( 20, int( ( ideal_rows + 2 ) * 1.25 ) ) )
#QP.SetMinClientSize( self, ( existing_min_width, ideal_client_height ) )
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
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
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . 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
2020-11-04 23:23:07 +00:00
def _GetRowHeightEstimate ( self ) :
if self . topLevelItemCount ( ) > 0 :
height = self . rowHeight ( self . indexFromItem ( self . topLevelItem ( 0 ) ) )
else :
( width_gumpf , height ) = ClientGUIFunctions . ConvertTextToPixels ( self , ( 20 , 1 ) )
return height
2020-07-15 20:52:09 +00:00
def minimumSizeHint ( self ) :
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
width = 0
for i in range ( self . columnCount ( ) - 1 ) :
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
width + = self . columnWidth ( i )
2017-08-02 21:32:54 +00:00
2020-11-04 23:23:07 +00:00
width + = self . _min_section_width # the last column
2020-07-15 20:52:09 +00:00
width + = self . frameWidth ( ) * 2
if self . _forced_height_num_chars is None :
min_num_rows = 4
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
else :
min_num_rows = self . _forced_height_num_chars
2020-11-04 23:23:07 +00:00
header_size = self . header ( ) . sizeHint ( ) # this is better than min size hint for some reason ?( 69, 69 )?
data_area_height = self . _GetRowHeightEstimate ( ) * min_num_rows
PADDING = 10
2020-07-15 20:52:09 +00:00
2020-11-04 23:23:07 +00:00
min_size_hint = QC . QSize ( width , header_size . height ( ) + data_area_height + PADDING )
2020-07-15 20:52:09 +00:00
return min_size_hint
2021-02-17 18:22:44 +00:00
def resizeEvent ( self , event ) :
self . _DoStatusChanged ( )
return QW . QTreeWidget . resizeEvent ( self , event )
2020-07-15 20:52:09 +00:00
def sizeHint ( self ) :
width = 0
2020-11-04 23:23:07 +00:00
# all but last column
2020-07-15 20:52:09 +00:00
for i in range ( self . columnCount ( ) - 1 ) :
width + = self . columnWidth ( i )
#
2020-11-04 23:23:07 +00:00
# ok, we are going full slippery dippery doo now
# the issue is: when we first boot up, we want to give a 'hey, it would be nice' size of the last actual recorded final column
# HOWEVER, after that: we want to use the current size of the last column
# so, if it is the first couple of seconds, lmao. after that, oaml
2021-04-14 21:54:17 +00:00
# I later updated this to use the columnWidth, rather than hickery dickery text-to-pixel-width, since it was juddering resize around text width phase
2020-07-15 20:52:09 +00:00
last_column_type = self . _column_list_status . GetColumnTypes ( ) [ - 1 ]
2020-11-04 23:23:07 +00:00
if HydrusData . TimeHasPassed ( self . _creation_time + 2 ) :
2021-04-14 21:54:17 +00:00
width + = self . columnWidth ( self . columnCount ( ) - 1 )
2020-11-04 23:23:07 +00:00
else :
last_column_chars = self . _original_column_list_status . GetColumnWidth ( last_column_type )
2021-04-14 21:54:17 +00:00
main_tlw = HG . client_controller . GetMainTLW ( )
width + = ClientGUIFunctions . ConvertTextToPixelWidth ( main_tlw , last_column_chars )
2020-07-15 20:52:09 +00:00
#
width + = self . frameWidth ( ) * 2
if self . _forced_height_num_chars is None :
num_rows = self . _initial_height_num_chars
else :
num_rows = self . _forced_height_num_chars
2020-11-04 23:23:07 +00:00
header_size = self . header ( ) . sizeHint ( )
data_area_height = self . _GetRowHeightEstimate ( ) * num_rows
PADDING = 10
2020-07-15 20:52:09 +00:00
2020-11-04 23:23:07 +00:00
size_hint = QC . QSize ( width , header_size . height ( ) + data_area_height + PADDING )
2020-07-15 20:52:09 +00:00
return size_hint
def Sort ( self , sort_column_type = None , sort_asc = None ) :
if sort_column_type is not None :
self . _sort_column_type = sort_column_type
if sort_asc is not None :
self . _sort_asc = sort_asc
2017-08-02 21:32:54 +00:00
2017-08-09 21:33:51 +00:00
self . _SortAndRefreshRows ( )
2017-08-02 21:32:54 +00:00
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . emit ( )
self . _DoStatusChanged ( )
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
2020-07-15 20:52:09 +00:00
sort_index = self . _column_list_status . GetColumnIndexFromType ( self . _sort_column_type )
2018-05-23 21:05:06 +00:00
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
2021-01-20 22:22:03 +00:00
if existing_sort_tuple is not None and sort_tuple is not None :
2018-05-23 21:05:06 +00:00
2021-01-20 22:22:03 +00:00
# this does not govern secondary sorts, but let's not spam sorts m8
if sort_tuple [ sort_index ] != existing_sort_tuple [ sort_index ] :
sort_data_has_changed = True
2018-05-23 21:05:06 +00:00
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
2020-07-15 20:52:09 +00:00
self . columnListContentsChanged . 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
2020-07-29 20:52:44 +00:00
QP . AddToLayout ( self . _buttonbox , button , CC . FLAGS_CENTER_PERPENDICULAR )
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-07-29 20:52:44 +00:00
from hydrus . client . gui import ClientGUIDialogsQuick
2018-02-21 21:59:37 +00:00
2020-07-29 20:52:44 +00:00
try :
2018-08-08 20:29:54 +00:00
2020-07-29 20:52:44 +00:00
defaults_to_add = ClientGUIDialogsQuick . SelectMultipleFromList ( self , ' select the defaults to add ' , choice_tuples )
2018-08-08 20:29:54 +00:00
2020-07-29 20:52:44 +00:00
except HydrusExceptions . CancelledException :
2018-02-21 21:59:37 +00:00
2020-07-29 20:52:44 +00:00
return
for default in defaults_to_add :
add_callable ( default )
2018-02-21 21:59:37 +00:00
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 )
2020-06-24 21:25:24 +00:00
def _ExportToJSON ( self ) :
export_object = self . _GetExportObject ( )
if export_object is not None :
json = export_object . DumpToString ( )
2020-07-08 22:00:33 +00:00
with QP . FileDialog ( self , ' select where to save the json file ' , default_filename = ' export.json ' , wildcard = ' JSON (*.json) ' , acceptMode = QW . QFileDialog . AcceptSave , fileMode = QW . QFileDialog . AnyFile ) as f_dlg :
2020-06-24 21:25:24 +00:00
if f_dlg . exec ( ) == QW . QDialog . Accepted :
path = f_dlg . GetPath ( )
if os . path . exists ( path ) :
from hydrus . client . gui import ClientGUIDialogsQuick
message = ' The path " {} " already exists! Ok to overwrite? ' . format ( path )
result = ClientGUIDialogsQuick . GetYesNo ( self , message )
if result != QW . QDialog . Accepted :
return
with open ( path , ' w ' , encoding = ' utf-8 ' ) as f :
f . write ( json )
def _ExportToPNG ( self ) :
2017-11-29 21:48:23 +00:00
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
2020-06-24 21:25:24 +00:00
panel = ClientGUISerialisable . PNGExportPanel ( dlg , export_object )
2017-11-29 21:48:23 +00:00
dlg . SetPanel ( panel )
2019-11-14 03:56:30 +00:00
dlg . exec ( )
2017-11-29 21:48:23 +00:00
2020-06-24 21:25:24 +00:00
def _ExportToPNGs ( self ) :
2018-02-07 23:40:33 +00:00
export_object = self . _GetExportObject ( )
if export_object is None :
return
if not isinstance ( export_object , HydrusSerialisable . SerialisableList ) :
2020-06-24 21:25:24 +00:00
self . _ExportToPNG ( )
2018-02-07 23:40:33 +00:00
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
2020-06-24 21:25:24 +00:00
panel = ClientGUISerialisable . PNGsExportPanel ( dlg , export_object )
2018-02-07 23:40:33 +00:00
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
2021-01-20 22:22:03 +00:00
obj = HydrusSerialisable . CreateFromString ( raw_text , raise_error_on_future_version = True )
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
2021-01-20 22:22:03 +00:00
except HydrusExceptions . SerialisationException as e :
QW . QMessageBox . critical ( self , ' Problem loading ' , str ( e ) )
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
2020-06-24 21:25:24 +00:00
def _ImportFromJSON ( self ) :
with QP . FileDialog ( self , ' select the json or jsons with the serialised data ' , acceptMode = QW . QFileDialog . AcceptOpen , fileMode = QW . QFileDialog . ExistingFiles , wildcard = ' JSON (*.json)|*.json ' ) as dlg :
if dlg . exec ( ) == QW . QDialog . Accepted :
paths = dlg . GetPaths ( )
self . _ImportJSONs ( paths )
self . _listctrl . Sort ( )
def _ImportFromPNG ( self ) :
2017-11-29 21:48:23 +00:00
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 ( )
2020-06-24 21:25:24 +00:00
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
2020-06-24 21:25:24 +00:00
def _ImportJSONs ( self , paths ) :
2021-01-20 22:22:03 +00:00
have_shown_load_error = False
2020-06-24 21:25:24 +00:00
for path in paths :
try :
with open ( path , ' r ' , encoding = ' utf-8 ' ) as f :
payload = f . read ( )
except Exception as e :
QW . QMessageBox . critical ( self , ' Error ' , str ( e ) )
return
try :
2021-01-20 22:22:03 +00:00
obj = HydrusSerialisable . CreateFromString ( payload , raise_error_on_future_version = True )
2020-06-24 21:25:24 +00:00
self . _ImportObject ( obj )
2021-01-20 22:22:03 +00:00
except HydrusExceptions . SerialisationException as e :
if not have_shown_load_error :
message = str ( e )
if len ( paths ) > 1 :
message + = os . linesep * 2
message + = ' If there are more objects in this import with similar load problems, they will now be skipped silently. '
QW . QMessageBox . critical ( self , ' Problem loading ' , str ( e ) )
have_shown_load_error = True
2020-06-24 21:25:24 +00:00
except :
QW . QMessageBox . critical ( self , ' Error ' , ' I could not understand what was encoded in " {} " ! ' . format ( path ) )
return
def _ImportPNGs ( self , paths ) :
2018-06-20 20:20:22 +00:00
2021-01-20 22:22:03 +00:00
have_shown_load_error = False
2018-06-20 20:20:22 +00:00
for path in paths :
try :
2020-06-24 21:25:24 +00:00
payload = ClientSerialisable . LoadFromPNG ( path )
2018-06-20 20:20:22 +00:00
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 :
2021-01-20 22:22:03 +00:00
obj = HydrusSerialisable . CreateFromNetworkBytes ( payload , raise_error_on_future_version = True )
2018-06-20 20:20:22 +00:00
self . _ImportObject ( obj )
2021-01-20 22:22:03 +00:00
except HydrusExceptions . SerialisationException as e :
if not have_shown_load_error :
message = str ( e )
if len ( paths ) > 1 :
message + = os . linesep * 2
message + = ' If there are more objects in this import with similar load problems, they will now be skipped silently. '
QW . QMessageBox . critical ( self , ' Problem loading ' , str ( e ) )
have_shown_load_error = True
2018-06-20 20:20:22 +00:00
except :
2020-06-24 21:25:24 +00:00
QW . QMessageBox . critical ( self , ' Error ' , ' I could not understand what was encoded in " {} " ! ' . format ( path ) )
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 ) )
2020-06-24 21:25:24 +00:00
export_menu_items . append ( ( ' normal ' , ' to json file ' , ' Serialise the selected data and export to a json file. ' , self . _ExportToJSON ) )
export_menu_items . append ( ( ' normal ' , ' to png file ' , ' Serialise the selected data and encode it to an image file you can easily share with other hydrus users. ' , self . _ExportToPNG ) )
2017-11-29 21:48:23 +00:00
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 :
2020-06-24 21:25:24 +00:00
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 ) )
2020-04-16 00:09:42 +00:00
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 ) )
2020-06-24 21:25:24 +00:00
import_menu_items . append ( ( ' normal ' , ' from json files ' , ' Load a data from .json files. ' , self . _ImportFromJSON ) )
import_menu_items . append ( ( ' normal ' , ' from png files (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
2021-03-17 21:59:28 +00:00
button = ClientGUIMenuButton . MenuButton ( self , label , menu_items )
2017-11-08 22:07:12 +00:00
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 ) :
2020-07-29 20:52:44 +00:00
self . _buttonbox . addSpacing ( 12 )
2017-09-27 21:52:54 +00:00
def AddWindow ( self , window ) :
2020-07-29 20:52:44 +00:00
QP . AddToLayout ( self . _buttonbox , window , CC . FLAGS_CENTER_PERPENDICULAR )
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
2020-11-11 22:20:16 +00:00
message = ' Try to import the {} dropped files to this list? I am expecting json or png files. ' . format ( HydrusData . ToHumanInt ( len ( paths ) ) )
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
2020-06-24 21:25:24 +00:00
( jsons , pngs ) = HydrusData . PartitionIteratorIntoLists ( lambda path : path . endswith ( ' .png ' ) , paths )
self . _ImportPNGs ( pngs )
self . _ImportJSONs ( jsons )
2019-09-05 00:05:32 +00:00
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
2020-07-29 20:52:44 +00:00
QP . AddToLayout ( self . _vbox , self . _buttonbox , CC . FLAGS_ON_RIGHT )
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 )
2020-07-29 20:52:44 +00:00
QP . AddToLayout ( self . _vbox , self . _buttonbox , CC . FLAGS_ON_RIGHT )
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 ( )