• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bleachbit / bleachbit / 22011198150

14 Feb 2026 04:31AM UTC coverage: 71.333% (-0.5%) from 71.882%
22011198150

push

github

az0
Fix NSIS escaping again

Error in the AppVeyer log
warning 6000: unknown variable/constant "\$\$\$\$\$\$\$\$\$\$\$\$\"{FOLDER}$\$\$\$\$\$\$\$\$\$\$\$\$\"." detected, ignoring (LangString MULTIUSER_INSTALLED_ALL_USERS:1067)

Follow up to 75956835c

This issue did not affect BleachBit 5.0.2 (stable).

6313 of 8850 relevant lines covered (71.33%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.94
/bleachbit/GuiTreeModels.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2
# Copyright (c) 2008-2026 Andrew Ziem.
3
#
4
# This work is licensed under the terms of the GNU GPL, version 3 or
5
# later.  See the COPYING file in the top-level directory.
6

7
from bleachbit import GuiBasic
1✔
8
from bleachbit.Cleaner import backends
1✔
9
from bleachbit.GUI import logger
1✔
10
from bleachbit.GtkShim import GObject, Gtk
1✔
11
from bleachbit.Language import get_text as _
1✔
12
from bleachbit.Options import options
1✔
13

14

15
class TreeDisplayModel:
1✔
16
    """Displays the info model in a view"""
17

18
    def make_view(self, model, parent, context_menu_event):
1✔
19
        """Create and return a TreeView object"""
20
        self.view = Gtk.TreeView.new_with_model(model)
1✔
21

22
        # hide headers
23
        self.view.set_headers_visible(False)
1✔
24

25
        # listen for right click (context menu)
26
        self.view.connect("button_press_event", context_menu_event)
1✔
27

28
        # first column: cleaner name
29
        renderer_name = Gtk.CellRendererText()
1✔
30
        column_name = Gtk.TreeViewColumn(_("Name"), renderer_name, text=0)
1✔
31
        self.view.append_column(column_name)
1✔
32
        self.view.set_search_column(0)
1✔
33

34
        # second column: alert icon
35
        renderer_alert = Gtk.CellRendererPixbuf()
1✔
36
        column_alert = Gtk.TreeViewColumn("", renderer_alert)
1✔
37
        column_alert.add_attribute(renderer_alert, "icon-name", 4)
1✔
38
        self.view.append_column(column_alert)
1✔
39

40
        # third column: checkbox
41
        renderer_active = Gtk.CellRendererToggle()
1✔
42
        renderer_active.set_property('activatable', True)
1✔
43
        renderer_active.connect('toggled', self.col1_toggled_cb, model, parent)
1✔
44
        column_active = Gtk.TreeViewColumn(_("Active"), renderer_active)
1✔
45
        column_active.add_attribute(renderer_active, "active", 1)
1✔
46
        self.view.append_column(column_active)
1✔
47

48
        # fourth column: size
49
        renderer_size = Gtk.CellRendererText()
1✔
50
        renderer_size.set_alignment(1.0, 0.0)
1✔
51
        # TRANSLATORS: Size is the label for the column that shows how
52
        # much space an option would clean or did clean
53
        column_size = Gtk.TreeViewColumn(_("Size"), renderer_size, text=3)
1✔
54
        column_size.set_alignment(1.0)
1✔
55
        self.view.append_column(column_size)
1✔
56

57
        # finish
58
        self.view.expand_all()
1✔
59
        return self.view
1✔
60

61
    def set_cleaner(self, path, model, parent_window, value):
1✔
62
        """Activate or deactivate option of cleaner."""
63
        assert isinstance(value, bool)
×
64
        assert isinstance(model, Gtk.TreeStore)
×
65
        cleaner_id = None
×
66
        i = path
×
67
        if isinstance(i, str):
×
68
            # type is either str or gtk.TreeIter
69
            i = model.get_iter(path)
×
70
        parent = model.iter_parent(i)
×
71
        if parent:
×
72
            # this is an option (child), not a cleaner (parent)
73
            cleaner_id = model[parent][2]
×
74
            option_id = model[path][2]
×
75
        if cleaner_id and value:
×
76
            # When enabling an option, present any warnings.
77
            # (When disabling an option, there is no need to present warnings.)
78
            warning = backends[cleaner_id].get_warning(option_id)
×
79
            if warning and not options.get('expert_mode'):
×
80
                if hasattr(parent_window, 'show_infobar'):
×
81
                    parent_window.show_infobar(
×
82
                        _("This option is protected. To bypass protection, enable expert mode."))
83
                # Show an alert icon by the option name.
84
                model[path][4] = "dialog-warning"
×
85
                return
×
86
            warning_key = 'cleaner:' + cleaner_id + ':' + option_id
×
87
            if warning and not options.get_warning_preference(warning_key):
×
88
                # TRANSLATORS: %(cleaner) may be Firefox, System, etc.
89
                # %(option) may be cache, logs, cookies, etc.
90
                option_name = _("%(cleaner)s - %(option)s") % {
×
91
                    'cleaner': model[parent][0],
92
                    'option': model[path][0],
93
                }
94
                confirmed, remember_choice = GuiBasic.warning_confirm_dialog(
×
95
                    parent_window, option_name, warning)
96
                if not confirmed:
×
97
                    # user cancelled, so don't toggle option
98
                    return
×
99
                if remember_choice:
×
100
                    options.remember_warning_preference(warning_key)
×
101
        model[path][1] = value
×
102
        model[path][4] = ""
×
103

104
    def col1_toggled_cb(self, cell, path, model, parent_window):
1✔
105
        """Callback for toggling cleaners"""
106
        is_toggled_on = not model[path][1]  # Is the new state enabled?
×
107
        self.set_cleaner(path, model, parent_window, is_toggled_on)
×
108
        i = model.get_iter(path)
×
109
        parent = model.iter_parent(i)
×
110
        if parent and is_toggled_on:
×
111
            # If child is enabled, then also enable the parent.
112
            model[parent][1] = True
×
113
        # If all siblings were toggled off, then also disable the parent.
114
        if parent and not is_toggled_on:
×
115
            sibling = model.iter_nth_child(parent, 0)
×
116
            any_sibling_enabled = False
×
117
            while sibling:
×
118
                if model[sibling][1]:
×
119
                    any_sibling_enabled = True
×
120
                sibling = model.iter_next(sibling)
×
121
            if not any_sibling_enabled:
×
122
                model[parent][1] = False
×
123
        # If toggled and has children, then do the same for each child.
124
        child = model.iter_children(i)
×
125
        while child:
×
126
            self.set_cleaner(child, model, parent_window, is_toggled_on)
×
127
            child = model.iter_next(child)
×
128
        return
×
129

130

131
class TreeInfoModel:
1✔
132
    """Model holds information to be displayed in the tree view"""
133

134
    def __init__(self):
1✔
135
        self.tree_store = Gtk.TreeStore(
1✔
136
            GObject.TYPE_STRING, GObject.TYPE_BOOLEAN, GObject.TYPE_PYOBJECT, GObject.TYPE_STRING, GObject.TYPE_STRING)
137
        if not self.tree_store:
1✔
138
            raise Exception("cannot create tree store")
×
139
        self.row_changed_handler_id = None
1✔
140
        self.refresh_rows()
1✔
141
        self.tree_store.set_sort_func(3, self.sort_func)
1✔
142
        self.tree_store.set_sort_column_id(3, Gtk.SortType.ASCENDING)
1✔
143

144
    def get_model(self):
1✔
145
        """Return the tree store"""
146
        return self.tree_store
1✔
147

148
    def on_row_changed(self, __treemodel, path, __iter):
1✔
149
        """Event handler for when a row changes"""
150
        parent = self.tree_store[path[0]][2]
1✔
151
        child = None
1✔
152
        if len(path) == 2:
1✔
153
            child = self.tree_store[path][2]
1✔
154
        value = self.tree_store[path][1]
1✔
155
        options.set_tree(parent, child, value)
1✔
156

157
    def refresh_rows(self):
1✔
158
        """Clear rows (cleaners) and add them fresh"""
159
        if self.row_changed_handler_id:
1✔
160
            self.tree_store.disconnect(self.row_changed_handler_id)
1✔
161
        self.tree_store.clear()
1✔
162
        hidden_cleaners = []
1✔
163
        for key in sorted(backends):
1✔
164
            if not any(backends[key].get_options()):
1✔
165
                # localizations has no options, so it should be hidden
166
                # https://github.com/az0/bleachbit/issues/110
167
                continue
1✔
168
            c_name = backends[key].get_name()
1✔
169
            c_id = backends[key].get_id()
1✔
170
            c_value = options.get_tree(c_id, None)
1✔
171
            if not c_value and options.get('auto_hide') and backends[key].auto_hide():
1✔
172
                hidden_cleaners.append(c_id)
1✔
173
                continue
1✔
174
            parent = self.tree_store.append(
1✔
175
                None, (c_name, c_value, c_id, "", ""))
176
            for (o_id, o_name) in backends[key].get_options():
1✔
177
                o_value = options.get_tree(c_id, o_id)
1✔
178
                self.tree_store.append(parent, (o_name, o_value, o_id, "", ""))
1✔
179
        if hidden_cleaners:
1✔
180
            logger.debug("automatically hid %d cleaners: %s", len(
1✔
181
                hidden_cleaners), ', '.join(hidden_cleaners))
182
        self.row_changed_handler_id = self.tree_store.connect("row-changed",
1✔
183
                                                              self.on_row_changed)
184

185
    def sort_func(self, model, iter1, iter2, _user_data):
1✔
186
        """Sort the tree by the id
187

188
        Index 0 is the display name
189
        Index 2 is the ID (e.g., cookies, vacuum).
190

191
        Sorting by ID is functionally important, so that vacuuming is done
192
        last, even for other languages. See https://github.com/bleachbit/bleachbit/issues/441
193
        """
194
        value1 = model[iter1][2].lower()
1✔
195
        value2 = model[iter2][2].lower()
1✔
196
        if value1 == value2:
1✔
197
            return 0
×
198
        if value1 > value2:
1✔
199
            return 1
×
200
        return -1
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc