File: conditional.py

package info (click to toggle)
treeline 3.1.5-1.1
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 6,508 kB
  • sloc: python: 20,489; javascript: 998; makefile: 54
file content (694 lines) | stat: -rw-r--r-- 27,130 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
#!/usr/bin/env python3

#******************************************************************************
# conditional.py, provides a class to store field comparison functions
#
# TreeLine, an information storage program
# Copyright (C) 2017, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, either Version 2 or any later
# version.  This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
#******************************************************************************

import re
import enum
from PyQt5.QtCore import QSize, Qt, pyqtSignal
from PyQt5.QtWidgets import (QComboBox, QDialog, QGroupBox, QHBoxLayout,
                             QLabel, QLineEdit, QListWidget, QPushButton,
                             QSizePolicy, QVBoxLayout)
import treeformats
import configdialog
import undo
import globalref

_operators = ['==', '<', '<=', '>', '>=', '!=', N_('starts with'),
              N_('ends with'), N_('contains'), N_('True'), N_('False')]
_functions = {'==': '__eq__', '<': '__lt__', '<=': '__le__',
              '>': '__gt__', '>=': '__ge__', '!=': '__ne__',
              'starts with': 'startswith', 'ends with': 'endswith',
              'contains': 'contains', 'True': 'true', 'False': 'false'}
_boolOper = [N_('and'), N_('or')]
_allTypeEntry = _('[All Types]')
_parseRe = re.compile(r'((?:and)|(?:or)) (\S+) (.+?) '
                      r'(?:(?<!\\)"|(?<=\\\\)")(.*?)(?:(?<!\\)"|(?<=\\\\)")')


class Conditional:
    """Stores and evaluates a conditional comparison for field data.
    """
    def __init__(self, conditionStr='', nodeFormatName=''):
        """Initialize the condition object.

        Accepts a string in the following format:
        'fieldname == "value" and otherFieldName > "othervalue"'
        Arguments:
            conditionStr -- the condition string to set
            nodeFormatName -- if name is set, restricts matches to type family
        """
        self.conditionLines = []
        conditionStr = 'and ' + conditionStr
        for boolOper, fieldName, oper, value in _parseRe.findall(conditionStr):
            value = value.replace('\\"', '"').replace('\\\\', '\\')
            self.conditionLines.append(ConditionLine(boolOper, fieldName,
                                                     oper, value))
        self.origNodeFormatName = nodeFormatName
        self.nodeFormatNames = set()
        if nodeFormatName:
            self.nodeFormatNames.add(nodeFormatName)
            nodeFormats = (globalref.mainControl.activeControl.structure.
                           treeFormats)
            for nodeType in nodeFormats[nodeFormatName].derivedTypes:
                self.nodeFormatNames.add(nodeType.name)

    def evaluate(self, node):
        """Evaluate this condition and return True or False.

        Arguments:
            node -- the node to check for a field match
        """
        if (self.nodeFormatNames and
            node.formatRef.name not in self.nodeFormatNames):
            return False
        result = True
        for conditon in self.conditionLines:
            result = conditon.evaluate(node, result)
        return result

    def conditionStr(self):
        """Return the condition string for this condition set.
        """
        return ' '.join([cond.conditionStr() for cond in
                         self.conditionLines])[4:]

    def renameFields(self, oldName, newName):
        """Rename the any fields found in condition lines.

        Arguments:
            oldName -- the previous field name
            newName -- the updated field name
        """
        for condition in self.conditionLines:
            if condition.fieldName == oldName:
                condition.fieldName = newName

    def removeField(self, fieldname):
        """Remove conditional lines referencing the given field.

        Arguments:
            fieldname -- the field name to be removed
        """
        for condition in self.conditionLines[:]:
            if condition.fieldName == fieldname:
                self.conditionLines.remove(condition)

    def __len__(self):
        """Return the number of conditions for truth testing.
        """
        return len(self.conditionLines)


