Example #1
0
    def on_selection_changed(self, _):
        """Called when the user clicks a specific row."""
        node = self.treeview.get_selected_node()
        if node is None:
            return

        if not node.children:
            # It is a single file.
            # Show a chart containing all twins of this file.
            # This is helpful to see quickly where those lie.

            cksum = node[Column.CKSUM]
            group = self.model.trie.group(cksum)

            paths = []
            if len(self.last_paths) > 1:
                paths = self.last_paths[0] + self.last_paths[1]

            group_model = PathTreeModel(paths)

            for twin_node in group or []:
                group_model.add_path(twin_node.build_path(),
                                     twin_node.row,
                                     immediately=True)

            self.group_treeview.set_model(group_model)
            self.group_revealer.show()
            self.group_revealer.get_child().show_all()
            self.group_revealer.set_reveal_child(True)
            self.chart_stack.render(group_model.trie.root)
        else:
            self.group_revealer.hide()
            self.group_revealer.set_reveal_child(False)
            self.chart_stack.render(node)
Example #2
0
    def on_selection_changed(self, _):
        """Called when the user clicks a specific row."""
        node = self.treeview.get_selected_node()

        # Nothing selected:
        if node is None:
            # "Filtered" trumps "All"
            if not self._is_filtered:
                self.actionbar.set_choice("All")
            return

        self.actionbar.set_choice("Selected")

        if not node.children:
            # It is a single file.
            # Show a chart containing all twins of this file.
            # This is helpful to see quickly where those lie.

            cksum = node[Column.CKSUM]
            group = self.model.trie.group(cksum)

            paths = []
            if len(self.last_paths) > 1:
                paths = self.last_paths[0] + self.last_paths[1]

            group_model = PathTreeModel(paths)

            for twin_node in group or []:
                group_model.add_path(
                    twin_node.build_path(),
                    twin_node.row,
                    immediately=True
                )

            self.group_treeview.set_model(group_model)
            self.group_revealer.show()
            self.group_revealer.get_child().show_all()
            self.group_revealer.set_reveal_child(True)
            self.chart_stack.render(group_model.trie.root)
        else:
            self.group_revealer.hide()
            self.group_revealer.set_reveal_child(False)
            self.chart_stack.render(node)
Example #3
0
    def trigger_run(self, untagged_paths, tagged_paths):
        """Trigger a new run on all paths in `paths`"""
        # Remember last paths for rerun()
        self.reset()
        self.last_paths = (untagged_paths, tagged_paths)

        # Make sure it looks busy:
        self.sub_title = 'Running…'

        # Fork off the rmlint process:
        self.runner = Runner(self.app.settings, untagged_paths, tagged_paths)
        self.runner.connect('lint-added', self.on_add_elem)
        self.runner.connect('process-finished', self.on_process_finish)
        self.runner.run()

        # Make sure the previous run is not visible anymore:
        self.model = PathTreeModel(untagged_paths + tagged_paths)
        self.treeview.set_model(self.model)

        # Indicate that we're in a fresh run:
        self.is_running = True
        self.show_progress(0)
Example #4
0
    def trigger_run(self, untagged_paths, tagged_paths):
        """Trigger a new run on all paths in `paths`"""
        # Remember last paths for rerun()
        self.reset()
        self.last_paths = (untagged_paths, tagged_paths)

        # Make sure it looks busy:
        self.sub_title = 'Running…'

        # Fork off the rmlint process:
        self.runner = Runner(self.app.settings, untagged_paths, tagged_paths)
        self.runner.connect('lint-added', self.on_add_elem)
        self.runner.connect('process-finished', self.on_process_finish)
        self.runner.run()

        # Make sure the previous run is not visible anymore:
        self.model = PathTreeModel(untagged_paths + tagged_paths)
        self.treeview.set_model(self.model)

        # Indicate that we're in a fresh run:
        self.is_running = True
        self.show_progress(0)
Example #5
0
    def main():
        """Stupid test main"""
        from shredder.tree import PathTreeModel
        model = PathTreeModel(['/home/sahib'])

        def push(size, path):
            """Helper for pushing a dummy path"""
            model.add_path(path, Column.make_row({'size': size}), True)

        push(500, '/home/sahib/docs/stuff.pdf')

        for idx, size in enumerate((700, 600, 200)):
            push(size, '/home/sahib/docs/more/' + 'stuff.pdf-' + str(idx))

        for idx in range(50):
            push(10, '/home/sahib/docs/more/' + 'small.pdf-' + str(idx))

        for idx in range(10):
            push(100, '/home/sahib/' + 'dummy-' + str(idx))

        push(1000, '/home/sahib/music/1.mp3')
        push(1200, '/home/sahib/music/sub/2.mp3')
        push(1200, '/home/sahib/music/sub/3.mp3')
        push(600, '/home/sahib/music/sub/4.mp3')
        model.trie.sort(Column.SIZE)
        print(model.trie)

        area = RingChart()
        area.render(model.trie.root)

        win = Gtk.Window()
        win.set_size_request(300, 500)
        win.connect('destroy', Gtk.main_quit)
        win.add(area)
        win.show_all()

        Gtk.main()
Example #6
0
    def __init__(self, app):
        View.__init__(self, app, 'Running…')

        # Public: The runner.
        self.runner = None

        self.last_paths = []

        # Disable scrolling for the main view:
        self.scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        # Public flag for checking if the view is still
        # in running mode (thus en/disabling certain features)
        self.is_running = False

        self._script_generated = False
        self._is_filtered = False

        self.model = PathTreeModel([])

        self.treeview = PathTreeView()
        self.treeview.set_model(self.model)
        self.treeview.get_selection().connect('changed',
                                              self.on_selection_changed)

        self.group_treeview = PathTreeView()
        self.group_treeview.set_vexpand(True)
        self.group_treeview.set_valign(Gtk.Align.FILL)

        # This is needed to make sure operations on the one update
        # the other. Internally the same nodes are updated, but it has
        # to be made sure that the models get updated.
        self.group_treeview.set_twin(self.treeview)
        self.treeview.set_twin(self.group_treeview)

        group_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        group_box.pack_start(scrolled(self.group_treeview), True, True, 0)
        group_box.pack_start(Gtk.HSeparator(), False, False, 0)

        self.group_revealer = Gtk.Revealer()
        self.group_revealer.set_vexpand(True)
        self.group_revealer.set_valign(Gtk.Align.FILL)
        self.group_revealer.add(group_box)
        self.group_revealer.set_no_show_all(True)
        self.group_revealer.set_size_request(-1, 70)

        for column in self.treeview.get_columns():
            column.connect('clicked', lambda _: self.rerender_chart())

        self.chart_stack = ChartStack()
        self.actionbar = ResultActionBar(self)
        self.actionbar.set_valign(Gtk.Align.END)
        self.actionbar.set_halign(Gtk.Align.FILL)

        # Right part of the view
        stats_box = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        stats_box.pack1(self.group_revealer, True, True)
        stats_box.pack2(self.chart_stack, True, True)
        stats_box.props.position = 200

        # Separator container for separator|chart (could have used grid)
        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
        right_pane = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        right_pane.pack_start(separator, False, False, 0)
        right_pane.pack_start(stats_box, True, True, 0)
        right_pane.set_size_request(100, -1)

        scw = scrolled(self.treeview)
        scw.set_size_request(200, -1)

        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.set_vexpand(True)
        paned.set_valign(Gtk.Align.FILL)
        paned.pack1(scw, True, True)
        paned.pack2(right_pane, True, True)
        paned.props.position = 720
        paned.set_hexpand(True)

        grid = Gtk.Grid()
        grid.attach(paned, 0, 0, 1, 1)
        grid.attach(self.actionbar, 0, 1, 1, 1)

        self.add(grid)

        self.search_entry.connect('search-changed', self.on_search_changed)

        self.actionbar.connect('generate-all-script', self.on_generate_script)
        self.actionbar.connect('generate-filtered-script',
                               self.on_generate_filtered_script)
        self.actionbar.connect('generate-selection-script',
                               self.on_generate_selection_script)

        self._menu = None
Example #7
0
class RunnerView(View):
    """Main action View.

    Public attributes:

        - script: A Script instance.
        - runner: The current run-instance.
        - model: The data.
    """
    def __init__(self, app):
        View.__init__(self, app, 'Running…')

        # Public: The runner.
        self.runner = None

        self.last_paths = []

        # Disable scrolling for the main view:
        self.scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        # Public flag for checking if the view is still
        # in running mode (thus en/disabling certain features)
        self.is_running = False

        self._script_generated = False
        self._is_filtered = False

        self.model = PathTreeModel([])

        self.treeview = PathTreeView()
        self.treeview.set_model(self.model)
        self.treeview.get_selection().connect('changed',
                                              self.on_selection_changed)

        self.group_treeview = PathTreeView()
        self.group_treeview.set_vexpand(True)
        self.group_treeview.set_valign(Gtk.Align.FILL)

        # This is needed to make sure operations on the one update
        # the other. Internally the same nodes are updated, but it has
        # to be made sure that the models get updated.
        self.group_treeview.set_twin(self.treeview)
        self.treeview.set_twin(self.group_treeview)

        group_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        group_box.pack_start(scrolled(self.group_treeview), True, True, 0)
        group_box.pack_start(Gtk.HSeparator(), False, False, 0)

        self.group_revealer = Gtk.Revealer()
        self.group_revealer.set_vexpand(True)
        self.group_revealer.set_valign(Gtk.Align.FILL)
        self.group_revealer.add(group_box)
        self.group_revealer.set_no_show_all(True)
        self.group_revealer.set_size_request(-1, 70)

        for column in self.treeview.get_columns():
            column.connect('clicked', lambda _: self.rerender_chart())

        self.chart_stack = ChartStack()
        self.actionbar = ResultActionBar(self)
        self.actionbar.set_valign(Gtk.Align.END)
        self.actionbar.set_halign(Gtk.Align.FILL)

        # Right part of the view
        stats_box = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        stats_box.pack1(self.group_revealer, True, True)
        stats_box.pack2(self.chart_stack, True, True)
        stats_box.props.position = 200

        # Separator container for separator|chart (could have used grid)
        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
        right_pane = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        right_pane.pack_start(separator, False, False, 0)
        right_pane.pack_start(stats_box, True, True, 0)
        right_pane.set_size_request(100, -1)

        scw = scrolled(self.treeview)
        scw.set_size_request(200, -1)

        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.set_vexpand(True)
        paned.set_valign(Gtk.Align.FILL)
        paned.pack1(scw, True, True)
        paned.pack2(right_pane, True, True)
        paned.props.position = 720
        paned.set_hexpand(True)

        grid = Gtk.Grid()
        grid.attach(paned, 0, 0, 1, 1)
        grid.attach(self.actionbar, 0, 1, 1, 1)

        self.add(grid)

        self.search_entry.connect('search-changed', self.on_search_changed)

        self.actionbar.connect('generate-all-script', self.on_generate_script)
        self.actionbar.connect('generate-filtered-script',
                               self.on_generate_filtered_script)
        self.actionbar.connect('generate-selection-script',
                               self.on_generate_selection_script)

        self._menu = None

    def reset(self):
        """Reset internally to freshly initialized."""
        self.is_running = False
        self._script_generated = False
        self.runner = None
        self.last_paths = []

        self.chart_stack.set_visible_child_name(ChartStack.LOADING)
        self.actionbar.set_sensitive(False)

    def trigger_run(self, untagged_paths, tagged_paths):
        """Trigger a new run on all paths in `paths`"""
        # Remember last paths for rerun()
        self.reset()
        self.last_paths = (untagged_paths, tagged_paths)

        # Make sure it looks busy:
        self.sub_title = 'Running…'

        # Fork off the rmlint process:
        self.runner = Runner(self.app.settings, untagged_paths, tagged_paths)
        self.runner.connect('lint-added', self.on_add_elem)
        self.runner.connect('process-finished', self.on_process_finish)
        self.runner.run()

        # Make sure the previous run is not visible anymore:
        self.model = PathTreeModel(untagged_paths + tagged_paths)
        self.treeview.set_model(self.model)

        # Indicate that we're in a fresh run:
        self.is_running = True
        self.show_progress(0)

    def rerun(self):
        """Rerun with last given paths."""
        self.trigger_run(*self.last_paths)

    ###########################
    #     SIGNAL CALLBACKS    #
    ###########################

    def on_search_changed(self, entry):
        """Called once the user entered a new query."""
        text = entry.get_text()

        if text != "":
            self._is_filtered, choice = True, "Filtered"
        else:
            self._is_filtered, choice = False, "All"

        self.actionbar.set_choice(choice)

        sub_model = self.model.filter_model(text)
        if sub_model is not self.treeview.get_model():
            self.chart_stack.render(sub_model.trie.root)
            self.treeview.set_model(sub_model)

    def on_add_elem(self, runner):
        """Called once the runner found a new element."""
        elem = runner.element
        self.model.add_path(elem['path'], Column.make_row(elem))

        # Decide how much progress to show (or just move a bit)
        tick = (elem.get('progress', 0) / 100.0) or None
        self.show_progress(tick)

    def on_process_finish(self, _, error_msg):
        """Called once self.runner finished running."""
        # Make sure we end up at 100% progress and show
        # the progress for a short time after (for the nice cozy feeling)
        LOGGER.info('`rmlint` finished.')
        self.show_progress(100)
        GLib.timeout_add(300, self.hide_progress)
        GLib.timeout_add(350, self.treeview.expand_all)

        self.sub_title = 'Finished scanning.'

        if error_msg is not None:
            self.app_window.show_infobar(error_msg,
                                         message_type=Gtk.MessageType.WARNING)

        GLib.timeout_add(1000, self.on_delayed_chart_render, -1)

    def on_delayed_chart_render(self, last_size):
        """Called after a short delay to reduce chart redraws."""
        model = self.treeview.get_model()
        current_size = len(model)

        if current_size == last_size:
            # Come back later:
            return False

        if model.trie.has_leaves():
            self.chart_stack.set_visible_child_name(ChartStack.CHART)
            self.rerender_chart()
            self.actionbar.set_sensitive(True)
        else:
            self.chart_stack.set_visible_child_name(ChartStack.EMPTY)

        GLib.timeout_add(1500, self.on_delayed_chart_render, current_size)

        return False

    def rerender_chart(self):
        """Re-render the chart from the current model root."""
        LOGGER.info('Refreshing chart.')
        model = self.treeview.get_model()
        self.chart_stack.render(model.trie.root)

    def on_view_enter(self):
        """Called when the view enters sight."""
        GLib.idle_add(lambda: self.app_window.views.go_right.set_sensitive(
            self._script_generated))

    def on_view_leave(self):
        """Called when the view leaves sight."""
        self.app_window.views.go_right.set_sensitive(True)

    def on_selection_changed(self, _):
        """Called when the user clicks a specific row."""
        node = self.treeview.get_selected_node()

        # Nothing selected:
        if node is None:
            # "Filtered" trumps "All"
            if not self._is_filtered:
                self.actionbar.set_choice("All")
            return

        self.actionbar.set_choice("Selected")

        if not node.children:
            # It is a single file.
            # Show a chart containing all twins of this file.
            # This is helpful to see quickly where those lie.

            cksum = node[Column.CKSUM]
            group = self.model.trie.group(cksum)

            paths = []
            if len(self.last_paths) > 1:
                paths = self.last_paths[0] + self.last_paths[1]

            group_model = PathTreeModel(paths)

            for twin_node in group or []:
                group_model.add_path(twin_node.build_path(),
                                     twin_node.row,
                                     immediately=True)

            self.group_treeview.set_model(group_model)
            self.group_revealer.show()
            self.group_revealer.get_child().show_all()
            self.group_revealer.set_reveal_child(True)
            self.chart_stack.render(group_model.trie.root)
        else:
            self.group_revealer.hide()
            self.group_revealer.set_reveal_child(False)
            self.chart_stack.render(node)

    def _generate_script(self, trie, nodes):
        """Do the actual generation work, starting at `node` in `trie`."""
        self._script_generated = True

        path_to_is_original = {}
        for node in nodes:
            for ch in trie.iterate(node=node):
                if not ch.is_leaf:
                    continue

                path_to_is_original[ch.build_path()] = \
                    NodeState.should_keep(ch[Column.TAG])

        self.runner.replay(path_to_is_original)
        self.app_window.views.go_right.set_sensitive(True)
        self.app_window.views.switch('editor')

    def on_generate_script(self, _):
        """Generate the full script."""
        self._generate_script(self.model.trie, [self.model.trie.root])

    def on_generate_filtered_script(self, _):
        """Generate the script with only the visible content."""
        model = self.treeview.get_model()
        self._generate_script(model.trie, [model.trie.root])

    def on_generate_selection_script(self, _):
        """Generate the script only from the current selected dir or files."""
        model = self.treeview.get_model()
        selected_nodes = self.treeview.get_selected_nodes()

        if not selected_nodes:
            LOGGER.info('Nothing selected to make script from.')
            return

        self._generate_script(model.trie, selected_nodes)

    def on_default_action(self):
        """Called on Ctrl-Enter"""
        if self.actionbar.is_sensitive():
            trie = self.model.trie
            self._generate_script(trie, trie.root)
Example #8
0
    def __init__(self, app):
        View.__init__(self, app, 'Running…')

        # Public: The runner.
        self.runner = None

        self.last_paths = []

        # Disable scrolling for the main view:
        self.scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        # Public flag for checking if the view is still
        # in running mode (thus en/disabling certain features)
        self.is_running = False

        self._script_generated = False

        self.model = PathTreeModel([])

        self.treeview = PathTreeView()
        self.treeview.set_model(self.model)
        self.treeview.get_selection().connect(
            'changed',
            self.on_selection_changed
        )

        self.group_treeview = PathTreeView()
        self.group_treeview.set_vexpand(True)
        self.group_treeview.set_valign(Gtk.Align.FILL)

        # This is needed to make sure operations on the one update
        # the other. Interally the same nodes are updated, but it has
        # to be made sure that the models get updated.
        self.group_treeview.set_twin(self.treeview)
        self.treeview.set_twin(self.group_treeview)

        group_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        group_box.pack_start(scrolled(self.group_treeview), True, True, 0)
        group_box.pack_start(Gtk.HSeparator(), False, False, 0)

        self.group_revealer = Gtk.Revealer()
        self.group_revealer.set_vexpand(True)
        self.group_revealer.set_valign(Gtk.Align.FILL)
        self.group_revealer.add(group_box)
        self.group_revealer.set_no_show_all(True)
        self.group_revealer.set_size_request(-1, 70)

        for column in self.treeview.get_columns():
            column.connect(
                'clicked',
                lambda _: self.rerender_chart()
            )

        self.chart_stack = ChartStack()
        self.actionbar = ResultActionBar(self)
        self.actionbar.set_valign(Gtk.Align.END)
        self.actionbar.set_halign(Gtk.Align.FILL)

        # Right part of the view
        stats_box = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        stats_box.pack1(self.group_revealer, True, True)
        stats_box.pack2(self.chart_stack, True, True)
        stats_box.props.position = 200

        # Separator container for separator|chart (could have used grid)
        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
        right_pane = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        right_pane.pack_start(separator, False, False, 0)
        right_pane.pack_start(stats_box, True, True, 0)
        right_pane.set_size_request(100, -1)

        scw = scrolled(self.treeview)
        scw.set_size_request(200, -1)

        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.set_vexpand(True)
        paned.set_valign(Gtk.Align.FILL)
        paned.pack1(scw, True, True)
        paned.pack2(right_pane, True, True)
        paned.props.position = 720
        paned.set_hexpand(True)

        grid = Gtk.Grid()
        grid.attach(paned, 0, 0, 1, 1)
        grid.attach(self.actionbar, 0, 1, 1, 1)

        self.add(grid)

        self.search_entry.connect(
            'search-changed', self.on_search_changed
        )

        self.actionbar.connect(
            'generate-all-script', self.on_generate_script
        )
        self.actionbar.connect(
            'generate-filtered-script', self.on_generate_filtered_script
        )
        self.actionbar.connect(
            'generate-selection-script', self.on_generate_selection_script
        )

        self._menu = None
