mirror of
				https://github.com/arsenetar/dupeguru.git
				synced 2025-09-11 17:58:17 +00:00 
			
		
		
		
	Implement exclude list dialog on the Qt side
This commit is contained in:
		
							parent
							
								
									a26de27c47
								
							
						
					
					
						commit
						2eaf7e7893
					
				| @ -26,7 +26,7 @@ from .pe.photo import get_delta_dimensions | ||||
| from .util import cmp_value, fix_surrogate_encoding | ||||
| from . import directories, results, export, fs, prioritize | ||||
| from .ignore import IgnoreList | ||||
| from .exclude import ExcludeList | ||||
| from .exclude import ExcludeList as ExcludeList | ||||
| from .scanner import ScanType | ||||
| from .gui.deletion_options import DeletionOptions | ||||
| from .gui.details_panel import DetailsPanel | ||||
| @ -139,10 +139,10 @@ class DupeGuru(Broadcaster): | ||||
|             os.makedirs(self.appdata) | ||||
|         self.app_mode = AppMode.Standard | ||||
|         self.discarded_file_count = 0 | ||||
|         self.exclude_list = ExcludeList() | ||||
|         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. | ||||
|  | ||||
| @ -5,7 +5,6 @@ | ||||
| # http://www.gnu.org/licenses/gpl-3.0.html | ||||
| 
 | ||||
| import os | ||||
| import re | ||||
| from xml.etree import ElementTree as ET | ||||
| import logging | ||||
| 
 | ||||
| @ -14,6 +13,7 @@ from hscommon.path import Path | ||||
| from hscommon.util import FileOrPath | ||||
| 
 | ||||
| from . import fs | ||||
| from .exclude import ExcludeList | ||||
| 
 | ||||