class ConditionLine:
    """Stores & evaluates a portion of a conditional comparison.
    """
    def __init__(self, boolOper, fieldName, oper, value):
        """Initialize the condition line.

        Arguments:
            boolOper -- a string for combining previous lines ('and' or 'or')
            fieldName -- the field name to evaluate
            oper -- the operator string
            value -- the string for comparison
        """
        self.boolOper = boolOper
        self.fieldName = fieldName
        self.oper = oper
        self.value = value

    def evaluate(self, node, prevResult=True):
        """Evaluate this line and return True or False.

        Arguments:
            node -- the node to check for a field match
            prevResult -- the result to combine with the boolOper
        """
        try:
            field = node.formatRef.fieldDict[self.fieldName]
        except KeyError:
            if self.boolOper == 'and':
                return False
            return prevResult
        dataStr = field.compareValue(node)
        value = field.adjustedCompareValue(self.value)
        try:
            func = getattr(dataStr, _functions[self.oper])
        except AttributeError:
            dataStr = StringOps(dataStr)
            func = getattr(dataStr, _functions[self.oper])
            value = str(value)
        if self.boolOper == 'and':
            return prevResult and func(value)
        else:
            return prevResult or func(value)

    def conditionStr(self):
        """Return the text line for this condition.
        """
        value = self.value.replace('\\', '\\\\').replace('"', '\\"')
        return '{0} {1} {2} "{3}"'.format(self.boolOper, self.fieldName,
                                          self.oper, value)


class StringOps(str):
    """A string class with extra comparison functions.
    """
    def __new__(cls, initStr=''):
        """Return the str object.

        Arguments:
            initStr -- the initial string value
        """
        return str.__new__(cls, initStr)

    def contains(self, substr):
        """Return True if self contains substr.

        Arguments:
            substr -- the substring to check
        """
        return self.find(substr) != -1

    def true(self, other=''):
        """Always return True.

        Arguments:
            other -- unused placeholder
        """
        return True

    def false(self, other=''):
        """Always return False.

        Arguments:
            other -- unused placeholder
        """
        return False


FindDialogType = enum.Enum('FindDialogType',
                           'typeDialog findDialog filterDialog')

