Index: src/sas/qtgui/Calculators/DataOperationUtilityPanel.py
===================================================================
--- src/sas/qtgui/Calculators/DataOperationUtilityPanel.py	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
+++ src/sas/qtgui/Calculators/DataOperationUtilityPanel.py	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
@@ -0,0 +1,473 @@
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+
+from sas.qtgui.Plotting.PlotterData import Data1D
+from sas.qtgui.Plotting.Plotter import PlotterWidget
+from sas.qtgui.Plotting.PlotterData import Data2D
+from sas.qtgui.Plotting.Plotter2D import Plotter2DWidget
+
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+from UI.DataOperationUtilityUI import Ui_DataOperationUtility
+
+import time
+import logging
+import re
+
+BG_WHITE = "background-color: rgb(255, 255, 255);"
+BG_RED = "background-color: rgb(244, 170, 164);"
+
+class DataOperationUtilityPanel(QtGui.QDialog, Ui_DataOperationUtility):
+    def __init__(self, parent=None):
+        super(DataOperationUtilityPanel, self).__init__()
+        self.setupUi(self)
+        self.manager = parent
+        self.communicator = self.manager.communicator()
+
+        # To store input datafiles
+        self.filenames = None
+        self.list_data_items = []
+        self.data1 = None
+        self.data2 = None
+        # To store the result
+        self.output = None
+
+        # To update content of comboboxes with files loaded in DataExplorer
+        self.communicator.sendDataToPanel.connect(self.updateCombobox)
+
+        # change index of comboboxes
+        self.cbData1.currentIndexChanged.connect(self.onSelectData1)
+        self.cbData2.currentIndexChanged.connect(self.onSelectData2)
+        self.cbOperator.currentIndexChanged.connect(self.onSelectOperator)
+
+        # edit Coefficient text edit
+        self.txtNumber.textChanged.connect(self.onInputCoefficient)
+        self.txtOutputData.textChanged.connect(self.onCheckOutputName)
+
+        # push buttons
+        self.cmdClose.clicked.connect(self.onClose)  # accept)
+        self.cmdHelp.clicked.connect(self.onHelp)
+        self.cmdCompute.clicked.connect(self.onCompute)
+        self.cmdReset.clicked.connect(self.onReset)
+
+        self.cmdCompute.setEnabled(False)
+
+        # validator for coefficient
+        self.txtNumber.setValidator(QtGui.QDoubleValidator())
+
+        # Add "?" to initial graphs (when they are still empty)
+        self.layoutOutput = QtGui.QHBoxLayout()
+        self.layoutData1 = QtGui.QHBoxLayout()
+        self.layoutData2 = QtGui.QHBoxLayout()
+
+        self.newPlot(self.graphOutput, self.layoutOutput)
+        self.newPlot(self.graphData1, self.layoutData1)
+        self.newPlot(self.graphData2, self.layoutData2)
+
+        # Flag to enable Compute pushbutton
+        self.data2OK = False
+        self.data1OK = False
+
+    def updateCombobox(self, filenames):
+        """ Function to fill comboboxes with names of datafiles loaded in
+         DataExplorer. For Data2, there is the additional option of choosing
+         a number to apply to data1 """
+        self.filenames = filenames
+
+        if filenames.keys():
+            # clear contents of comboboxes
+            self.cbData1.clear()
+            self.cbData1.addItems(['Select Data'])
+            self.cbData2.clear()
+            self.cbData2.addItems(['Select Data', 'Number'])
+
+            list_datafiles = []
+
+            for id in filenames.keys():
+                if filenames[id].get_data().title != '':
+                    # filenames with titles
+                    new_title = filenames[id].get_data().title
+                    list_datafiles.append(new_title)
+                    self.list_data_items.append(new_title)
+
+                else:
+                    # filenames without titles by removing time.time()
+                    new_title = re.sub('\d{10}\.\d{2}', '', str(id))
+                    self.list_data_items.append(new_title)
+                    list_datafiles.append(new_title)
+
+            # update contents of comboboxes
+            self.cbData1.addItems(list_datafiles)
+            self.cbData2.addItems(list_datafiles)
+
+    def onHelp(self):
+        """
+        Bring up the Data Operation Utility Documentation whenever
+        the HELP button is clicked.
+        Calls Documentation Window with the path of the location within the
+        documentation tree (after /doc/ ....".
+        """
+        try:
+            location = GuiUtils.HELP_DIRECTORY_LOCATION + \
+                       "/user/sasgui/perspectives/calculator/data_operator_help.html"
+            self.manager._helpView.load(QtCore.QUrl(location))
+            self.manager._helpView.show()
+
+        except AttributeError:
+            # No manager defined - testing and standalone runs
+            pass
+
+    def onClose(self):
+        """ Close dialog """
+        self.onReset()
+        self.close()
+
+    def onCompute(self):
+        """ perform calculation """
+        # check compatibility of datasets before calculation
+        if self.onCheckChosenData():
+            # set operator to be applied
+            operator = self.cbOperator.currentText()
+            # calculate and send data to DataExplorer
+            output = None
+            try:
+                data1 = self.data1
+                data2 = self.data2
+                exec "output = data1 %s data2" % operator
+            except:
+                raise
+
+            self.output = output
+
+            # if outputname was unused, write output result to it
+            # and display plot
+            if self.onCheckOutputName():
+                # add outputname to self.filenames
+                self.list_data_items.append(str(self.txtOutputData.text()))
+                # send result to DataExplorer
+                self.onPrepareOutputData()
+                # plot result
+                self.updatePlot(self.graphOutput, self.layoutOutput, self.output)
+
+    def onPrepareOutputData(self):
+        """ Prepare datasets to be added to DataExplorer and DataManager """
+        new_item = GuiUtils.createModelItemWithPlot(
+            QtCore.QVariant(self.output),
+            name=self.txtOutputData.text())
+
+        new_datalist_item = {str(self.txtOutputData.text()) + str(time.time()):
+                                 self.output}
+        self.communicator. \
+            updateModelFromDataOperationPanelSignal.emit(new_item, new_datalist_item)
+
+    def onSelectOperator(self):
+        """ Change GUI when operator changed"""
+        self.lblOperatorApplied.setText(self.cbOperator.currentText())
+        self.newPlot(self.graphOutput, self.layoutOutput)
+
+    def onReset(self):
+        """
+        Reset Panel to its initial state (default values) keeping
+        the names of loaded data
+        """
+        self.txtNumber.setText('1.0')
+        self.txtOutputData.setText('MyNewDataName')
+
+        self.txtNumber.setEnabled(False)
+        self.cmdCompute.setEnabled(False)
+
+        self.cbData1.setCurrentIndex(0)
+        self.cbData2.setCurrentIndex(0)
+        self.cbOperator.setCurrentIndex(0)
+
+        self.output = None
+        self.data1 = None
+        self.data2 = None
+
+        self.data1OK = False
+        self.data2OK = False
+
+        # Empty graphs
+        self.newPlot(self.graphOutput, self.layoutOutput)
+        self.newPlot(self.graphData1, self.layoutData1)
+        self.newPlot(self.graphData2, self.layoutData2)
+
+    def onSelectData1(self):
+        """ Plot for selection of Data1 """
+        choice_data1 = str(self.cbData1.currentText())
+
+        wrong_choices = ['No Data Available', 'Select Data', '']
+
+        if choice_data1 in wrong_choices:
+            # check validity of choice: input = filename
+            self.newPlot(self.graphData1, self.layoutData1)
+            self.data1 = None
+            self.data1OK = False
+            self.cmdCompute.setEnabled(False)
+            # logging.info('Choose a file for Data1')
+            return
+
+        else:
+            # Enable Compute button only if Data2 is defined
+            self.cmdCompute.setEnabled(self.data2OK)
+            self.data1OK = True
+            # get Data1
+            id1 = self._findId(choice_data1)
+            self.data1 = self._extractData(id1)
+            # plot Data1
+            self.updatePlot(self.graphData1, self.layoutData1, self.data1)
+            # plot default for output graph
+            self.newPlot(self.graphOutput, self.layoutOutput)
+
+    def onSelectData2(self):
+        """ Plot for selection of Data2 """
+        choice_data2 = str(self.cbData2.currentText())
+        wrong_choices = ['No Data Available', 'Select Data', '']
+
+        if choice_data2 in wrong_choices:
+            self.newPlot(self.graphData2, self.layoutData2)
+            self.txtNumber.setEnabled(False)
+            self.data2OK = False
+            # logging.info('Choose a file or a number for Data2')
+            return
+
+        elif choice_data2 == 'Number':
+            self.data2OK = True
+            self.txtNumber.setEnabled(True)
+            self.data2 = float(self.txtNumber.text())
+
+            # Enable Compute button only if Data1 is defined
+            self.cmdCompute.setEnabled(self.data1OK)
+            # Display value of coefficient in graphData2
+            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
+            # plot default for output graph
+            self.newPlot(self.graphOutput, self.layoutOutput)
+
+        else:
+            self.cmdCompute.setEnabled(self.data1OK)
+            self.data2OK = True
+            self.txtNumber.setEnabled(False)
+            id2 = self._findId(choice_data2)
+            self.data2 = self._extractData(id2)
+            # plot Data2
+            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
+            # plot default for output graph
+            self.newPlot(self.graphOutput, self.layoutOutput)
+
+    def onInputCoefficient(self):
+        """ Check input of number when a coefficient is required
+        for operation """
+        if self.txtNumber.isModified():
+            input_to_check = str(self.txtNumber.text())
+
+            if input_to_check is None or input_to_check is '':
+                msg = 'DataOperation: Number requires a float number'
+                logging.info(msg)
+                self.txtNumber.setStyleSheet(QtCore.QString(BG_RED))
+
+            elif float(self.txtNumber.text()) == 0.:
+                # should be check that 0 is not chosen
+                msg = 'DataOperation: Number requires a non zero number'
+                logging.info(msg)
+                self.txtNumber.setStyleSheet(QtCore.QString(BG_RED))
+
+            else:
+                self.txtNumber.setStyleSheet(QtCore.QString(BG_WHITE))
+                self.data2 = float(self.txtNumber.text())
+                self.updatePlot(self.graphData2, self.layoutData2, self.data2)
+
+    def onCheckChosenData(self):
+        """ Before running compute, check that data1 and 2 are compatible """
+
+        if self.cbData2.currentText() == 'Number':
+            self.cbData1.setStyleSheet(QtCore.QString(BG_WHITE))
+            self.cbData2.setStyleSheet(QtCore.QString(BG_WHITE))
+            return True
+
+        elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
+            self.cbData1.setStyleSheet(QtCore.QString(BG_RED))
+            self.cbData2.setStyleSheet(QtCore.QString(BG_RED))
+            logging.info('Cannot compute data of different dimensions')
+            return False
+
+        elif self.data1.__class__.__name__ == 'Data1D'\
+                and (len(self.data2.x) != len(self.data1.x) or
+                         not all(i == j for i, j in zip(self.data1.x, self.data2.x))):
+            logging.info('Cannot compute 1D data of different lengths')
+            self.cbData1.setStyleSheet(QtCore.QString(BG_RED))
+            self.cbData2.setStyleSheet(QtCore.QString(BG_RED))
+            return False
+
+        elif self.data1.__class__.__name__ == 'Data2D' \
+                and (len(self.data2.qx_data) != len(self.data1.qx_data) \
+                or len(self.data2.qy_data) != len(self.data1.qy_data)
+                or not all(i == j for i, j in
+                                 zip(self.data1.qx_data, self.data2.qx_data))
+                or not all(i == j for i, j in
+                            zip(self.data1.qy_data, self.data2.qy_data))
+                     ):
+            self.cbData1.setStyleSheet(QtCore.QString(BG_RED))
+            self.cbData2.setStyleSheet(QtCore.QString(BG_RED))
+            logging.info('Cannot compute 2D data of different lengths')
+            return False
+
+        else:
+            self.cbData1.setStyleSheet(QtCore.QString(BG_WHITE))
+            self.cbData2.setStyleSheet(QtCore.QString(BG_WHITE))
+            return True
+
+    def onCheckOutputName(self):
+        """ Check that name of output does not already exist """
+        name_to_check = str(self.txtOutputData.text())
+        self.txtOutputData.setStyleSheet(QtCore.QString(BG_WHITE))
+
+        if name_to_check is None or name_to_check == '':
+            self.txtOutputData.setStyleSheet(QtCore.QString(BG_RED))
+            logging.info('no output name')
+            return False
+
+        elif name_to_check in self.list_data_items:
+            self.txtOutputData.setStyleSheet(QtCore.QString(BG_RED))
+            logging.info('The Output Data Name already exists')
+            return False
+
+        else:
+            self.txtOutputData.setStyleSheet(QtCore.QString(BG_WHITE))
+            return True
+
+    # ########
+    # Modification of inputs
+    # ########
+    def _findId(self, name):
+        """ find id of name in list of filenames """
+        isinstance(name, basestring)
+
+        for id in self.filenames.keys():
+            # data with title
+            if self.filenames[id].get_data().title:
+                input = self.filenames[id].get_data().title
+            # data without title
+            else:
+                input = str(id)
+            if name in input:
+                return id
+
+    def _extractData(self, id):
+        """ Extract data from file with id contained in list of filenames """
+        data_complete = self.filenames[id].get_data()
+        dimension = data_complete.__class__.__name__
+
+        if dimension == 'Data1D':
+            data_set = Data1D(x=data_complete.x,
+                              y=data_complete.y,
+                              dx=data_complete.dx,
+                              dy=data_complete.dy)
+
+        elif dimension == 'Data2D':
+            data_set = Data2D(image=data_complete.data,
+                              err_image=data_complete.err_data,
+                              mask=data_complete.mask,
+                              qx_data=data_complete.qx_data,
+                              qy_data=data_complete.qy_data,
+                              dqx_data=data_complete.dqx_data,
+                              dqy_data=data_complete.dqy_data,
+                              q_data=data_complete.q_data,
+                              xmin=data_complete.xmin,
+                              xmax=data_complete.xmax,
+                              ymin=data_complete.ymin,
+                              ymax=data_complete.ymax,
+                              zmin=data_complete.zmin,
+                              zmax=data_complete.zmax)
+
+        else:
+            logging.info('Error with data format')
+            return
+
+        return data_set
+
+    # ########
+    # PLOTS
+    # ########
+    def newPlot(self, graph, layout):
+        """ Create template for graphs with default '?' layout"""
+        assert isinstance(graph, QtGui.QGraphicsView)
+        assert isinstance(layout, QtGui.QHBoxLayout)
+
+        # clear layout
+        if layout.count() > 0:
+            item = layout.takeAt(0)
+            layout.removeItem(item)
+
+        layout.setContentsMargins(0, 0, 0, 0)
+
+        # define default content: '?'
+        scene = QtGui.QGraphicsScene()
+        scene.addText("?")
+
+        subgraph = QtGui.QGraphicsView()
+        subgraph.setScene(scene)
+
+        layout.addWidget(subgraph)
+
+        graph.setLayout(layout)
+
+    def updatePlot(self, graph, layout, data):
+        """ plot data in graph after clearing its layout """
+
+        assert isinstance(graph, QtGui.QGraphicsView)
+        assert isinstance(layout, QtGui.QHBoxLayout)
+
+        # clear layout
+        if layout.count() > 0:
+            item = layout.takeAt(0)
+            layout.removeItem(item)
+
+        layout.setContentsMargins(0, 0, 0, 0)
+
+        if isinstance(data, Data2D):
+            # plot 2D data
+            plotter2D = Plotter2DWidget(self, quickplot=True)
+            plotter2D.data = data
+            plotter2D.scale = 'linear'
+
+            plotter2D.ax.tick_params(axis='x', labelsize=8)
+            plotter2D.ax.tick_params(axis='y', labelsize=8)
+
+            # Draw zero axis lines.
+            plotter2D.ax.axhline(linewidth=1)
+            plotter2D.ax.axvline(linewidth=1)
+
+            graph.setLayout(layout)
+            layout.addWidget(plotter2D)
+            # remove x- and ylabels
+            plotter2D.y_label = ''
+            plotter2D.x_label = ''
+            plotter2D.plot(show_colorbar=False)
+            plotter2D.show()
+
+        elif isinstance(data, Data1D):
+            # plot 1D data
+            plotter = PlotterWidget(self, quickplot=True)
+            plotter.data = data
+
+            graph.setLayout(layout)
+            layout.addWidget(plotter)
+
+            plotter.ax.tick_params(axis='x', labelsize=8)
+            plotter.ax.tick_params(axis='y', labelsize=8)
+
+            plotter.plot(hide_error=True, marker='.', show_legend=False)
+
+            plotter.show()
+
+        elif float(data) and self.cbData2.currentText() == 'Number':
+            # display value of coefficient (to be applied to Data1)
+            # in graphData2
+            scene = QtGui.QGraphicsScene()
+            scene.addText(str(data))
+
+            subgraph = QtGui.QGraphicsView()
+            subgraph.setScene(scene)
+
+            layout.addWidget(subgraph)
+
+            graph.setLayout(layout)
Index: src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui
===================================================================
--- src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
+++ src/sas/qtgui/Calculators/UI/DataOperationUtilityUI.ui	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
@@ -0,0 +1,482 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DataOperationUtility</class>
+ <widget class="QDialog" name="DataOperationUtility">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>951</width>
+    <height>425</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>950</width>
+    <height>425</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>1000</width>
+    <height>500</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Data Operation</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>:/res/ball.ico</normaloff>:/res/ball.ico</iconset>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_3">
+   <item row="0" column="0">
+    <widget class="QGroupBox" name="groupBox">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>870</width>
+       <height>361</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>950</width>
+       <height>400</height>
+      </size>
+     </property>
+     <property name="title">
+      <string>Data Operation [ + (add); - (subtract); * (multiply); / (divide); | (append)]</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout_2">
+      <item row="0" column="0">
+       <layout class="QGridLayout" name="gridLayout">
+        <item row="0" column="0">
+         <widget class="QLabel" name="lblOutputDataName">
+          <property name="text">
+           <string>Output Data Name</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="8">
+         <widget class="QLabel" name="lblData2OrNumber">
+          <property name="text">
+           <string>Data2 (or Number)</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="2">
+         <widget class="QLabel" name="lblBigEqual">
+          <property name="minimumSize">
+           <size>
+            <width>21</width>
+            <height>21</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>=</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="7">
+         <spacer name="horizontalSpacer_5">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="3" column="0">
+         <widget class="QGraphicsView" name="graphOutput">
+          <property name="minimumSize">
+           <size>
+            <width>260</width>
+            <height>260</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>300</width>
+            <height>300</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="4">
+         <widget class="QComboBox" name="cbData1">
+          <property name="minimumSize">
+           <size>
+            <width>170</width>
+            <height>26</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>200</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="baseSize">
+           <size>
+            <width>0</width>
+            <height>26</height>
+           </size>
+          </property>
+          <property name="editable">
+           <bool>false</bool>
+          </property>
+          <item>
+           <property name="text">
+            <string>No Data Available</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="3" column="5">
+         <spacer name="horizontalSpacer_3">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="3" column="3">
+         <spacer name="horizontalSpacer_4">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="0" column="4">
+         <widget class="QLabel" name="lblData1">
+          <property name="text">
+           <string>Data1</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLineEdit" name="txtOutputData">
+          <property name="minimumSize">
+           <size>
+            <width>260</width>
+            <height>21</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>300</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>MyNewDataName</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="4">
+         <widget class="QGraphicsView" name="graphData1">
+          <property name="minimumSize">
+           <size>
+            <width>260</width>
+            <height>260</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>300</width>
+            <height>300</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1">
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="1" column="2">
+         <widget class="QLabel" name="lblEqual">
+          <property name="minimumSize">
+           <size>
+            <width>21</width>
+            <height>21</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>=</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="9">
+         <widget class="QLineEdit" name="txtNumber">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>50</width>
+            <height>21</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>150</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="toolTip">
+           <string>If no Data2 loaded, enter a number to be applied to Data1 using the operator</string>
+          </property>
+          <property name="text">
+           <string>1.0</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="6">
+         <widget class="QLabel" name="lblOperatorApplied">
+          <property name="minimumSize">
+           <size>
+            <width>21</width>
+            <height>21</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>+</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+          <property name="textInteractionFlags">
+           <set>Qt::LinksAccessibleByMouse|Qt::TextEditable</set>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="8">
+         <widget class="QComboBox" name="cbData2">
+          <property name="minimumSize">
+           <size>
+            <width>170</width>
+            <height>26</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>200</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="editable">
+           <bool>false</bool>
+          </property>
+          <item>
+           <property name="text">
+            <string>No Data Available</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="1" column="6">
+         <widget class="QComboBox" name="cbOperator">
+          <property name="minimumSize">
+           <size>
+            <width>51</width>
+            <height>26</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>60</width>
+            <height>30</height>
+           </size>
+          </property>
+          <property name="toolTip">
+           <string>Add: +
+Subtract: - 
+Multiply: *
+Divide: /
+Append(Combine): |</string>
+          </property>
+          <item>
+           <property name="text">
+            <string>+</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>-</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>*</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>/</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>|</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="3" column="8" colspan="2">
+         <widget class="QGraphicsView" name="graphData2">
+          <property name="minimumSize">
+           <size>
+            <width>260</width>
+            <height>260</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>300</width>
+            <height>300</height>
+           </size>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdReset">
+       <property name="minimumSize">
+        <size>
+         <width>75</width>
+         <height>25</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Reset</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdCompute">
+       <property name="minimumSize">
+        <size>
+         <width>75</width>
+         <height>25</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Generate the Data and send to Data Explorer.</string>
+       </property>
+       <property name="text">
+        <string>Compute</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdClose">
+       <property name="minimumSize">
+        <size>
+         <width>75</width>
+         <height>25</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Close this panel.</string>
+       </property>
+       <property name="text">
+        <string>Close</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdHelp">
+       <property name="minimumSize">
+        <size>
+         <width>75</width>
+         <height>25</height>
+        </size>
+       </property>
+       <property name="toolTip">
+        <string>Get help on Data Operations.</string>
+       </property>
+       <property name="text">
+        <string>Help</string>
+       </property>
+       <property name="autoDefault">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
Index: src/sas/qtgui/Calculators/UnitTesting/DataOperationUtilityTest.py
===================================================================
--- src/sas/qtgui/Calculators/UnitTesting/DataOperationUtilityTest.py	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
+++ src/sas/qtgui/Calculators/UnitTesting/DataOperationUtilityTest.py	(revision d5c5d3dac558e84d4de22e41b9d47bbbcd007de1)
@@ -0,0 +1,416 @@
+import sys
+import time
+import numpy
+import logging
+import unittest
+from PyQt4 import QtGui
+from PyQt4 import QtCore
+from PyQt4.QtTest import QTest
+from PyQt4.QtCore import Qt
+from mock import MagicMock
+from mock import patch
+
+from twisted.internet import threads
+
+from sas.qtgui.Calculators.DataOperationUtilityPanel import DataOperationUtilityPanel
+from sas.qtgui.Utilities.GuiUtils import *
+from sas.qtgui.Plotting.PlotterData import Data1D
+from sas.qtgui.Plotting.PlotterData import Data2D
+from sas.qtgui.MainWindow.DataState import DataState
+
+if not QtGui.QApplication.instance():
+    app = QtGui.QApplication(sys.argv)
+
+BG_COLOR_ERR = 'background-color: rgb(244, 170, 164);'
+
+class DataOperationUtilityTest(unittest.TestCase):
+    """Test the ResolutionCalculator"""
+    def setUp(self):
+        """Create the ResolutionCalculator"""
+
+        class dummy_manager(object):
+            def communicator(self):
+                return Communicate()
+
+        self.widget = DataOperationUtilityPanel(dummy_manager())
+
+    def tearDown(self):
+        """Destroy the DataOperationUtility"""
+        self.widget.close()
+        self.widget = None
+
+    def testDefaults(self):
+        """Test the GUI in its default state"""
+
+        self.assertIsInstance(self.widget, QtGui.QDialog)
+
+        self.assertEqual(self.widget.windowTitle(), "Data Operation")
+        self.assertEqual(self.widget.groupBox.title(), "Data Operation "
+                                                       "[ + (add); "
+                                                       "- (subtract); "
+                                                       "* (multiply); "
+                                                       "/ (divide); "
+                                                       "| (append)]")
+        # size
+        self.assertEqual(self.widget.size().height(), 425)
+        self.assertEqual(self.widget.size().width(), 951)
+
+        # content of line edits
+        self.assertEqual(self.widget.txtNumber.text(), '1.0')
+        self.assertEqual(self.widget.txtOutputData.text(), 'MyNewDataName')
+
+        # content of comboboxes and default text / index
+        self.assertFalse(self.widget.cbData1.isEditable())
+        self.assertEqual(self.widget.cbData1.count(), 1)
+        self.assertEqual(self.widget.cbData1.currentText(),
+                         'No Data Available')
+
+        self.assertFalse(self.widget.cbData2.isEditable())
+        self.assertEqual(self.widget.cbData2.count(), 1)
+        self.assertEqual(self.widget.cbData2.currentText(),
+                         'No Data Available')
+
+        self.assertFalse(self.widget.cbOperator.isEditable())
+        self.assertEqual(self.widget.cbOperator.count(), 5)
+        self.assertEqual(self.widget.cbOperator.currentText(), '+')
+        self.assertListEqual([self.widget.cbOperator.itemText(i) for i in
+                              range(self.widget.cbOperator.count())],
+                             ['+', '-', '*', '/', '|'])
+
+        # Tooltips
+        self.assertEqual(str(self.widget.cmdCompute.toolTip()), "Generate the Data "
+                                                           "and send to Data "
+                                                           "Explorer.")
+        self.assertEqual(str(self.widget.cmdClose.toolTip()), "Close this panel.")
+        self.assertEqual(str(self.widget.cmdHelp.toolTip()),
+                         "Get help on Data Operations.")
+        self.assertEqual(self.widget.txtNumber.toolTip(), "If no Data2 loaded, "
+                                                "enter a number to be "
+                                                "applied to Data1 using "
+                                                "the operator")
+        self.assertEqual(str(self.widget.cbOperator.toolTip()), "Add: +\n"
+                                                           "Subtract: - \n"
+                                                           "Multiply: *\n"
+                                                           "Divide: /\n"
+                                                           "Append(Combine): |")
+
+        self.assertFalse(self.widget.cmdCompute.isEnabled())
+        self.assertFalse(self.widget.txtNumber.isEnabled())
+
+        self.assertIsInstance(self.widget.layoutOutput,QtGui.QHBoxLayout)
+        self.assertIsInstance(self.widget.layoutData1,QtGui.QHBoxLayout)
+        self.assertIsInstance(self.widget.layoutData2,QtGui.QHBoxLayout)
+
+        # To store input datafiles
+        self.assertIsNone(self.widget.filenames)
+        self.assertEqual(self.widget.list_data_items, [])
+        self.assertIsNone(self.widget.data1)
+        self.assertIsNone(self.widget.data2)
+        # To store the result
+        self.assertIsNone(self.widget.output)
+        self.assertFalse(self.widget.data2OK)
+        self.assertFalse(self.widget.data1OK)
+
+        self.widget.newPlot = MagicMock()
+        self.assertTrue(self.widget.newPlot.called_once())
+        self.assertTrue(self.widget.newPlot.called_once())
+        self.assertTrue(self.widget.newPlot.called_once())
+
+    def testHelp(self):
+        """ Assure help file is shown """
+        # this should not rise
+        self.widget.onHelp()
+
+    def testOnReset(self):
+        """ Test onReset function """
+        # modify gui
+        self.widget.txtNumber.setText('2.3')
+        self.widget.onReset()
+        self.assertEqual(self.widget.txtNumber.text(), '1.0')
+
+    def testOnClose(self):
+        """ test Closing window """
+        closeButton = self.widget.cmdClose
+        QTest.mouseClick(closeButton, Qt.LeftButton)
+
+
+    # @patch('DataOperationUtilityPanel.')
+    def testOnCompute(self):
+        """ Test onCompute function """
+
+        # define the data
+        self.widget.data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+        self.widget.data2 = 1
+
+        # mock update of plot
+        self.widget.updatePlot = MagicMock()
+
+        # enable onCompute to run (check on data type)
+        self.widget.onCheckChosenData = MagicMock(return_value=True)
+
+        # run onCompute
+        self.widget.onCompute()
+
+        # check output:
+        # x unchanged, y incremented by 1 (=data2) and updatePlot called
+        self.assertListEqual(self.widget.output.x.tolist(),
+                             self.widget.data1.x.tolist())
+        self.assertListEqual(self.widget.output.y.tolist(), [12.0, 13.0, 14.0])
+        self.assertTrue(self.widget.updatePlot.called_once())
+
+        self.widget.onPrepareOutputData = MagicMock()
+
+        self.assertTrue(self.widget.onPrepareOutputData.called_once())
+
+        # GuiManager.updateModelFromDataOperationPanel
+
+    def testOnSelectData1(self):
+        """ Test ComboBox for Data1 """
+        # Case 1: no data loaded
+        self.widget.onSelectData1()
+        self.assertIsNone(self.widget.data1)
+        self.assertFalse(self.widget.data1OK)
+        self.assertFalse(self.widget.cmdCompute.isEnabled())
+
+        # Case 2: data1 is a datafile
+        self.widget.filenames = MagicMock(
+            return_value={'datafile1': 'details'})
+        self.widget.updatePlot = MagicMock()
+
+        self.widget.cbData1.addItems(['Select Data', 'datafile1'])
+        self.widget.cbData1.setCurrentIndex(self.widget.cbData1.count()-1)
+        self.assertTrue(self.widget.updatePlot.called_once())
+        # Compute button disabled if data2OK == False
+        self.assertEqual(self.widget.cmdCompute.isEnabled(), self.widget.data2OK)
+
+    def testOnSelectData2(self):
+        """ Test ComboBox for Data2 """
+        self.widget.updatePlot = MagicMock()
+        # Case 1: no data loaded
+        self.widget.onSelectData2()
+        self.assertIsNone(self.widget.data2)
+        self.assertFalse(self.widget.data2OK)
+        self.assertFalse(self.widget.cmdCompute.isEnabled())
+
+        # Case 2: when empty combobox
+        self.widget.cbData2.clear()
+        self.widget.onSelectData2()
+        self.assertFalse(self.widget.txtNumber.isEnabled())
+        self.assertFalse(self.widget.cmdCompute.isEnabled())
+
+        # Case 3: when Data2 is Number
+        # add 'Number' to combobox Data2
+        self.widget.cbData2.addItem('Number')
+        # select 'Number' for cbData2
+        self.widget.cbData2.setCurrentIndex(self.widget.cbData2.count()-1)
+        self.widget.onSelectData2()
+        # check that line edit is now enabled
+        self.assertTrue(self.widget.txtNumber.isEnabled())
+        # Compute button enabled only if data1OK True
+        self.assertEqual(self.widget.cmdCompute.isEnabled(), self.widget.data1OK)
+        self.assertIsInstance(self.widget.data2, float)
+        # call updatePlot
+        self.assertTrue(self.widget.updatePlot.called_once())
+
+        # Case 4: when Data2 is a file
+        self.widget.filenames = MagicMock(
+            return_value={'datafile2': 'details'})
+        self.widget.cbData2.addItems(['Select Data', 'Number', 'datafile2'])
+        self.widget.cbData2.setCurrentIndex(self.widget.cbData2.count() - 1)
+        self.assertTrue(self.widget.updatePlot.called_once())
+        # editing of txtNumber is disabled when Data2 is a file
+        self.assertFalse(self.widget.txtNumber.isEnabled())
+        # Compute button enabled only if data1OK True
+        self.assertEqual(self.widget.cmdCompute.isEnabled(),
+                         self.widget.data1OK)
+        # call updatePlot
+        self.assertTrue(self.widget.updatePlot.called_once())
+
+    def testUpdateCombobox(self):
+        """ Test change of contents of comboboxes for Data1 and Data2 """
+        # Create input data
+        data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                       dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        data2 = Data2D(image=[0.1] * 4,
+                       qx_data=[1.0, 2.0, 3.0, 4.0],
+                       qy_data=[10.0, 11.0, 12.0, 13.0],
+                       dqx_data=[0.1, 0.2, 0.3, 0.4],
+                       dqy_data=[0.1, 0.2, 0.3, 0.4],
+                       q_data=[1, 2, 3, 4],
+                       xmin=-1.0, xmax=5.0,
+                       ymin=-1.0, ymax=15.0,
+                       zmin=-1.0, zmax=20.0)
+
+        filenames = {'datafile2': DataState(data2),
+                                 'datafile1': DataState(data1)}
+
+        # call function
+        self.widget.updateCombobox(filenames)
+
+        # check modifications of comboboxes
+        AllItemsData1 = [self.widget.cbData1.itemText(indx)
+                         for indx in range(self.widget.cbData1.count())]
+        self.assertListEqual(AllItemsData1, ['Select Data',
+                                             'datafile2',
+                                             'datafile1'])
+
+        AllItemsData2 = [self.widget.cbData2.itemText(indx)
+                         for indx in range(self.widget.cbData2.count())]
+        self.assertListEqual(AllItemsData2,
+                             ['Select Data', 'Number',
+                              'datafile2', 'datafile1'])
+
+    def testOnSelectOperator(self):
+        """ Change GUI when operator changed """
+        self.assertEqual(self.widget.lblOperatorApplied.text(),self.widget.cbOperator.currentText())
+
+        self.widget.cbOperator.setCurrentIndex(2)
+        self.assertEqual(self.widget.lblOperatorApplied.text(),
+                         self.widget.cbOperator.currentText())
+
+    def testOnInputCoefficient(self):
+        """
+        Check input of number when a coefficient is required for operation
+        """
+        # clear input for coefficient -> error
+        self.widget.txtNumber.clear()
+        # check that color of background changed to notify error
+        self.assertIn(BG_COLOR_ERR, self.widget.txtNumber.styleSheet())
+
+
+    def testCheckChosenData(self):
+        """ Test check of data compatibility """
+        # Case 1: incompatible dimensions
+        self.widget.data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.widget.data2 = Data2D(image=[0.1] * 4,
+                           qx_data=[1.0, 2.0, 3.0, 4.0],
+                           qy_data=[10.0, 11.0, 12.0, 13.0],
+                           dqx_data=[0.1, 0.2, 0.3, 0.4],
+                           dqy_data=[0.1, 0.2, 0.3, 0.4],
+                           q_data=[1, 2, 3, 4],
+                           xmin=-1.0, xmax=5.0,
+                           ymin=-1.0, ymax=15.0,
+                           zmin=-1.0, zmax=20.0)
+
+        self.assertFalse(self.widget.onCheckChosenData())
+
+        # Case 2 : compatible 1 dimension
+        self.widget.data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.widget.data2 = Data1D(x=[1.0, 2.0, 3.0], y=[1.0, 2.0, 3.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.assertTrue(self.widget.onCheckChosenData())
+
+        # Case 3: compatible 2 dimension
+        self.widget.data1 = Data2D(image=[0.1] * 4,
+                                   qx_data=[1.0, 2.0, 3.0, 4.0],
+                                   qy_data=[10.0, 11.0, 12.0, 13.0],
+                                   dqx_data=[0.1, 0.2, 0.3, 0.4],
+                                   dqy_data=[0.1, 0.2, 0.3, 0.4],
+                                   q_data=[1, 2, 3, 4],
+                                   xmin=-1.0, xmax=5.0,
+                                   ymin=-1.0, ymax=15.0,
+                                   zmin=-1.0, zmax=20.0)
+
+        self.widget.data2 = Data2D(image=[0.1] * 4,
+                                   qx_data=[1.0, 2.0, 3.0, 4.0],
+                                   qy_data=[10.0, 11.0, 12.0, 13.0],
+                                   dqx_data=[0.1, 0.2, 0.3, 0.4],
+                                   dqy_data=[0.1, 0.2, 0.3, 0.4],
+                                   q_data=[1, 2, 3, 4],
+                                   xmin=-1.0, xmax=5.0,
+                                   ymin=-1.0, ymax=15.0,
+                                   zmin=-1.0, zmax=20.0)
+
+        self.assertTrue(self.widget.onCheckChosenData())
+
+        # Case 4: Different 1D
+        self.widget.data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.widget.data2 = Data1D(x=[0.0, 1.0, 2.0], y=[1.0, 2.0, 3.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.assertFalse(self.widget.onCheckChosenData())
+
+        # Case 5: Data2 is a Number
+        self.widget.cbData2.clear()
+        self.widget.cbData2.addItem('Number')
+        self.widget.cbData2.setCurrentIndex(0)
+        self.assertEqual(self.widget.cbData2.currentText(), 'Number')
+        self.assertTrue(self.widget.onCheckChosenData())
+
+    def testOnCheckOutputName(self):
+        """ Test OutputName for result of operation """
+        self.widget.txtOutputData.clear()
+        self.assertFalse(self.widget.onCheckOutputName())
+
+        self.widget.list_data_items = ['datafile1', 'datafile2']
+        self.widget.txtOutputData.setText('datafile0')
+        self.assertTrue(self.widget.onCheckOutputName())
+        self.assertIn('', self.widget.txtOutputData.styleSheet())
+
+        self.widget.txtOutputData.clear()
+        self.widget.txtOutputData.setText('datafile1')
+        self.assertFalse(self.widget.onCheckOutputName())
+        self.assertIn(BG_COLOR_ERR, self.widget.txtOutputData.styleSheet())
+
+    def testFindId(self):
+        """ Test function to find id of file in list of filenames"""
+        data_for_id = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                                   dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        self.widget.filenames = {'datafile2': DataState(data_for_id),
+                                 'datafile1': DataState(data_for_id)}
+
+        id_out = self.widget._findId('datafile2')
+        self.assertEqual(id_out, 'datafile2')
+
+    def testExtractData(self):
+        """
+        Test function to extract data to be computed from input filenames
+        """
+        data1 = Data1D(x=[1.0, 2.0, 3.0], y=[11.0, 12.0, 13.0],
+                       dx=[0.1, 0.2, 0.3], dy=[0.1, 0.2, 0.3])
+
+        data2 = Data2D(image=[0.1] * 4,
+                                   qx_data=[1.0, 2.0, 3.0, 4.0],
+                                   qy_data=[10.0, 11.0, 12.0, 13.0],
+                                   dqx_data=[0.1, 0.2, 0.3, 0.4],
+                                   dqy_data=[0.1, 0.2, 0.3, 0.4],
+                                   q_data=[1, 2, 3, 4],
+                                   xmin=-1.0, xmax=5.0,
+                                   ymin=-1.0, ymax=15.0,
+                                   zmin=-1.0, zmax=20.0)
+
+        self.widget.filenames = {'datafile2': DataState(data2),
+                                 'datafile1': DataState(data1)}
+
+        output1D = self.widget._extractData('datafile1')
+        self.assertTrue(isinstance(output1D, Data1D))
+
+        output2D = self.widget._extractData('datafile2')
+        self.assertIsInstance(output2D, Data2D)
+
+    # TODO
+    def testOnPrepareOutputData(self):
+        """ """
+        pass
+
+    # new_item = GuiUtils.createModelItemWithPlot(
+    #     QtCore.QVariant(self.output), name=self.txtOutputData.text())
+    # new_datalist_item = {str(self.txtOutputData.text()) + str(time.time()):
+    #                          self.output}
+    # self.communicator. \
+    #     updateModelFromDataOperationPanelSignal.emit(new_item,
+    #                                                  new_datalist_item)
+
+
+if __name__ == "__main__":
+    unittest.main()