| __all__ = [ | ||||
|     "Directories", | ||||
| @ -53,34 +53,17 @@ class Directories: | ||||
|     Then, when the user starts the scan, :meth:`get_files` is called to retrieve all files (wrapped | ||||
|     in :mod:`core.fs`) that have to be scanned according to the chosen folders/states. | ||||
|     """ | ||||
|     # FIXME: if there is zero item in these sets, the for each loops will yield NOTHING | ||||
|     deny_list_str = set() | ||||
|     deny_list_re = set() | ||||
|     deny_list_re_files = set() | ||||
| 
 | ||||
|     # ---Override | ||||
|     def __init__(self): | ||||
|     def __init__(self, excluded=ExcludeList()): | ||||
|         self._dirs = [] | ||||
|         # {path: state} | ||||
|         self.states = {} | ||||
|         self.deny_list_str.add(r".*Recycle\.Bin$") | ||||
|         self.deny_list_str.add(r"denyme.*") | ||||
|         self.deny_list_str.add(r".*denyme") | ||||
|         self.deny_list_str.add(r".*/test/denyme*") | ||||
|         self.deny_list_str.add(r".*/test/*denyme") | ||||
|         self.deny_list_str.add(r"denyme") | ||||
|         self.deny_list_str.add(r".*\/\..*") | ||||
|         self.deny_list_str.add(r"^\..*") | ||||
|         self.compile_re() | ||||
| 
 | ||||
|     def compile_re(self): | ||||
|         for expr in self.deny_list_str: | ||||
|             try: | ||||
|                 self.deny_list_re.add(re.compile(expr)) | ||||
|                 if os.sep not in expr: | ||||
|                     self.deny_list_re_files.add(re.compile(expr)) | ||||
|             except Exception as e: | ||||
|                 logging.debug(f"Invalid regular expression \"{expr}\" in exclude list: {e}") | ||||
|         print(f"re_all: {self.deny_list_re}\nre_files: {self.deny_list_re_files}") | ||||
|         self._excluded = excluded | ||||
| 
 | ||||
|     def __contains__(self, path): | ||||
|         for p in self._dirs: | ||||
| @ -217,7 +200,7 @@ class Directories: | ||||
|             for folder in self._get_folders(from_folder, j): | ||||
|                 yield folder | ||||
| 
 | ||||
|     def get_state(self, path, denylist=deny_list_re): | ||||
|     def get_state(self, path, deny_list_re=deny_list_re): | ||||
|         """Returns the state of ``path``. | ||||
| 
 | ||||
|         :rtype: :class:`DirectoryState` | ||||
| @ -225,7 +208,7 @@ class Directories: | ||||
|         # direct match? easy result. | ||||
|         if path in self.states: | ||||
|             return self.states[path] | ||||
|         state = self._default_state_for_path(path, denylist) or DirectoryState.Normal | ||||
|         state = self._default_state_for_path(path, deny_list_re) or DirectoryState.Normal | ||||
|         prevlen = 0 | ||||
|         # we loop through the states to find the longest matching prefix | ||||
|         for p, s in self.states.items(): | ||||
|  | ||||
							
								
								
									
										365
									
								
								core/exclude.py
									
									
									
									
									
								
							
							
						
						
									
										365
									
								
								core/exclude.py
									
									
									
									
									
								
							| @ -4,38 +4,172 @@ | ||||
| 
 | ||||
| from .markable import Markable | ||||
| from xml.etree import ElementTree as ET | ||||
| import re | ||||
| from os import sep | ||||
| import logging | ||||
| import functools | ||||
| from hscommon.util import FileOrPath | ||||
| import time | ||||
| 
 | ||||
| default_regexes = [r".*thumbs", r"\.DS.Store", r"\.Trash", r".*Trash-Bin"] | ||||
| forbidden_regexes = [r".*", r"\/.*", r".*\/.*"] | ||||
| 
 | ||||
| 
 | ||||
| def timer(func): | ||||
|     @functools.wraps(func) | ||||
|     def wrapper_timer(*args): | ||||
|         start = time.perf_counter_ns() | ||||
|         value = func(*args) | ||||
|         end = time.perf_counter_ns() | ||||
|         print(f"DEBUG: func {func.__name__!r} took {end - start} ns.") | ||||
|         return value | ||||
|     return wrapper_timer | ||||
| 
 | ||||
| 
 | ||||
| def memoize(func): | ||||
|     func.cache = dict() | ||||
| 
 | ||||
|     @functools.wraps(func) | ||||
|     def _memoize(*args): | ||||
|         if args not in func.cache: | ||||
|             func.cache[args] = func(*args) | ||||
|         return func.cache[args] | ||||
|     return _memoize | ||||
| 
 | ||||
| 
 | ||||
| class AlreadyThereException(Exception): | ||||
|     """Expression already in the list""" | ||||
|     def __init__(self, arg="Expression is already in excluded list."): | ||||
|         super().__init__(arg) | ||||
| 
 | ||||
| 
 | ||||
| class ExcludeList(Markable): | ||||
|     """Exclude list of regular expression strings to filter out directories | ||||
|     and files that we want to avoid scanning.""" | ||||
|     and files that we want to avoid scanning. | ||||
|     The list() class allows us to preserve item order without too much hassle. | ||||
|     The downside is we have to compare strings every time we look for an item in the list | ||||
|     since we use regex strings as keys. | ||||
|     [regex:str, compilable:bool, error:Exception, compiled:Pattern]) | ||||
|     """ | ||||
| 
 | ||||
|     # ---Override | ||||
|     def __init__(self, app): | ||||
|     def __init__(self): | ||||
|         Markable.__init__(self) | ||||
|         self.app = app | ||||
|         self._excluded = []  # set of strings | ||||
|         self._excluded = [] | ||||
|         self._count = 0 | ||||
|         self._excluded_compiled = set() | ||||
| 
 | ||||
|     def __debug_test(self): | ||||
|         self.test_regexes = [ | ||||
|             r".*Recycle\.Bin$", r"denyme.*", r".*denyme", r".*/test/denyme*", | ||||
|             r".*/test/*denyme", r"denyme", r".*\/\..*", r"^\..*"] | ||||
|         for regex in self.test_regexes: | ||||
|             try: | ||||
|                 self.add(regex) | ||||
|             except Exception as e: | ||||
|                 print(f"Exception loading test regex {regex}: {e}") | ||||
|                 continue | ||||
|             try: | ||||
|                 self.mark(regex) | ||||
|             except Exception as e: | ||||
|                 print(f"Exception marking test regex {regex}: {e}") | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         for regex in self._excluded: | ||||
|         """Iterate in order.""" | ||||
|         for item in self._excluded: | ||||
|             regex = item[0] | ||||
|             yield self.is_marked(regex), regex | ||||
| 
 | ||||
|     def __len__(self): | ||||
|         return self._count | ||||
| 
 | ||||
|     def _is_markable(self, row): | ||||
|         return True | ||||
|     def is_markable(self, regex): | ||||
|         return self._is_markable(regex) | ||||
| 
 | ||||
|     def _is_markable(self, regex): | ||||
|         """Return the cached result of "compilable" property""" | ||||
|         # FIXME save result of compilation via memoization | ||||
|         # return self._excluded.get(regex)[0] | ||||
|         for item in self._excluded: | ||||
|             if item[0] == regex: | ||||
|                 return item[1] | ||||
|         return False  # FIXME should not be needed | ||||
| 
 | ||||
|     def _did_mark(self, regex): | ||||
|         for item in self._excluded: | ||||
|             if item[0] == regex: | ||||
|                 # no need to test if already present since it's a set() | ||||
|                 self._excluded_compiled.add(item[3]) | ||||
| 
 | ||||
|     def _did_unmark(self, regex): | ||||
|         self._remove_compiled(regex) | ||||
| 
 | ||||
|     def _remove_compiled(self, regex): | ||||
|         for item in self._excluded_compiled: | ||||
|             if regex in item.pattern: | ||||
|                 self._excluded_compiled.remove(item) | ||||
|                 break | ||||
| 
 | ||||
|     # @timer | ||||
|     @memoize | ||||
|     def _do_compile(self, expr): | ||||
|         try: | ||||
|             return re.compile(expr) | ||||
|         except Exception as e: | ||||
|             raise(e) | ||||
| 
 | ||||
|     # @timer | ||||
|     # @memoize  # probably not worth memoizing this one if we memoize the above | ||||
|     def compile_re(self, regex): | ||||
|         compiled = None | ||||
|         try: | ||||
|             compiled = self._do_compile(regex) | ||||
|         except Exception as e: | ||||
|             return False, e, compiled | ||||
|         return True, None, compiled | ||||
| 
 | ||||
|     def error(self, regex): | ||||
|         """Return the compilation error Exception for regex. It should have a "msg" attr.""" | ||||
|         for item in self._excluded: | ||||
|             if item[0] == regex: | ||||
|                 return item[2] | ||||
| 
 | ||||
|     @property | ||||
|     def compiled(self): | ||||
|         """Should be used by other classes to retrieve the up-to-date list of patterns.""" | ||||
|         return self._excluded_compiled | ||||
| 
 | ||||
|     @property | ||||
|     def compiled_files(self): | ||||
|         """Should be used by other classes to retrieve the up-to-date list of patterns | ||||
|         for files only.""" | ||||
|         return [compiled_pattern for compiled_pattern in self.compiled if sep not in compiled_pattern.pattern] | ||||
| 
 | ||||
|     # ---Public | ||||
|     def add(self, regex): | ||||
|         self._excluded.insert(0, regex) | ||||
|     def add(self, regex, forced=False): | ||||
|         """This interface should throw exceptions if there is an error during regex compilation""" | ||||
|         if self.isExcluded(regex): | ||||
|             # This exception should never be ignored | ||||
|             raise AlreadyThereException() | ||||
|         if regex in forbidden_regexes: | ||||
|             raise Exception("Forbidden (dangerous) expression.") | ||||
| 
 | ||||
|         iscompilable, exception, compiled = self.compile_re(regex) | ||||
|         if not iscompilable and not forced: | ||||
|             # This exception can be ignored, but taken into account to avoid adding to compiled set | ||||
|             raise exception | ||||
|         else: | ||||
|             self._do_add(regex, iscompilable, exception, compiled) | ||||
| 
 | ||||
|     def _do_add(self, regex, iscompilable, exception, compiled): | ||||
|         # We need to insert at the top | ||||
|         self._excluded.insert(0, [regex, iscompilable, exception, compiled]) | ||||
|         self._count = len(self._excluded) | ||||
| 
 | ||||
|     def isExcluded(self, regex): | ||||
|         if regex in self._excluded: | ||||
|             return True | ||||
|         for item in self._excluded: | ||||
|             if regex == item[0]: | ||||
|                 return True | ||||
|         return False | ||||
| 
 | ||||
|     def clear(self): | ||||
| @ -43,21 +177,48 @@ class ExcludeList(Markable): | ||||
|         self._count = 0 | ||||
| 
 | ||||
|     def remove(self, regex): | ||||
|         return self._excluded.remove(regex) | ||||
|         for item in self._excluded: | ||||
|             if item[0] == regex: | ||||
|                 self._excluded.remove(item) | ||||
|         self._remove_compiled(regex) | ||||
| 
 | ||||
|     def rename(self, regex, newregex): | ||||
|         if regex not in self._excluded: | ||||
|         # if regex not in self._excluded or regex == newregex: | ||||
|         #     return | ||||
|         if regex == newregex: | ||||
|             return | ||||
|         found = False | ||||
|         for item in self._excluded: | ||||
|             if regex == item[0]: | ||||
|                 found = True | ||||
|                 break | ||||
|         if not found: | ||||
|             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) | ||||
|         was_marked = self.is_marked(regex) | ||||
|         is_compilable, exception, compiled = self.compile_re(newregex) | ||||
|         for item in self._excluded: | ||||
|             if item[0] == regex: | ||||
|                 # We overwrite the found entry | ||||
|                 self._excluded[self._excluded.index(item)] =\ | ||||
|                     [newregex, is_compilable, exception, compiled] | ||||
|         if is_compilable and was_marked: | ||||
|             # Not marked by default when added, add it back | ||||
|             self.mark(newregex) | ||||
| 
 | ||||
|     # def change_index(self, regex, new_index): | ||||
|     # """Internal list must be a list, not dict.""" | ||||
|     #     item = self._excluded.pop(regex) | ||||
|     #     self._excluded.insert(new_index, item) | ||||
| 
 | ||||
|     def restore_defaults(self): | ||||
|         for _, regex in self: | ||||
|             if regex not in default_regexes: | ||||
|                 self.unmark(regex) | ||||
|         for default_regex in default_regexes: | ||||
|             if not self.isExcluded(default_regex): | ||||
|                 self.add(default_regex) | ||||
|             self.mark(default_regex) | ||||
| 
 | ||||
|     def load_from_xml(self, infile): | ||||
|         """Loads the ignore list from a XML created with save_to_xml. | ||||
| @ -67,20 +228,29 @@ class ExcludeList(Markable): | ||||
|         try: | ||||
|             root = ET.parse(infile).getroot() | ||||
|         except Exception as e: | ||||
|             print(f"Error while loading {infile}: {e}") | ||||
|             return | ||||
|             logging.warning(f"Error while loading {infile}: {e}") | ||||
|             self.restore_defaults() | ||||
|             self.__debug_test() | ||||
|             return e | ||||
| 
 | ||||
