Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Exclusion Filters #705

Merged
merged 23 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement dialog and base classes for model/view
  • Loading branch information
glubsy committed Aug 14, 2020
commit a26de27c479c8b7d74569cc5f2a647e350139f1e
9 changes: 9 additions & 0 deletions core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
from .util import cmp_value, fix_surrogate_encoding
from . import directories, results, export, fs, prioritize
from .ignore import IgnoreList
from .exclude import ExcludeList
from .scanner import ScanType
from .gui.deletion_options import DeletionOptions
from .gui.details_panel import DetailsPanel
from .gui.directory_tree import DirectoryTree
from .gui.ignore_list_dialog import IgnoreListDialog
from .gui.exclude_list_dialog import ExcludeListDialogCore
from .gui.problem_dialog import ProblemDialog
from .gui.stats_label import StatsLabel

Expand Down Expand Up @@ -140,6 +142,7 @@ def __init__(self, view):
self.directories = directories.Directories()
self.results = results.Results(self)
self.ignore_list = IgnoreList()
self.exclude_list = ExcludeList(self)
# In addition to "app-level" options, this dictionary also holds options that will be
# sent to the scanner. They don't have default values because those defaults values are
# defined in the scanner class.
Expand All @@ -155,6 +158,7 @@ def __init__(self, view):
self.directory_tree = DirectoryTree(self)
self.problem_dialog = ProblemDialog(self)
self.ignore_list_dialog = IgnoreListDialog(self)
self.exclude_list_dialog = ExcludeListDialogCore(self)
self.stats_label = StatsLabel(self)
self.result_table = None
self.deletion_options = DeletionOptions()
Expand Down Expand Up @@ -587,6 +591,9 @@ def load(self):
p = op.join(self.appdata, "ignore_list.xml")
self.ignore_list.load_from_xml(p)
self.ignore_list_dialog.refresh()
p = op.join(self.appdata, "exclude_list.xml")
self.exclude_list.load_from_xml(p)
self.exclude_list_dialog.refresh()

def load_from(self, filename):
"""Start an async job to load results from ``filename``.
Expand Down Expand Up @@ -773,6 +780,8 @@ def save(self):
self.directories.save_to_file(op.join(self.appdata, "last_directories.xml"))
p = op.join(self.appdata, "ignore_list.xml")
self.ignore_list.save_to_xml(p)
p = op.join(self.appdata, "exclude_list.xml")
self.exclude_list.save_to_xml(p)
self.notify("save_session")

def save_as(self, filename):
Expand Down
97 changes: 97 additions & 0 deletions core/exclude.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html

from .markable import Markable
from xml.etree import ElementTree as ET
from hscommon.util import FileOrPath


class ExcludeList(Markable):
"""Exclude list of regular expression strings to filter out directories
and files that we want to avoid scanning."""

# ---Override
def __init__(self, app):
Markable.__init__(self)
self.app = app
self._excluded = [] # set of strings
self._count = 0

def __iter__(self):
for regex in self._excluded:
yield self.is_marked(regex), regex

def __len__(self):
return self._count

def _is_markable(self, row):
return True

# ---Public
def add(self, regex):
self._excluded.insert(0, regex)
self._count = len(self._excluded)

def isExcluded(self, regex):
if regex in self._excluded:
return True
return False

def clear(self):
self._excluded = []
self._count = 0

def remove(self, regex):
return self._excluded.remove(regex)

def rename(self, regex, newregex):
if regex not in self._excluded:
return
marked = self.is_marked(regex)
index = self._excluded.index(regex)
self._excluded[index] = newregex
if marked:
# Not marked by default when added
self.mark(self._excluded[index])

def change_index(self, regex, new_index):
item = self._excluded.pop(regex)
self._excluded.insert(new_index, item)

def load_from_xml(self, infile):
"""Loads the ignore list from a XML created with save_to_xml.

