class CreateSequenceWindow(QDialog): """ Window for creating a new sequence """ def __init__(self, motors={}, listOfSequenceHandler=None, sequence=None, modifySequence=False): """ Initializtion of the window for creating a new sequence :param motors: The dictionary of all the motors :param listOfSequenceHandler: The handler of the list of sequence """ QDialog.__init__(self) # Set the window icon appIcon = QIcon(icon) self.setWindowIcon(appIcon) ## Flag if the sequence is a modified one or a new one self.__modifySequence = modifySequence ## The handler of the list of sequence self.__listOfSequenceHandler = listOfSequenceHandler ## The dictionary of all the motors self.__motors = motors ## The new sequence self.__sequence = sequence ## A dictionary of the positions of the new sequence self.__wantedPositions = {} ## The layout of the create sequence window self.__layout = QVBoxLayout(self) ## The widget for the name of the sequenc self.nameEntry = QLineEdit() self.nameEntry.setText(self.__sequence.getName()) ## The label for the widget in which the name of the sequence is written self.__nameLabel = QLabel("Sequence Name") ## The list of the different moves that forms the sequence self.__listOfMoveLabels = QListWidget() moveNumber = 1 for move in self.__sequence.getMoves(): # Set text for the move label labelText = "move " + str(moveNumber) + ": " moveNumber += 1 for motor in self.__motors: labelText += self.__motors[motor].getName() + " " + \ str(move.getMotorPosition(self.__motors[motor].getName())) + ", " label = moveLabel(move, labelText, self.__motors) # insert label to the head of the list self.__listOfMoveLabels.insertItem(0, label) # Put the sliders of the create sequence window in a list ## List of sliders in the create sequence window self.listOfSliders = [] dictOfSlider = dict() for motor in self.__motors: dictOfSlider[motor] = QSlider(Qt.Horizontal) dictOfSlider[motor].setMaximum(4095) dictOfSlider[motor].setValue( self.__motors[motor].getCurrentPosition()) dictOfSlider[motor].sliderMoved.connect( self.__motors[motor].setGoalPosition) self.listOfSliders.append(dictOfSlider[motor]) ## Message to make the user put a name to the sequence self.__noNameMessage = QMessageBox() self.__noNameMessage.setIcon(QMessageBox.Warning) self.__noNameMessage.setWindowIcon(appIcon) self.__noNameMessage.setText( "Please name your sequence before saving it") self.__noNameMessage.setStandardButtons(QMessageBox.Ok) # Renable the create sequence window and closes the message self.__noNameMessage.accepted.connect(self.enableWindow) ## Warning message to make sure the user doen't want to save the sequence self.__warningMessage = QMessageBox() self.__warningMessage.setIcon(QMessageBox.Warning) self.__warningMessage.setWindowIcon(appIcon) self.__warningMessage.setText( "Are you sure you want to close this window? Your sequence will not be saved" ) self.__warningMessage.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # Close the create sequence window and the message self.__warningMessage.accepted.connect(self.reject) # Renable the create sequence window and closes the message self.__warningMessage.rejected.connect(self.enableWindow) # Set the text for the labels ## Labels for the motors in the UI self.__motorLabels = [] for motorNumber in range(0, len(motors)): self.__motorLabels.append( QLabel("Motor " + str(motorNumber + 1) + " position")) ## Button to add a move to the sequence and procede to the next move self.nextMoveButton = QPushButton("Save Move") ## Buttons to accept or cancel the creation of a sequence self.buttonBox = QDialogButtonBox() self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) # If ok pressed add the sequence to the list self.buttonBox.accepted.connect( lambda: self.addSequenceToList(self.__modifySequence)) # If cancel pressed close the create sequence window self.buttonBox.rejected.connect(self.__warningMessage.exec) # Renable the main window when the create sequence closes self.rejected.connect(self.__listOfSequenceHandler.enableUi) self.accepted.connect(self.__listOfSequenceHandler.enableUi) self.nextMoveButton.clicked.connect(self.addMovetoSequence) self.__listOfMoveLabels.itemDoubleClicked.connect( self.moveDoubleClicked) # Build the vertical layout with the different widgets self.__layout.addWidget(self.__nameLabel) self.__layout.addWidget(self.nameEntry) self.__layout.addWidget(self.__listOfMoveLabels) for motorNumber in range(len(self.__motors)): self.__layout.addWidget(self.__motorLabels[motorNumber]) self.__layout.addWidget(self.listOfSliders[motorNumber]) self.__layout.addWidget(self.nextMoveButton) self.__layout.addWidget(self.buttonBox) # Connect the qwidgetlist to the custom right click menu self.__listOfMoveLabels.setContextMenuPolicy(Qt.CustomContextMenu) self.__listOfMoveLabels.customContextMenuRequested.connect( self.rightClickMenu) def setName(self, name): """ Sets the name of the sequence with the user input :param name: The name of the sequence :return: No return """ self.__sequence.setName(name) def getSequence(self): """ Accessor of the sequence (for tests) :return: the sequence """ return self.__sequence def getListofMoveLabels(self): """ Accessor of the list of move labels (for tests) :return: the list of move labels """ return self.__listOfMoveLabels def addSequenceToList(self, modifySequence=False): """ Add the sequence to the list of sequence :param modifySequence: bool, if true there's a selected sequence that needs to be modified if false it's a new sequence :return: No return """ # TODO: look to move this method to the list of sequence handler # TODO: don't let the user enter a sequence that has the same name as an old one self.setName(self.nameEntry.text()) if self.__sequence.getName() != "": # Load previously saved sequences try: with open('SaveSequence.json') as save: savedListOfSequences = json.load(save) except FileNotFoundError: savedListOfSequences = [] if modifySequence: # Get the item that needs to be modified # selectedSequence = self.__listOfSequenceHandler.getSelectedItems() # Find the selected sequence in the list of saved ones for sequence in savedListOfSequences: if self.__sequence.getName() in sequence: indexOfTheSequence = savedListOfSequences.index( sequence) # remove the unmodified sequence to insert the modified sequence savedListOfSequences.remove(sequence) self.__listOfSequenceHandler.addItem(self.__sequence) # Make the sequence json seriable newSequence = dict() newSequence[self.__sequence.getName()] = [] for moveNumber in range(len( self.__sequence.getMoves())): newSequence[self.__sequence.getName()].append( self.__sequence.getMoves() [moveNumber].getMovePositions()) # Append new sequence to the old ones savedListOfSequences.insert(indexOfTheSequence, newSequence) # Write the sequences to the file with open('SaveSequence.json', 'w') as outfile: json.dump(savedListOfSequences, outfile) self.accept() else: self.__listOfSequenceHandler.addItem(self.__sequence) # Make the sequence json seriable newSequence = dict() newSequence[self.__sequence.getName()] = [] for moveNumber in range(len(self.__sequence.getMoves())): newSequence[self.__sequence.getName()].append( self.__sequence.getMoves() [moveNumber].getMovePositions()) # Append new sequence to the old ones savedListOfSequences.append(newSequence) # Write the sequences to the file with open('SaveSequence.json', 'w') as outfile: json.dump(savedListOfSequences, outfile) self.accept() else: self.setEnabled(False) self.__noNameMessage.exec_() def addMovetoSequence(self): """ Add the last move to the sequence :return: No return """ move = None labelToModify = None # Check if there's a move in modifying state for row in range(self.__listOfMoveLabels.count()): if not self.__listOfMoveLabels.item(row).getMove().isNew: move = self.__listOfMoveLabels.item(row).getMove() labelToModify = self.__listOfMoveLabels.item(row) break # verify if the move is a new one if move is None: # Create the new move and set his positions move = Move(self.__motors) i = 0 for motorName in self.__motors: move.setMotorPosition(motorName, self.listOfSliders[i].value()) i += 1 self.__sequence.addMove(move) # Set text for the move label labelText = "move " + str( self.__sequence.getNumberofMoves()) + ": " i = 0 for motor in self.__motors: labelText += self.__motors[motor].getName() + " " +\ str(self.listOfSliders[i].value()) + ", " i += 1 label = moveLabel(move, labelText, self.__motors) # insert label to the head of the list self.__listOfMoveLabels.insertItem(0, label) else: # modify the move i = 0 for motorName in self.__motors: move.setMotorPosition(motorName, self.listOfSliders[i].value()) i += 1 # modify the label of the move textToEdit = labelToModify.text() listOfTextToEdit = textToEdit.split(' ') labelText = listOfTextToEdit[0] + " " + listOfTextToEdit[1] + " " i = 0 for motor in self.__motors: labelText += self.__motors[motor].getName() + " " + \ str(self.listOfSliders[i].value()) + ", " i += 1 labelToModify.setText(labelText) labelToModify.setSelected(False) labelToModify.setBackground(Qt.white) # reset the state of the move move.isNew = True # Access the move positions when double clicked on def moveDoubleClicked(self, moveItem): """ Called when a move in the sequence is double clicked :param moveItem: the move that was double clicked :return: No return """ moveItem.goToMoveOfTheLabel() self.updateSlidersPositions() def updateSlidersPositions(self): counterMotors = 0 for motor in self.__motors: self.listOfSliders[counterMotors].setValue( self.__motors[motor].getGoalPosition()) counterMotors += 1 def enableWindow(self): """ Enable the create sequence window :return: """ self.setEnabled(True) def rightClickMenu(self, event): """ The right click menu of the move list :param event: The event (here right click) that makes the menu come up :return: No return """ menu = QMenu() # Add a button in the menu that when clicked, it puts a move in modifying state menu.addAction( "Modify Move", lambda: self.modifyMove(self.__listOfMoveLabels. selectedItems()[0])) # Add a button in the menu that when clicked, it deletes a move in the list menu.addAction( "Delete Move", lambda: self.deleteMove(self.__listOfMoveLabels. selectedItems()[0])) menu.exec_(self.__listOfMoveLabels.mapToGlobal(event)) def deleteMove(self, label): """ Delete a move and its label of the sequence :param label: label of the move :return: No return """ # remove the label from the list self.__listOfMoveLabels.takeItem(self.__listOfMoveLabels.row(label)) # remove the move from the sequence self.__sequence.deleteMove(label.getMove()) # rename the labels in the list of moves for index in range(self.__sequence.getNumberofMoves() - 1, -1, -1): labelToModify = self.__listOfMoveLabels.item(index) textToEdit = labelToModify.text() listOfTextToEdit = textToEdit.split(' ') listOfTextToEdit[1] = str(self.__sequence.getNumberofMoves() - index) + ':' textToEdit = ' '.join(listOfTextToEdit) self.__listOfMoveLabels.item(index).setText(textToEdit) def modifyMove(self, label): """ Put a move to a modified state :param label: label of the move :return: No return """ # Check if there's a move in modifying state for row in range(self.__listOfMoveLabels.count()): if not self.__listOfMoveLabels.item(row).getMove().isNew: self.__listOfMoveLabels.item(row).getMove().isNew = True self.__listOfMoveLabels.item(row).setBackground( QBrush(Qt.white)) moveToModify = label.getMove() moveToModify.isNew = False label.setBackground(QBrush(Qt.darkCyan)) label.goToMoveOfTheLabel() self.updateSlidersPositions()
class FileSystemWidget(QWidget, DirectoryObserver): """ Widget for listing directory contents and download files from the RDP client. """ # fileDownloadRequested(file, targetPath, dialog) fileDownloadRequested = Signal(File, str, FileDownloadDialog) def __init__(self, root: Directory, parent: QObject = None): """ :param root: root of all directories. Directories in root will be displayed with drive icons. :param parent: parent object. """ super().__init__(parent) self.root = root self.breadcrumbLabel = QLabel() self.titleLabel = QLabel() self.titleLabel.setStyleSheet("font-weight: bold") self.titleSeparator: QFrame = QFrame() self.titleSeparator.setFrameShape(QFrame.HLine) self.listWidget = QListWidget() self.listWidget.setSortingEnabled(True) self.listWidget.setContextMenuPolicy(Qt.CustomContextMenu) self.listWidget.customContextMenuRequested.connect(self.onCustomContextMenu) self.verticalLayout = QVBoxLayout() self.verticalLayout.addWidget(self.breadcrumbLabel) self.verticalLayout.addWidget(self.listWidget) self.setLayout(self.verticalLayout) self.listWidget.itemDoubleClicked.connect(self.onItemDoubleClicked) self.currentPath: Path = Path("/") self.currentDirectory: Directory = root self.listCurrentDirectory() self.currentDirectory.addObserver(self) def setWindowTitle(self, title: str): """ Set the window title. When the title is not blank, a title label and a separator is displayed. :param title: the new title. """ previousTitle = self.windowTitle() super().setWindowTitle(title) self.titleLabel.setText(title) if previousTitle == "" and title != "": self.verticalLayout.insertWidget(0, self.titleLabel) self.verticalLayout.insertWidget(1, self.titleSeparator) elif title == "" and previousTitle != "": self.verticalLayout.removeWidget(self.titleLabel) self.verticalLayout.removeWidget(self.titleSeparator) # noinspection PyTypeChecker self.titleLabel.setParent(None) # noinspection PyTypeChecker self.titleSeparator.setParent(None) def onItemDoubleClicked(self, item: FileSystemItem): """ Handle double-clicks on items in the list. When the item is a directory, the current path changes and the contents of the directory are listed. Files are ignored. :param item: the item that was clicked. """ if not item.isDirectory() and not item.isDrive(): return if item.text() == "..": self.currentPath = self.currentPath.parent else: self.currentPath = self.currentPath / item.text() self.listCurrentDirectory() def listCurrentDirectory(self): """ Refresh the list widget with the current directory's contents. """ node = self.root for part in self.currentPath.parts[1 :]: node = next(d for d in node.directories if d.name == part) self.listWidget.clear() self.breadcrumbLabel.setText(f"Location: {str(self.currentPath)}") if node != self.root: self.listWidget.addItem(FileSystemItem("..", FileSystemItemType.Directory)) for directory in node.directories: self.listWidget.addItem(FileSystemItem(directory.name, directory.type)) for file in node.files: self.listWidget.addItem(FileSystemItem(file.name, file.type)) if node is not self.currentDirectory: self.currentDirectory.removeObserver(self) node.addObserver(self) self.currentDirectory = node node.list() def onDirectoryChanged(self): """ Refresh the directory view when the directory has changed. """ self.listCurrentDirectory() def currentItemText(self) -> str: try: return self.listWidget.selectedItems()[0].text() except IndexError: return "" def selectedFile(self) -> Optional[File]: text = self.currentItemText() if text == "": return None if text == "..": return self.currentDirectory.parent for sequence in [self.currentDirectory.files, self.currentDirectory.directories]: for file in sequence: if text == file.name: return file return None def canDownloadSelectedItem(self) -> bool: return self.selectedFile().type == FileSystemItemType.File def onCustomContextMenu(self, localPosition: QPoint): """ Show a custom context menu with a "Download file" action when a file is right-clicked. :param localPosition: position where the user clicked. """ selectedFile = self.selectedFile() if selectedFile is None: return globalPosition = self.listWidget.mapToGlobal(localPosition) downloadAction = QAction("Download file") downloadAction.setEnabled(selectedFile.type in [FileSystemItemType.File]) downloadAction.triggered.connect(self.downloadFile) itemMenu = QMenu() itemMenu.addAction(downloadAction) itemMenu.exec_(globalPosition) def downloadFile(self): file = self.selectedFile() if file.type != FileSystemItemType.File: return filePath = file.getFullPath() targetPath, _ = QFileDialog.getSaveFileName(self, f"Download file {filePath}", file.name) if targetPath != "": dialog = FileDownloadDialog(filePath, targetPath, self) dialog.show() self.fileDownloadRequested.emit(file, targetPath, dialog)
class ReportListComponent(QGroupBox): currentAnalysisChanged = Signal(object) def __init__(self): super().__init__() self.setTitle("Rapports disponibles") main_layout = QVBoxLayout(self) self._list = QListWidget() self._list.setContextMenuPolicy(Qt.CustomContextMenu) self._list.customContextMenuRequested.connect(self._showItemMenu) self._list.currentRowChanged.connect(self._currentAnalysisChanged) self._list.itemDoubleClicked.connect(lambda item: self._renameItem()) self._analysis = None main_layout.addWidget(self._list) def reset(self, analysis: Analysis): self._current_analysis_file = None self._analysis = analysis self._list.clear() for analysis in HistoryManager.analysisList(): item = QListWidgetItem("%s (%s)" % (analysis["name"], analysis["date"])) item.setData(Qt.UserRole, analysis["file"]) item.setData(Qt.UserRole + 1, analysis["name"]) self._list.addItem(item) self._list.setCurrentRow(0) @Slot(int) def _currentAnalysisChanged(self, row: int): if row < 0: return new_analysis_file = self._list.item(row).data(Qt.UserRole) if self._current_analysis_file == new_analysis_file: return if self._current_analysis_file is None: self._current_analysis_file = new_analysis_file return self._current_analysis_file = new_analysis_file self._analysis = HistoryManager.loadAnalysis(new_analysis_file) self.currentAnalysisChanged.emit(self._analysis) @Slot(QPoint) def _showItemMenu(self, pos: QPoint): globalPos = self._list.mapToGlobal(pos) actions_menu = QMenu() actions_menu.addAction("Renommer", self._renameItem) actions_menu.addAction("Supprimer", self._eraseItem) actions_menu.exec_(globalPos) @Slot() def _renameItem(self): item = self._list.currentItem() input_dialog = QInputDialog(self.parentWidget(), Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) font = input_dialog.font() font.setPixelSize(16) input_dialog.setFont(font) input_dialog.setMinimumWidth(300) input_dialog.setInputMode(QInputDialog.TextInput) input_dialog.setWindowTitle("Renommer l'analyse") input_dialog.setLabelText("Nouveau nom pour '%s' :" % item.data(Qt.UserRole + 1)) input_dialog.setTextValue(item.data(Qt.UserRole + 1)) input_dialog.setOkButtonText("OK") input_dialog.setCancelButtonText("Annuler") if not input_dialog.exec_(): return new_name = input_dialog.textValue() if self._analysis is None: return if new_name == self._analysis.parameters().name(): return regexp = QRegExp("^[a-zA-Z0-9_-#éèêëàîï ]{5,30}$") if not regexp.exactMatch(new_name): QMessageBox.warning(self, "Nouveau nom invalide", "Caractères autorisés : alphanumérique, espace, #, - et _ avec une longueur maximale de 30 caractères") return self._analysis.parameters().setName(new_name) HistoryManager.renameAnalysis(item.data(Qt.UserRole), self._analysis) current_row = self._list.currentRow() self.reset(self._analysis) self._list.setCurrentRow(current_row) self.currentAnalysisChanged.emit(self._analysis) @Slot() def _eraseItem(self): item = self._list.currentItem() message_box = QMessageBox() message_box.setIcon(QMessageBox.Warning) message_box.setWindowTitle("Supprimer une analyse ?") message_box.setText("Voulez vous vraiment supprimer l'analyse '%s' de façon définitive ?" % item.data(Qt.UserRole + 1)) message_box.setInformativeText("Assurez vous d'avoir exportez toutes les données dont vous avez besoin.") message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) ret = message_box.exec_() if ret == QMessageBox.Yes: HistoryManager.deleteAnalysis(item.data(Qt.UserRole)) self._list.takeItem(self._list.currentRow()) if self._list.currentRow() == -1: self.currentAnalysisChanged.emit(None)