|         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) | ||||
|             try: | ||||
|                 # "forced" avoids compilation exceptions and adds anyway | ||||
|                 self.add(regex_string, forced=True) | ||||
|             except AlreadyThereException: | ||||
|                 logging.error(f"Regex \"{regex_string}\" loaded from XML was already present in the list.") | ||||
|                 continue | ||||
|             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) | ||||
| 
 | ||||
|         for item in marked: | ||||
|             self.mark(item) | ||||
|         self.__debug_test() | ||||
| 
 | ||||
|     def save_to_xml(self, outfile): | ||||
|         """Create a XML file that can be used by load_from_xml. | ||||
| @ -88,10 +258,143 @@ class ExcludeList(Markable): | ||||
|         outfile can be a file object or a filename. | ||||
|         """ | ||||
|         root = ET.Element("exclude_list") | ||||
|         for regex in self._excluded: | ||||
|         # reversed in order to keep order of entries when reloading from xml later | ||||
|         for item in reversed(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")) | ||||
|             exclude_node.set("regex", str(item[0])) | ||||
|             exclude_node.set("marked", ("y" if self.is_marked(item[0]) else "n")) | ||||
|         tree = ET.ElementTree(root) | ||||
|         with FileOrPath(outfile, "wb") as fp: | ||||
|             tree.write(fp, encoding="utf-8") | ||||
| 
 | ||||
| 
 | ||||
| class ExcludeDict(ExcludeList): | ||||
|     """Version implemented around a dictionary instead of a list, which implies | ||||
|     to keep the index of each string-key as its sub-element and keep it updated | ||||
|     whenever insert/remove is done.""" | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         Markable.__init__(self) | ||||
|         # { "regex": { "index": int, "compilable": bool, "error": str, "compiled": Pattern or None}} | ||||
|         # Note: "compilable" key should only be updated on add / rename | ||||
|         self._excluded = {} | ||||
|         self._count = 0 | ||||
|         self._excluded_compiled = set() | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         """Iterate in order.""" | ||||
|         for regex in ordered_keys(self._excluded): | ||||
|             yield self.is_marked(regex), regex | ||||
| 
 | ||||
|     def __len__(self): | ||||
|         return self._count | ||||
| 
 | ||||
|     def is_markable(self, regex): | ||||
|         return self._is_markable(regex) | ||||
| 
 | ||||
|     def _is_markable(self, regex): | ||||
|         """Return the cached result of "compilable" property""" | ||||
|         exists = self._excluded.get(regex) | ||||
|         if exists: | ||||
|             return exists.get("compilable") | ||||
|         return False | ||||
| 
 | ||||
|     def _did_mark(self, regex): | ||||
|         # self._excluded[regex][0] = True  # is compilable | ||||
|         try: | ||||
|             self._excluded_compiled.add(self._excluded[regex]["compiled"]) | ||||
|         except Exception as e: | ||||
|             print(f"Exception while adding regex {regex} to compiled set: {e}") | ||||
|             return | ||||
| 
 | ||||
|     def _did_unmark(self, regex): | ||||
|         self._remove_compiled(regex) | ||||
| 
 | ||||
|     def is_compilable(self, regex): | ||||
|         """Returns the cached "compilable" value""" | ||||
|         return self._excluded[regex]["compilable"] | ||||
| 
 | ||||
|     def error(self, regex): | ||||
|         """Return the compilation error message for regex string""" | ||||
|         return self._excluded.get(regex).get("error") | ||||
| 
 | ||||
|     @property | ||||
|     def compiled(self): | ||||
|         """Should be used by other classes to retrieve the up-to-date list of patterns.""" | ||||
|         return self._excluded_compiled | ||||
| 
 | ||||
|     @property | ||||
|     def compiled_files(self): | ||||
|         """Should be used by other classes to retrieve the up-to-date list of patterns | ||||
|         for files only.""" | ||||
|         return [compiled_pattern for compiled_pattern in self.compiled if sep not in compiled_pattern.pattern] | ||||
| 
 | ||||
|     # ---Public | ||||
|     def _do_add(self, regex, iscompilable, exception, compiled): | ||||
|         # We always insert at the top, so index should be 0 and other indices should be pushed by one | ||||
|         for value in self._excluded.values(): | ||||
|             value["index"] += 1 | ||||
|         self._excluded[regex] = {"index": 0, "compilable": iscompilable, "error": exception, "compiled": compiled} | ||||
|         self._count = len(self._excluded) | ||||
| 
 | ||||
|     def isExcluded(self, regex): | ||||
|         if regex in self._excluded.keys(): | ||||
|             return True | ||||
|         return False | ||||
| 
 | ||||
|     def clear(self): | ||||
|         self._excluded = {} | ||||
|         self._count = 0 | ||||
| 
 | ||||
|     def remove(self, regex): | ||||
|         old_value = self._excluded.pop(regex) | ||||
|         # Bring down all indices which where above it | ||||
|         index = old_value["index"] | ||||
|         if index == len(self._excluded): | ||||
|             self._remove_compiled(regex) | ||||
|             return | ||||
| 
 | ||||
|         for value in self._excluded.values(): | ||||
|             if value.get("index") > old_value["index"]: | ||||
|                 value["index"] -= 1 | ||||
|         self._remove_compiled(regex) | ||||
| 
 | ||||
|     def rename(self, regex, newregex): | ||||
|         if regex == newregex or regex not in self._excluded.keys(): | ||||
|             return | ||||
|         was_marked = self.is_marked(regex) | ||||
|         previous = self._excluded.pop(regex) | ||||
|         iscompilable, error, compiled = self.compile_re(newregex) | ||||
|         self._excluded[newregex] = {"index": previous["index"], "compilable": iscompilable, "error": error, "compiled": compiled} | ||||
|         if was_marked and iscompilable: | ||||
|             self.mark(newregex) | ||||
| 
 | ||||
|     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") | ||||
|         # reversed in order to keep order of entries when reloading from xml later | ||||
|         reversed_list = [] | ||||
|         for key in ordered_keys(self._excluded): | ||||
|             reversed_list.append(key) | ||||
|         for item in reversed(reversed_list): | ||||
|             exclude_node = ET.SubElement(root, "exclude") | ||||
|             exclude_node.set("regex", str(item)) | ||||
|             exclude_node.set("marked", ("y" if self.is_marked(item) else "n")) | ||||
|         tree = ET.ElementTree(root) | ||||
|         with FileOrPath(outfile, "wb") as fp: | ||||
|             tree.write(fp, encoding="utf-8") | ||||
| 
 | ||||
| 
 | ||||
| def ordered_keys(_dict): | ||||
|     """Returns an iterator over the keys of dictionary sorted by "index" key""" | ||||
|     if not len(_dict): | ||||
|         return | ||||
|     list_of_items = [] | ||||
|     for item in _dict.items(): | ||||
|         list_of_items.append(item) | ||||
|     list_of_items.sort(key=lambda x: x[1].get("index")) | ||||
|     for item in list_of_items: | ||||
|         yield item[0] | ||||
|  | ||||
| @ -8,8 +8,6 @@ | ||||
| # from hscommon.trans import tr | ||||
| from .exclude_list_table import ExcludeListTable | ||||
| 
 | ||||
| default_regexes = [".*thumbs", "\.DS.Store", "\.Trash", "Trash-Bin"] | ||||
| 
 | ||||
| 
 | ||||
| class ExcludeListDialogCore: | ||||
|     # --- View interface | ||||
| @ -22,13 +20,7 @@ class ExcludeListDialogCore: | ||||
|         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.exclude_list.restore_defaults() | ||||
|         self.refresh() | ||||
| 
 | ||||
|     def refresh(self): | ||||
| @ -55,9 +47,11 @@ class ExcludeListDialogCore: | ||||
|         return False | ||||
| 
 | ||||
|     def add(self, regex): | ||||
|         self.exclude_list.add(regex) | ||||
|         try: | ||||
|             self.exclude_list.add(regex) | ||||
|         except Exception as e: | ||||
|             raise(e) | ||||
|         self.exclude_list.mark(regex) | ||||
|         # TODO make checks here before adding to GUI | ||||
|         self.exclude_list_table.add(regex) | ||||
| 
 | ||||
|     def show(self): | ||||
|  | ||||
| @ -12,21 +12,18 @@ tr = trget("ui") | ||||
| class ExcludeListTable(GUITable, DupeGuruGUIObject): | ||||
|     COLUMNS = [ | ||||
|         Column("marked", ""), | ||||
|         Column("regex", tr("Regex")) | ||||
|         Column("regex", tr("Regular Expressions")) | ||||
|     ] | ||||
| 
 | ||||
|     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) | ||||
| @ -95,7 +92,7 @@ class ExcludeListRow(Row): | ||||
| 
 | ||||
