#
# overlayinfopanel.py - The OverlayInfoPanel class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the ``OverlayInfoPanel`` class, a *FSLeyes control*
panel which displays information about the currently selected overlay.
"""
import logging
import collections
import wx
import numpy as np
import fsl.utils.bids as fslbids
import fsl.data.image as fslimage
import fsl.data.constants as constants
import fsl.transform.affine as affine
import fsleyes_widgets.utils.typedict as td
import fsleyes.controls.controlpanel as ctrlpanel
import fsleyes.strings as strings
USE_HTML2 = False
"""Toggle this flag to switch between the simple wx.html renderer,
and the webkit-backed wx.html2 renderer. Webkit is not necessarily
present on all systems, and there's no neat way to dynamically
test whether wx.html2 will work. So I'm sticking with wx.html for
now.
"""
if USE_HTML2: import wx.html2 as wxhtml
else: import wx.html as wxhtml
log = logging.getLogger(__name__)
# The wx.html2.WebView.SetPage method differs from
# the wx.html.HtmlWindow.SetPage method - it requires
# two parameters. Here we're sub-classing the
# HtmlWindow method so that it also accepts two
# parameters, but ignores the second.
#
# The sub-class also forces links to be opened
# in the system browser, as opposed to in the
# same window.
if not USE_HTML2:
class HtmlWindow(wxhtml.HtmlWindow):
def __init__(self, *args, **kwargs):
wxhtml.HtmlWindow.__init__(self, *args, **kwargs)
self.Bind(wxhtml.EVT_HTML_LINK_CLICKED, self.__onLink)
# wx.html.HtmlWindow defaults
# to a slightly bigger font size
self.SetStandardFonts(self.GetFont().GetPointSize())
def __onLink(self, ev):
wx.LaunchDefaultBrowser(ev.GetLinkInfo().GetHref())
def SetPage(self, html, url=None):
wxhtml.HtmlWindow.SetPage(self, html)
[docs]class OverlayInfoPanel(ctrlpanel.ControlPanel):
"""An ``OverlayInfoPanel`` is a :class:`.ControlPanel` which displays
information about the currently selected overlay in a
``wx.html.HtmlWindow``. The currently selected overlay is defined by the
:attr:`.DisplayContext.selectedOverlay` property. An ``OverlayInfoPanel``
looks something like the following:
.. image:: images/overlayinfopanel.png
:scale: 50%
:align: center
Slightly different information is shown depending on the overlay type,
and is generated by the following methods:
======================== =============================
:class:`.Image` :meth:`__getImageInfo`
:class:`.FEATImage` :meth:`__getFEATImageInfo`
:class:`.MelodicImage` :meth:`__getMelodicImageInfo`
:class:`.DTIFitTensor` :meth:`__getDTIFitTensorInfo`
:class:`.Mesh` :meth:`__getMeshInfo`
:class:`.VTKMesh` :meth:`__getVTKMeshInfo`
:class:`.GiftiMesh` :meth:`__getGiftiMeshInfo`
:class:`.FreesurferMesh` :meth:`__getFreesurferMeshInfo`
======================== =============================
"""
[docs] def __init__(self, parent, overlayList, displayCtx, frame):
"""Create an ``OverlayInfoPanel``.
:arg parent: The :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg frame: The :class:`.FSLeyesFrame` instance.
"""
ctrlpanel.ControlPanel.__init__(
self, parent, overlayList, displayCtx, frame)
if USE_HTML2: self.__info = wxhtml.WebView.New(self)
else: self.__info = HtmlWindow( self)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__info, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
displayCtx .addListener('selectedOverlay',
self.name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self.name,
self.__selectedOverlayChanged)
self.__currentOverlay = None
self.__currentDisplay = None
self.__currentOpts = None
self.__selectedOverlayChanged()
self.SetMinSize((350, 500))
self.Layout()
[docs] def destroy(self):
"""Must be called when this ``OverlayInfoPanel`` is no longer
needed. Removes some property listeners, and calls the
:meth:`.ControlPanel.destroy` method.
"""
self.displayCtx .removeListener('selectedOverlay', self.name)
self.overlayList.removeListener('overlays', self.name)
self.__deregisterOverlay()
ctrlpanel.ControlPanel.destroy(self)
def __selectedOverlayChanged(self, *a):
"""Called when the :class:`.OverlayList` or
:attr:`.DisplayContext.selectedOverlay` changes. Refreshes the
information shown on this ``OverlayInfoPanel``.
"""
overlay = self.displayCtx.getSelectedOverlay()
# Overlay list is empty
if overlay is None:
self.__info.SetPage('', '')
self.__info.Refresh()
return
self.__deregisterOverlay()
if overlay is not None:
self.__registerOverlay(overlay)
self.__updateInformation()
_optProps = td.TypeDict({
'Image' : ['transform'],
'Mesh' : ['refImage', 'coordSpace'],
'DTIFitTensor' : ['transform'],
})
"""This dictionary contains a list of :class:`.DisplayOpts` properties
that, when changed, should result in the information being refreshed.
It is used by the :meth:`__registerOverlay` and :meth:`__deregisterOverlay`
methods.
"""
def __registerOverlay(self, overlay):
"""Registers property listeners with the given overlay so the
information can be refreshed when necessary.
"""
display = self.displayCtx.getDisplay(overlay)
opts = display.opts
self.__currentOverlay = overlay
self.__currentDisplay = display
self.__currentOpts = opts
display.addListener('name',
self.name,
self.__overlayNameChanged)
display.addListener('overlayType',
self.name,
self.__overlayTypeChanged)
for propName in OverlayInfoPanel._optProps.get(overlay, []):
opts.addListener(propName, self.name, self.__overlayOptsChanged)
def __deregisterOverlay(self):
"""De-registers property listeners from the overlay that was
previously registered via :meth:`__registerOverlay`.
"""
if self.__currentOverlay is None:
return
overlay = self.__currentOverlay
display = self.__currentDisplay
opts = self.__currentOpts
self.__currentOverlay = None
self.__currentDisplay = None
self.__currentOpts = None
display.removeListener('name', self.name)
display.removeListener('overlayType', self.name)
for propName in OverlayInfoPanel._optProps[overlay]:
opts.removeListener(propName, self.name)
def __overlayTypeChanged(self, *a):
"""Called when the :attr:`.Display.overlayType` for the current
overlay changes. Re-registers with the ``Display`` and
``DisplayOpts`` instances associated with the overlay.
"""
self.__selectedOverlayChanged()
def __overlayNameChanged(self, *a):
"""Called when the :attr:`.Display.name` for the current overlay
changes. Updates the information display.
"""
self.__updateInformation()
def __overlayOptsChanged(self, *a):
"""Called when any :class:`.DisplayOpts` properties for the current
overlay change. Updates the information display. The properties that
trigger a refresh are defined in the :attr:`_optProps` dictionary.
"""
self.__updateInformation()
def __updateInformation(self):
"""Refreshes the information shown on this ``OverlayInfoPanel``.
Called by the :meth:`__selectedOverlayChanged` and
:meth:`__overlayNameChanged` methods.
"""
overlay = self.__currentOverlay
display = self.__currentDisplay
infoFunc = '_{}__get{}Info'.format(type(self) .__name__,
type(overlay).__name__)
infoFunc = getattr(self, infoFunc, None)
# Overlay is none, or the overlay
# type is not supported
if infoFunc is None:
self.__info.SetPage('', '')
self.__info.Refresh()
return
info = infoFunc(overlay, display)
self.__info.SetPage(self.__formatOverlayInfo(info), '')
self.__info.Refresh()
def __getImageInfo(self, overlay, display, title=None, metadata=True):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.Image` overlay.
:arg overlay: A :class:`.Image` instance.
:arg display: The :class:`.Display` instance assocated with the
``Image``.
"""
img = overlay.nibImage
hdr = overlay.header
isNifti = overlay.niftiVersion >= 1
hasMeta = metadata and len(overlay.metaKeys()) > 0
opts = display.opts
if isNifti: title = strings.labels[self, overlay]
else: title = strings.labels[self, 'Analyze']
if overlay.dataSource is not None and \
fslbids.isBIDSFile(overlay.dataSource):
metaSect = strings.labels[self, overlay, 'bidsMeta']
else:
metaSect = strings.labels[self, overlay, 'jsonMeta']
info = OverlayInfo('{} - {}'.format(display.name, title))
generalSect = strings.labels[self, 'general']
dimSect = strings.labels[self, overlay, 'dimensions']
xformSect = strings.labels[self, overlay, 'transform']
orientSect = strings.labels[self, overlay, 'orient']
info.addSection(generalSect)
info.addSection(dimSect)
info.addSection(xformSect)
info.addSection(orientSect)
if hasMeta:
info.addSection(metaSect)
displaySpace = strings.labels[self,
overlay,
'displaySpace',
opts.transform]
if opts.transform == 'reference':
dsImg = self.displayCtx.displaySpace
if isinstance(dsImg, fslimage.Nifti):
dsDisplay = self.displayCtx.getDisplay(dsImg)
displaySpace = displaySpace.format(dsDisplay.name)
else:
log.warn('{} transform ({}) seems to be out '
'of date (display space: {})'.format(
overlay,
opts.transform,
self.displayCtx.displaySpace))
dataType = strings.nifti.get(('datatype', int(hdr['datatype'])),
'Unknown')
info.addInfo(strings.labels[self, 'niftiVersion'],
strings.nifti['version.{}'.format(overlay.niftiVersion)],
section=generalSect)
info.addInfo(strings.labels[self, 'dataSource'],
overlay.dataSource,
section=generalSect)
info.addInfo(strings.nifti['datatype'],
dataType,
section=generalSect)
info.addInfo(strings.nifti['descrip'],
overlay.strval('descrip'),
section=generalSect)
if isNifti:
intent = strings.nifti.get(
('intent_code', int(hdr['intent_code'])),
'Unknown')
info.addInfo(strings.nifti['intent_code'],
intent,
section=generalSect)
info.addInfo(strings.nifti['intent_name'],
overlay.strval('intent_name'),
section=generalSect)
info.addInfo(strings.nifti['aux_file'],
overlay.strval('aux_file'),
section=generalSect)
info.addInfo(strings.labels[self, 'overlayType'],
strings.choices[display, 'overlayType'][
display.overlayType],
section=generalSect)
info.addInfo(strings.labels[self, 'displaySpace'],
displaySpace,
section=generalSect)
info.addInfo(strings.nifti['dimensions'],
'{}D'.format(len(overlay.shape)),
section=dimSect)
for i in range(len(overlay.shape)):
info.addInfo(strings.nifti['dim{}'.format(i + 1)],
str(overlay.shape[i]),
section=dimSect)
# NIFTI images can specify different units.
if isNifti:
voxUnits = overlay.xyzUnits
timeUnits = overlay.timeUnits
# Assume mm / seconds for ANALYZE images
# We are using nifti xyzt_unit code values
# here
else:
voxUnits, timeUnits = 2, 8
# Convert the unit codes into labels
voxUnits = strings.nifti.get(('xyz_unit', voxUnits), 'INVALID CODE')
timeUnits = strings.nifti.get(('t_unit', timeUnits), 'INVALID CODE')
for i in range(len(overlay.shape)):
pixdim = hdr['pixdim'][i + 1]
if i < 3: pixdim = '{:0.4g} {}'.format(pixdim, voxUnits)
elif i == 3: pixdim = '{:0.4g} {}'.format(pixdim, timeUnits)
info.addInfo(
strings.nifti['pixdim{}'.format(i + 1)],
pixdim,
section=dimSect)
bounds = affine.axisBounds(overlay.shape[:3],
opts.getTransform('voxel', 'world'))
lens = bounds[1] - bounds[0]
lens = 'X={:0.0f} {} Y={:0.0f} {} Z={:0.0f} {}'.format(
lens[0], voxUnits,
lens[1], voxUnits,
lens[2], voxUnits)
info.addInfo(
strings.labels[self, overlay, 'size'],
lens,
section=dimSect)
# For NIFTI images, we show both
# the sform and qform matrices,
# in addition to the effective
# transformation
if isNifti:
qformCode = int(hdr['qform_code'])
sformCode = int(hdr['sform_code'])
info.addInfo(strings.nifti['transform'],
self.__formatArray(overlay.voxToWorldMat),
section=xformSect)
info.addInfo(strings.nifti['sform_code'],
strings.anatomy['Nifti', 'space', sformCode],
section=xformSect)
info.addInfo(strings.nifti['qform_code'],
strings.anatomy['Nifti', 'space', qformCode],
section=xformSect)
if sformCode != constants.NIFTI_XFORM_UNKNOWN:
sform = img.get_sform()
info.addInfo(strings.nifti['sform'],
self.__formatArray(sform),
section=xformSect)
if qformCode != constants.NIFTI_XFORM_UNKNOWN:
try:
qform = img.get_qform()
except Exception as e:
log.warning('Could not read qform from {}: {}'.format(
overlay.name, str(e)))
qform = np.eye(4) * np.nan
info.addInfo(strings.nifti['qform'],
self.__formatArray(qform),
section=xformSect)
# For ANALYZE images, we show
# the scale/offset matrix
else:
info.addInfo(strings.nifti['affine'],
self.__formatArray(hdr.get_best_affine()),
section=xformSect)
if overlay.getXFormCode() == constants.NIFTI_XFORM_UNKNOWN:
storageOrder = 'unknown'
elif overlay.isNeurological(): storageOrder = 'neuro'
else: storageOrder = 'radio'
storageOrder = strings.nifti['storageOrder.{}'.format(storageOrder)]
info.addInfo(strings.nifti['storageOrder'],
storageOrder,
section=orientSect)
for i in range(3):
xform = opts.getTransform('voxel', 'world')
orient = overlay.getOrientation(i, xform)
orient = '{} - {}'.format(
strings.anatomy['Nifti', 'lowlong', orient],
strings.anatomy['Nifti', 'highlong', orient])
info.addInfo(strings.nifti['voxOrient.{}'.format(i)],
orient,
section=orientSect)
for i in range(3):
xform = np.eye(4)
orient = overlay.getOrientation(i, xform)
orient = '{} - {}'.format(
strings.anatomy['Nifti', 'lowlong', orient],
strings.anatomy['Nifti', 'highlong', orient])
info.addInfo(strings.nifti['worldOrient.{}'.format(i)],
orient,
section=orientSect)
if hasMeta:
for k, v in overlay.metaItems():
info.addInfo(k, str(v), metaSect)
return info
def __getFEATImageInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.FEATImage` overlay.
:arg overlay: A :class:`.FEATImage` instance.
:arg display: The :class:`.Display` instance assocated with the
``FEATImage``.
"""
info = self.__getImageInfo(overlay, display)
featInfo = [
('analysisName', overlay.getAnalysisName()),
('analysisDir', overlay.getFEATDir()),
('numPoints', overlay.numPoints()),
('numEVs', overlay.numEVs()),
('numContrasts', overlay.numContrasts())]
topLevel = overlay.getTopLevelAnalysisDir()
report = overlay.getReportFile()
if topLevel is not None:
featInfo.insert(2, ('partOfAnalysis', topLevel))
if report is not None:
report = '<a href="file://{}">{}</a>'.format(report, report)
featInfo.insert(2, ('report', report))
secName = strings.labels[self, overlay, 'featInfo']
info.addSection(secName)
for k, v in featInfo:
info.addInfo(strings.feat[k], v, section=secName)
return info
def __getMelodicImageInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.MelodicImage` overlay.
:arg overlay: A :class:`.MelodicImage` instance.
:arg display: The :class:`.Display` instance assocated with the
``MelodicImage``.
"""
info = self.__getImageInfo(overlay, display)
melInfo = [
('dataFile', overlay.getDataFile()),
('analysisDir', overlay.getMelodicDir()),
('tr', overlay.tr),
('numComponents', overlay.numComponents())]
topLevel = overlay.getTopLevelAnalysisDir()
report = overlay.getReportFile()
if topLevel is not None:
melInfo.insert(2, ('partOfAnalysis', topLevel))
if report is not None:
report = '<a href="file://{}">{}</a>'.format(report, report)
melInfo.insert(2, ('report', report))
secName = strings.labels[self, overlay, 'melodicInfo']
info.addSection(secName)
for k, v in melInfo:
info.addInfo(strings.melodic[k], v, section=secName)
return info
def __getMeshInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.Mesh` overlay.
:arg overlay: A :class:`.Mesh` instance.
:arg display: The :class:`.Display` instance assocated with the
``Mesh``.
"""
opts = display.opts
refImg = opts.refImage
modelInfo = [
('numVertices', overlay.vertices.shape[0]),
('numTriangles', overlay.indices .shape[0]),
]
if refImg is None:
modelInfo.append(
('displaySpace', strings.labels[
self, overlay, 'coordSpace', 'display']))
mesh2worldXform = np.eye(4)
else:
refOpts = self.displayCtx.getOpts(refImg)
dsImg = self.displayCtx.displaySpace
displaySpace = strings.labels[
self, refImg, 'displaySpace', refOpts.transform]
coordSpace = strings.labels[
self, overlay,
'coordSpace', opts.coordSpace].format(refImg.name)
mesh2worldXform = affine.concat(
refOpts.getTransform('display', 'world'),
opts.getTransform('mesh', 'display'))
if refOpts.transform == 'reference':
dsDisplay = self.displayCtx.getDisplay(dsImg)
displaySpace = displaySpace.format(dsDisplay.name)
modelInfo.append(('refImage', refImg.dataSource))
modelInfo.append(('coordSpace', coordSpace))
modelInfo.append(('displaySpace', displaySpace))
bounds = affine.transform(overlay.bounds, mesh2worldXform)
lens = bounds[1] - bounds[0]
lens = 'X={:0.0f} mm Y={:0.0f} mm Z={:0.0f} mm'.format(*lens)
modelInfo.append(('size', lens))
info = OverlayInfo('{} - {}'.format(
display.name,
strings.labels[self, overlay]))
info.addInfo(strings.labels[self, 'dataSource'], overlay.dataSource)
for name, value in modelInfo:
info.addInfo(strings.labels[self, overlay, name], value)
return info
def __getVTKMeshInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.VTKMesh` overlay.
:arg overlay: A :class:`.VTKMesh` instance.
:arg display: The :class:`.Display` instance assocated with the
``VTKMesh``.
"""
info = self.__getMeshInfo(overlay, display)
info.title = strings.labels[self, overlay]
return info
def __getGiftiMeshInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.GiftiMesh` overlay.
:arg overlay: A :class:`.GiftiMesh` instance.
:arg display: The :class:`.Display` instance assocated with the
``GiftiMesh``.
"""
info = self.__getMeshInfo(overlay, display)
info.title = strings.labels[self, overlay]
return info
def __getFreesurferMeshInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.FreesurferMesh` overlay.
:arg overlay: A :class:`.FreesurferMesh` instance.
:arg display: The :class:`.Display` instance assocated with the
``FreesurferMesh``.
"""
info = self.__getMeshInfo(overlay, display)
info.title = strings.labels[self, overlay]
return info
def __getDTIFitTensorInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.DTIFitTensor` overlay.
:arg overlay: A :class:`.DTIFitTensor` instance.
:arg display: The :class:`.Display` instance assocated with the
``DTIFitTensor``.
"""
info = self.__getImageInfo(overlay.L1(), display)
generalSect = strings.labels[self, 'general']
dataSource = strings.labels[self, 'dataSource']
info.title = strings.labels[self, overlay]
info.sections[generalSect][dataSource] = overlay.dataSource
tensorInfo = [
('v1', overlay.V1().dataSource),
('v2', overlay.V2().dataSource),
('v3', overlay.V3().dataSource),
('l1', overlay.L1().dataSource),
('l2', overlay.L2().dataSource),
('l3', overlay.L3().dataSource),
]
section = strings.labels[self, overlay, 'tensorInfo']
info.addSection(section)
for name, val in tensorInfo:
info.addInfo(strings.tensor[name], val, section)
return info
def __getDicomImageInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.DicomImage` overlay.
:arg overlay: A :class:`.DicomImage` instance.
:arg display: The :class:`.Display` instance assocated with the
``DicomImage``.
"""
info = self.__getImageInfo(overlay, display, metadata=False)
dicomInfo = strings.labels[self, overlay, 'dicomMeta']
info.addInfo(strings.labels[self, overlay, 'dicomDir'],
overlay.dicomDir)
info.addSection(dicomInfo)
for k, v in overlay.metaItems():
info.addInfo(k, str(v), dicomInfo)
return info
def __getMGHImageInfo(self, overlay, display):
"""Creates and returns an :class:`OverlayInfo` object containing
information about the given :class:`.MGHImage` overlay.
:arg overlay: A :class:`.MGHImage` instance.
:arg display: The :class:`.Display` instance assocated with the
``DicomImage``.
"""
info = self.__getImageInfo(overlay, display)
filename = overlay.mghImageFile
if filename is not None:
info.addInfo(strings.labels[self, overlay, 'filename'], filename)
return info
def __formatArray(self, array):
"""Creates and returns a string containing a HTML table which
formats the data in the given ``numpy.array``.
"""
lines = []
lines.append('<table border="0" style="font-size: small;">')
for rowi in range(array.shape[0]):
lines.append('<tr>')
for coli in range(array.shape[1]):
lines.append('<td>{:0.4g}</td>'.format(array[rowi, coli]))
lines.append('</tr>')
lines.append('</table>')
return ''.join(lines)
def __formatOverlayInfo(self, info):
"""Creates and returns a string containing some HTML which formats
the information in the given ``OverlayInfo`` instance.
"""
lines = []
lines.append('<html>')
lines.append('<body style="font-family: '
'sans-serif; font-size: small;">')
lines.append('<h2>{}</h2>'.format(info.title))
sections = []
if len(info.info) > 0:
sections.append((None, list(info.info.items())))
for secName, secInf in info.sections.items():
sections.append((secName, list(secInf.items())))
for i, (secName, secInf) in enumerate(sections):
lines.append('<div style="float:left; margin: 5px; '
'background-color: #f0f0f0;">')
if secName is not None:
lines.append('<h3>{}</h3>'.format(secName))
lines.append('<table border="0" style="font-size: small;">')
for i, (infName, infData) in enumerate(secInf):
if i % 2: bgColour = '#f0f0f0'
else: bgColour = '#cdcdff'
lines.append('<tr bgcolor="{}">'
'<td><b>{}</b></td>'
'<td>{}</td></tr>'.format(
bgColour,
infName,
infData))
lines.append('</table>')
lines.append('</div>')
lines.append('</body></html>')
return '\n'.join(lines)
[docs]class OverlayInfo(object):
"""A little class which encapsulates human-readable information about
one overlay. ``OverlayInfo`` objects are created and returned by the
``OverlayInfoPanel.__get*Info`` methods.
The information stored in an ``OverlayInfo`` instance is organised into
*sections*. Within each section, information is organised into key-value
pairs. The order in which both ``OverlayInfo`` sections, and information,
is ultimately output, is the order in which the sections/information are
added, via the :meth:`addSection` and :meth:`addInfo` methods.
"""
[docs] def __init__(self, title):
"""Create an ``OverlayInfo`` instance.
:arg title: The ``OverlaytInfo`` title.
"""
self.title = title
self.info = collections.OrderedDict()
self.sections = collections.OrderedDict()
[docs] def addSection(self, section):
"""Add a section to this ``OverlayInfo`` instance.
:arg section: The section name.
"""
self.sections[section] = collections.OrderedDict()
[docs] def addInfo(self, name, info, section=None):
"""Add some information to this ``OverlayInfo`` instance.
:arg name: The information name.
:arg info: The information value.
:arg section: Section to place the information in.
"""
if section is None: self.info[ name] = info
else: self.sections[section][name] = info