Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 12 additions & 12 deletions CustomQt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from aqt.utils import showInfo

#Import all of the Qt GUI library
from aqt.qt import *
from aqt.qt import QPalette, QBrush, QColor, QComboBox, QStyledItemDelegate, QEvent, QFontMetrics, QStandardItem, Qt # Qt classes from Anki's qt wrapper


#Taken from: https://gis.stackexchange.com/questions/350148/qcombobox-multiple-selection-pyqt5
Expand All @@ -28,8 +28,8 @@ def __init__(self, *args, **kwargs):
self.setEditable(True)
self.lineEdit().setReadOnly(True)
# Make the lineedit the same color as QPushButton
palette = qApp.palette()
palette.setBrush(QPalette.Base, palette.button())
palette = mw.palette()
palette.setBrush(QPalette.ColorRole.Base, palette.button())
self.lineEdit().setPalette(palette)

# Use custom delegate
Expand All @@ -53,7 +53,7 @@ def resizeEvent(self, event):
def eventFilter(self, object, event):

if object == self.lineEdit():
if event.type() == QEvent.MouseButtonRelease:
if event.type() == QEvent.Type.MouseButtonRelease:
if self.closeOnLineEditClick:
self.hidePopup()
else:
Expand All @@ -62,14 +62,14 @@ def eventFilter(self, object, event):
return False

if object == self.view().viewport():
if event.type() == QEvent.MouseButtonRelease:
if event.type() == QEvent.Type.MouseButtonRelease:
index = self.view().indexAt(event.pos())
item = self.model().item(index.row())

if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
if item.checkState() == Qt.CheckState.Checked:
item.setCheckState(Qt.CheckState.Unchecked)
else:
item.setCheckState(Qt.Checked)
item.setCheckState(Qt.CheckState.Checked)
return True
return False

Expand All @@ -93,13 +93,13 @@ def timerEvent(self, event):
def updateText(self):
texts = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
if self.model().item(i).checkState() == Qt.CheckState.Checked:
texts.append(self.model().item(i).text())
text = ", ".join(texts)

# Compute elided text (with "...")
metrics = QFontMetrics(self.lineEdit().font())
elidedText = metrics.elidedText(text, Qt.ElideRight, self.lineEdit().width())
elidedText = metrics.elidedText(text, Qt.TextElideMode.ElideRight, self.lineEdit().width())
self.lineEdit().setText(elidedText)

def addItem(self, text, data=None):
Expand All @@ -109,8 +109,8 @@ def addItem(self, text, data=None):
item.setData(text)
else:
item.setData(data)
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
item.setData(Qt.Unchecked, Qt.CheckStateRole)
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
self.model().appendRow(item)

def addItems(self, texts, datalist=None):
Expand Down
6 changes: 3 additions & 3 deletions FieldTable.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def __init__(self, group, *args, **kwargs):
self.addFieldRow()

#Resize the columns
self.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed)
self.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
self.horizontalHeader().resizeSection(2, 65)

#Method trigger to update the field in the Comparer object and also adds a new row if the index = row count - 1
Expand Down
6 changes: 3 additions & 3 deletions GroupWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, Comparer, parent, *args, **kwargs):
#Create and add widgets to this layout group window
self.groupNum = self.groupIndex + 1
self.title = QLabel(f"<b>Group {self.groupNum} (G{self.groupNum})</b>", parent)
self.title.setAlignment(Qt.AlignCenter)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.addWidget(self.title)

#Add widget to select group type and link trigger method
Expand Down Expand Up @@ -77,7 +77,7 @@ def __init__(self, Comparer, parent, *args, **kwargs):
#Add widget with autocompleter to enter a tag for the duplicate action
self.tagBox = QLineEdit(parent)
completer = QCompleter(self.group.fieldInfo['Tags'].keys(), parent)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.tagBox.setCompleter(completer)
self.tagBox.setPlaceholderText('Enter an existing tag or a new one.')
self.addWidget(self.tagBox)
Expand Down Expand Up @@ -282,4 +282,4 @@ def setEnabledAll(self, boolean):
self.tagBox.setEnabled(boolean)

self.fieldTableLabel.setEnabled(boolean)
self.fieldTable.setEnabledAll(boolean)
self.fieldTable.setEnabledAll(boolean)
13 changes: 11 additions & 2 deletions MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, *args, **kwargs):
\nBy default, notes in different groups are marked as duplicates when their fields with the same number matches.\
\nYou can disable this and specificy your own conditions for duplicate notes if you enable \'advanced mode\' below.\
\nHover over \'advanced mode\' or \'RegEx capture\' for more explanation.')
self.intro.setAlignment(Qt.AlignCenter)
self.intro.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.intro)