|     @property | ||||
|     def markable(self): | ||||
|         return True | ||||
|         return self._app.exclude_list.is_markable(self.regex) | ||||
| 
 | ||||
|     @property | ||||
|     def marked(self): | ||||
| @ -108,10 +105,18 @@ class ExcludeListRow(Row): | ||||
|         else: | ||||
|             self._app.exclude_list.unmark(self.regex) | ||||
| 
 | ||||
|     @property | ||||
|     def error(self): | ||||
|         # This assumes error() returns an Exception() | ||||
|         message = self._app.exclude_list.error(self.regex) | ||||
|         if hasattr(message, "msg"): | ||||
|             return self._app.exclude_list.error(self.regex).msg | ||||
|         else: | ||||
|             return message  # Exception object | ||||
|     # @property | ||||
|     # def regex(self): | ||||
|     #     return self.regex | ||||
| 
 | ||||
|     # @regex.setter | ||||
|     # def regex(self, value): | ||||
|     #     self._app.exclude_list.add(self._regex, value) | ||||
|     #     self._app.exclude_list.add(self._regex, value) | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								images/dialog-error.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/dialog-error.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
| @ -10,5 +10,6 @@ | ||||
|     <file alias="zoom_out">../images/old_zoom_out.png</file> | ||||
|     <file alias="zoom_original">../images/old_zoom_original.png</file> | ||||
|     <file alias="zoom_best_fit">../images/old_zoom_best_fit.png</file> | ||||
|     <file alias="error">../images/dialog-error.png</file> | ||||
| </qresource> | ||||
| </RCC> | ||||
|  | ||||
| @ -9,6 +9,7 @@ from PyQt5.QtWidgets import ( | ||||
| ) | ||||
| from .exclude_list_table import ExcludeListTable, ExcludeView | ||||
| 
 | ||||
