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