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()