class ConditionDialog(QDialog):
    """Dialog for defining field condition tests.

    Used for defining conditional types (modal), for finding by condition
    (nonmodal) and for filtering by condition (nonmodal).
    """
    dialogShown = pyqtSignal(bool)
    def __init__(self, dialogType, caption, nodeFormat=None, parent=None):
        """Create the conditional dialog.

        Arguments:
            dialogType -- either typeDialog, findDialog or filterDialog
            caption -- the window title for this dialog
            nodeFormat -- the current node format for the typeDialog
            parent -- the parent overall dialog
        """
        super().__init__(parent)
        self.setWindowTitle(caption)
        self.dialogType = dialogType
        self.ruleList = []
        self.combiningBoxes = []
        self.typeCombo = None
        self.resultLabel = None
        self.endFilterButton = None
        self.fieldNames = []
        if nodeFormat:
            self.fieldNames = nodeFormat.fieldNames()
        topLayout = QVBoxLayout(self)

        if dialogType == FindDialogType.typeDialog:
            self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
                                Qt.WindowCloseButtonHint)
        else:
            self.setAttribute(Qt.WA_QuitOnClose, False)
            self.setWindowFlags(Qt.Window |
                                Qt.WindowStaysOnTopHint)
            typeBox = QGroupBox(_('Node Type'))
            topLayout.addWidget(typeBox)
            typeLayout = QVBoxLayout(typeBox)
            self.typeCombo = QComboBox()
            typeLayout.addWidget(self.typeCombo)
            self.typeCombo.currentIndexChanged.connect(self.updateDataType)

        self.mainLayout = QVBoxLayout()
        topLayout.addLayout(self.mainLayout)

        upCtrlLayout = QHBoxLayout()
        topLayout.addLayout(upCtrlLayout)
        addButton = QPushButton(_('&Add New Rule'))
        upCtrlLayout.addWidget(addButton)
        addButton.clicked.connect(self.addNewRule)
        self.removeButton = QPushButton(_('&Remove Rule'))
        upCtrlLayout.addWidget(self.removeButton)
        self.removeButton.clicked.connect(self.removeRule)
        upCtrlLayout.addStretch()

        if dialogType == FindDialogType.typeDialog:
            okButton = QPushButton(_('&OK'))
            upCtrlLayout.addWidget(okButton)
            okButton.clicked.connect(self.accept)
            cancelButton = QPushButton(_('&Cancel'))
            upCtrlLayout.addWidget(cancelButton)
            cancelButton.clicked.connect(self.reject)
        else:
            self.removeButton.setEnabled(False)
            saveBox = QGroupBox(_('Saved Rules'))
            topLayout.addWidget(saveBox)
            saveLayout = QVBoxLayout(saveBox)
            self.saveListBox = SmallListWidget()
            saveLayout.addWidget(self.saveListBox)
            self.saveListBox.itemDoubleClicked.connect(self.loadSavedRule)
            nameLayout = QHBoxLayout()
            saveLayout.addLayout(nameLayout)
            label = QLabel(_('Name:'))
            nameLayout.addWidget(label)
            self.saveNameEdit = QLineEdit()
            nameLayout.addWidget(self.saveNameEdit)
            self.saveNameEdit.textChanged.connect(self.updateSaveEnable)
            saveButtonLayout = QHBoxLayout()
            saveLayout.addLayout(saveButtonLayout)
            self.loadSavedButton = QPushButton(_('&Load'))
            saveButtonLayout.addWidget(self.loadSavedButton)
            self.loadSavedButton.clicked.connect(self.loadSavedRule)
            self.saveButton = QPushButton(_('&Save'))
            saveButtonLayout.addWidget(self.saveButton)
            self.saveButton.clicked.connect(self.saveRule)
            self.saveButton.setEnabled(False)
            self.delSavedButton = QPushButton(_('&Delete'))
            saveButtonLayout.addWidget(self.delSavedButton)
            self.delSavedButton.clicked.connect(self.deleteRule)
            saveButtonLayout.addStretch()

            if dialogType == FindDialogType.findDialog:
                self.resultLabel = QLabel()
                topLayout.addWidget(self.resultLabel)
            lowCtrlLayout = QHBoxLayout()
            topLayout.addLayout(lowCtrlLayout)
            if dialogType == FindDialogType.findDialog:
                previousButton = QPushButton(_('Find &Previous'))
                lowCtrlLayout.addWidget(previousButton)
                previousButton.clicked.connect(self.findPrevious)
                nextButton = QPushButton(_('Find &Next'))
                nextButton.setDefault(True)
                lowCtrlLayout.addWidget(nextButton)
                nextButton.clicked.connect(self.findNext)
            else:
                filterButton = QPushButton(_('&Filter'))
                lowCtrlLayout.addWidget(filterButton)
                filterButton.clicked.connect(self.startFilter)
                self.endFilterButton = QPushButton(_('&End Filter'))
                lowCtrlLayout.addWidget(self.endFilterButton)
                self.endFilterButton.setEnabled(False)
                self.endFilterButton.clicked.connect(self.endFilter)
            lowCtrlLayout.addStretch()
            closeButton = QPushButton(_('&Close'))
            lowCtrlLayout.addWidget(closeButton)
            closeButton.clicked.connect(self.close)
            origTypeName = nodeFormat.name if nodeFormat else ''
            self.loadTypeNames(origTypeName)
            self.loadSavedNames()
        self.ruleList.append(ConditionRule(1, self.fieldNames))
        self.mainLayout.addWidget(self.ruleList[0])

    def addNewRule(self, checked=False, combineBool='and'):
        """Add a new empty rule to the dialog.

        Arguments:
            checked -- unused placekeeper variable for signal
            combineBool -- the boolean op for combining with the previous rule
        """
        if self.ruleList:
            boolBox = QComboBox()
            boolBox.setEditable(False)
            self.combiningBoxes.append(boolBox)
            boolBox.addItems([_(op) for op in _boolOper])
            if combineBool != 'and':
                boolBox.setCurrentIndex(1)
            self.mainLayout.insertWidget(len(self.ruleList) * 2 - 1, boolBox,
                                        0, Qt.AlignHCenter)
        rule = ConditionRule(len(self.ruleList) + 1, self.fieldNames)
        self.ruleList.append(rule)
        self.mainLayout.insertWidget(len(self.ruleList) * 2 - 2, rule)
        self.removeButton.setEnabled(True)

    def removeRule(self):
        """Remove the last rule from the dialog.
        """
        if self.ruleList:
            if self.combiningBoxes:
                self.combiningBoxes[-1].hide()
                del self.combiningBoxes[-1]
            self.ruleList[-1].hide()
            del self.ruleList[-1]
            if self.dialogType == FindDialogType.typeDialog:
                self.removeButton.setEnabled(len(self.ruleList) > 0)
            else:
                self.removeButton.setEnabled(len(self.ruleList) > 1)

    def clearRules(self):
        """Remove all rules from the dialog and add default rule.
        """
        for box in self.combiningBoxes:
            box.hide()
        for rule in self.ruleList:
            rule.hide()
        self.combiningBoxes = []
        self.ruleList = [ConditionRule(1, self.fieldNames)]
        self.mainLayout.insertWidget(0, self.ruleList[0])
        self.removeButton.setEnabled(True)

    def setCondition(self, conditional, typeName=''):
        """Set rule values to match the given conditional.

        Arguments:
            conditional -- the Conditional class to match
            typeName -- an optional type name used with some dialog types
        """
        if self.typeCombo:
            if typeName:
                self.typeCombo.setCurrentIndex(self.typeCombo.
                                               findText(typeName))
            else:
                self.typeCombo.setCurrentIndex(0)
        while len(self.ruleList) > 1:
            self.removeRule()
        if conditional:
            self.ruleList[0].setCondition(conditional.conditionLines[0])
        for conditionLine in conditional.conditionLines[1:]:
            self.addNewRule(combineBool=conditionLine.boolOper)
            self.ruleList[-1].setCondition(conditionLine)

    def conditional(self):
        """Return a Conditional instance for the current settings.
        """
        combineBools = [0] + [boolBox.currentIndex() for boolBox in
                              self.combiningBoxes]
        typeName = self.typeCombo.currentText() if self.typeCombo else ''
        if typeName == _allTypeEntry:
            typeName = ''
        conditional = Conditional('', typeName)
        for boolIndex, rule in zip(combineBools, self.ruleList):
            condition = rule.conditionLine()
            if boolIndex != 0:
                condition.boolOper = 'or'
            conditional.conditionLines.append(condition)
        return conditional

    def loadTypeNames(self, origTypeName=''):
        """Load format type names into combo box.

        Arguments:
            origTypeName -- a starting type name if given
        """
        if not origTypeName:
            origTypeName = self.typeCombo.currentText()
        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
        self.typeCombo.blockSignals(True)
        self.typeCombo.clear()
        self.typeCombo.addItem(_allTypeEntry)
        typeNames = nodeFormats.typeNames()
        self.typeCombo.addItems(typeNames)
        if origTypeName and origTypeName != _allTypeEntry:
            try:
                self.typeCombo.setCurrentIndex(typeNames.index(origTypeName)
                                               + 1)
            except ValueError:
                if self.endFilterButton and self.endFilterButton.isEnabled():
                    self.endFilter()
                self.clearRules()
        self.typeCombo.blockSignals(False)
        self.updateDataType()

    def updateDataType(self):
        """Update the node format based on a data type change.
        """
        typeName = self.typeCombo.currentText()
        if not typeName:
            return
        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
        if typeName == _allTypeEntry:
            fieldNameSet = set()
            for typeFormat in nodeFormats.values():
                fieldNameSet.update(typeFormat.fieldNames())
            self.fieldNames = sorted(list(fieldNameSet))
        else:
            self.fieldNames = nodeFormats[typeName].fieldNames()
        for rule in self.ruleList:
            currentField = rule.conditionLine().fieldName
            if currentField not in self.fieldNames:
                if self.endFilterButton and self.endFilterButton.isEnabled():
                    self.endFilter()
                self.clearRules()
                break
            rule.reloadFieldBox(self.fieldNames, currentField)

    def loadSavedNames(self, updateOtherDialog=False):
        """Refresh the list of saved rule names.
        """
        selNum = 0
        if self.saveListBox.count():
            selNum = self.saveListBox.currentRow()
        self.saveListBox.clear()
        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
        savedRules = nodeFormats.savedConditions()
        ruleNames = sorted(list(savedRules.keys()))
        if ruleNames:
            self.saveListBox.addItems(ruleNames)
            if selNum >= len(ruleNames):
                selNum = len(ruleNames) - 1
            self.saveListBox.setCurrentRow(selNum)
        self.loadSavedButton.setEnabled(len(ruleNames) > 0)
        self.delSavedButton.setEnabled(len(ruleNames) > 0)
        if updateOtherDialog:
            if (self != globalref.mainControl.findConditionDialog and
                globalref.mainControl.findConditionDialog and
                globalref.mainControl.findConditionDialog.isVisible()):
                globalref.mainControl.findConditionDialog.loadSavedNames()
            elif (self != globalref.mainControl.filterConditionDialog and
                  globalref.mainControl.filterConditionDialog and
                  globalref.mainControl.filterConditionDialog .isVisible()):
                globalref.mainControl.filterConditionDialog.loadSavedNames()

    def updateSaveEnable(self):
        """Set the save rule button enabled based on save name entry.
        """
        self.saveButton.setEnabled(len(self.saveNameEdit.text()))

    def updateFilterControls(self):
        """Set filter button status based on active window changes.
        """
        window = globalref.mainControl.activeControl.activeWindow
        if window.treeFilterView:
            filterView = window.treeFilterView
            conditional = filterView.conditionalFilter
            self.setCondition(conditional, conditional.origNodeFormatName)
            self.endFilterButton.setEnabled(True)
        else:
            self.endFilterButton.setEnabled(False)

    def loadSavedRule(self):
        """Load the current saved rule into the dialog.
        """
        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
        savedRules = nodeFormats.savedConditions()
        ruleName = self.saveListBox.currentItem().text()
        conditional = savedRules[ruleName]
        self.setCondition(conditional, conditional.origNodeFormatName)

    def saveRule(self):
        """Save the current rule settings.
        """
        name = self.saveNameEdit.text()
        self.saveNameEdit.setText('')
        treeStructure = globalref.mainControl.activeControl.structure
        undo.FormatUndo(treeStructure.undoList, treeStructure.treeFormats,
                        treeformats.TreeFormats())
        typeName = self.typeCombo.currentText()
        if typeName == _allTypeEntry:
            nodeFormat = treeStructure.treeFormats
        else:
            nodeFormat = treeStructure.treeFormats[typeName]
        nodeFormat.savedConditionText[name] = (self.conditional().
                                               conditionStr())
        self.loadSavedNames(True)
        self.saveListBox.setCurrentItem(self.saveListBox.
                                        findItems(name, Qt.MatchExactly)[0])
        globalref.mainControl.activeControl.setModified()

    def deleteRule(self):
        """Remove the current saved rule.
        """
        treeStructure = globalref.mainControl.activeControl.structure
        nodeFormats = treeStructure.treeFormats
        undo.FormatUndo(treeStructure.undoList, nodeFormats,
                        treeformats.TreeFormats())
        savedRules = nodeFormats.savedConditions()
        ruleName = self.saveListBox.currentItem().text()
        conditional = savedRules[ruleName]
        if conditional.origNodeFormatName:
            typeFormat = nodeFormats[conditional.
                                     origNodeFormatName]
            del typeFormat.savedConditionText[ruleName]
        else:
            del nodeFormats.savedConditionText[ruleName]
        self.loadSavedNames(True)
        globalref.mainControl.activeControl.setModified()

    def find(self, forward=True):
        """Find another match in the indicated direction.

        Arguments:
            forward -- next if True, previous if False
        """
        self.resultLabel.setText('')
        conditional = self.conditional()
        control = globalref.mainControl.activeControl
        if not control.findNodesByCondition(conditional, forward):
            self.resultLabel.setText(_('No conditional matches were found'))

    def findPrevious(self):
        """Find the previous match.
        """
        self.find(False)

    def  findNext(self):
        """Find the next match.
        """
        self.find(True)

    def startFilter(self):
        """Start filtering nodes.
        """
        window = globalref.mainControl.activeControl.activeWindow
        filterView = window.filterView()
        filterView.conditionalFilter = self.conditional()
        filterView.updateContents()
        self.endFilterButton.setEnabled(True)

    def endFilter(self):
        """Stop filtering nodes.
        """
        window = globalref.mainControl.activeControl.activeWindow
        window.removeFilterView()
        self.endFilterButton.setEnabled(False)

    def closeEvent(self, event):
        """Signal that the dialog is closing.

        Arguments:
            event -- the close event
        """
        self.dialogShown.emit(False)


