def actionImport(self):
        if not self._checkHasOpenFile():
            return
        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getOpenFileNameAndFilter(parent=self.mainWindow, caption='Import from HDF5 file',
                                                           directory=str(os.getcwd()), filter='*.h5')
        fileName = str(dialogReturn[0])
        if not fileName:
            return
        h5file = h5py.File(fileName)
        devices = ShotPrepToolModel.getListOfDevices(h5file)
        dialog = ListSelectionDialog(self.mainWindow)
        dialog.addItems(devices)
        response = dialog.exec_()
        if response == QtGui.QDialog.Accepted:
            checkedItems = dialog.getCheckedItems()
            if set(checkedItems).intersection(self.model.returnModelsInFile().keys()):
                if (self._verifyOverwriteExistingDevices(checkedItems)):
                    self.model.importDevices(dialog.getCheckedItems(), h5file)
                    self._clearTabs()
                    self._initTabs(self.model.returnModelsInFile())
                    self._modelChanged()
            else:
                self.model.importDevices(dialog.getCheckedItems(), h5file)
                self._clearTabs()
                self._initTabs(self.model.returnModelsInFile())
                self._modelChanged()

        h5file.close()
 def setUp(self):
     h5pathname = 'test_file.h5'
     self.h5file = h5py.File(h5pathname)
     devices = self.h5file.create_group('devices')
     RGA = devices.create_group('RGA')
     MOT = devices.create_group('MOT')
     RGA['test data point'] = '1'
     MOT['test data point 2'] = '2'
     self.h5file.close()
     self.testModel = ShotPrepToolModel(h5pathname)
 def actionNew(self):
     if self._checkShouldDiscardAnyUnsavedChanges():
         fileDialog = QtGui.QFileDialog(self.mainWindow)
         dialogReturn = fileDialog.getSaveFileNameAndFilter(parent=self.mainWindow, caption='New HDF5 file',
                                                            directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
         if dialogReturn[0]:
             self._close()
             self.fileName = str(dialogReturn[0])
             self.model = ShotPrepToolModel(self.fileName)
             self._initTabs(self.model.returnModelsInFile())
             self._modelSaved()
    def actionSave_As(self):
        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getSaveFileNameAndFilter(parent=self.mainWindow, caption='Save As HDF5 file',
                                                           directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
        fileName = str(dialogReturn[0])
        if fileName:
            self.model.saveAs(fileName)
            self._close()
            self.fileName = fileName
            self._modelSaved()

            self.model = ShotPrepToolModel(self.fileName)
            self._initTabs(self.model.returnModelsInFile())
    def actionOpen(self):
        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getOpenFileNameAndFilter(parent=self.mainWindow, caption='Open existing HDF5 file',
                                                           directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
        fileName = str(dialogReturn[0])

        if fileName:
            self.actionClose()
            self.fileName = fileName
            try:
                self.model = ShotPrepToolModel(self.fileName)
            except RuntimeError as e:
                self._warnUser('File locked', e.message)
                return
            self._initTabs(self.model.returnModelsInFile())
            self._modelSaved()
 def actionNew(self):
     if self._checkShouldDiscardAnyUnsavedChanges():
         fileDialog = QtGui.QFileDialog(self.mainWindow)
         dialogReturn = fileDialog.getSaveFileNameAndFilter(parent=self.mainWindow, caption='New HDF5 file',
                                                            directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
         filename = str(dialogReturn[0])
         if filename:
             if os.path.exists(filename):
                 os.remove(filename)
             self._close()
             self.fileName = str(dialogReturn[0])
             self.model = ShotPrepToolModel(self.fileName)
             try:
                 self.model.importFromDefaults()
             except Exception as e:
                 self._warnUser('Error importing defaults', e.message)
             self._initTabs(self.model.returnModelsInFile())
             self._modelSaved()
class ShotPreparationToolUi(object):
    def __init__(self, application):
        self.fileName = None
        self.model = None

        self.mainWindow = QtGui.QMainWindow()
        self.uiForm = Ui_MainWindow()
        self.uiForm.setupUi(self.mainWindow)
        self.app = application
        self._initUI()
        self._connectButtons()
        self._initShortcuts()
        self._hookCloseEvent()
        self.unsavedChanges = False

    def _initUI(self):
        self.app.setStyle("Plastique")

    def _connectButtons(self):
        form = self.uiForm

        form.actionNew.triggered.connect(self.actionNew)
        form.actionOpen.triggered.connect(self.actionOpen)
        form.actionSave.triggered.connect(self.actionSave)
        form.actionSave_As.triggered.connect(self.actionSave_As)
        form.actionClose.triggered.connect(self.actionClose)
        form.actionExit.triggered.connect(self.actionExit)
        form.actionAddDevice.triggered.connect(self.actionAddDevice)
        form.actionRemoveDevice.triggered.connect(self.actionRemoveDevice)
        form.actionRemoveRow.triggered.connect(self.actionRemoveRow)
        form.actionImport.triggered.connect(self.actionImport)
        form.actionRename.triggered.connect(self.actionRename)

    def _setTitle(self):
        if self.fileName is not None:
            pathLeaf = os.path.basename(self.fileName)
            if self.unsavedChanges:
                pathLeaf += '*'
            self.mainWindow.setWindowTitle('%s - QDG Lab Shot Preparation Tool' % pathLeaf)
        else:
            self.mainWindow.setWindowTitle('QDG Lab Shot Preparation Tool')

    def _checkShouldDiscardAnyUnsavedChanges(self):
        if self.unsavedChanges:
            messageBox = QtGui.QMessageBox()
            response = messageBox.question(self.mainWindow, 'Unsaved changes',
                                           'You have unsaved changes.  Are you sure you wish to continue?',
                                            QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel,
                                            QtGui.QMessageBox.Cancel)
            if response == QtGui.QMessageBox.Cancel:
                return False
        return True

    def _checkShouldRemoveDevice(self):
        messageBox = QtGui.QMessageBox()
        response = messageBox.question(self.mainWindow, 'Remove device',
                                       'Are you sure you wish to remove this device?',
                                       QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Yes,
                                       QtGui.QMessageBox.Cancel)
        if response == QtGui.QMessageBox.Yes:
            return True
        else:
            return False

    def _checkHasOpenFile(self):
        if self.model is None:
            self._warnUser('Please load first', 'Please open an H5 file first.')
            return False
        return True

    def _hookCloseEvent(self):
        def handleCloseEvent(event):
            if self._checkShouldDiscardAnyUnsavedChanges():
                self._close()
                event.accept()
            else:
                event.ignore()

        self.mainWindow.closeEvent = handleCloseEvent
        self.app.closeEvent = handleCloseEvent

    def _modelChanged(self):
        self.unsavedChanges = True
        self._setTitle()

    def _modelSaved(self):
        self.unsavedChanges = False
        self._setTitle()

    def actionNew(self):
        if self._checkShouldDiscardAnyUnsavedChanges():
            fileDialog = QtGui.QFileDialog(self.mainWindow)
            dialogReturn = fileDialog.getSaveFileNameAndFilter(parent=self.mainWindow, caption='New HDF5 file',
                                                               directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
            filename = str(dialogReturn[0])
            if filename:
                if os.path.exists(filename):
                    os.remove(filename)
                self._close()
                self.fileName = str(dialogReturn[0])
                self.model = ShotPrepToolModel(self.fileName)
                try:
                    self.model.importFromDefaults()
                except Exception as e:
                    self._warnUser('Error importing defaults', e.message)
                self._initTabs(self.model.returnModelsInFile())
                self._modelSaved()

    def actionOpen(self):
        if not self._checkShouldDiscardAnyUnsavedChanges():
            return

        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getOpenFileNameAndFilter(parent=self.mainWindow, caption='Open existing HDF5 file',
                                                           directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
        fileName = str(dialogReturn[0])

        if fileName:
            self.actionClose()
            self.fileName = fileName
            try:
                self.model = ShotPrepToolModel(self.fileName)
            except RuntimeError as e:
                self._warnUser('Error opening H5 file', e.message)
                return
            self._initTabs(self.model.returnModelsInFile())
            self._modelSaved()

    def actionSave(self):
        if self.fileName is not None:
            self.model.saveChanges()
            self._modelSaved()

    def actionSave_As(self):
        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getSaveFileNameAndFilter(parent=self.mainWindow, caption='Save As HDF5 file',
                                                           directory=str(os.getcwd()), filter=H5_FILE_EXTENSION)
        fileName = str(dialogReturn[0])
        if fileName:
            self.model.saveAs(fileName)
            self._close()
            self.fileName = fileName
            self._modelSaved()

            self.model = ShotPrepToolModel(self.fileName)
            self._initTabs(self.model.returnModelsInFile())

    def actionClose(self):
        if self.model is not None and self._checkShouldDiscardAnyUnsavedChanges():
            self._close()

    def _close(self):
        if self.model is not None:
            self._clearTabs()
            self.model.cleanUp()
            self.model = None
            self.fileName = None
            self._modelSaved()

    def actionExit(self):
        if self.model is not None and self._checkShouldDiscardAnyUnsavedChanges():
            self._close()
            self.app.quit()

    def actionAddDevice(self):
        if self._checkHasOpenFile():
            dialog = QtGui.QInputDialog(self.mainWindow)
            response = dialog.getText(self.mainWindow, 'Add group', 'Enter name of device:')
            groupName = response[0]
            if groupName:
                try:
                    self.model.addDevice(str(groupName))
                except KeyError as e:
                    self._warnUser('Device name in use', e.message)
                    return
                except SyntaxError as e:
                    self._warnUser('Invalid device name', e.message)
                self._initTabs(self.model.returnModelsInFile())
                self._changeCurrentTab(str(groupName))
                self._modelChanged()

    def actionRemoveDevice(self):
        if self._checkHasOpenFile() and self._checkShouldRemoveDevice():
            currentTab = self.uiForm.tabWidget.currentWidget()
            deviceName = str(currentTab.windowTitle())
            self.model.removeDevice(deviceName)
            self._initTabs(self.model.returnModelsInFile())
            self._modelChanged()

    def actionRemoveRow(self):
        if self._checkHasOpenFile():
            currentTab = self.uiForm.tabWidget.currentWidget()
            table = currentTab.findChild(QtGui.QTableView)
            selected = table.selectedIndexes()
            keyIndices = [i.sibling(i.row(), 0) for i in selected]
            model = table.model()
            for index in keyIndices:
                name = model.data(index, role=QtCore.Qt.DisplayRole)
                model.removeRowByName(name)
            if keyIndices:
                self._modelChanged()

    def _verifyOverwriteExistingDevices(self, checkedItems):
        messageBox = QtGui.QMessageBox()
        response = messageBox.question(self.mainWindow, 'Overwriting existing device',
                                       'One or more devices to be imported were already found.  These devices will be overwritten.',
                                        QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel,
                                        QtGui.QMessageBox.Cancel)
        if response == QtGui.QMessageBox.Cancel:
            return False
        return True

    def actionImport(self):
        if not self._checkHasOpenFile():
            return
        fileDialog = QtGui.QFileDialog(self.mainWindow)
        dialogReturn = fileDialog.getOpenFileNameAndFilter(parent=self.mainWindow, caption='Import from HDF5 file',
                                                           directory=str(os.getcwd()), filter='*.h5')
        fileName = str(dialogReturn[0])
        if not fileName:
            return
        h5file = h5py.File(fileName)
        devices = ShotPrepToolModel.getListOfDevices(h5file)
        dialog = ListSelectionDialog(self.mainWindow)
        dialog.addItems(devices)
        response = dialog.exec_()
        if response == QtGui.QDialog.Accepted:
            checkedItems = dialog.getCheckedItems()
            if set(checkedItems).intersection(self.model.returnModelsInFile().keys()):
                if (self._verifyOverwriteExistingDevices(checkedItems)):
                    self.model.importDevices(dialog.getCheckedItems(), h5file)
                    self._clearTabs()
                    self._initTabs(self.model.returnModelsInFile())
                    self._modelChanged()
            else:
                self.model.importDevices(dialog.getCheckedItems(), h5file)
                self._clearTabs()
                self._initTabs(self.model.returnModelsInFile())
                self._modelChanged()

        h5file.close()

    def show(self):
        self.mainWindow.show()

    def _clearTabs(self):
        self.uiForm.tabWidget.clear()

    def _initTabs(self, models):
        self._connectModelSignals(models)
        self._clearTabs()
        tabWidget = self.uiForm.tabWidget
        for title, model in models.items():
            page = QtGui.QWidget()
            layout = QtGui.QHBoxLayout(page)

            tableView = QtGui.QTableView(page)
            tableView.horizontalHeader().setResizeMode(1) #fit to width
            tableView.horizontalHeader().setVisible(False)
            tableView.verticalHeader().setVisible(False)
            tableView.setFont(QtGui.QFont("Courier New"))
            tableView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)

            layout.addWidget(tableView)
            tableView.setModel(model)
            tabWidget.addTab(page, title)
            page.setWindowTitle(title)

    def _connectModelSignals(self, models):
        for _, model in models.items():
            model.dataChanged.connect(self._modelChanged)
            model.rowsInserted.connect(self._modelChanged)
            model.rowsRemoved.connect(self._modelChanged)

    def _warnUser(self, title, message):
        warningDialog = QtGui.QMessageBox(self.mainWindow)
        warningDialog.warning(self.mainWindow, title, message)

    def actionRename(self):
        if self._checkHasOpenFile():
            dialog = QtGui.QInputDialog(self.mainWindow)
            response = dialog.getText(self.mainWindow, 'Rename device', 'Enter new name:')
            newDeviceName = str(response[0])

            currentTab = self.uiForm.tabWidget.currentWidget()
            oldDeviceName = str(currentTab.windowTitle())

            if oldDeviceName != newDeviceName:
                try:
                    self.model.renameDevice(oldDeviceName, newDeviceName)
                except KeyError:
                    self._warnUser("Error renaming device", "Device with name \'%s\' already exists." % newDeviceName)
                except SyntaxError:
                    self._warnUser("Error renaming device", "Device name \'%s\' is not a valid Python variable name." % newDeviceName)
                else:
                    self._initTabs(self.model.returnModelsInFile())
                    self._modelChanged()

    def _changeCurrentTab(self, name):
        tabWidget = self.uiForm.tabWidget
        for index in range(tabWidget.count()):
            tab = tabWidget.widget(index)
            if tab.windowTitle() == name:
                tabWidget.setCurrentIndex(index)
                break
        else:
            raise RuntimeError("Could not find tab with title %s" % name)

    def _initShortcuts(self):
        self._saveShortcut = QtGui.QShortcut(QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_S), self.mainWindow, self.actionSave)
class TestShotPrepToolModel(unittest.TestCase):
    #test case #1: make sure that it will import and create files (h5 files)
    #test case #2: make sure that it will copy from the original file to the test file
    #test case #3: check that discard changes works as expected
    #test case #4: check that save works as expected
    #test case #5: check that cleanup works as expected
    #test case #6: check that adding a row will work
    #test case #7: check that adding in a new device will work
    #test case #8: check that removing a group will work
    #test case #9: check that removing a row will work
    def setUp(self):
        h5pathname = 'test_file.h5'
        self.h5file = h5py.File(h5pathname)
        devices = self.h5file.create_group('devices')
        RGA = devices.create_group('RGA')
        MOT = devices.create_group('MOT')
        RGA['test data point'] = '1'
        MOT['test data point 2'] = '2'
        self.h5file.close()
        self.testModel = ShotPrepToolModel(h5pathname)

    def tearDown(self):
        self.testModel.cleanUp()
        if os.path.exists('test_file.h5'):
            os.remove('test_file.h5')

    def test_InitWillLoadModel(self):
        devices = self.testModel.returnModelsInFile()
        self.assertIsInstance(devices['RGA'], GroupTableModel)
        self.assertIsInstance(devices['MOT'], GroupTableModel)

    def test_ModelActuallyHasTheData(self):
        devices = self.testModel.returnModelsInFile()
        self.assertEqual(devices['RGA'].group['test data point'][()], '1')
        self.assertEqual(devices['MOT'].group['test data point 2'][()], '2')

    def test_DiscardChangesWillRevertChanges(self):
        devices = self.testModel.returnModelsInFile()

        devices['RGA'].group['test data point'][()] = '2'
        self.assertEqual(devices['RGA'].group['test data point'][()], '2')

        devices['MOT'].group['test data point 2'][()] = '1'
        self.assertEqual(devices['MOT'].group['test data point 2'][()], '1')

        self.testModel.discardCharges()
        devices = self.testModel.returnModelsInFile()

        self.assertEqual('1', devices['RGA'].group['test data point'][()])
        self.assertEqual('2', devices['MOT'].group['test data point 2'][()])

    def test_SaveChangesWillSaveChanges(self):
        devices = self.testModel.returnModelsInFile()

        devices['RGA'].group['test data point'][()] = '2'
        self.assertEqual(devices['RGA'].group['test data point'][()], '2')
        devices['MOT'].group['test data point 2'][()] = '1'
        self.assertEqual(devices['MOT'].group['test data point 2'][()], '1')

        self.testModel.saveChanges()

        saveFile = h5py.File('test_file.h5')
        self.assertEqual('2', saveFile['devices/RGA/test data point'][()])
        self.assertEqual('1', saveFile['devices/MOT/test data point 2'][()])
        saveFile.close()