#Create Comparer object and then using the Comparer, create subwindows.
Expand Down Expand Up @@ -91,6 +91,15 @@ def __init__(self, *args, **kwargs):
<br><b>Example</b>: '<code>(G1F1 = G2F1 and G1F2 = G2F2) or (G1F3 = G2F3 and G1F4 = G2F4)</code>' means that either fields 1 and 2 must match OR fields 3 and 4
in order for all of these conditions together to be seen as '<code>True</code>' and the notes to be seen as duplicates.
</li>
<li>With adding negation it is possible to compare original and new notes for missing or wrong values. The operator is:
<ul>
<li>'<code>not</code>': This means that if the conditions at the right from '<code>not</code>' are '<code>True</code>' they will be set to '<code>False</code>'
and if the conditions at the right from '<code>not</code>' are '<code>False</code>' they will be set to '<code>True</code>'.</li>
</ul>
<div><b>Example 1</b>: '<code>(G1F1 = G2F1) and not (G1F2 = G2F2)</code>' means that the first field of both groups must match AND that field 2 of group 1 and group 2 does NOT match.
<div><b>Example 2</b>: '<code>(G1F1 = G2F1) and not ("" = G1F2) and ("" = G2F2)</code>' means that the first field of both groups must match
AND that field 2 of group 1 IS NOT empty, but field 2 of group 2 IS empty.
</li>
</ul>''')

self.regexCheckBox = QCheckBox('Enable RegEx capture for advanced mode', self)
Expand Down Expand Up @@ -249,7 +258,7 @@ def reportCompareProgress(self, percentage, timeLeft, activity):
self.progressBar.setValue(percentage)
self.progressActivityLabel.setText(activity)
if timeLeft != None:
self.timeLeftLabel.setText(f'Time left: {str(datetime.timedelta(seconds=timeLeft))}\nDuplicates found: {len(self.Comparer.queue)}')
self.timeLeftLabel.setText(f'Time left: {str(datetime.timedelta(seconds=timeLeft))}\nHits found: {len(self.Comparer.queue)}')

#Method to enable / disable all of the GUI elements
def setEnabledAll(self, boolean):
Expand Down
94 changes: 77 additions & 17 deletions Node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#Class to build a conditional tree to compare notes
class Node:

_negation = False

def __init__(self, string, depth = 0, removeBrackets = True):
self.children = []
if removeBrackets:
Expand All @@ -23,17 +25,30 @@ def __init__(self, string, depth = 0, removeBrackets = True):
self.rightValue = None
self.solveMethod = None

# using property decorator as getter function for negation
@property
def negation(self):
return Node._negation

# a setter function for negation
@negation.setter
def negation(self, neg):
if neg:
Node._negation = False
else:
Node._negation = True

#Function to set a string
def setString(self, string):
try:
self.string = Utils.removeBrackets(string.replace('\n', ' ').replace('\t', ' '))
except re.error as e:
raise e

#Recursive tree method to create children by chopping the condition string set to 'value' into parts
#to base new children on
def createChildren(self):
#echo(self.string + ':' + str(self.depth))
#echo(self.string + ': depth:' + str(self.depth))

#Remove any current children
self.children = []
Expand All @@ -58,20 +73,21 @@ def createChildren(self):
#Then, split the top level array based on 'and'/'or' operators
#and clean up any empty values
#these are the values for the children
childValues = [cv for cv in re.split(r'( and )|( or )', string) if cv not in [None, '']]
childValues = [cv for cv in re.split(r'( and )|( or )|(not )', string) if cv not in [None, '']]

#If there are child values, rebuild every child value using the saved brackets,
#create new children and also let them make their own
if len(childValues) > 1:
i = 0
for cv in childValues:

#Rebuild the child value using the saved brackets
while '$' in cv:
for match in re.finditer(r'\$(\d+)', cv):
cv = cv.replace(match.group(0), brackets[int(match.group(1))])

#Remove any excess brackets from the child value,
#create a new child, add it and activate it
#echo(string + ': depth:' + str(self.depth) + ' :cv:' + cv)
child = Node(cv, self.depth + 1)
self.children.append(child)
child.createChildren()
Expand All @@ -83,15 +99,15 @@ def createChildren(self):
else:

#When the end child node is an operator return
if string in ['and', 'or']:
if string in ['and', 'or', 'not']:
return

#Split the current value into left operand, operator and right operand
stringSplit = [i for i in re.split(r'(=)|(in)|(>)', string) if i not in [None, '']]

#When the length isn't 3 raise an error
if len(stringSplit) != 3:
raise re.error(f'"{string}" is not a valid condition.')
raise re.error(f'"{string}" is too short and not a valid condition. Childvalues: "{childValues}"')

#Retrieve the types of the operands
leftType, self.leftValue = self.__class__.operandType(stringSplit[0])
Expand Down Expand Up @@ -130,7 +146,7 @@ def createChildren(self):

#The neither operands can be an regular expression
if leftType == 'regex' or rightType == 'regex':
raise re.error(f"The neither the left or right part of \"{string}\" can be a regular expression.")
raise re.error(f"Neither the left or right part of \"{string}\" can be a regular expression.")

#Save the solve method
self.solveMethod = self.insideCompare
Expand Down Expand Up @@ -170,7 +186,7 @@ def solve(self, notes):
if numChildren == 0:

#When the value is an operator, return it
if self.string in ['and', 'or']:
if self.string in ['and', 'or', 'not']:
return self.string

#When the value isn't an operator, solve and return the elemental condition
Expand All @@ -182,13 +198,28 @@ def solve(self, notes):
else:
totalCondition = self.children[0].solve(notes)
currentOperator = ''
for i in range(1, numChildren):
oldOperator = ''
i = 1
while i < numChildren:
condString = ''
'''
for j in range(1, numChildren):
newCondition = self.children[j].solve(notes)
condString = condString + 'j:' + str(j) + ':' + str(newCondition) + ':'
'''
newCondition = self.children[i].solve(notes)

#When a child is an operator save it temporarily
if newCondition in ['and', 'or']:
if newCondition in ['and', 'or', 'not']:
#When the next child is a 'not' operator save it temporarily
if newCondition == 'not':
#setter assignment via getter call
self.negation = self.negation
oldOperator = currentOperator
currentOperator = newCondition

# (G1F1 = G2F1) and not ('sound:hello' in G1F2)
# (G1F1 = G2F1) and not ('sound' in G1F2) and ('sound' in G2F2)

#When it is a condition combine it with the total condition
#depending on the current operator, and delete the current operator afterwards.
#if there is no currentOperator, raise an error
Expand All @@ -197,9 +228,21 @@ def solve(self, notes):
totalCondition = totalCondition and newCondition
elif currentOperator == 'or':
totalCondition = totalCondition or newCondition
elif currentOperator == 'not':
if isinstance(newCondition, bool):
#setter assignment via getter call
self.negation = self.negation
if oldOperator == 'and':
totalCondition = totalCondition and newCondition
elif oldOperator == 'or':
totalCondition = totalCondition or newCondition
else:
raise re.error(f'The use of the \'not\' operator is incorrect. i: "{str(i)}" cOp: "{currentOperator}" :newCond: "{newCondition}" :nc: "{str(numChildren)}" :string: "{self.string}" :total: "{totalCondition}"')
oldOperator = ''
else:
raise re.error('The use of \'and\'/\'or\' operators is incorrect.')
raise re.error(f'The use of \'and\'/\'or\' operators is incorrect. i: "{str(i)}" cOp: "{currentOperator}" :newCond: "{newCondition}" :nc: "{str(numChildren)}" :string: "{self.string}" :total: "{totalCondition}"')
currentOperator = ''
i = i + 1

#Return the total condition
return totalCondition
Expand Down Expand Up @@ -246,7 +289,10 @@ def equalCompare(self, notes):
if isinstance(left, bool) or isinstance(right, bool):
return False
else:
return left == right
if self.negation:
return not(left == right)
else:
return left == right

def inCompare(self, notes):

Expand All @@ -258,7 +304,10 @@ def inCompare(self, notes):
if isinstance(left, bool) or isinstance(right, bool):
return False
else:
return left in right if ' ' in left else Utils.wordIn(left, right)
if self.negation:
return not(left in right if ' ' in left else Utils.wordIn(left, right))
else:
return left in right if ' ' in left else Utils.wordIn(left, right)

def insideCompare(self, notes):

Expand All @@ -270,7 +319,12 @@ def insideCompare(self, notes):
if isinstance(left, bool) or isinstance(right, bool):
return False
else:
return left in right
if self.negation:
#self.logger.debug(f'inside neg2: "{bNegation}"')
return not(left in right)
else:
#self.logger.debug(f'insComp: "{self.string}" :inside: "{left in right}"')
return left in right

def equalRegexCompare(self, notes):

Expand All @@ -281,7 +335,10 @@ def equalRegexCompare(self, notes):
if isinstance(left, bool):
return False
else:
return re.fullmatch(self.rightValue, left) != None
if self.negation:
return not(re.fullmatch(self.rightValue, left) != None)
else:
return re.fullmatch(self.rightValue, left) != None

def inRegexCompare(self, notes):

Expand All @@ -292,4 +349,7 @@ def inRegexCompare(self, notes):
if isinstance(right, bool):
return False
else:
return re.search(self.leftValue, right) != None
if self.negation:
return not(re.search(self.leftValue, right) != None)
else:
return re.search(self.leftValue, right) != None
Loading