1# Copyright (c) 2011 John Stowers 2# SPDX-License-Identifier: GPL-3.0+ 3# License-Filename: LICENSES/GPL-3.0 4 5import os.path 6import subprocess 7import logging 8 9from gi.repository import Gtk, Gdk, GLib, Gio, GObject 10 11from gtweak.tweakmodel import Tweak 12from gtweak.widgets import ListBoxTweakGroup, UI_BOX_SPACING 13from gtweak.utils import AutostartManager, AutostartFile 14 15def _image_from_gicon(gicon): 16 image = Gtk.Image.new_from_gicon(gicon, Gtk.IconSize.DIALOG) 17 (_, _, h) = Gtk.IconSize.lookup(Gtk.IconSize.DIALOG) 18 image.set_pixel_size(h) 19 return image 20 21def _list_header_func(row, before, user_data): 22 if before and not row.get_header(): 23 row.set_header (Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) 24 25 26class AutostartTitle(Gtk.Box, Tweak): 27 28 def __init__(self, **options): 29 Gtk.Box.__init__(self) 30 desc = _("Startup applications are automatically started when you log in.") 31 Tweak.__init__(self, _("Startup Applications"), desc, **options) 32 33 label = Gtk.Label(desc) 34 label.get_style_context().add_class("dim-label") 35 self.props.margin_bottom = 10 36 self.add(label) 37 38 39class _AppChooser(Gtk.Dialog): 40 def __init__(self, main_window, running_exes, startup_apps): 41 uhb = Gtk.Settings.get_default().props.gtk_dialogs_use_header 42 Gtk.Dialog.__init__(self, title=_("Applications"), use_header_bar=uhb) 43 44 self._running = {} 45 self._all = {} 46 47 self.entry = Gtk.SearchEntry( 48 placeholder_text=_("Search Applications…")) 49 self.entry.set_width_chars(30) 50 self.entry.props.activates_default=True 51 if (Gtk.check_version(3, 22, 20) == None): 52 self.entry.set_input_hints(Gtk.InputHints.NO_EMOJI) 53 54 self.searchbar = Gtk.SearchBar() 55 self.searchbar.add(self.entry) 56 self.searchbar.props.hexpand = True 57 # Translators: This is the accelerator for opening the AppChooser search-bar 58 self._search_key, self._search_mods = Gtk.accelerator_parse(_("<primary>f")) 59 60 lb = Gtk.ListBox() 61 lb.props.activate_on_single_click = False 62 lb.set_sort_func(self._sort_apps, None) 63 lb.set_header_func(_list_header_func, None) 64 lb.set_filter_func(self._list_filter_func, None) 65 self.entry.connect("search-changed", self._on_search_entry_changed) 66 lb.connect("row-activated", lambda b, r: self.response(Gtk.ResponseType.OK) if r.get_mapped() else None) 67 lb.connect("row-selected", self._on_row_selected) 68 69 apps = Gio.app_info_get_all() 70 for a in apps: 71 if a.get_id() not in startup_apps: 72 if a.should_show(): 73 running = a.get_executable() in running_exes 74 w = self._build_widget( 75 a, 76 _("running") if running else "") 77 if w: 78 self._all[w] = a 79 self._running[w] = running 80 lb.add(w) 81 82 sw = Gtk.ScrolledWindow() 83 sw.props.margin = 2 84 sw.props.hscrollbar_policy = Gtk.PolicyType.NEVER 85 sw.add(lb) 86 87 self.add_button(_("_Close"), Gtk.ResponseType.CANCEL) 88 self.add_button(_("_Add"), Gtk.ResponseType.OK) 89 self.set_default_response(Gtk.ResponseType.OK) 90 91 if self.props.use_header_bar: 92 searchbtn = Gtk.ToggleButton() 93 searchbtn.props.valign = Gtk.Align.CENTER 94 image = Gtk.Image(icon_name = "edit-find-symbolic", icon_size = Gtk.IconSize.MENU) 95 searchbtn.add(image) 96 context = searchbtn.get_style_context() 97 context.add_class("image-button") 98 context.remove_class("text-button") 99 self.get_header_bar().pack_end(searchbtn) 100 self._binding = searchbtn.bind_property("active", self.searchbar, "search-mode-enabled", GObject.BindingFlags.BIDIRECTIONAL) 101 102 self.get_content_area().pack_start(self.searchbar, False, False, 0) 103 self.get_content_area().pack_start(sw, True, True, 0) 104 self.set_modal(True) 105 self.set_transient_for(main_window) 106 self.set_size_request(400,300) 107 108 self.listbox = lb 109 110 self.connect("key-press-event", self._on_key_press) 111 112 def _sort_apps(self, a, b, user_data): 113 arun = self._running.get(a) 114 brun = self._running.get(b) 115 116 if arun and not brun: 117 return -1 118 elif not arun and brun: 119 return 1 120 else: 121 aname = self._all.get(a).get_name() 122 bname = self._all.get(b).get_name() 123 124 if aname < bname: 125 return -1 126 elif aname > bname: 127 return 1 128 else: 129 return 0 130 131 def _build_widget(self, a, extra): 132 row = Gtk.ListBoxRow() 133 g = Gtk.Grid() 134 g.props.margin = 5 135 136 if not a.get_name(): 137 return None 138 icn = a.get_icon() 139 if icn: 140 img = _image_from_gicon(icn) 141 g.attach(img, 0, 0, 1, 1) 142 img.props.hexpand = False 143 else: 144 img = None #attach_next_to treats this correctly 145 lbl = Gtk.Label(label=a.get_name(), xalign=0) 146 g.attach_next_to(lbl,img,Gtk.PositionType.RIGHT,1,1) 147 lbl.props.hexpand = True 148 lbl.props.halign = Gtk.Align.START 149 lbl.props.vexpand = False 150 lbl.props.valign = Gtk.Align.CENTER 151 if extra: 152 g.attach_next_to( 153 Gtk.Label(label=extra), 154 lbl,Gtk.PositionType.RIGHT,1,1) 155 row.add(g) 156 #row.get_style_context().add_class('tweak-white') 157 return row 158 159 def _list_filter_func(self, row, unused): 160 txt = self.entry.get_text().lower() 161 grid = row.get_child() 162 for sib in grid.get_children(): 163 if type(sib) == Gtk.Label: 164 if txt in sib.get_text().lower(): 165 return True 166 return False 167 168 def _on_search_entry_changed(self, editable): 169 self.listbox.invalidate_filter() 170 selected = self.listbox.get_selected_row() 171 if selected and selected.get_mapped(): 172 self.set_response_sensitive(Gtk.ResponseType.OK, True) 173 else: 174 self.set_response_sensitive(Gtk.ResponseType.OK, False) 175 176 def _on_row_selected(self, box, row): 177 if row and row.get_mapped(): 178 self.set_response_sensitive(Gtk.ResponseType.OK, True) 179 else: 180 self.set_response_sensitive(Gtk.ResponseType.OK, False) 181 182 def _on_key_press(self, widget, event): 183 mods = event.state & Gtk.accelerator_get_default_mod_mask() 184 if event.keyval == self._search_key and mods == self._search_mods: 185 self.searchbar.set_search_mode(not self.searchbar.get_search_mode()) 186 return True 187 keyname = Gdk.keyval_name(event.keyval) 188 if keyname == 'Escape': 189 if self.searchbar.get_search_mode(): 190 self.searchbar.set_search_mode(False) 191 return True 192 elif keyname not in ['Up', 'Down']: 193 if not self.entry.is_focus() and self.searchbar.get_search_mode(): 194 if self.entry.im_context_filter_keypress(event): 195 self.entry.grab_focus() 196 l = self.entry.get_text_length() 197 self.entry.select_region(l, l) 198 return True 199 200 return self.searchbar.handle_event(event) 201 202 return False 203 204 def get_selected_app(self): 205 row = self.listbox.get_selected_row() 206 if row: 207 return self._all.get(row) 208 return None 209 210class _StartupTweak(Gtk.ListBoxRow, Tweak): 211 def __init__(self, df, **options): 212 213 Gtk.ListBoxRow.__init__(self) 214 Tweak.__init__(self, 215 df.get_name(), 216 df.get_description(), 217 **options) 218 219 grid = Gtk.Grid(column_spacing=10) 220 221 icn = df.get_icon() 222 if icn: 223 img = _image_from_gicon(icn) 224 grid.attach(img, 0, 0, 1, 1) 225 else: 226 img = None #attach_next_to treats this correctly 227 228 lbl = Gtk.Label(label=df.get_name(), xalign=0.0) 229 grid.attach_next_to(lbl,img,Gtk.PositionType.RIGHT,1,1) 230 lbl.props.hexpand = True 231 lbl.props.halign = Gtk.Align.START 232 233 btn = Gtk.Button(label=_("Remove")) 234 grid.attach_next_to(btn,lbl,Gtk.PositionType.RIGHT,1,1) 235 btn.props.vexpand = False 236 btn.props.valign = Gtk.Align.CENTER 237 238 self.add(grid) 239 240 self.props.margin_start = 1 241 self.props.margin_end = 1 242 self.get_style_context().add_class('tweak-startup') 243 244 self.btn = btn 245 self.app_id = df.get_id() 246 self.connect("key-press-event", self._on_key_press_event) 247 248 def _on_key_press_event(self, row, event): 249 if event.keyval in [Gdk.KEY_Delete, Gdk.KEY_KP_Delete, Gdk.KEY_BackSpace]: 250 self.btn.activate() 251 return True 252 return False 253 254class AddStartupTweak(Gtk.ListBoxRow, Tweak): 255 def __init__(self, **options): 256 Gtk.ListBoxRow.__init__(self) 257 Tweak.__init__(self, _("New startup application"), 258 _("Add a new application to be run at startup"), 259 **options) 260 261 img = Gtk.Image() 262 img.set_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON) 263 self.btn = Gtk.Button(label="", image=img, always_show_image=True) 264 self.btn.get_style_context().remove_class("button") 265 self.add(self.btn) 266 self.get_style_context().add_class('tweak-startup') 267 self.connect("map", self._on_map) 268 self.connect("unmap", self._on_unmap) 269 270 def _on_map(self, row): 271 toplevel=self.get_toplevel() 272 if toplevel.is_toplevel: 273 for k in [Gdk.KEY_equal, Gdk.KEY_plus, Gdk.KEY_KP_Add]: 274 toplevel.add_mnemonic(k, self.btn) 275 276 def _on_unmap(self, row): 277 toplevel=self.get_toplevel() 278 if toplevel.is_toplevel: 279 for k in [Gdk.KEY_equal, Gdk.KEY_plus, Gdk.KEY_KP_Add]: 280 toplevel.remove_mnemonic(k, self.btn) 281 282class AutostartListBoxTweakGroup(ListBoxTweakGroup): 283 def __init__(self): 284 tweaks = [AutostartTitle()] 285 286 self.asm = AutostartManager() 287 files = self.asm.get_user_autostart_files() 288 for f in files: 289 try: 290 df = Gio.DesktopAppInfo.new_from_filename(f) 291 except TypeError: 292 logging.warning("Error loading desktopfile: %s" % f) 293 continue 294 295 if not AutostartFile(df).is_start_at_login_enabled(): 296 continue 297 298 sdf = _StartupTweak(df) 299 sdf.btn.connect("clicked", self._on_remove_clicked, sdf, df) 300 tweaks.append( sdf ) 301 302 add = AddStartupTweak() 303 add.btn.connect("clicked", self._on_add_clicked) 304 tweaks.append(add) 305 306 ListBoxTweakGroup.__init__(self, 307 _("Startup Applications"), 308 *tweaks, 309 css_class='tweak-group-startup') 310 self.set_header_func(_list_header_func, None) 311 self.connect("row-activated", lambda b, row: add.btn.activate() if row == add else None) 312 313 def _on_remove_clicked(self, btn, widget, df): 314 self.remove(widget) 315 AutostartFile(df).update_start_at_login(False) 316 317 def _on_add_clicked(self, btn): 318 Gio.Application.get_default().mark_busy() 319 startup_apps = set() 320 self.foreach(lambda row: startup_apps.add(row.app_id) if type(row) is _StartupTweak else None) 321 a = _AppChooser( 322 self.main_window, 323 set(self._get_running_executables()), 324 startup_apps) 325 a.show_all() 326 Gio.Application.get_default().unmark_busy() 327 resp = a.run() 328 if resp == Gtk.ResponseType.OK: 329 df = a.get_selected_app() 330 if df: 331 AutostartFile(df).update_start_at_login(True) 332 sdf = _StartupTweak(df) 333 sdf.btn.connect("clicked", self._on_remove_clicked, sdf, df) 334 self.add_tweak_row(sdf, 0).show_all() 335 a.destroy() 336 337 def _get_running_executables(self): 338 exes = [] 339 cmd = subprocess.Popen([ 340 'ps','-e','-w','-w','-U', 341 str(os.getuid()),'-o','args'], 342 stdout=subprocess.PIPE) 343 out = cmd.communicate()[0] 344 for l in out.decode('utf8').split('\n'): 345 exe = l.split(' ')[0] 346 if exe and exe[0] != '[': #kernel process 347 exes.append( os.path.basename(exe) ) 348 349 return exes 350 351TWEAK_GROUPS = [ 352 AutostartListBoxTweakGroup(), 353] 354