# -*- coding: utf-8 -*-
from __future__ import print_function, division
#import sip
#sip.setapi('QString', 2)
#sip.setapi('QVariant', 2)
from . qtCompat import Qt, Signal, QtCore, QtGui
import sys
import textwrap
import signal
from pint import DimensionalityError
from pint.unit import UndefinedUnitError
from datetime import datetime
import pylab
from io import BytesIO
import numpy as np
import itertools
# Local imports
from qcobj.qconfigobj import (QConfigObj, QValidator, ValidateError, Q_,
eng_string, extract, isStringLike, splitPolygons)
ROOTNAME = 'General'
BOOL_COLOR = QtGui.QColor('green')
QUANTITY_COLOR = QtGui.QColor('red')
STR_COLOR = QtGui.QColor('blue')
qvalidator = QValidator()
EXPAND_ALL = 'Expand All'
COLLAPSE_ALL = 'Collapse All'
BACKGND_COLOR = QtGui.QColor('white')
#------------------------------------------------------------------------------
[docs]def split_list(L, n, stringify=True):
""" Return a generator with the list `L` splitted in groups of `n`
elements.
If stringify evaluates as true, the groups of `n` elements
are joined and terminated by \n
"""
assert type(L) is list, "%s is not a list!" % L
for i in range(0, len(L), n):
if stringify:
yield " ".join(L[i: i + n]) + "\n"
else:
yield L[i: i + n]
#------------------------------------------------------------------------------
[docs]def noBlanks(withblanks, wordsPerLine=2):
""" Remove blanks and format with `wordsPerLine` words per line
"""
return "".join(list(split_list(withblanks, wordsPerLine)))
#------------------------------------------------------------------------------
[docs]def deBlank(section, key, wordsPerLine=2):
""" Remove blanks and format with `wordsPerLine` words per line
every value with the key == 'polygon'
"""
if key == 'polygon':
section[key] = noBlanks(section[key].split(), wordsPerLine)
#------------------------------------------------------------------------------
[docs]def createPolygons(pols, k):
data =[]
for pol in splitPolygons(pols):
if pol:
data.append(k * np.loadtxt(BytesIO(pol.encode())))
return data
#------------------------------------------------------------------------------
[docs]def colorize(s, color):
""" Return an HTML colorized string for `s`
"""
color = color.lower()
return "<font color=%s>%s</font>" % (color, s)
#------------------------------------------------------------------------------
[docs]def getPath(index):
""" Return section path at `index`
"""
# Build section path
path = []
parentIndex = index.parent()
while parentIndex.isValid():
secname = parentIndex.internalPointer().name()
if secname != ROOTNAME:
path.insert(0, secname)
parentIndex = parentIndex.parent()
return path
#------------------------------------------------------------------------------
[docs]def valueAtPath(cobj, path, name):
""" Return cobj value at `path` or raise RuntimeError
"""
if cobj is not None:
section = cobj
while path:
try:
section = section[path.pop(0)]
except KeyError:
return "__???__"
if name == ROOTNAME:
return section
else:
try:
return section[name]
except KeyError:
return "__???__"
#==============================================================================
[docs]class TreeItem(QtCore.QObject):
[docs] def __init__(self, name='', parent=None, data=None):
self.parentItem = parent
self.childItems = []
self._name = name
self._data = data
[docs] def name(self):
return self._name
[docs] def appendChild(self, item):
self.childItems.append(item)
[docs] def child(self, row):
return self.childItems[row]
[docs] def childCount(self):
return len(self.childItems)
[docs] def columnCount(self):
return 3
#return len(self.itemData)
[docs] def data(self):
return self._data
[docs] def parent(self):
return self.parentItem
[docs] def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
[docs] def setData(self, value, validRange, column):
""" Set node data with value converted to appropriate units as
stated in validRange and return it or return None
"""
qtype = validRange.partition('(')[0]
if column == 1:
if qtype.endswith('list'):
try:
tmp = eval(value)
except (NameError, TypeError) as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Validation Error!", str(e), QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
elif qtype == 'quantity':
# Quantity: get units
if isinstance(self._data, (tuple, list)):
units = self._data[0].units
else:
units = self._data.units
# Make value a list
values = [v.strip() for v in value.split(',')]
tmp = ["%s %s" % (v, units) for v in values]
# Validate all elements in list
try:
valid = qvalidator.check(validRange, tmp)
except (ValidateError, ValueError) as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Validation Error!", str(e), QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
except UndefinedUnitError as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Undefned Unit Error!", str(e),
QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
self._data = valid
return self._data
else:
# String, boolean, ...
tmp = value
try:
valid = qvalidator.check(validRange, tmp)
except (ValidateError, ValueError) as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Validation Error!", str(e), QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
else:
self._data = valid
return self._data
elif column == 2:
# Units
# Only quantities have values in column 2
if value:
if isinstance(self._data, (tuple, list)):
data = self._data
else:
data = (self._data, )
newdata = ()
for d in data:
try:
newq = d.to(value)
except DimensionalityError as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Dimensions disagree!", str(e),
QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
except UndefinedUnitError as e:
msgbox = QtGui.QMessageBox(QtGui.QMessageBox.Warning,
"Undefned Unit Error!", str(e),
QtGui.QMessageBox.Ok)
msgbox.exec_()
return None
else:
newdata += (newq, )
if len(newdata) == 1:
self._data = newdata[0]
else:
self._data = newdata
return self._data
return None
else:
return None
#==============================================================================
[docs]class TreeModel(QtCore.QAbstractItemModel):
[docs] def __init__(self, parent, qcobj=None):
super(TreeModel, self).__init__()
self._parent = parent
self._header = ["Item", "Value", "Units"]
self.rootItem = TreeItem(name='root', parent=None)
self._loaded = False
self.setupModelData(qcobj)
self._compareQcobj = None
[docs] def columnCount(self, parent):
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()
[docs] def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
name = item.name()
val = item.data()
refval = valueAtPath(self._compareQcobj, getPath(index), name)
if refval is None:
# Only one Qcobj!
refval = val
# Units
try:
if isinstance(val, tuple):
units = str(val[0].units)
else:
units = str(val.units)
except AttributeError:
units = None
# Reference Units
try:
if isinstance(val, tuple):
refunits = str(refval[0].units)
else:
refunits = str(refval.units)
except AttributeError:
refunits = None
#########
# Tooltip
#########
if role == Qt.ToolTipRole:
return self.qcobj.comment(getPath(index), name)
##################
# Foreground Color
##################
elif role == Qt.ForegroundRole:
try:
if isinstance(val, tuple):
val[0].magnitude
else:
val.magnitude
if index.column() == 1:
if val == refval:
return QUANTITY_COLOR
else:
return BACKGND_COLOR
elif index.column() == 2:
if units == refunits:
return QUANTITY_COLOR
else:
return BACKGND_COLOR
except AttributeError:
if isStringLike(val) and index.column() == 1:
if val == refval:
return STR_COLOR
else:
return BACKGND_COLOR
elif isStringLike(val) and index.column() == 2:
if units == refunits:
return STR_COLOR
else:
return BACKGND_COLOR
if (str(val) in "True False".split()
and index.column() in (1, 2)):
if val == refval:
return BOOL_COLOR
else:
return BACKGND_COLOR
return None
##################
# Background Color
##################
elif role == Qt.BackgroundRole:
try:
if isinstance(val, tuple):
val[0].magnitude
else:
val.magnitude
if index.column() == 1:
if val == refval:
return BACKGND_COLOR
else:
return QUANTITY_COLOR
elif index.column() == 2:
if units == refunits:
return BACKGND_COLOR
else:
return QUANTITY_COLOR
except AttributeError:
if isStringLike(val) and index.column() == 1:
if val == refval:
return BACKGND_COLOR
else:
return STR_COLOR
elif isStringLike(val) and index.column() == 2:
if units == refunits:
return BACKGND_COLOR
else:
return STR_COLOR
if (str(val) in "True False".split()
and index.column() == 1):
if val == refval:
return BACKGND_COLOR
else:
return BOOL_COLOR
elif (str(val) in "True False".split()
and index.column() == 2):
if units == refunits:
return BACKGND_COLOR
else:
return BOOL_COLOR
return None
return None
###########
# Display
###########
elif role in (Qt.DisplayRole, ):
if item.childCount() == 0:
if index.column() == 0:
return name
elif index.column() == 1:
# Value column
try:
if isinstance(val, tuple):
return ", ".join(
[str(eng_string(v.magnitude, doround=6))
for v in val])
else:
return str(eng_string(val.magnitude, doround=6))
except AttributeError:
return textwrap.dedent(str(val)).strip('\n')
elif index.column() == 2:
# Units column
return units
else:
if index.column() == 0:
return name.title()
#########
# Edit
#########
elif role in (Qt.EditRole, ):
if item.childCount() == 0:
if index.column() == 0:
return name
elif index.column() == 1:
# Value column
try:
if isinstance(val, tuple):
return ", ".join(
[str(eng_string(v.magnitude, doround=6))
for v in val])
else:
return str(eng_string(val.magnitude, doround=6))
except AttributeError:
return textwrap.dedent(str(val)).strip('\n')
elif index.column() == 2:
# Units column
return units
else:
if index.column() == 0:
return name.title()
[docs] def flags(self, index):
""" Must be implemented
"""
if not index.isValid():
return Qt.NoItemFlags
default = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 0:
return default
else:
return default | Qt.ItemIsEditable
[docs] def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
[docs] def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
[docs] def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
[docs] def setComparison(self, qcobj):
""" Set comparison qcobj for highlighting differences
"""
self._compareQcobj = qcobj
[docs] def setData(self, index, value, role):
if role == Qt.EditRole:
path = getPath(index)
key = index.internalPointer().name()
validRange = self.qcobj.validRange(path, key)
retval = index.internalPointer().setData(value, validRange,
index.column())
if retval is not None:
# Save value into qcobj
section = self.qcobj
for p in path:
section = section[p]
section[key] = retval
self.dataChanged.emit(index, index)
self._parent.setFileChanged(self.qcobj.filename)
return True
else:
return False
else:
print("Treemodel setData", args)
return False
[docs] def setupModelData(self, qcobj):
""" Populate model with data from QCconfigObj instance
"""
parent = self.rootItem
self.qcobj = qcobj
def fill(obj, parent):
if parent != self.rootItem:
for item in sorted(obj.scalars):
section = TreeItem(name=item, parent=parent,
data=obj[item])
parent.appendChild(section)
for item in sorted(obj.sections):
section = TreeItem(name=item, parent=parent, data=obj[item])
parent.appendChild(section)
if obj[item].sections is not None:
fill(obj[item], section)
if self._loaded:
# Make a new root item
self.rootItem = TreeItem(name='root', parent=None)
generalItem = TreeItem(name=ROOTNAME, parent=self.rootItem)
self.rootItem.appendChild(generalItem)
if qcobj:
for item in sorted(qcobj.scalars):
section = TreeItem(name=item, parent=generalItem,
data=qcobj[item])
generalItem.appendChild(section)
fill(qcobj, self.rootItem)
self._loaded = True
self.layoutChanged.emit()
#==============================================================================
[docs]class TreeView(QtGui.QTreeView):
[docs] def __init__(self, *args):
super(TreeView, self).__init__(*args)
self.collapsed.connect(self._resized)
self.expanded.connect(self._resized)
def _resized(self, *args):
self.resizeColumns()
[docs] def resizeColumns(self):
for column in range(self.model().columnCount(QtCore.QModelIndex())):
self.resizeColumnToContents(column)
#==============================================================================
[docs]class QuantityDialog(QtGui.QDialog):
[docs] def __init__(self, text, parent=None):
super(QuantityDialog, self).__init__(parent)
layout = QtGui.QVBoxLayout(self)
#text = "".join(["<p>%s</p>" % line for line in text])
#text = "<PRE>%s</PRE>" % text
self._text = QtGui.QTextEdit("", self)
#self._text.setAcceptRichText(True)
self._text.setHtml(text)
self._text.setFont(QtGui.QFont("Courier New", 11))
self._text.setReadOnly(True)
#self._bbox = QtGui.QDialogButtonBox()
#self._bbox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
#self._bbox.clicked.connect(self._onOk)
layout.addWidget(self._text)
#layout.addWidget(self._bbox)
self.resize(600, 400)
def _onOk(self, btn):
self.close()
#==============================================================================
[docs]class CfgGui(QtGui.QMainWindow):
[docs] def __init__(self, opts):
super(CfgGui, self).__init__()
# Avoid QtCore.QObject::startTimer: QTimer can only be used with
# threads...
QtGui.QFileSystemModel(self)
self._options = opts
self._filesThatChanged = []
if opts.configspec is None:
# Load configspec
self._configSpec = QtGui.QFileDialog.getOpenFileNames(self,
"Open configspec File", '.',
"configspec (*.cfg)",
options=QtGui.QFileDialog.DontUseNativeDialog)
else:
self._configSpec = opts.configspec
#self._toolbar = self.addToolBar("Toolbar")
openFile = QtGui.QAction("&Open...", self,
shortcut=QtGui.QKeySequence.Open,
statusTip="Open configuration file",
triggered=self.openFile)
saveFile = QtGui.QAction("&Save", self,
shortcut=QtGui.QKeySequence.Save,
statusTip="Save configuration to disk",
triggered=self.saveFile)
#plotAction = QtGui.QAction("Plot", self,
#shortcut=QtGui.QKeySequence.Save,
#statusTip="Plot all polygons",
#triggered=self.onPlot)
fileMenu = self.menuBar().addMenu('&File')
fileMenu.addAction(openFile)
fileMenu.addAction(saveFile)
#self.menuBar().addAction(plotAction)
self.setAttribute(Qt.WA_DeleteOnClose)
if self._options.cfg:
self._loadQCobjs(self._options.cfg)
[docs] def _loadQCobjs(self, pn):
""" Load all QConfigObj instances form file(s) in pn
Remove blanks in polygons and create the widgets for every
instance.
"""
if isinstance(pn, list):
qcobjs = [QConfigObj(p, configspec=self._configSpec,
strict=self._options.strict,
noextra=self._options.noextra) for p in pn]
else:
qcobjs = [QConfigObj(pn, configspec=self._configSpec,
strict=self._options.strict,
noextra=self._options.noextra), ]
# Remove blanks in polygons
for q in qcobjs:
q.walk(deBlank, call_on_sections=True, wordsPerLine=2)
ntrees = len(qcobjs)
self._trees = [TreeView(self) for i in range(ntrees)]
self.models = [TreeModel(self) for i in range(ntrees)]
allCfgLayout = QtGui.QHBoxLayout()
self._filesThatChanged = []
#self._allLithologies = []
i = 0
for tree, model, qcobj in zip(self._trees, self.models, qcobjs):
tree.setModel(model)
model.dataChanged.connect(tree.resizeColumns)
model.setupModelData(qcobj)
#self._allLithologies.append(qcobj['Lithologies'])
if i:
model.setComparison(qcobjs[i -1])
else:
pass
## Only for first cfg file
#definedlithos = self._allLithologies[0].keys()
#quantities = self._allLithologies[0][definedlithos[0]].keys()
#quantities.insert(0, RHEOLOGY)
# The Buttons
btnsWidget = QtGui.QWidget(self)
hbl = QtGui.QHBoxLayout()
#layerBtn = QtGui.QPushButton('Lithologies', btnsWidget)
#layerBtn.setEnabled(True)
#layerBtn.clicked.connect(self.showLitho)
#layerBtn.layer = LayerDialog(i, self)
expandBtn = QtGui.QPushButton(EXPAND_ALL, btnsWidget)
expandBtn.setEnabled(True)
expandBtn.clicked.connect(self.toggleExpand)
expandBtn.tree = tree
#quantCombo = QtGui.QComboBox(self)
#quantCombo.setEnabled(True)
#quantCombo.addItems(quantities)
#quantCombo.currentIndexChanged.connect(self._quantityChanged)
#quantCombo.setCurrentIndex(-1)
#hbl.addWidget(layerBtn)
hbl.addWidget(expandBtn)
#hbl.addWidget(quantCombo)
btnsWidget.setLayout(hbl)
singleCfgWidget = QtGui.QWidget(self)
singleCfgLayout = QtGui.QVBoxLayout()
singleCfgLayout.addWidget(btnsWidget)
singleCfgLayout.addWidget(tree)
singleCfgWidget.setLayout(singleCfgLayout)
tree.resizeColumns()
allCfgLayout.addWidget(singleCfgWidget)
i += 1
# Add a row for widgets shared between allCfgLayout
allCfgWidget = QtGui.QWidget()
allCfgWidget.setLayout(allCfgLayout)
# Set Central widget
cWidget = QtGui.QWidget()
vlo = QtGui.QVBoxLayout()
if len(qcobjs) > 1:
self._scrollLock = QtGui.QCheckBox("Lock scrollbars")
self._scrollLock.stateChanged.connect(self._onScroll)
vlo.addWidget(self._scrollLock)
vlo.addWidget(allCfgWidget)
cWidget.setLayout(vlo)
self.setCentralWidget(cWidget)
self.setWindowTitle('%s: %s' % (pn,
[model.qcobj['description'] for model in self.models]))
def _onScroll(self, value):
if value:
# Connect all scrollbars together
for t1, t2 in itertools.permutations(self._trees, 2):
t1.verticalScrollBar().valueChanged.connect(
t2.verticalScrollBar().setValue)
else:
# Connect all scrollbars together
for t1, t2 in itertools.permutations(self._trees, 2):
t1.verticalScrollBar().valueChanged.disconnect(
t2.verticalScrollBar().setValue)
def _quantityChanged(self, index):
if index < 0:
return
sender = self.sender()
quant = sender.itemText(index)
html = self.makeHtml(quant)
q = QuantityDialog(html, self)
q.exec_()
[docs] def setFileChanged(self, filename):
if filename not in self._filesThatChanged:
self._filesThatChanged.append(filename)
[docs] def closeEvent(self, event):
# Changes?
if self._filesThatChanged:
self.saveFile()
event.accept() # let the window close
#def makeHtml(self, quant):
#""" Return HTML table for quantity `quant`
#"""
#numLithologies = len(self._allLithologies)
## Create HTML output
#html = ('<table border="1" width="100%%">')
#if quant != RHEOLOGY:
#html += '<tr>'
#html += '<th bgcolor="LightGreen">Config file</th>'
#for model in self.models:
#html += ('<th bgcolor="LightGreen">%s</th>' %
#os.path.basename(model.qcobj.filename))
#html += '</tr>' # end row
#msglines = []
#values = []
#for i, lithosInCfg in enumerate(self._allLithologies):
#if quant == RHEOLOGY:
## Add file name to html
#html += (
#'<tr>'
#'<td bgcolor="LightGreen"> </td>'
#'<td colspan="5" align="center" bgcolor="LightGreen">'
#'%s</td>'
#'</tr>' %
#os.path.basename(self.models[i].qcobj.filename))
#html += (
#'<tr>'
#'<th bgcolor="GreenYellow">Given name</th>'
#'<th bgcolor="IndianRed">Standard name</th>'
#'<th bgcolor="DarkSalmon">AD</th>'
#'<th bgcolor="DarkSalmon">Ea</th>'
#'<th bgcolor="DarkSalmon">n</th>'
#'<th bgcolor="DarkSalmon">Va</th>'
#'</tr>')
#for j, litho in enumerate(lithosInCfg):
#lit = lithosInCfg[litho]
#creep = Creep(
#lit['AD'], lit['Ea'], lit['n'], lit['Va'])
#for knownCreep in KNOWN_CREEPS:
#if knownCreep[0] == creep:
#html += (
#'<tr>'
#'<th bgcolor="White">%s</th>'
#'<th bgcolor="White">%s</th>'
#'<th bgcolor="White">%s</th>'
#'<th bgcolor="White">%s</th>'
#'<th bgcolor="White">%s</th>'
#'<th bgcolor="White">%s</th>'
#'</tr>' %
#(litho, knownCreep[1], creep.strAD(),
#creep.strEa(), creep.strn(),
#creep.strVa())
#)
#break
#else:
#for j, litho in enumerate(lithosInCfg):
#value = lithosInCfg[litho][quant]
## Check if value is a quantity
#if hasattr(value, 'magnitude'):
#value = '{:~P}'.format(value)
#if i > 0:
## More than one config file:
## search for line starting with the same litho
#found = False
#for k, line in enumerate(msglines):
#if line.startswith(litho):
## k is our index
#found = True
#break
#if found:
#if values[k].endswith(value):
## Same value use soft color
#msglines[k] = ("%s:%s" %
#(msglines[k],
#colorize(value, 'LightBlue')))
#else:
## Different value use strong color
#msglines[k] = ("%s:%s" %
#(msglines[j], colorize(value, 'Red')))
#values[k] = "%s %s" % (values[k], value)
#else:
## Add a line with the new litho
#msglines.append("%s::%s" % (litho, value))
#else:
#values.append(value)
#msglines.append("%s:%s" % (litho, value))
#for line in msglines:
## Split line at ":" and put result in cells
#html += ("<tr>" + "".join(
#["<th>%s</th>" % f for f in line.split(":")]) + "</tr>")
#html += "</table>"
#return html
[docs] def openFile(self):
pn = QtGui.QFileDialog.getOpenFileNames(self,
"Open Configuration File", '.',
"Configuration (*.cfg)",
options=QtGui.QFileDialog.DontUseNativeDialog)
if pn:
self._loadQCobjs(pn)
#def onPlot(self):
#width = 0
#depth = 0
#for model in self.models:
#width = max(width, model.qcobj['Mesh']['X']['width'].magnitude)
#depth = max(depth, model.qcobj['Mesh']['Y']['depth'].magnitude)
#plotPolygons(self._allLithologies, width, depth)
[docs] def saveFile(self):
for pn in self._filesThatChanged:
extensions = "CFG (*.cfg)"
from IPython import embed; embed()
dlg = QtGui.QFileDialog(self, "Save configuration", pn)
dlg.setNameFilter(extensions)
dlg.setOptions(QtGui.QFileDialog.DontUseNativeDialog)
dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave)
if dlg.exec_():
newpn = dlg.selectedFiles()[0]
if newpn:
# Search for the model with pn filename
for model in self.models:
if model.qcobj.filename == pn:
break
cfg = model.qcobj.write_to_string()
now = datetime.now().strftime("%Y%m%d% at %H:%M:%S")
with open(newpn, "w") as theFile:
theFile.write("# Created by %s at %s\n\n"
% (__file__, now))
theFile.write(cfg.decode('utf-8'))
theFile.close()
#def showLitho(self, *args):
#senderBtn = self.sender()
#senderBtn.layer.setModal(False)
#senderBtn.layer.show()
[docs] def toggleExpand(self, *args):
senderBtn = self.sender()
if senderBtn.text() == EXPAND_ALL:
senderBtn.tree.expandAll()
senderBtn.setText(COLLAPSE_ALL)
else:
senderBtn.tree.collapseAll()
senderBtn.setText(EXPAND_ALL)