infile can be a file object or a filename.
"""
try:
root = ET.parse(infile).getroot()
except Exception as e:
print(f"Error while loading {infile}: {e}")
return
marked = set()
exclude_elems = (e for e in root if e.tag == "exclude")
for exclude_item in exclude_elems:
regex_string = exclude_item.get("regex")
if not regex_string:
continue
self.add(regex_string)
if exclude_item.get("marked") == "y":
marked.add(regex_string)
for item in marked:
# this adds item to the Markable "marked" set
self.mark(item)

def save_to_xml(self, outfile):
"""Create a XML file that can be used by load_from_xml.

outfile can be a file object or a filename.
"""
root = ET.Element("exclude_list")
for regex in self._excluded:
exclude_node = ET.SubElement(root, "exclude")
exclude_node.set("regex", str(regex))
exclude_node.set("marked", ("y" if self.is_marked(regex) else "n"))
tree = ET.ElementTree(root)
with FileOrPath(outfile, "wb") as fp:
tree.write(fp, encoding="utf-8")
64 changes: 64 additions & 0 deletions core/gui/exclude_list_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Created On: 2012/03/13
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html

# from hscommon.trans import tr
from .exclude_list_table import ExcludeListTable

default_regexes = [".*thumbs", "\.DS.Store", "\.Trash", "Trash-Bin"]


class ExcludeListDialogCore:
# --- View interface
# show()
#

def __init__(self, app):
self.app = app
self.exclude_list = self.app.exclude_list # Markable from exclude.py
self.exclude_list_table = ExcludeListTable(self, app) # GUITable, this is the "model"

def restore_defaults(self):
for _, regex in self.exclude_list:
if regex not in default_regexes:
self.exclude_list.unmark(regex)
for default_regex in default_regexes:
if not self.exclude_list.isExcluded(default_regex):
self.exclude_list.add(default_regex)
self.exclude_list.mark(default_regex)
self.refresh()

def refresh(self):
self.exclude_list_table.refresh()

def remove_selected(self):
for row in self.exclude_list_table.selected_rows:
self.exclude_list_table.remove(row)
self.exclude_list.remove(row.regex)
self.refresh()

def rename_selected(self, newregex):
"""Renames the selected regex to ``newregex``.
If there's more than one selected row, the first one is used.
:param str newregex: The regex to rename the row's regex to.
"""
try:
r = self.exclude_list_table.selected_rows[0]
self.exclude_list.rename(r.regex, newregex)
self.refresh()
return True
except Exception as e:
print(f"dupeGuru Warning: {e}")
return False

def add(self, regex):
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
# TODO make checks here before adding to GUI
self.exclude_list_table.add(regex)

def show(self):
self.view.show()
117 changes: 117 additions & 0 deletions core/gui/exclude_list_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html

from .base import DupeGuruGUIObject
from hscommon.gui.table import GUITable, Row
from hscommon.gui.column import Column, Columns
from hscommon.trans import trget
tr = trget("ui")


class ExcludeListTable(GUITable, DupeGuruGUIObject):
COLUMNS = [
Column("marked", ""),
Column("regex", tr("Regex"))
]

def __init__(self, exclude_list_dialog, app):
GUITable.__init__(self)
DupeGuruGUIObject.__init__(self, app)
# self.columns = Columns(self, prefaccess=app, savename="ExcludeTable")
self.columns = Columns(self)
self.dialog = exclude_list_dialog

def rename_selected(self, newname):
row = self.selected_row
if row is None:
# There's all kinds of way the current row can be swept off during rename. When it
# happens, selected_row will be None.
return False
row._data = None
return self.dialog.rename_selected(newname)

# --- Virtual
def _do_add(self, regex):
"""(Virtual) Creates a new row, adds it in the table.
Returns ``(row, insert_index)``.
"""
# Return index 0 to insert at the top
return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0

def _do_delete(self):
self.dalog.exclude_list.remove(self.selected_row.regex)

# --- Override
def add(self, regex):
row, insert_index = self._do_add(regex)
self.insert(insert_index, row)
# self.select([insert_index])
self.view.refresh()

def _fill(self):
for enabled, regex in self.dialog.exclude_list:
self.append(ExcludeListRow(self, enabled, regex))

# def remove(self):
# super().remove(super().selected_rows)

# def _update_selection(self):
# # rows = self.selected_rows
# # self.dialog._select_rows(list(map(attrgetter("_dupe"), rows)))
# self.dialog.remove_selected()

def refresh(self, refresh_view=True):
"""Override to avoid keeping previous selection in case of multiple rows
selected previously."""
self.cancel_edits()
del self[:]
self._fill()
# sd = self._sort_descriptor
# if sd is not None:
# super().sort_by(self, column_name=sd.column, desc=sd.desc)
if refresh_view:
self.view.refresh()


class ExcludeListRow(Row):
def __init__(self, table, enabled, regex):
Row.__init__(self, table)
self._app = table.app
self._data = None
self.enabled_original = enabled
self.regex_original = regex
self.enabled = str(enabled)
self.regex = str(regex)

@property
def data(self):
def get_display_info(row):
return {"marked": row.enabled, "regex": row.regex}

if self._data is None:
self._data = get_display_info(self)
return self._data

@property
def markable(self):
return True

@property
def marked(self):
return self._app.exclude_list.is_marked(self.regex)

@marked.setter
def marked(self, value):
if value:
self._app.exclude_list.mark(self.regex)
else:
self._app.exclude_list.unmark(self.regex)

# @property
# def regex(self):
# return self.regex

# @regex.setter
# def regex(self, value):
# self._app.exclude_list.add(self._regex, value)
2 changes: 1 addition & 1 deletion core/gui/ignore_list_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class IgnoreListDialog:
def __init__(self, app):
self.app = app
self.ignore_list = self.app.ignore_list
self.ignore_list_table = IgnoreListTable(self)
self.ignore_list_table = IgnoreListTable(self) # GUITable

def clear(self):
if not self.ignore_list:
Expand Down
24 changes: 21 additions & 3 deletions qt/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .directories_dialog import DirectoriesDialog
from .problem_dialog import ProblemDialog
from .ignore_list_dialog import IgnoreListDialog
from .exclude_list_dialog import ExcludeListDialog
from .deletion_options import DeletionOptions
from .se.details_dialog import DetailsDialog as DetailsDialogStandard
from .me.details_dialog import DetailsDialog as DetailsDialogMusic
Expand Down Expand Up @@ -87,10 +88,16 @@ def _setup(self):
parent=self.main_window,
model=self.model.ignore_list_dialog)
self.ignoreListDialog.accepted.connect(self.main_window.onDialogAccepted)

self.excludeListDialog = self.main_window.createPage(
"ExcludeListDialog",
app=self,
parent=self.main_window,
model=self.model.exclude_list_dialog)
else:
self.ignoreListDialog = IgnoreListDialog(
parent=parent_window, model=self.model.ignore_list_dialog
)
parent=parent_window, model=self.model.ignore_list_dialog)
self.excludeDialog = ExcludeListDialog(parent=parent_window)

self.deletionOptions = DeletionOptions(
parent=parent_window,
Expand Down Expand Up @@ -130,6 +137,7 @@ def _setupActions(self):
tr("Clear Picture Cache"),
self.clearPictureCacheTriggered,
),
("actionExcludeList", "", "", tr("Exclude list"), self.excludeListTriggered),
("actionShowHelp", "F1", "", tr("dupeGuru Help"), self.showHelpTriggered),
("actionAbout", "", "", tr("About dupeGuru"), self.showAboutBoxTriggered),
(
Expand Down Expand Up @@ -276,10 +284,20 @@ def ignoreListTriggered(self):
# if not self.main_window.tabWidget.isTabVisible(index):
self.main_window.setTabVisible(index, True)
self.main_window.setCurrentIndex(index)
return
else:
self.model.ignore_list_dialog.show()

def excludeListTriggered(self):
if self.main_window:
index = self.main_window.indexOfWidget(self.excludeListDialog)
if index < 0:
index = self.main_window.addTab(
self.excludeListDialog, "Exclude List", switch=True)
self.main_window.setTabVisible(index, True)
self.main_window.setCurrentIndex(index)
else:
self.excludeListDialog.show()

def openDebugLogTriggered(self):
debugLogPath = op.join(self.model.appdata, "debug.log")
desktop.open_path(debugLogPath)
Expand Down
Loading