def __init__(self, auto_exit, *args, **kwargs): super(GUI, self).__init__(*args, **kwargs) self.auto_exit = auto_exit self.set_wmclass(APP_NAME, APP_NAME) self.populate_window() # Redirect logging to the GUI. bb_logger = logging.getLogger('bleachbit') from bleachbit.Log import GtkLoggerHandler self.gtklog = GtkLoggerHandler(self.append_text) bb_logger.addHandler(self.gtklog) # process any delayed logs from bleachbit.Log import DelayLog if isinstance(sys.stderr, DelayLog): for msg in sys.stderr.read(): self.append_text(msg) # if stderr was redirected - keep redirecting it sys.stderr = self.gtklog self.set_windows10_theme() Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', options.get('dark_mode')) if options.is_corrupt(): logger.error( _('Resetting the configuration file because it is corrupt: %s') % bleachbit.options_file) bleachbit.Options.init_configuration() GLib.idle_add(self.cb_refresh_operations)
class GUI(Gtk.ApplicationWindow): """The main application GUI""" _style_provider = None _style_provider_regular = None _style_provider_dark = None def __init__(self, auto_exit, *args, **kwargs): super(GUI, self).__init__(*args, **kwargs) self.auto_exit = auto_exit self.set_wmclass(APP_NAME, APP_NAME) self.populate_window() # Redirect logging to the GUI. bb_logger = logging.getLogger('bleachbit') from bleachbit.Log import GtkLoggerHandler self.gtklog = GtkLoggerHandler(self.append_text) bb_logger.addHandler(self.gtklog) # process any delayed logs from bleachbit.Log import DelayLog if isinstance(sys.stderr, DelayLog): for msg in sys.stderr.read(): self.append_text(msg) # if stderr was redirected - keep redirecting it sys.stderr = self.gtklog self.set_windows10_theme() Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', options.get('dark_mode')) if options.is_corrupt(): logger.error( _('Resetting the configuration file because it is corrupt: %s') % bleachbit.options_file) bleachbit.Options.init_configuration() GLib.idle_add(self.cb_refresh_operations) def get_preferences_dialog(self): return PreferencesDialog( self, self.cb_refresh_operations, self.set_windows10_theme) def shred_paths(self, paths, shred_settings=False): """Shred file or folders When shredding_settings=True: If user confirms to delete, then returns True. If user aborts, returns False. When quit_when_done=True: Always returns False to remove function from the idle queue. """ # create a temporary cleaner object backends['_gui'] = Cleaner.create_simple_cleaner(paths) # preview and confirm operations = {'_gui': ['files']} self.preview_or_run_operations(False, operations) if GuiBasic.delete_confirmation_dialog(self, mention_preview=False, shred_settings=shred_settings): # delete self.preview_or_run_operations(True, operations) return True # user aborted return False def append_text(self, text, tag=None, __iter=None, scroll=True): """Add some text to the main log""" if not __iter: __iter = self.textbuffer.get_end_iter() if tag: self.textbuffer.insert_with_tags_by_name(__iter, text, tag) else: self.textbuffer.insert(__iter, text) # Scroll to end. If the command is run directly instead of # through the idle loop, it may only scroll most of the way # as seen on Ubuntu 9.04 with Italian and Spanish. if scroll: GLib.idle_add(lambda: self.textview.scroll_mark_onscreen( self.textbuffer.get_insert())) def update_log_level(self): """This gets called when the log level might have changed via the preferences.""" self.gtklog.update_log_level() def on_selection_changed(self, selection): """When the tree view selection changed""" model = self.view.get_model() selected_rows = selection.get_selected_rows() if not selected_rows[1]: # empty # happens when searching in the tree view return paths = selected_rows[1][0] row = paths[0] name = model[row][0] cleaner_id = model[row][2] self.progressbar.hide() description = backends[cleaner_id].get_description() self.textbuffer.set_text("") self.append_text(name + "\n", 'operation', scroll=False) if not description: description = "" self.append_text(description + "\n\n\n", 'description', scroll=False) for (label, description) in backends[cleaner_id].get_option_descriptions(): self.append_text(label, 'option_label', scroll=False) if description: self.append_text(': ', 'option_label', scroll=False) self.append_text(description, scroll=False) self.append_text("\n\n", scroll=False) def get_selected_operations(self): """Return a list of the IDs of the selected operations in the tree view""" ret = [] model = self.tree_store.get_model() path = Gtk.TreePath(0) __iter = model.get_iter(path) while __iter: if model[__iter][1]: ret.append(model[__iter][2]) __iter = model.iter_next(__iter) return ret def get_operation_options(self, operation): """For the given operation ID, return a list of the selected option IDs.""" ret = [] model = self.tree_store.get_model() path = Gtk.TreePath(0) __iter = model.get_iter(path) while __iter: if operation == model[__iter][2]: iterc = model.iter_children(__iter) if not iterc: return None while iterc: if model[iterc][1]: # option is enabled ret.append(model[iterc][2]) iterc = model.iter_next(iterc) return ret __iter = model.iter_next(__iter) return None def set_sensitive(self, is_sensitive): """Disable commands while an operation is running""" self.view.set_sensitive(is_sensitive) self.preview_button.set_sensitive(is_sensitive) self.run_button.set_sensitive(is_sensitive) self.stop_button.set_sensitive(not is_sensitive) def run_operations(self, __widget): """Event when the 'delete' toolbar button is clicked.""" # fixme: should present this dialog after finding operations # Disable delete confirmation message. # if the option is selected under preference. if options.get("delete_confirmation"): if not GuiBasic.delete_confirmation_dialog(self, True): return self.preview_or_run_operations(True) def preview_or_run_operations(self, really_delete, operations=None): """Preview operations or run operations (delete files)""" assert isinstance(really_delete, bool) from bleachbit import Worker self.start_time = None if not operations: operations = {} for operation in self.get_selected_operations(): operations[operation] = self.get_operation_options(operation) assert isinstance(operations, dict) if not operations: # empty GuiBasic.message_dialog(self, _("You must select an operation"), Gtk.MessageType.WARNING, Gtk.ButtonsType.OK) return try: self.set_sensitive(False) self.textbuffer.set_text("") self.progressbar.show() self.worker = Worker.Worker(self, really_delete, operations) except Exception: logger.exception('Error in Worker()') else: self.start_time = time.time() worker = self.worker.run() GLib.idle_add(worker.__next__) def worker_done(self, worker, really_delete): """Callback for when Worker is done""" self.progressbar.set_text("") self.progressbar.set_fraction(1) self.progressbar.set_text(_("Done.")) self.textview.scroll_mark_onscreen(self.textbuffer.get_insert()) self.set_sensitive(True) # Close the program after cleaning is completed. # if the option is selected under preference. if really_delete: if options.get("exit_done"): sys.exit() # notification for long-running process elapsed = (time.time() - self.start_time) logger.debug('elapsed time: %d seconds', elapsed) if elapsed < 10 or self.is_active(): return notify(_("Done.")) def create_operations_box(self): """Create and return the operations box (which holds a tree view)""" scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy( Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.tree_store = TreeInfoModel() display = TreeDisplayModel() mdl = self.tree_store.get_model() self.view = display.make_view( mdl, self, self.context_menu_event) self.view.get_selection().connect("changed", self.on_selection_changed) scrolled_window.add(self.view) return scrolled_window def cb_refresh_operations(self): """Callback to refresh the list of cleaners""" # Is this the first time in this session? if not hasattr(self, 'recognized_cleanerml') and not self.auto_exit: from bleachbit import RecognizeCleanerML RecognizeCleanerML.RecognizeCleanerML() self.recognized_cleanerml = True # reload cleaners from disk self.view.expand_all() self.progressbar.show() rc = register_cleaners(self.update_progress_bar, self.cb_register_cleaners_done) GLib.idle_add(rc.__next__) return False def cb_register_cleaners_done(self): """Called from register_cleaners()""" self.progressbar.hide() # update tree view self.tree_store.refresh_rows() # expand tree view self.view.expand_all() # Check for online updates. if not self.auto_exit and \ bleachbit.online_update_notification_enabled and \ options.get("check_online_updates") and \ not hasattr(self, 'checked_for_updates'): self.checked_for_updates = True self.check_online_updates() # Show information for first start. # (The first start flag is set also for each new version.) if options.get("first_start") and not self.auto_exit: if os.name == 'posix': self.append_text( _('Access the application menu by clicking the hamburger icon on the title bar.')) pref = self.get_preferences_dialog() pref.run() if os.name == 'nt': self.append_text( _('Access the application menu by clicking the logo on the title bar.')) options.set('first_start', False) if os.name == 'nt': # BitDefender false positive. BitDefender didn't mark BleachBit as infected or show # anything in its log, but sqlite would fail to import unless BitDefender was in "game mode." # http://bleachbit.sourceforge.net/forum/074-fails-errors try: import sqlite3 except ImportError as e: self.append_text( _("Error loading the SQLite module: the antivirus software may be blocking it."), 'error') # Show notice about admin privileges. if os.name == 'posix' and os.path.expanduser('~') == '/root': self.append_text( _('You are running BleachBit with administrative privileges for cleaning shared parts of the system, and references to the user profile folder will clean only the root account.')+'\n') if os.name == 'nt' and options.get('shred'): from win32com.shell.shell import IsUserAnAdmin if not IsUserAnAdmin(): self.append_text( _('Run BleachBit with administrator privileges to improve the accuracy of overwriting the contents of files.')) self.append_text('\n') # remove from idle loop (see GObject.idle_add) return False def cb_run_option(self, widget, really_delete, cleaner_id, option_id): """Callback from context menu to delete/preview a single option""" operations = {cleaner_id: [option_id]} # preview if not really_delete: self.preview_or_run_operations(False, operations) return # delete if GuiBasic.delete_confirmation_dialog(self, mention_preview=False): self.preview_or_run_operations(True, operations) return def cb_stop_operations(self, __widget): """Callback to stop the preview/cleaning process""" self.worker.abort() def context_menu_event(self, treeview, event): """When user right clicks on the tree view""" if event.button != 3: return False pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y)) if not pathinfo: return False path, col, _cellx, _celly = pathinfo treeview.grab_focus() treeview.set_cursor(path, col, 0) # context menu applies only to children, not parents if len(path) != 2: return False # find the selected option model = treeview.get_model() option_id = model[path][2] cleaner_id = model[path[0]][2] # make a menu menu = Gtk.Menu() menu.connect('hide', lambda widget: widget.detach()) # TRANSLATORS: this is the context menu preview_item = Gtk.MenuItem(label=_("Preview")) preview_item.connect('activate', self.cb_run_option, False, cleaner_id, option_id) menu.append(preview_item) # TRANSLATORS: this is the context menu clean_item = Gtk.MenuItem(label=_("Clean")) clean_item.connect('activate', self.cb_run_option, True, cleaner_id, option_id) menu.append(clean_item) # show the context menu menu.attach_to_widget(treeview) menu.show_all() menu.popup(None, None, None, None, event.button, event.time) return True def setup_drag_n_drop(self): def cb_drag_data_received(widget, _context, _x, _y, data, info, _time): if info == 80: uris = data.get_uris() paths = FileUtilities.uris_to_paths(uris) self.shred_paths(paths) def setup_widget(widget): widget.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, [Gtk.TargetEntry.new("text/uri-list", 0, 80)], Gdk.DragAction.COPY) widget.connect('drag_data_received', cb_drag_data_received) setup_widget(self) setup_widget(self.textview) self.textview.connect('drag_motion', lambda widget, context, x, y, time: True) def update_progress_bar(self, status): """Callback to update the progress bar with number or text""" if isinstance(status, float): self.progressbar.set_fraction(status) elif isinstance(status, str): self.progressbar.set_show_text(True) self.progressbar.set_text(status) else: raise RuntimeError('unexpected type: ' + str(type(status))) def update_item_size(self, option, option_id, bytes_removed): """Update size in tree control""" model = self.view.get_model() text = FileUtilities.bytes_to_human(bytes_removed) if bytes_removed == 0: text = "" treepath = Gtk.TreePath(0) try: __iter = model.get_iter(treepath) except ValueError as e: logger.warning( 'ValueError in get_iter() when updating file size for tree path=%s' % treepath) return while __iter: if model[__iter][2] == option: if option_id == -1: model[__iter][3] = text else: child = model.iter_children(__iter) while child: if model[child][2] == option_id: model[child][3] = text child = model.iter_next(child) __iter = model.iter_next(__iter) def update_total_size(self, bytes_removed): """Callback to update the total size cleaned""" context_id = self.status_bar.get_context_id('size') text = FileUtilities.bytes_to_human(bytes_removed) if bytes_removed == 0: text = "" self.status_bar.push(context_id, text) def create_headerbar(self): """Create the headerbar""" hbar = Gtk.HeaderBar() hbar.props.show_close_button = True hbar.props.title = APP_NAME box = Gtk.Box() Gtk.StyleContext.add_class(box.get_style_context(), "linked") if os.name == 'nt': icon_size = Gtk.IconSize.BUTTON else: icon_size = Gtk.IconSize.LARGE_TOOLBAR # create the preview button self.preview_button = Gtk.Button.new_from_icon_name( 'edit-find', icon_size) self.preview_button.set_always_show_image(True) self.preview_button.connect( 'clicked', lambda *dummy: self.preview_or_run_operations(False)) self.preview_button.set_tooltip_text( _("Preview files in the selected operations (without deleting any files)")) # TRANSLATORS: This is the preview button on the main window. It # previews changes. self.preview_button.set_label(_('Preview')) box.add(self.preview_button) # create the delete button self.run_button = Gtk.Button.new_from_icon_name( 'edit-clear-all', icon_size) self.run_button.set_always_show_image(True) # TRANSLATORS: This is the clean button on the main window. # It makes permanent changes: usually deleting files, sometimes # altering them. self.run_button.set_label(_('Clean')) self.run_button.set_tooltip_text( _("Clean files in the selected operations")) self.run_button.connect("clicked", self.run_operations) box.add(self.run_button) # stop cleaning self.stop_button = Gtk.Button.new_from_icon_name( 'process-stop', icon_size) self.stop_button.set_always_show_image(True) self.stop_button.set_label(_('Abort')) self.stop_button.set_tooltip_text( _('Abort the preview or cleaning process')) self.stop_button.set_sensitive(False) self.stop_button.connect('clicked', self.cb_stop_operations) box.add(self.stop_button) hbar.pack_start(box) # Add hamburger menu on the right. # This is not needed for Microsoft Windows because other code places its # menu on the left side. if os.name == 'nt': return hbar menu_button = Gtk.MenuButton() icon = Gio.ThemedIcon(name="open-menu-symbolic") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) builder = Gtk.Builder() builder.add_from_file(bleachbit.app_menu_filename) menu_button.set_menu_model(builder.get_object('app-menu')) menu_button.add(image) hbar.pack_end(menu_button) return hbar def on_configure_event(self, widget, event): (x, y) = self.get_position() (width, height) = self.get_size() # fixup maximized window position: # on Windows if a window is maximized on a secondary monitor it is moved off the screen if 'nt' == os.name: window = self.get_window() if window.get_state() & Gdk.WindowState.MAXIMIZED != 0: screen = self.get_screen() monitor_num = screen.get_monitor_at_window(window) g = screen.get_monitor_geometry(monitor_num) if x < g.x or x >= g.x + g.width or y < g.y or y >= g.y + g.height: logger.debug("Maximized window {}+{}: monitor ({}) geometry = {}+{}".format( (x, y), (width, height), monitor_num, (g.x, g.y), (g.width, g.height))) self.move(g.x, g.y) return True # save window position and size options.set("window_x", x, commit=False) options.set("window_y", y, commit=False) options.set("window_width", width, commit=False) options.set("window_height", height, commit=False) return False def on_window_state_event(self, widget, event): # save window state fullscreen = event.new_window_state & Gdk.WindowState.FULLSCREEN != 0 options.set("window_fullscreen", fullscreen, commit=False) maximized = event.new_window_state & Gdk.WindowState.MAXIMIZED != 0 options.set("window_maximized", maximized, commit=False) return False def on_delete_event(self, widget, event): # commit options to disk options.commit() return False def on_show(self, widget): # restore window position, size and state if options.has_option("window_x") and options.has_option("window_y") and \ options.has_option("window_width") and options.has_option("window_height"): r = Gdk.Rectangle() (r.x, r.y) = (options.get("window_x"), options.get("window_y")) (r.width, r.height) = (options.get( "window_width"), options.get("window_height")) screen = self.get_screen() monitor_num = screen.get_monitor_at_point(r.x, r.y) g = screen.get_monitor_geometry(monitor_num) # only restore position and size if window left corner # is within the closest monitor if r.x >= g.x and r.x < g.x + g.width and \ r.y >= g.y and r.y < g.y + g.height: logger.debug("closest monitor ({}) geometry = {}+{}, window geometry = {}+{}".format( monitor_num, (g.x, g.y), (g.width, g.height), (r.x, r.y), (r.width, r.height))) self.move(r.x, r.y) self.resize(r.width, r.height) if options.get("window_fullscreen"): self.fullscreen() elif options.get("window_maximized"): self.maximize() def set_windows10_theme(self): """Toggle the Windows 10 theme""" if not 'nt' == os.name: return if not self._style_provider_regular: self._style_provider_regular = Gtk.CssProvider() self._style_provider_regular.load_from_path( os.path.join(windows10_theme_path, 'gtk.css')) if not self._style_provider_dark: self._style_provider_dark = Gtk.CssProvider() self._style_provider_dark.load_from_path( os.path.join(windows10_theme_path, 'gtk-dark.css')) screen = Gdk.Display.get_default_screen(Gdk.Display.get_default()) if self._style_provider is not None: Gtk.StyleContext.remove_provider_for_screen( screen, self._style_provider) if options.get("win10_theme"): if options.get("dark_mode"): self._style_provider = self._style_provider_dark else: self._style_provider = self._style_provider_regular Gtk.StyleContext.add_provider_for_screen( screen, self._style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) else: self._style_provider = None def populate_window(self): """Create the main application window""" screen = self.get_screen() self.set_default_size(min(screen.width(), 800), min(screen.height(), 600)) self.set_position(Gtk.WindowPosition.CENTER) self.connect("configure-event", self.on_configure_event) self.connect("window-state-event", self.on_window_state_event) self.connect("delete-event", self.on_delete_event) self.connect("show", self.on_show) if appicon_path and os.path.exists(appicon_path): self.set_icon_from_file(appicon_path) # add headerbar self.headerbar = self.create_headerbar() self.set_titlebar(self.headerbar) # split main window twice hbox = Gtk.Box(homogeneous=False) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False) self.add(vbox) vbox.add(hbox) # add operations to left operations = self.create_operations_box() hbox.pack_start(operations, False, True, 0) # create the right side of the window right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.progressbar = Gtk.ProgressBar() right_box.pack_start(self.progressbar, False, True, 0) # add output display on right self.textbuffer = Gtk.TextBuffer() swindow = Gtk.ScrolledWindow() swindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) swindow.set_property('expand', True) self.textview = Gtk.TextView.new_with_buffer(self.textbuffer) self.textview.set_editable(False) self.textview.set_wrap_mode(Gtk.WrapMode.WORD) swindow.add(self.textview) right_box.add(swindow) hbox.add(right_box) # add markup tags tt = self.textbuffer.get_tag_table() style_operation = Gtk.TextTag.new('operation') style_operation.set_property('size-points', 14) style_operation.set_property('weight', 700) style_operation.set_property('pixels-above-lines', 10) style_operation.set_property('justification', Gtk.Justification.CENTER) tt.add(style_operation) style_description = Gtk.TextTag.new('description') style_description.set_property( 'justification', Gtk.Justification.CENTER) tt.add(style_description) style_option_label = Gtk.TextTag.new('option_label') style_option_label.set_property('weight', 700) style_option_label.set_property('left-margin', 20) tt.add(style_option_label) style_operation = Gtk.TextTag.new('error') style_operation.set_property('foreground', '#b00000') tt.add(style_operation) self.status_bar = Gtk.Statusbar() vbox.add(self.status_bar) # setup drag&drop self.setup_drag_n_drop() # done self.show_all() self.progressbar.hide() @threaded def check_online_updates(self): """Check for software updates in background""" from bleachbit import Update try: updates = Update.check_updates(options.get('check_beta'), options.get('update_winapp2'), self.append_text, lambda: GLib.idle_add(self.cb_refresh_operations)) if updates: GLib.idle_add( lambda: Update.update_dialog(self, updates)) except Exception: logger.exception(_("Error when checking for updates: "))
def __init__(self, auto_exit, *args, **kwargs): super(GUI, self).__init__(*args, **kwargs) self.auto_exit = auto_exit self.set_wmclass(APP_NAME, APP_NAME) self.populate_window() # Redirect logging to the GUI. bb_logger = logging.getLogger('bleachbit') from bleachbit.Log import GtkLoggerHandler self.gtklog = GtkLoggerHandler(self.append_text) bb_logger.addHandler(self.gtklog) # process any delayed logs from bleachbit.Log import DelayLog if isinstance(sys.stderr, DelayLog): for msg in sys.stderr.read(): self.append_text(msg) # if stderr was redirected - keep redirecting it sys.stderr = self.gtklog Gtk.Settings.get_default().set_property( 'gtk-application-prefer-dark-theme', options.get('dark_mode')) if options.is_corrupt(): logger.error( _('Resetting the configuration file because it is corrupt: %s') % bleachbit.options_file) bleachbit.Options.init_configuration() if options.get("first_start") and not auto_exit: if os.name == 'posix': self.append_text( _('Access the application menu by clicking the hamburger icon on the title bar.' )) pref = PreferencesDialog(self, self.cb_refresh_operations) pref.run() if os.name == 'nt': self.append_text( _('Access the application menu by clicking the logo on the title bar.' )) options.set('first_start', False) if os.name == 'nt': # BitDefender false positive. BitDefender didn't mark BleachBit as infected or show # anything in its log, but sqlite would fail to import unless BitDefender was in "game mode." # http://bleachbit.sourceforge.net/forum/074-fails-errors try: import sqlite3 except ImportError as e: self.append_text( _("Error loading the SQLite module: the antivirus software may be blocking it." ), 'error') if os.name == 'posix' and bleachbit.expanduser('~') == '/root': self.append_text( _('You are running BleachBit with administrative privileges for cleaning shared parts of the system, and references to the user profile folder will clean only the root account.' )) if os.name == 'nt' and options.get('shred'): from win32com.shell.shell import IsUserAnAdmin if not IsUserAnAdmin(): self.append_text( _('Run BleachBit with administrator privileges to improve the accuracy of overwriting the contents of files.' )) self.append_text('\n') GLib.idle_add(self.cb_refresh_operations)