| from core.exclude import AlreadyThereException | ||||
| from hscommon.trans import trget | ||||
| tr = trget("ui") | ||||
| 
 | ||||
| @ -17,16 +18,18 @@ class ExcludeListDialog(QDialog): | ||||
|     def __init__(self, app, parent, model, **kwargs): | ||||
|         flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | ||||
|         super().__init__(parent, flags, **kwargs) | ||||
|         self.app = app | ||||
|         self.specific_actions = frozenset() | ||||
|         self._setupUI() | ||||
|         self.model = model  # ExcludeListDialogCore | ||||
|         self.model.view = self | ||||
|         self.table = ExcludeListTable(app, view=self.tableView) | ||||
|         self.table = ExcludeListTable(app, view=self.tableView)  # Qt ExcludeListTable | ||||
| 
 | ||||
|         self.buttonAdd.clicked.connect(self.addItem) | ||||
|         self.buttonRemove.clicked.connect(self.removeItem) | ||||
|         self.buttonAdd.clicked.connect(self.addStringFromLineEdit) | ||||
|         self.buttonRemove.clicked.connect(self.removeSelected) | ||||
|         self.buttonRestore.clicked.connect(self.restoreDefaults) | ||||
|         self.buttonClose.clicked.connect(self.accept) | ||||
|         self.buttonHelp.clicked.connect(self.display_help_message) | ||||
| 
 | ||||
