@@ -193,6 +193,8 @@ def __init__(self):
193193 self .is_lmde = False
194194 self .app_restart_required = False
195195 self .show_cinnamon_enabled = False
196+ self .column_upgrade = None
197+ self ._bulk_toggle_paths = None
196198 self .settings .connect ("changed" , self ._on_settings_changed )
197199 self ._on_settings_changed (self .settings , None )
198200
@@ -237,10 +239,10 @@ def __init__(self):
237239 cr .connect ("toggled" , self .toggled )
238240 cr .set_property ("activatable" , True )
239241
240- column_upgrade = Gtk .TreeViewColumn (_ ("Upgrade" ), cr )
241- column_upgrade .add_attribute (cr , "active" , UPDATE_CHECKED )
242- column_upgrade .set_sort_column_id (UPDATE_CHECKED )
243- column_upgrade .set_resizable (True )
242+ self . column_upgrade = Gtk .TreeViewColumn (_ ("Upgrade" ), cr )
243+ self . column_upgrade .add_attribute (cr , "active" , UPDATE_CHECKED )
244+ self . column_upgrade .set_sort_column_id (UPDATE_CHECKED )
245+ self . column_upgrade .set_resizable (True )
244246
245247 column_name = Gtk .TreeViewColumn (_ ("Name" ), Gtk .CellRendererText (), markup = UPDATE_DISPLAY_NAME )
246248 column_name .set_sort_column_id (UPDATE_DISPLAY_NAME )
@@ -270,7 +272,7 @@ def __init__(self):
270272
271273 self .treeview .set_search_equal_func (name_search_func )
272274 self .treeview .append_column (column_type )
273- self .treeview .append_column (column_upgrade )
275+ self .treeview .append_column (self . column_upgrade )
274276 self .treeview .append_column (column_name )
275277 self .treeview .append_column (column_old_version )
276278 self .treeview .append_column (column_new_version )
@@ -281,10 +283,12 @@ def __init__(self):
281283 self .treeview .set_reorderable (False )
282284 self .treeview .show ()
283285
286+ self .treeview .connect ("button-press-event" , self .treeview_button_pressed )
284287 self .treeview .connect ("button-release-event" , self .treeview_right_clicked )
285288 self .treeview .connect ("row-activated" , self .treeview_row_activated )
286289
287290 selection = self .treeview .get_selection ()
291+ selection .set_mode (Gtk .SelectionMode .MULTIPLE )
288292 selection .connect ("changed" , self .display_selected_update )
289293 self .ui_notebook_details .connect ("switch-page" , self .switch_page )
290294 self .ui_window .connect ("delete_event" , self .close_window )
@@ -872,14 +876,69 @@ def show_welcome_page(self, widget=None):
872876 def treeview_row_activated (self , treeview , path , view_column ):
873877 self .toggled (None , path )
874878
879+ def treeview_button_pressed (self , widget , event ):
880+ if event .type != Gdk .EventType .BUTTON_PRESS :
881+ return False
882+
883+ if event .button == Gdk .BUTTON_SECONDARY :
884+ path_info = widget .get_path_at_pos (int (event .x ), int (event .y ))
885+ if path_info is not None :
886+ sel_paths = widget .get_selection ().get_selected_rows ()[1 ]
887+ if any (p .compare (path_info [0 ]) == 0 for p in sel_paths ):
888+ return True
889+ return False
890+
891+ if event .button != Gdk .BUTTON_PRIMARY :
892+ return False
893+ if event .state & (Gdk .ModifierType .CONTROL_MASK | Gdk .ModifierType .SHIFT_MASK ):
894+ return False
895+ path_info = widget .get_path_at_pos (int (event .x ), int (event .y ))
896+ if path_info is None :
897+ return False
898+ path , column , _x , _y = path_info
899+ if column is not self .column_upgrade :
900+ return False
901+ sel_paths = widget .get_selection ().get_selected_rows ()[1 ]
902+ if len (sel_paths ) > 1 and any (p .compare (path ) == 0 for p in sel_paths ):
903+ self ._bulk_toggle_paths = sel_paths
904+ return False
905+
875906 def toggled (self , renderer , path ):
876907 model = self .treeview .get_model ()
908+ if isinstance (path , str ):
909+ path = Gtk .TreePath .new_from_string (path )
877910 iter = model .get_iter (path )
878- if iter is not None :
879- model .set_value (iter , UPDATE_CHECKED , (not model .get_value (iter , UPDATE_CHECKED )))
911+ if iter is None :
912+ return
913+
914+ new_value = not model .get_value (iter , UPDATE_CHECKED )
915+
916+ bulk_paths = self ._bulk_toggle_paths
917+ self ._bulk_toggle_paths = None
918+
919+ if bulk_paths is None :
920+ sel_paths = self .treeview .get_selection ().get_selected_rows ()[1 ]
921+ if len (sel_paths ) > 1 and any (p .compare (path ) == 0 for p in sel_paths ):
922+ bulk_paths = sel_paths
923+
924+ if bulk_paths is None :
925+ model .set_value (iter , UPDATE_CHECKED , new_value )
926+ else :
927+ for p in bulk_paths :
928+ it = model .get_iter (p )
929+ if it is not None :
930+ model .set_value (it , UPDATE_CHECKED , new_value )
931+ GLib .idle_add (self ._restore_selection , bulk_paths )
880932
881933 self .update_installable_state ()
882934
935+ def _restore_selection (self , paths ):
936+ sel = self .treeview .get_selection ()
937+ sel .unselect_all ()
938+ for p in paths :
939+ sel .select_path (p )
940+ return GLib .SOURCE_REMOVE
941+
883942 @_idle
884943 def set_textview_changes_text (self , text ):
885944 self .textview_changes .set_text (text )
@@ -889,8 +948,12 @@ def display_selected_update(self, selection):
889948 self .textview_packages .set_text ("" )
890949 self .textview_description .set_text ("" )
891950 self .textview_changes .set_text ("" )
892- (model , iter ) = selection .get_selected ()
893- if iter is not None :
951+ model , paths = selection .get_selected_rows ()
952+ if len (paths ) > 1 :
953+ self .display_multi_selection (model , paths )
954+ return
955+ if len (paths ) == 1 :
956+ iter = model .get_iter (paths [0 ])
894957 update = model .get_value (iter , UPDATE_OBJ )
895958 description = update .description .replace ("\\ n" , "\n " )
896959 desc_tab = self .ui_notebook_details .get_nth_page (TAB_DESC )
@@ -1170,11 +1233,10 @@ def _compute_refresh_interval(self):
11701233 + self .settings .get_int (f"{ prefix } refresh-days" ) * DAY )
11711234
11721235 def switch_page (self , notebook , page , page_num ):
1173- selection = self .treeview .get_selection ()
1174- (model , iter ) = selection .get_selected ()
1175- if iter and page_num == 2 and not self .changelog_retriever_started :
1236+ model , paths = self .treeview .get_selection ().get_selected_rows ()
1237+ if len (paths ) == 1 and page_num == 2 and not self .changelog_retriever_started :
11761238 # Changelog tab
1177- update = model .get_value (iter , UPDATE_OBJ )
1239+ update = model .get_value (model . get_iter ( paths [ 0 ]) , UPDATE_OBJ )
11781240 self .retrieve_changelog (update )
11791241 self .changelog_retriever_started = True
11801242
@@ -1198,21 +1260,79 @@ def display_package_list(self, update, is_flatpak=False):
11981260 size_label , size_to_string (update .size ))
11991261 self .textview_packages .set_text (packages )
12001262
1263+ def display_multi_selection (self , model , paths ):
1264+ self .textview_description .set_text (
1265+ _ ("Select a single update to view its description and changelog." ))
1266+
1267+ package_names = set ()
1268+ total_size = 0
1269+ for p in paths :
1270+ update = model .get_value (model .get_iter (p ), UPDATE_OBJ )
1271+ package_names .update (update .package_names )
1272+ total_size += update .size
1273+
1274+ prefix = "\n • "
1275+ count = len (package_names )
1276+ packages = "%s%s%s\n %s %s\n \n " % \
1277+ (gettext .ngettext ("The selected updates affect the following installed package:" ,
1278+ "The selected updates affect the following installed packages:" ,
1279+ count ),
1280+ prefix ,
1281+ prefix .join (sorted (package_names )),
1282+ _ ("Total size:" ), size_to_string (total_size ))
1283+ self .textview_packages .set_text (packages )
1284+
1285+ self .ui_notebook_details .get_nth_page (TAB_PACKAGES ).show ()
1286+ self .ui_notebook_details .get_nth_page (TAB_CHANGELOG ).hide ()
1287+ self .changelog_retriever_started = False
1288+
12011289 def treeview_right_clicked (self , widget , event ):
1202- if event .button == 3 :
1203- (model , iter ) = widget .get_selection ().get_selected ()
1204- if iter is not None :
1205- update = model .get_value (iter , UPDATE_OBJ )
1206- menu = Gtk .Menu ()
1207- menuItem = Gtk .MenuItem .new_with_mnemonic (_ ("Ignore the current update for this package" ))
1208- menuItem .connect ("activate" , self .add_to_ignore_list , update .source_packages , True )
1209- menu .append (menuItem )
1210- menuItem = Gtk .MenuItem .new_with_mnemonic (_ ("Ignore all future updates for this package" ))
1211- menuItem .connect ("activate" , self .add_to_ignore_list , update .source_packages , False )
1212- menu .append (menuItem )
1213- menu .attach_to_widget (widget , None )
1214- menu .show_all ()
1215- menu .popup (None , None , None , None , event .button , event .time )
1290+ if event .button != Gdk .BUTTON_SECONDARY :
1291+ return
1292+ path_info = widget .get_path_at_pos (int (event .x ), int (event .y ))
1293+ if path_info is None :
1294+ return
1295+ clicked_path = path_info [0 ]
1296+ model = widget .get_model ()
1297+ selection = widget .get_selection ()
1298+ sel_paths = selection .get_selected_rows ()[1 ]
1299+
1300+ if any (p .compare (clicked_path ) == 0 for p in sel_paths ):
1301+ target_paths = sel_paths
1302+ else :
1303+ target_paths = [clicked_path ]
1304+ selection .unselect_all ()
1305+ selection .select_path (clicked_path )
1306+
1307+ source_packages = []
1308+ seen = set ()
1309+ for p in target_paths :
1310+ update = model .get_value (model .get_iter (p ), UPDATE_OBJ )
1311+ for pkg in update .source_packages :
1312+ if pkg not in seen :
1313+ seen .add (pkg )
1314+ source_packages .append (pkg )
1315+
1316+ count = len (target_paths )
1317+ ignore_current = gettext .ngettext (
1318+ "Ignore the current update for this package" ,
1319+ "Ignore the current updates for these packages" ,
1320+ count )
1321+ ignore_future = gettext .ngettext (
1322+ "Ignore all future updates for this package" ,
1323+ "Ignore all future updates for these packages" ,
1324+ count )
1325+
1326+ menu = Gtk .Menu ()
1327+ menuItem = Gtk .MenuItem .new_with_mnemonic (ignore_current )
1328+ menuItem .connect ("activate" , self .add_to_ignore_list , source_packages , True )
1329+ menu .append (menuItem )
1330+ menuItem = Gtk .MenuItem .new_with_mnemonic (ignore_future )
1331+ menuItem .connect ("activate" , self .add_to_ignore_list , source_packages , False )
1332+ menu .append (menuItem )
1333+ menu .attach_to_widget (widget , None )
1334+ menu .show_all ()
1335+ menu .popup (None , None , None , None , event .button , event .time )
12161336
12171337 def add_to_ignore_list (self , widget , source_packages , versioned ):
12181338 blacklist = self .settings .get_strv ("blacklisted-packages" )
0 commit comments