Example #9
0
class RunnerView(View):
    """Main action View.

    Public attributes:

        - script: A Script instance.
        - runner: The current run-instance.
        - model: The data.
    """
    def __init__(self, app):
        View.__init__(self, app, 'Running…')

        # Public: The runner.
        self.runner = None

        self.last_paths = []

        # Disable scrolling for the main view:
        self.scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        # Public flag for checking if the view is still
        # in running mode (thus en/disabling certain features)
        self.is_running = False

        self._script_generated = False

        self.model = PathTreeModel([])

        self.treeview = PathTreeView()
        self.treeview.set_model(self.model)
        self.treeview.get_selection().connect(
            'changed',
            self.on_selection_changed
        )

        self.group_treeview = PathTreeView()
        self.group_treeview.set_vexpand(True)
        self.group_treeview.set_valign(Gtk.Align.FILL)

        # This is needed to make sure operations on the one update
        # the other. Interally the same nodes are updated, but it has
        # to be made sure that the models get updated.
        self.group_treeview.set_twin(self.treeview)
        self.treeview.set_twin(self.group_treeview)

        group_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        group_box.pack_start(scrolled(self.group_treeview), True, True, 0)
        group_box.pack_start(Gtk.HSeparator(), False, False, 0)

        self.group_revealer = Gtk.Revealer()
        self.group_revealer.set_vexpand(True)
        self.group_revealer.set_valign(Gtk.Align.FILL)
        self.group_revealer.add(group_box)
        self.group_revealer.set_no_show_all(True)
        self.group_revealer.set_size_request(-1, 70)

        for column in self.treeview.get_columns():
            column.connect(
                'clicked',
                lambda _: self.rerender_chart()
            )

        self.chart_stack = ChartStack()
        self.actionbar = ResultActionBar(self)
        self.actionbar.set_valign(Gtk.Align.END)
        self.actionbar.set_halign(Gtk.Align.FILL)

        # Right part of the view
        stats_box = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        stats_box.pack1(self.group_revealer, True, True)
        stats_box.pack2(self.chart_stack, True, True)
        stats_box.props.position = 200

        # Separator container for separator|chart (could have used grid)
        separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
        right_pane = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        right_pane.pack_start(separator, False, False, 0)
        right_pane.pack_start(stats_box, True, True, 0)
        right_pane.set_size_request(100, -1)

        scw = scrolled(self.treeview)
        scw.set_size_request(200, -1)

        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.set_vexpand(True)
        paned.set_valign(Gtk.Align.FILL)
        paned.pack1(scw, True, True)
        paned.pack2(right_pane, True, True)
        paned.props.position = 720
        paned.set_hexpand(True)

        grid = Gtk.Grid()
        grid.attach(paned, 0, 0, 1, 1)
        grid.attach(self.actionbar, 0, 1, 1, 1)

        self.add(grid)

        self.search_entry.connect(
            'search-changed', self.on_search_changed
        )

        self.actionbar.connect(
            'generate-all-script', self.on_generate_script
        )
        self.actionbar.connect(
            'generate-filtered-script', self.on_generate_filtered_script
        )
        self.actionbar.connect(
            'generate-selection-script', self.on_generate_selection_script
        )

        self._menu = None

    def reset(self):
        """Reset internally to freshly initialized."""
        self.is_running = False
        self._script_generated = False
        self.runner = None
        self.last_paths = []

        self.chart_stack.set_visible_child_name(ChartStack.LOADING)
        self.actionbar.set_sensitive(False)

    def trigger_run(self, untagged_paths, tagged_paths):
        """Trigger a new run on all paths in `paths`"""
        # Remember last paths for rerun()
        self.reset()
        self.last_paths = (untagged_paths, tagged_paths)

        # Make sure it looks busy:
        self.sub_title = 'Running…'

        # Fork off the rmlint process:
        self.runner = Runner(self.app.settings, untagged_paths, tagged_paths)
        self.runner.connect('lint-added', self.on_add_elem)
        self.runner.connect('process-finished', self.on_process_finish)
        self.runner.run()

        # Make sure the previous run is not visible anymore:
        self.model = PathTreeModel(untagged_paths + tagged_paths)
        self.treeview.set_model(self.model)

        # Indicate that we're in a fresh run:
        self.is_running = True
        self.show_progress(0)

    def rerun(self):
        """Rerun with last given paths."""
        self.trigger_run(*self.last_paths)

    ###########################
    #     SIGNAL CALLBACKS    #
    ###########################

    def on_search_changed(self, entry):
        """Called once the user entered a new query."""
        text = entry.get_text()

        sub_model = self.model.filter_model(text)
        if sub_model is not self.treeview.get_model():
            self.chart_stack.render(sub_model.trie.root)
            self.treeview.set_model(sub_model)

    def on_add_elem(self, runner):
        """Called once the runner found a new element."""
        elem = runner.element
        self.model.add_path(elem['path'], Column.make_row(elem))

        # Decide how much progress to show (or just move a bit)
        tick = (elem.get('progress', 0) / 100.0) or None
        self.show_progress(tick)

    def on_process_finish(self, _, error_msg):
        """Called once self.runner finished running."""
        # Make sure we end up at 100% progress and show
        # the progress for a short time after (for the nice cozy feeling)
        LOGGER.info('`rmlint` finished.')
        self.show_progress(100)
        GLib.timeout_add(300, self.hide_progress)
        GLib.timeout_add(350, self.treeview.expand_all)

        self.sub_title = 'Finished scanning.'

        if error_msg is not None:
            self.app_window.show_infobar(
                error_msg, message_type=Gtk.MessageType.WARNING
            )

        GLib.timeout_add(1000, self.on_delayed_chart_render, -1)

    def on_delayed_chart_render(self, last_size):
        """Called after a short delay to reduce chart redraws."""
        model = self.treeview.get_model()
        current_size = len(model)

        if current_size == last_size:
            # Come back later:
            return False

        if model.trie.has_leaves():
            self.chart_stack.set_visible_child_name(ChartStack.CHART)
            self.rerender_chart()
            self.actionbar.set_sensitive(True)
        else:
            self.chart_stack.set_visible_child_name(ChartStack.EMPTY)

        GLib.timeout_add(1500, self.on_delayed_chart_render, current_size)

        return False

    def rerender_chart(self):
        """Re-render the chart from the current model root."""
        LOGGER.info('Refreshing chart.')
        model = self.treeview.get_model()
        self.chart_stack.render(model.trie.root)

    def on_view_enter(self):
        """Called when the view enters sight."""
        GLib.idle_add(
            lambda: self.app_window.views.go_right.set_sensitive(
                self._script_generated
            )
        )

    def on_view_leave(self):
        """Called when the view leaves sight."""
        self.app_window.views.go_right.set_sensitive(True)

    def on_selection_changed(self, _):
        """Called when the user clicks a specific row."""
        node = self.treeview.get_selected_node()
        if node is None:
            return

        if not node.children:
            # It is a single file.
            # Show a chart containing all twins of this file.
            # This is helpful to see quickly where those lie.

            cksum = node[Column.CKSUM]
            group = self.model.trie.group(cksum)

            paths = []
            if len(self.last_paths) > 1:
                paths = self.last_paths[0] + self.last_paths[1]

            group_model = PathTreeModel(paths)

            for twin_node in group or []:
                group_model.add_path(
                    twin_node.build_path(),
                    twin_node.row,
                    immediately=True
                )

            self.group_treeview.set_model(group_model)
            self.group_revealer.show()
            self.group_revealer.get_child().show_all()
            self.group_revealer.set_reveal_child(True)
            self.chart_stack.render(group_model.trie.root)
        else:
            self.group_revealer.hide()
            self.group_revealer.set_reveal_child(False)
            self.chart_stack.render(node)

    def _generate_script(self, trie, node):
        """Do the actual generation work, starting at `node` in `trie`."""
        self._script_generated = True

        gen = trie.iterate(node=node)
        self.runner.replay({
            ch.build_path(): NodeState.should_keep(ch[Column.TAG]) for ch in gen if ch.is_leaf
        })

        self.app_window.views.go_right.set_sensitive(True)
        self.app_window.views.switch('editor')

    def on_generate_script(self, _):
        """Generate the full script."""
        self._generate_script(self.model.trie, self.model.trie.root)

    def on_generate_filtered_script(self, _):
        """Generate the script with only the visible content."""
        model = self.treeview.get_model()
        self._generate_script(model.trie, model.trie.root)

    def on_generate_selection_script(self, _):
        """Generate the script only from the current selected dir or files."""
        model = self.treeview.get_model()
        selected_node = self.treeview.get_selected_node()

        if selected_node is None:
            LOGGER.info('Nothing selected to make script from.')
            return

        self._generate_script(model.trie, selected_node)

    def on_default_action(self):
        """Called on Ctrl-Enter"""
        if self.actionbar.is_sensitive():
            trie = self.model.trie
            self._generate_script(trie, trie.root)