|     def _setupUI(self): | ||||
|         layout = QVBoxLayout(self) | ||||
| @ -35,6 +38,7 @@ class ExcludeListDialog(QDialog): | ||||
|         self.buttonRemove = QPushButton(tr("Remove Selected")) | ||||
|         self.buttonRestore = QPushButton(tr("Restore defaults")) | ||||
|         self.buttonClose = QPushButton(tr("Close")) | ||||
|         self.buttonHelp = QPushButton(tr("Help")) | ||||
|         self.linedit = QLineEdit() | ||||
|         self.tableView = ExcludeView() | ||||
|         triggers = ( | ||||
| @ -43,25 +47,26 @@ class ExcludeListDialog(QDialog): | ||||
|             | QAbstractItemView.SelectedClicked | ||||
|         ) | ||||
|         self.tableView.setEditTriggers(triggers) | ||||
|         self.tableView.horizontalHeader().setVisible(True) | ||||
|         self.tableView.setSelectionMode(QTableView.ExtendedSelection) | ||||
|         self.tableView.setSelectionBehavior(QTableView.SelectRows) | ||||
|         # vheader = self.tableView.verticalHeader() | ||||
|         # vheader.setSectionsMovable(True) | ||||
|         # vheader.setVisible(True) | ||||
|         # vheader.setDefaultSectionSize(50) | ||||
|         self.tableView.setShowGrid(False) | ||||
|         vheader = self.tableView.verticalHeader() | ||||
|         vheader.setSectionsMovable(True) | ||||
|         vheader.setVisible(False) | ||||
|         hheader = self.tableView.horizontalHeader() | ||||
|         hheader.setSectionsMovable(False) | ||||
|         hheader.setSectionResizeMode(QHeaderView.Fixed) | ||||
|         hheader.setStretchLastSection(True) | ||||
|         hheader.setHighlightSections(False) | ||||
|         hheader.setVisible(True) | ||||
|         gridlayout.addWidget(self.linedit, 0, 0) | ||||
|         gridlayout.addWidget(self.buttonAdd, 0, 1, Qt.AlignLeft) | ||||
|         gridlayout.addWidget(self.buttonRemove, 1, 1, Qt.AlignLeft) | ||||
|         gridlayout.addWidget(self.buttonRestore, 2, 1, Qt.AlignLeft) | ||||
|         gridlayout.addWidget(self.tableView, 1, 0, 4, 1) | ||||
|         gridlayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), 3, 1) | ||||
|         gridlayout.addWidget(self.buttonClose, 4, 1) | ||||
|         gridlayout.addWidget(self.buttonHelp, 3, 1, Qt.AlignLeft) | ||||
|         gridlayout.addWidget(self.tableView, 1, 0, 5, 1) | ||||
|         gridlayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), 4, 1) | ||||
|         gridlayout.addWidget(self.buttonClose, 5, 1) | ||||
|         layout.addLayout(gridlayout) | ||||
| 
 | ||||