class ConditionRule(QGroupBox):
    """Group boxes for conditional rules in the ConditionDialog.
    """
    def __init__(self, num, fieldNames, parent=None):
        """Create the conditional rule group box.

        Arguments:
            num -- the sequence number for the title
            fieldNames -- a list of available field names
            parent -- the parent dialog
        """
        super().__init__(parent)
        self.fieldNames = fieldNames
        self.setTitle(_('Rule {0}').format(num))
        layout = QHBoxLayout(self)
        self.fieldBox = QComboBox()
        self.fieldBox.setEditable(False)
        self.fieldBox.addItems(fieldNames)
        layout.addWidget(self.fieldBox)

        self.operBox = QComboBox()
        self.operBox.setEditable(False)
        self.operBox.addItems([_(op) for op in _operators])
        layout.addWidget(self.operBox)
        self.operBox.currentIndexChanged.connect(self.changeOper)

        self.editor = QLineEdit()
        layout.addWidget(self.editor)
        self.fieldBox.setFocus()

    def reloadFieldBox(self, fieldNames, currentField=''):
        """Load the field combo box with a new field list.

        Arguments:
            fieldNames -- list of field names to add
            currentField -- a field name to make current if given
        """
        self.fieldNames = fieldNames
        self.fieldBox.clear()
        self.fieldBox.addItems(fieldNames)
        if currentField:
            fieldNum = fieldNames.index(currentField)
            self.fieldBox.setCurrentIndex(fieldNum)
        self.changeOper()

    def setCondition(self, conditionLine):
        """Set values to match the given condition.

        Arguments:
            conditionLine -- the ConditionLine to match
        """
        fieldNum = self.fieldNames.index(conditionLine.fieldName)
        self.fieldBox.setCurrentIndex(fieldNum)
        operNum = _operators.index(conditionLine.oper)
        self.operBox.setCurrentIndex(operNum)
        self.editor.setText(conditionLine.value)

    def conditionLine(self):
        """Return a conditionLine for the current settings.
        """
        operTransDict = dict([(_(name), name) for name in _operators])
        oper = operTransDict[self.operBox.currentText()]
        return ConditionLine('and', self.fieldBox.currentText(), oper,
                             self.editor.text())

    def changeOper(self):
        """Set the field available based on an operator change.
        """
        realOp = self.operBox.currentText() not in (_(op) for op in
                                                       ('True', 'False'))
        self.editor.setEnabled(realOp)
        if (not realOp and
            self.parent().typeCombo.currentText() == _allTypeEntry):
            realOp = True
        self.fieldBox.setEnabled(realOp)


class SmallListWidget(QListWidget):
    """ListWidget with a smaller size hint.
    """
    def __init__(self, parent=None):
        """Initialize the widget.

        Arguments:
            parent -- the parent, if given
        """
        super().__init__(parent)

    def sizeHint(self):
        """Return smaller height.
        """
        if self.count():
            rowHeight = self.sizeHintForRow(0)
        else:
            self.addItem('tmp')
            rowHeight = self.sizeHintForRow(0)
            self.takeItem(0)
        newHeight = rowHeight * 3 + self.frameWidth() * 2
        return QSize(super().sizeHint().width(), newHeight)