def main(): """Show a window with the dupe-contents of a user specified path.""" import sys model = PathTreeModel(sys.argv[1:]) for arg_path in sys.argv[1:]: model.add_path(arg_path, Column.make_row({ 'mtime': time.time(), 'size': 0 })) from shredder.runner import Runner settings = Gio.Settings.new('org.gnome.Shredder') runner = Runner(settings, sys.argv[1:], []) runner.connect( 'lint-added', lambda *_: model.add_path( runner.element['path'], Column.make_row(runner.element))) runner.connect('process-finished', lambda _, msg: print('Status:', msg)) runner.run() view = PathTreeView() view.set_model(model) runner.connect('process-finished', lambda _, msg: GLib.timeout_add(500, view.expand_all)) runner.connect( 'process-finished', lambda *_: GLib.timeout_add(500, lambda: model.sort(Column.SIZE))) runner.connect( 'process-finished', lambda *_: GLib.timeout_add(600, lambda: print(model.trie))) def _search_changed(entry): view.set_model(model.filter_model(entry.get_text())) view.expand_all() entry = Gtk.SearchEntry() entry.connect('search-changed', _search_changed) scw = Gtk.ScrolledWindow() scw.add(view) win = Gtk.Window() win.set_default_size(640, 480) win.connect('destroy', Gtk.main_quit) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.pack_start(scw, True, True, 0) box.pack_start(entry, False, False, 0) win.add(box) win.show_all() Gtk.main()
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)
def main(): """Show a window with the dupe-contents of a user specified path.""" import sys model = PathTreeModel(sys.argv[1:]) for arg_path in sys.argv[1:]: model.add_path( arg_path, Column.make_row({'mtime': time.time(), 'size': 0})) from shredder.runner import Runner settings = Gio.Settings.new('org.gnome.Shredder') runner = Runner(settings, sys.argv[1:], []) runner.connect( 'lint-added', lambda *_: model.add_path( runner.element['path'], Column.make_row( runner.element))) runner.connect( 'process-finished', lambda _, msg: print( 'Status:', msg)) runner.run() view = PathTreeView() view.set_model(model) runner.connect( 'process-finished', lambda _, msg: GLib.timeout_add( 500, view.expand_all)) runner.connect( 'process-finished', lambda *_: GLib.timeout_add( 500, lambda: model.sort(Column.SIZE) ) ) runner.connect( 'process-finished', lambda *_: GLib.timeout_add( 600, lambda: print(model.trie) ) ) def _search_changed(entry): view.set_model(model.filter_model(entry.get_text())) view.expand_all() entry = Gtk.SearchEntry() entry.connect('search-changed', _search_changed) scw = Gtk.ScrolledWindow() scw.add(view) win = Gtk.Window() win.set_default_size(640, 480) win.connect('destroy', Gtk.main_quit) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.pack_start(scw, True, True, 0) box.pack_start(entry, False, False, 0) win.add(box) win.show_all() Gtk.main()
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)