|     # --- model --> view | ||||
| @ -69,15 +74,28 @@ class ExcludeListDialog(QDialog): | ||||
|         super().show() | ||||
| 
 | ||||
|     @pyqtSlot() | ||||
|     def addItem(self): | ||||
|     def addStringFromLineEdit(self): | ||||
|         text = self.linedit.text() | ||||
|         if not text: | ||||
|             return | ||||
|         self.model.add(text) | ||||
|         try: | ||||
|             self.model.add(text) | ||||
|         except AlreadyThereException: | ||||
|             self.app.show_message("Expression already in the list.") | ||||
|             return | ||||
|         except Exception as e: | ||||
|             self.app.show_message(f"Expression is invalid: {e}") | ||||
|             return | ||||
|         self.linedit.clear() | ||||
| 
 | ||||
|     def removeItem(self): | ||||
|     def removeSelected(self): | ||||
|         self.model.remove_selected() | ||||
| 
 | ||||
|     def restoreDefaults(self): | ||||
|         self.model.restore_defaults() | ||||
| 
 | ||||
|     def display_help_message(self): | ||||
|         self.app.show_message("""\ | ||||
| These python regular expressions will filter out files and directory paths \ | ||||
| specified here.\nDuring directory selection, paths filtered here will be added as \ | ||||
| "Skipped" by default, but regular files will be ignored altogether during scans.""") | ||||
|  | ||||
| @ -2,8 +2,8 @@ | ||||
| # which should be included with this package. The terms are also available at | ||||
| # http://www.gnu.org/licenses/gpl-3.0.html | ||||
| 
 | ||||
