1#!/usr/bin/env python3
2
3#******************************************************************************
4# conditional.py, provides a class to store field comparison functions
5#
6# TreeLine, an information storage program
7# Copyright (C) 2017, Douglas W. Bell
8#
9# This is free software; you can redistribute it and/or modify it under the
10# terms of the GNU General Public License, either Version 2 or any later
11# version.  This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
13#******************************************************************************
14
15import re
16import enum
17from PyQt5.QtCore import QSize, Qt, pyqtSignal
18from PyQt5.QtWidgets import (QComboBox, QDialog, QGroupBox, QHBoxLayout,
19                             QLabel, QLineEdit, QListWidget, QPushButton,
20                             QSizePolicy, QVBoxLayout)
21import treeformats
22import configdialog
23import undo
24import globalref
25
26_operators = ['==', '<', '<=', '>', '>=', '!=', N_('starts with'),
27              N_('ends with'), N_('contains'), N_('True'), N_('False')]
28_functions = {'==': '__eq__', '<': '__lt__', '<=': '__le__',
29              '>': '__gt__', '>=': '__ge__', '!=': '__ne__',
30              'starts with': 'startswith', 'ends with': 'endswith',
31              'contains': 'contains', 'True': 'true', 'False': 'false'}
32_boolOper = [N_('and'), N_('or')]
33_allTypeEntry = _('[All Types]')
34_parseRe = re.compile(r'((?:and)|(?:or)) (\S+) (.+?) '
35                      r'(?:(?<!\\)"|(?<=\\\\)")(.*?)(?:(?<!\\)"|(?<=\\\\)")')
36
37
38class Conditional:
39    """Stores and evaluates a conditional comparison for field data.
40    """
41    def __init__(self, conditionStr='', nodeFormatName=''):
42        """Initialize the condition object.
43
44        Accepts a string in the following format:
45        'fieldname == "value" and otherFieldName > "othervalue"'
46        Arguments:
47            conditionStr -- the condition string to set
48            nodeFormatName -- if name is set, restricts matches to type family
49        """
50        self.conditionLines = []
51        conditionStr = 'and ' + conditionStr
52        for boolOper, fieldName, oper, value in _parseRe.findall(conditionStr):
53            value = value.replace('\\"', '"').replace('\\\\', '\\')
54            self.conditionLines.append(ConditionLine(boolOper, fieldName,
55                                                     oper, value))
56        self.origNodeFormatName = nodeFormatName
57        self.nodeFormatNames = set()
58        if nodeFormatName:
59            self.nodeFormatNames.add(nodeFormatName)
60            nodeFormats = (globalref.mainControl.activeControl.structure.
61                           treeFormats)
62            for nodeType in nodeFormats[nodeFormatName].derivedTypes:
63                self.nodeFormatNames.add(nodeType.name)
64
65    def evaluate(self, node):
66        """Evaluate this condition and return True or False.
67
68        Arguments:
69            node -- the node to check for a field match
70        """
71        if (self.nodeFormatNames and
72            node.formatRef.name not in self.nodeFormatNames):
73            return False
74        result = True
75        for conditon in self.conditionLines:
76            result = conditon.evaluate(node, result)
77        return result
78
79    def conditionStr(self):
80        """Return the condition string for this condition set.
81        """
82        return ' '.join([cond.conditionStr() for cond in
83                         self.conditionLines])[4:]
84
85    def renameFields(self, oldName, newName):
86        """Rename the any fields found in condition lines.
87
88        Arguments:
89            oldName -- the previous field name
90            newName -- the updated field name
91        """
92        for condition in self.conditionLines:
93            if condition.fieldName == oldName:
94                condition.fieldName = newName
95
96    def removeField(self, fieldname):
97        """Remove conditional lines referencing the given field.
98
99        Arguments:
100            fieldname -- the field name to be removed
101        """
102        for condition in self.conditionLines[:]:
103            if condition.fieldName == fieldname:
104                self.conditionLines.remove(condition)
105
106    def __len__(self):
107        """Return the number of conditions for truth testing.
108        """
109        return len(self.conditionLines)
110
111
112class ConditionLine:
113    """Stores & evaluates a portion of a conditional comparison.
114    """
115    def __init__(self, boolOper, fieldName, oper, value):
116        """Initialize the condition line.
117
118        Arguments:
119            boolOper -- a string for combining previous lines ('and' or 'or')
120            fieldName -- the field name to evaluate
121            oper -- the operator string
122            value -- the string for comparison
123        """
124        self.boolOper = boolOper
125        self.fieldName = fieldName
126        self.oper = oper
127        self.value = value
128
129    def evaluate(self, node, prevResult=True):
130        """Evaluate this line and return True or False.
131
132        Arguments:
133            node -- the node to check for a field match
134            prevResult -- the result to combine with the boolOper
135        """
136        try:
137            field = node.formatRef.fieldDict[self.fieldName]
138        except KeyError:
139            if self.boolOper == 'and':
140                return False
141            return prevResult
142        dataStr = field.compareValue(node)
143        value = field.adjustedCompareValue(self.value)
144        try:
145            func = getattr(dataStr, _functions[self.oper])
146        except AttributeError:
147            dataStr = StringOps(dataStr)
148            func = getattr(dataStr, _functions[self.oper])
149            value = str(value)
150        if self.boolOper == 'and':
151            return prevResult and func(value)
152        else:
153            return prevResult or func(value)
154
155    def conditionStr(self):
156        """Return the text line for this condition.
157        """
158        value = self.value.replace('\\', '\\\\').replace('"', '\\"')
159        return '{0} {1} {2} "{3}"'.format(self.boolOper, self.fieldName,
160                                          self.oper, value)
161
162
163class StringOps(str):
164    """A string class with extra comparison functions.
165    """
166    def __new__(cls, initStr=''):
167        """Return the str object.
168
169        Arguments:
170            initStr -- the initial string value
171        """
172        return str.__new__(cls, initStr)
173
174    def contains(self, substr):
175        """Return True if self contains substr.
176
177        Arguments:
178            substr -- the substring to check
179        """
180        return self.find(substr) != -1
181
182    def true(self, other=''):
183        """Always return True.
184
185        Arguments:
186            other -- unused placeholder
187        """
188        return True
189
190    def false(self, other=''):
191        """Always return False.
192
193        Arguments:
194            other -- unused placeholder
195        """
196        return False
197
198
199FindDialogType = enum.Enum('FindDialogType',
200                           'typeDialog findDialog filterDialog')
201
202class ConditionDialog(QDialog):
203    """Dialog for defining field condition tests.
204
205    Used for defining conditional types (modal), for finding by condition
206    (nonmodal) and for filtering by condition (nonmodal).
207    """
208    dialogShown = pyqtSignal(bool)
209    def __init__(self, dialogType, caption, nodeFormat=None, parent=None):
210        """Create the conditional dialog.
211
212        Arguments:
213            dialogType -- either typeDialog, findDialog or filterDialog
214            caption -- the window title for this dialog
215            nodeFormat -- the current node format for the typeDialog
216            parent -- the parent overall dialog
217        """
218        super().__init__(parent)
219        self.setWindowTitle(caption)
220        self.dialogType = dialogType
221        self.ruleList = []
222        self.combiningBoxes = []
223        self.typeCombo = None
224        self.resultLabel = None
225        self.endFilterButton = None
226        self.fieldNames = []
227        if nodeFormat:
228            self.fieldNames = nodeFormat.fieldNames()
229        topLayout = QVBoxLayout(self)
230
231        if dialogType == FindDialogType.typeDialog:
232            self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
233                                Qt.WindowCloseButtonHint)
234        else:
235            self.setAttribute(Qt.WA_QuitOnClose, False)
236            self.setWindowFlags(Qt.Window |
237                                Qt.WindowStaysOnTopHint)
238            typeBox = QGroupBox(_('Node Type'))
239            topLayout.addWidget(typeBox)
240            typeLayout = QVBoxLayout(typeBox)
241            self.typeCombo = QComboBox()
242            typeLayout.addWidget(self.typeCombo)
243            self.typeCombo.currentIndexChanged.connect(self.updateDataType)
244
245        self.mainLayout = QVBoxLayout()
246        topLayout.addLayout(self.mainLayout)
247
248        upCtrlLayout = QHBoxLayout()
249        topLayout.addLayout(upCtrlLayout)
250        addButton = QPushButton(_('&Add New Rule'))
251        upCtrlLayout.addWidget(addButton)
252        addButton.clicked.connect(self.addNewRule)
253        self.removeButton = QPushButton(_('&Remove Rule'))
254        upCtrlLayout.addWidget(self.removeButton)
255        self.removeButton.clicked.connect(self.removeRule)
256        upCtrlLayout.addStretch()
257
258        if dialogType == FindDialogType.typeDialog:
259            okButton = QPushButton(_('&OK'))
260            upCtrlLayout.addWidget(okButton)
261            okButton.clicked.connect(self.accept)
262            cancelButton = QPushButton(_('&Cancel'))
263            upCtrlLayout.addWidget(cancelButton)
264            cancelButton.clicked.connect(self.reject)
265        else:
266            self.removeButton.setEnabled(False)
267            saveBox = QGroupBox(_('Saved Rules'))
268            topLayout.addWidget(saveBox)
269            saveLayout = QVBoxLayout(saveBox)
270            self.saveListBox = SmallListWidget()
271            saveLayout.addWidget(self.saveListBox)
272            self.saveListBox.itemDoubleClicked.connect(self.loadSavedRule)
273            nameLayout = QHBoxLayout()
274            saveLayout.addLayout(nameLayout)
275            label = QLabel(_('Name:'))
276            nameLayout.addWidget(label)
277            self.saveNameEdit = QLineEdit()
278            nameLayout.addWidget(self.saveNameEdit)
279            self.saveNameEdit.textChanged.connect(self.updateSaveEnable)
280            saveButtonLayout = QHBoxLayout()
281            saveLayout.addLayout(saveButtonLayout)
282            self.loadSavedButton = QPushButton(_('&Load'))
283            saveButtonLayout.addWidget(self.loadSavedButton)
284            self.loadSavedButton.clicked.connect(self.loadSavedRule)
285            self.saveButton = QPushButton(_('&Save'))
286            saveButtonLayout.addWidget(self.saveButton)
287            self.saveButton.clicked.connect(self.saveRule)
288            self.saveButton.setEnabled(False)
289            self.delSavedButton = QPushButton(_('&Delete'))
290            saveButtonLayout.addWidget(self.delSavedButton)
291            self.delSavedButton.clicked.connect(self.deleteRule)
292            saveButtonLayout.addStretch()
293
294            if dialogType == FindDialogType.findDialog:
295                self.resultLabel = QLabel()
296                topLayout.addWidget(self.resultLabel)
297            lowCtrlLayout = QHBoxLayout()
298            topLayout.addLayout(lowCtrlLayout)
299            if dialogType == FindDialogType.findDialog:
300                previousButton = QPushButton(_('Find &Previous'))
301                lowCtrlLayout.addWidget(previousButton)
302                previousButton.clicked.connect(self.findPrevious)
303                nextButton = QPushButton(_('Find &Next'))
304                nextButton.setDefault(True)
305                lowCtrlLayout.addWidget(nextButton)
306                nextButton.clicked.connect(self.findNext)
307            else:
308                filterButton = QPushButton(_('&Filter'))
309                lowCtrlLayout.addWidget(filterButton)
310                filterButton.clicked.connect(self.startFilter)
311                self.endFilterButton = QPushButton(_('&End Filter'))
312                lowCtrlLayout.addWidget(self.endFilterButton)
313                self.endFilterButton.setEnabled(False)
314                self.endFilterButton.clicked.connect(self.endFilter)
315            lowCtrlLayout.addStretch()
316            closeButton = QPushButton(_('&Close'))
317            lowCtrlLayout.addWidget(closeButton)
318            closeButton.clicked.connect(self.close)
319            origTypeName = nodeFormat.name if nodeFormat else ''
320            self.loadTypeNames(origTypeName)
321            self.loadSavedNames()
322        self.ruleList.append(ConditionRule(1, self.fieldNames))
323        self.mainLayout.addWidget(self.ruleList[0])
324
325    def addNewRule(self, checked=False, combineBool='and'):
326        """Add a new empty rule to the dialog.
327
328        Arguments:
329            checked -- unused placekeeper variable for signal
330            combineBool -- the boolean op for combining with the previous rule
331        """
332        if self.ruleList:
333            boolBox = QComboBox()
334            boolBox.setEditable(False)
335            self.combiningBoxes.append(boolBox)
336            boolBox.addItems([_(op) for op in _boolOper])
337            if combineBool != 'and':
338                boolBox.setCurrentIndex(1)
339            self.mainLayout.insertWidget(len(self.ruleList) * 2 - 1, boolBox,
340                                        0, Qt.AlignHCenter)
341        rule = ConditionRule(len(self.ruleList) + 1, self.fieldNames)
342        self.ruleList.append(rule)
343        self.mainLayout.insertWidget(len(self.ruleList) * 2 - 2, rule)
344        self.removeButton.setEnabled(True)
345
346    def removeRule(self):
347        """Remove the last rule from the dialog.
348        """
349        if self.ruleList:
350            if self.combiningBoxes:
351                self.combiningBoxes[-1].hide()
352                del self.combiningBoxes[-1]
353            self.ruleList[-1].hide()
354            del self.ruleList[-1]
355            if self.dialogType == FindDialogType.typeDialog:
356                self.removeButton.setEnabled(len(self.ruleList) > 0)
357            else:
358                self.removeButton.setEnabled(len(self.ruleList) > 1)
359
360    def clearRules(self):
361        """Remove all rules from the dialog and add default rule.
362        """
363        for box in self.combiningBoxes:
364            box.hide()
365        for rule in self.ruleList:
366            rule.hide()
367        self.combiningBoxes = []
368        self.ruleList = [ConditionRule(1, self.fieldNames)]
369        self.mainLayout.insertWidget(0, self.ruleList[0])
370        self.removeButton.setEnabled(True)
371
372    def setCondition(self, conditional, typeName=''):
373        """Set rule values to match the given conditional.
374
375        Arguments:
376            conditional -- the Conditional class to match
377            typeName -- an optional type name used with some dialog types
378        """
379        if self.typeCombo:
380            if typeName:
381                self.typeCombo.setCurrentIndex(self.typeCombo.
382                                               findText(typeName))
383            else:
384                self.typeCombo.setCurrentIndex(0)
385        while len(self.ruleList) > 1:
386            self.removeRule()
387        if conditional:
388            self.ruleList[0].setCondition(conditional.conditionLines[0])
389        for conditionLine in conditional.conditionLines[1:]:
390            self.addNewRule(combineBool=conditionLine.boolOper)
391            self.ruleList[-1].setCondition(conditionLine)
392
393    def conditional(self):
394        """Return a Conditional instance for the current settings.
395        """
396        combineBools = [0] + [boolBox.currentIndex() for boolBox in
397                              self.combiningBoxes]
398        typeName = self.typeCombo.currentText() if self.typeCombo else ''
399        if typeName == _allTypeEntry:
400            typeName = ''
401        conditional = Conditional('', typeName)
402        for boolIndex, rule in zip(combineBools, self.ruleList):
403            condition = rule.conditionLine()
404            if boolIndex != 0:
405                condition.boolOper = 'or'
406            conditional.conditionLines.append(condition)
407        return conditional
408
409    def loadTypeNames(self, origTypeName=''):
410        """Load format type names into combo box.
411
412        Arguments:
413            origTypeName -- a starting type name if given
414        """
415        if not origTypeName:
416            origTypeName = self.typeCombo.currentText()
417        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
418        self.typeCombo.blockSignals(True)
419        self.typeCombo.clear()
420        self.typeCombo.addItem(_allTypeEntry)
421        typeNames = nodeFormats.typeNames()
422        self.typeCombo.addItems(typeNames)
423        if origTypeName and origTypeName != _allTypeEntry:
424            try:
425                self.typeCombo.setCurrentIndex(typeNames.index(origTypeName)
426                                               + 1)
427            except ValueError:
428                if self.endFilterButton and self.endFilterButton.isEnabled():
429                    self.endFilter()
430                self.clearRules()
431        self.typeCombo.blockSignals(False)
432        self.updateDataType()
433
434    def updateDataType(self):
435        """Update the node format based on a data type change.
436        """
437        typeName = self.typeCombo.currentText()
438        if not typeName:
439            return
440        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
441        if typeName == _allTypeEntry:
442            fieldNameSet = set()
443            for typeFormat in nodeFormats.values():
444                fieldNameSet.update(typeFormat.fieldNames())
445            self.fieldNames = sorted(list(fieldNameSet))
446        else:
447            self.fieldNames = nodeFormats[typeName].fieldNames()
448        for rule in self.ruleList:
449            currentField = rule.conditionLine().fieldName
450            if currentField not in self.fieldNames:
451                if self.endFilterButton and self.endFilterButton.isEnabled():
452                    self.endFilter()
453                self.clearRules()
454                break
455            rule.reloadFieldBox(self.fieldNames, currentField)
456
457    def loadSavedNames(self, updateOtherDialog=False):
458        """Refresh the list of saved rule names.
459        """
460        selNum = 0
461        if self.saveListBox.count():
462            selNum = self.saveListBox.currentRow()
463        self.saveListBox.clear()
464        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
465        savedRules = nodeFormats.savedConditions()
466        ruleNames = sorted(list(savedRules.keys()))
467        if ruleNames:
468            self.saveListBox.addItems(ruleNames)
469            if selNum >= len(ruleNames):
470                selNum = len(ruleNames) - 1
471            self.saveListBox.setCurrentRow(selNum)
472        self.loadSavedButton.setEnabled(len(ruleNames) > 0)
473        self.delSavedButton.setEnabled(len(ruleNames) > 0)
474        if updateOtherDialog:
475            if (self != globalref.mainControl.findConditionDialog and
476                globalref.mainControl.findConditionDialog and
477                globalref.mainControl.findConditionDialog.isVisible()):
478                globalref.mainControl.findConditionDialog.loadSavedNames()
479            elif (self != globalref.mainControl.filterConditionDialog and
480                  globalref.mainControl.filterConditionDialog and
481                  globalref.mainControl.filterConditionDialog .isVisible()):
482                globalref.mainControl.filterConditionDialog.loadSavedNames()
483
484    def updateSaveEnable(self):
485        """Set the save rule button enabled based on save name entry.
486        """
487        self.saveButton.setEnabled(len(self.saveNameEdit.text()))
488
489    def updateFilterControls(self):
490        """Set filter button status based on active window changes.
491        """
492        window = globalref.mainControl.activeControl.activeWindow
493        if window.treeFilterView:
494            filterView = window.treeFilterView
495            conditional = filterView.conditionalFilter
496            self.setCondition(conditional, conditional.origNodeFormatName)
497            self.endFilterButton.setEnabled(True)
498        else:
499            self.endFilterButton.setEnabled(False)
500
501    def loadSavedRule(self):
502        """Load the current saved rule into the dialog.
503        """
504        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
505        savedRules = nodeFormats.savedConditions()
506        ruleName = self.saveListBox.currentItem().text()
507        conditional = savedRules[ruleName]
508        self.setCondition(conditional, conditional.origNodeFormatName)
509
510    def saveRule(self):
511        """Save the current rule settings.
512        """
513        name = self.saveNameEdit.text()
514        self.saveNameEdit.setText('')
515        treeStructure = globalref.mainControl.activeControl.structure
516        undo.FormatUndo(treeStructure.undoList, treeStructure.treeFormats,
517                        treeformats.TreeFormats())
518        typeName = self.typeCombo.currentText()
519        if typeName == _allTypeEntry:
520            nodeFormat = treeStructure.treeFormats
521        else:
522            nodeFormat = treeStructure.treeFormats[typeName]
523        nodeFormat.savedConditionText[name] = (self.conditional().
524                                               conditionStr())
525        self.loadSavedNames(True)
526        self.saveListBox.setCurrentItem(self.saveListBox.
527                                        findItems(name, Qt.MatchExactly)[0])
528        globalref.mainControl.activeControl.setModified()
529
530    def deleteRule(self):
531        """Remove the current saved rule.
532        """
533        treeStructure = globalref.mainControl.activeControl.structure
534        nodeFormats = treeStructure.treeFormats
535        undo.FormatUndo(treeStructure.undoList, nodeFormats,
536                        treeformats.TreeFormats())
537        savedRules = nodeFormats.savedConditions()
538        ruleName = self.saveListBox.currentItem().text()
539        conditional = savedRules[ruleName]
540        if conditional.origNodeFormatName:
541            typeFormat = nodeFormats[conditional.
542                                     origNodeFormatName]
543            del typeFormat.savedConditionText[ruleName]
544        else:
545            del nodeFormats.savedConditionText[ruleName]
546        self.loadSavedNames(True)
547        globalref.mainControl.activeControl.setModified()
548
549    def find(self, forward=True):
550        """Find another match in the indicated direction.
551
552        Arguments:
553            forward -- next if True, previous if False
554        """
555        self.resultLabel.setText('')
556        conditional = self.conditional()
557        control = globalref.mainControl.activeControl
558        if not control.findNodesByCondition(conditional, forward):
559            self.resultLabel.setText(_('No conditional matches were found'))
560
561    def findPrevious(self):
562        """Find the previous match.
563        """
564        self.find(False)
565
566    def  findNext(self):
567        """Find the next match.
568        """
569        self.find(True)
570
571    def startFilter(self):
572        """Start filtering nodes.
573        """
574        window = globalref.mainControl.activeControl.activeWindow
575        filterView = window.filterView()
576        filterView.conditionalFilter = self.conditional()
577        filterView.updateContents()
578        self.endFilterButton.setEnabled(True)
579
580    def endFilter(self):
581        """Stop filtering nodes.
582        """
583        window = globalref.mainControl.activeControl.activeWindow
584        window.removeFilterView()
585        self.endFilterButton.setEnabled(False)
586
587    def closeEvent(self, event):
588        """Signal that the dialog is closing.
589
590        Arguments:
591            event -- the close event
592        """
593        self.dialogShown.emit(False)
594
595
596class ConditionRule(QGroupBox):
597    """Group boxes for conditional rules in the ConditionDialog.
598    """
599    def __init__(self, num, fieldNames, parent=None):
600        """Create the conditional rule group box.
601
602        Arguments:
603            num -- the sequence number for the title
604            fieldNames -- a list of available field names
605            parent -- the parent dialog
606        """
607        super().__init__(parent)
608        self.fieldNames = fieldNames
609        self.setTitle(_('Rule {0}').format(num))
610        layout = QHBoxLayout(self)
611        self.fieldBox = QComboBox()
612        self.fieldBox.setEditable(False)
613        self.fieldBox.addItems(fieldNames)
614        layout.addWidget(self.fieldBox)
615
616        self.operBox = QComboBox()
617        self.operBox.setEditable(False)
618        self.operBox.addItems([_(op) for op in _operators])
619        layout.addWidget(self.operBox)
620        self.operBox.currentIndexChanged.connect(self.changeOper)
621
622        self.editor = QLineEdit()
623        layout.addWidget(self.editor)
624        self.fieldBox.setFocus()
625
626    def reloadFieldBox(self, fieldNames, currentField=''):
627        """Load the field combo box with a new field list.
628
629        Arguments:
630            fieldNames -- list of field names to add
631            currentField -- a field name to make current if given
632        """
633        self.fieldNames = fieldNames
634        self.fieldBox.clear()
635        self.fieldBox.addItems(fieldNames)
636        if currentField:
637            fieldNum = fieldNames.index(currentField)
638            self.fieldBox.setCurrentIndex(fieldNum)
639        self.changeOper()
640
641    def setCondition(self, conditionLine):
642        """Set values to match the given condition.
643
644        Arguments:
645            conditionLine -- the ConditionLine to match
646        """
647        fieldNum = self.fieldNames.index(conditionLine.fieldName)
648        self.fieldBox.setCurrentIndex(fieldNum)
649        operNum = _operators.index(conditionLine.oper)
650        self.operBox.setCurrentIndex(operNum)
651        self.editor.setText(conditionLine.value)
652
653    def conditionLine(self):
654        """Return a conditionLine for the current settings.
655        """
656        operTransDict = dict([(_(name), name) for name in _operators])
657        oper = operTransDict[self.operBox.currentText()]
658        return ConditionLine('and', self.fieldBox.currentText(), oper,
659                             self.editor.text())
660
661    def changeOper(self):
662        """Set the field available based on an operator change.
663        """
664        realOp = self.operBox.currentText() not in (_(op) for op in
665                                                       ('True', 'False'))
666        self.editor.setEnabled(realOp)
667        if (not realOp and
668            self.parent().typeCombo.currentText() == _allTypeEntry):
669            realOp = True
670        self.fieldBox.setEnabled(realOp)
671
672
673class SmallListWidget(QListWidget):
674    """ListWidget with a smaller size hint.
675    """
676    def __init__(self, parent=None):
677        """Initialize the widget.
678
679        Arguments:
680            parent -- the parent, if given
681        """
682        super().__init__(parent)
683
684    def sizeHint(self):
685        """Return smaller height.
686        """
687        if self.count():
688            rowHeight = self.sizeHintForRow(0)
689        else:
690            self.addItem('tmp')
691            rowHeight = self.sizeHintForRow(0)
692            self.takeItem(0)
693        newHeight = rowHeight * 3 + self.frameWidth() * 2
694        return QSize(super().sizeHint().width(), newHeight)
695