Implement exclude list dialog on the Qt side

This commit is contained in:
glubsy 2020-08-17 04:13:20 +02:00
parent a26de27c47
commit 2eaf7e7893
9 changed files with 400 additions and 92 deletions

View File

@ -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.

View File

@ -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():

View File

@ -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]

View File

@ -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):

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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>

View File

@ -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.""")

View File

@ -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):