| from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal | ||||
| from PyQt5.QtGui import QBrush, QFont, QFontMetrics, QColor | ||||
| from PyQt5.QtCore import Qt, QModelIndex | ||||
| from PyQt5.QtGui import QFont, QFontMetrics, QIcon | ||||
| from PyQt5.QtWidgets import QTableView | ||||
| 
 | ||||
| from qtlib.column import Column | ||||
| @ -20,7 +20,7 @@ class ExcludeListTable(Table): | ||||
|     def __init__(self, app, view, **kwargs): | ||||
|         model = app.model.exclude_list_dialog.exclude_list_table  # pointer to GUITable | ||||
|         super().__init__(model, view, **kwargs) | ||||
|         view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder) | ||||
|         # view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder) | ||||
|         font = view.font() | ||||
|         font.setPointSize(app.prefs.tableFontSize) | ||||
|         view.setFont(font) | ||||
| @ -32,6 +32,10 @@ class ExcludeListTable(Table): | ||||
|         if column.name == "marked": | ||||
|             if role == Qt.CheckStateRole and row.markable: | ||||
|                 return Qt.Checked if row.marked else Qt.Unchecked | ||||
|             if role == Qt.ToolTipRole and not row.markable: | ||||
|                 return "Compilation error: " + row.get_cell_value("error") | ||||
|             if role == Qt.DecorationRole and not row.markable: | ||||
|                 return QIcon.fromTheme("dialog-error", QIcon(":/error")) | ||||
|             return None | ||||
|         if role == Qt.DisplayRole: | ||||
|             return row.data[column.name] | ||||
| @ -43,12 +47,12 @@ class ExcludeListTable(Table): | ||||
|         return None | ||||
| 
 | ||||
|     def _getFlags(self, row, column): | ||||
|         flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | ||||
|         flags = Qt.ItemIsEnabled | ||||
|         if column.name == "marked": | ||||
|             if row.markable: | ||||
|                 flags |= Qt.ItemIsUserCheckable | ||||
|         elif column.name == "regex": | ||||
|             flags |= Qt.ItemIsEditable | ||||
|             flags |= Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | ||||
|         return flags | ||||
| 
 | ||||
|     def _setData(self, row, column, value, role): | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user