class HarmTemplate(QWidget): def __init__(self, node, parent=None): super(HarmTemplate, self).__init__(parent) self.attrs = [ "decay", "termination", "numWaves", "amplitude", "waveLength" ] self.lbl = QLabel("Wave:", self) self.wave = WaveDraw(self) self.layout = QVBoxLayout(self) self.layout.insertWidget(0, self.wave) self.layout.insertWidget(0, self.lbl) self.scriptJobs = None self._node = None self.setNode(node) def updateUI(self): ''' Load the UI values from the node ''' vals = [cmds.getAttr(self._node + "." + a) for a in self.attrs] self.wave.updateValues(*vals) def _buildScriptJobs(self): self.scriptJobs = [] for attr in self.attrs: name = '{0}.{1}'.format(self._node, attr) self.scriptJobs.append( cmds.scriptJob(ac=[name, self.updateUI], killWithScene=1)) def _killScriptJobs(self): for sj in self.scriptJobs: for _ in range(10): try: if cmds.scriptJob(exists=sj): cmds.scriptJob(kill=sj, force=True) break except RuntimeError: #This happens very rarely when that scriptJob is #being executed at the same time we try to kill it. cmds.warning( "Got RuntimeError trying to kill scriptjob...trying again" ) time.sleep(0.1) else: cmds.warning("Killing scriptjob is taking too long...skipping") self.scriptJobs = None def setNode(self, node): ''' This widget should now represent the same attr on a different node. ''' oldNode = self._node self._node = node self.updateUI() if self.scriptJobs is None: self._buildScriptJobs() elif oldNode != self._node: self._killScriptJobs() self._buildScriptJobs()
def _build_ui(self): layout = QVBoxLayout() self.results = QTextBrowser() font = QFontDatabase.systemFont(QFontDatabase.FixedFont) self.results.setFont(font) layout.insertWidget(0, self.results, 1) self.ui_area.setLayout(layout) self.manage(None)
class Deck(QVBoxLayout): def __init__(self, heading, color=True): super().__init__() logging.info(f'[Deck] {heading} init') self.addWidget(Heading(heading)) self.use_color = color self.cards = [] self.buttons = [] self.heading = heading self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.addWidget(self.scroll_area) self.v_scroll = QVBoxLayout() self.v_scroll.setSpacing(SPACING) self.v_scroll.addStretch() self.scroll_widget = QWidget() self.scroll_widget.setLayout(self.v_scroll) self.scroll_area.setFixedWidth(WIDTH_WITH_SCROLL) self.scroll_area.setWidget(self.scroll_widget) def add_card_button(self, card): return self.insert_button_at_index(card, 0) def insert_button_at_index(self, card, index): button = CardButton(card) self.v_scroll.insertWidget(index, button) color = COLOR[card.color] if self.use_color else COLOR['gray'] button.set_color(color) self.cards.insert(index, card.name) self.buttons.append(button) return button def remove_card_button(self, button): self.cards.remove(button.card.name) self.buttons.remove(button) self.removeWidget(button) button.deleteLater() def clear(self): logging.info(f'[Deck] clear {self.heading}') for button in self.buttons: button.deleteLater() self.cards.clear() self.buttons.clear()
def addPortWidget(self, port: 'Port', layout: QVBoxLayout): """ Add a single port editor to a specified layout. The layout provided should be the layout for either the input or output port editors. :param port: The port to create the editor widget for. :type port: Port :param layout: The layout to place the editor widget in. :type layout: QVBoxLayout :return: None :rtype: NoneType """ if layout == self.ui.outputLayout: pew = PortEditorWidget(port, allowOptional=False) else: pew = PortEditorWidget(port, allowOptional=True) layout.insertWidget(0, pew)
def __init__(self): QObject.__init__(self) # Initialize visual elements self.mainWindow = MainWindow() self.mainContent = MainContent() # Add Main Content ot Main Window layout = QVBoxLayout() layout.insertWidget(0, self.mainContent) self.mainWindow.ui.centralwidget.setLayout(layout) # Initialize the menu actions self.__initializeMenuActions() # Initialize the button and their signals self.__initializeButtons() # Initialize the table widget self.__initializeTableWidget()
def _build_ui(self): """ ui should have: * table with a list of available tests and show results after they are done * way to filter tests * button to run tests """ layout = QVBoxLayout() # table to list test classes and the results self.table = QTableWidget() self.table.setColumnCount(2) self.table.setHorizontalHeaderLabels(["test", "result"]) self.table.horizontalHeader().setSectionResizeMode( 0, self.table.horizontalHeader().Interactive) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.horizontalHeader().setSectionResizeMode( 1, self.table.horizontalHeader().Stretch) self.table.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.table.setSelectionBehavior(self.table.SelectRows) layout.insertWidget(0, self.table, 1) self.filter = QLineEdit() self.filter.setPlaceholderText("filter test names") self.filter.setClearButtonEnabled(True) self.filter.textChanged.connect(self.apply_filter) layout.insertWidget(1, self.filter, 0) self.run_button = QPushButton("run tests") self.run_button.clicked.connect(self.run_tests) self.run_button.setToolTip( "if no tests are selected on the table, run all tests\n" + "otherwise, run selected tests") layout.insertWidget(2, self.run_button) self.fill_table() self.table.resizeColumnToContents(0) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None)
class GroupEcephys(QGroupBox): def __init__(self, parent): """Groupbox for Ecephys module fields filling form.""" super().__init__() self.setTitle('Ecephys') self.group_type = 'Ecephys' self.groups_list = [] self.combo1 = CustomComboBox() self.combo1.addItem('-- Add group --') self.combo1.addItem('Device') self.combo1.addItem('ElectrodeGroup') self.combo1.addItem('ElectricalSeries') self.combo1.addItem('SpikeEventSeries') self.combo1.addItem('EventDetection') self.combo1.addItem('EventWaveform') self.combo1.addItem('LFP') self.combo1.addItem('FilteredEphys') self.combo1.addItem('FeatureExtraction') self.combo1.addItem('DecompositionSeries') self.combo1.setCurrentIndex(0) self.combo1.activated.connect(lambda: self.add_group('combo')) self.combo2 = CustomComboBox() self.combo2.addItem('-- Del group --') self.combo2.setCurrentIndex(0) self.combo2.activated.connect(lambda: self.del_group('combo')) self.vbox1 = QVBoxLayout() self.vbox1.addStretch() self.grid = QGridLayout() self.grid.setColumnStretch(5, 1) if parent.show_add_del: self.grid.addWidget(self.combo1, 1, 0, 1, 2) self.grid.addWidget(self.combo2, 1, 2, 1, 2) self.grid.addLayout(self.vbox1, 2, 0, 1, 6) self.setLayout(self.grid) def add_group(self, group, metadata=None): """Adds group form.""" if metadata is not None: group.write_fields(metadata=metadata) group.form_name.textChanged.connect(self.refresh_del_combo) self.groups_list.append(group) nWidgetsVbox = self.vbox1.count() self.vbox1.insertWidget(nWidgetsVbox - 1, group) # insert before the stretch self.combo1.setCurrentIndex(0) self.combo2.addItem(group.form_name.text()) self.refresh_children(metadata=metadata) def del_group(self, group_name): """Deletes group form by name.""" if group_name == 'combo': group_name = str(self.combo2.currentText()) if group_name != '-- Del group --': # Tests if any other group references this one if self.is_referenced(grp_unique_name=group_name): QMessageBox.warning( self, "Cannot delete subgroup", group_name + " is being referenced by another subgroup(s).\n" "You should remove any references of " + group_name + " before " "deleting it!") self.combo2.setCurrentIndex(0) else: nWidgetsVbox = self.vbox1.count() for i in range(nWidgetsVbox): if self.vbox1.itemAt(i) is not None: if hasattr(self.vbox1.itemAt(i).widget(), 'form_name'): if self.vbox1.itemAt( i).widget().form_name.text() == group_name: self.groups_list.remove( self.vbox1.itemAt( i).widget()) # deletes list item self.vbox1.itemAt(i).widget().setParent( None) # deletes widget self.combo2.removeItem( self.combo2.findText(group_name)) self.combo2.setCurrentIndex(0) self.refresh_children() def is_referenced(self, grp_unique_name): """Tests if a group is being referenced any other groups. Returns boolean.""" nWidgetsVbox = self.vbox1.count() for i in range(nWidgetsVbox): if self.vbox1.itemAt(i).widget() is not None: other_grp = self.vbox1.itemAt(i).widget() # check if this subgroup has any ComboBox referencing grp_unique_name for ch in other_grp.children(): if isinstance(ch, (CustomComboBox, QComboBox)): if ch.currentText() == grp_unique_name: return True return False def refresh_children(self, metadata=None): """Refreshes references with existing objects in child groups.""" for child in self.groups_list: child.refresh_objects_references(metadata=metadata) def refresh_del_combo(self): """Refreshes del combobox with existing objects names in child groups.""" self.combo2.clear() self.combo2.addItem('-- Del group --') for child in self.groups_list: self.combo2.addItem(child.form_name.text()) self.refresh_children() def read_fields(self): """Reads fields and returns them structured in a dictionary.""" error = None data = {} # group_type counts, if there are multiple groups of same type, they are saved in a list grp_types = [grp.group_type for grp in self.groups_list] grp_type_count = { value: len(list(freq)) for value, freq in groupby(sorted(grp_types)) } # initiate lists as values for groups keys with count > 1 for k, v in grp_type_count.items(): if v > 1 or k == 'Device' or k == 'ElectrodeGroup' or k == 'ElectricalSeries': data[k] = [] # iterate over existing groups and copy their metadata for grp in self.groups_list: if grp_type_count[grp.group_type] > 1 or grp.group_type == 'Device' \ or grp.group_type == 'ElectrodeGroup' or grp.group_type == 'ElectricalSeries': data[grp.group_type].append(grp.read_fields()) else: data[grp.group_type] = grp.read_fields() return data, error
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 main(QWidget): def __init__(self, parent=None): super(main, self).__init__(parent) self.setup() # connections, widgets, layouts, etc. self.blksize = 2**20 # 1 MB; must be divisible by 16 self.ext = '.enc' # extension is appended to encrypted files self.path = '' self.encrypted = [] # to highlight done files in list self.decrypted = [] self.clipboard = QApplication.clipboard() self.timeout = None # to clear message label, see setMessage # this program was just an excuse to play with QprogressBar if not hash(os.urandom(11)) % 11: QTimer().singleShot(50, self.windDown) # various random hints hints = [ 'Freshly encrypted files can be renamed in the table!', 'Clipboard is always cleared on program close!', 'Keys can contain emoji if you <em>really</em> want: \U0001f4e6', 'Keys can contain emoji if you <em>really</em> want: \U0001F511', 'This isn\'t a tip, I just wanted to say hello!', 'Keys can be anywhere from 8 to 4096 characters long!', 'This program was just an excuse to play with the progress bars!', 'Select \'Party\' in the hash button for progress bar fun!', ('Did you know you can donate one or all of your vital organs to ' 'the Aperture Science Self-Esteem Fund for Girls? It\'s true!'), ('It\'s been {:,} days since Half-Life 2: Episode ' 'Two'.format(int((time.time() - 1191988800) / 86400))), 'I\'m version {}!'.format(VERSION), 'I\'m version {}.whatever!'.format(VERSION.split('.')[0]), ('Brought to you by me, I\'m <a href="https://orthallelous.word' 'press.com/">Orthallelous!</a>'), #'Brought to you by me, I\'m Htom Sirveaux!', 'I wonder if there\'s beer on the sun', 'Raspberry World: For all your raspberry needs. Off the beltline', #'I\'ve plummented to my death and I can\'t get up', '<em>NOT</em> compatible with the older version!', ('Hello there, fellow space travellers! Until somebody gives me ' 'some new lines in KAS, that is all I can say. - Bentusi Exchange' ) ] if not hash(os.urandom(9)) % 4: self.extraLabel.setText(random.choice(hints)) def genKey(self): "generate a random key" n = self.keySizeSB.value() char = string.printable.rstrip() #map(chr, range(256)) while len(char) < n: char += char key = ''.join(random.sample(char, n)) self.keyInput.setText(key) def showKey(self, state=None): "hide/show key characters" if state is None: state = bool(self.showKeyCB.checkState()) else: state = bool(state) if state: self.keyInput.setEchoMode(QLineEdit.Normal) else: self.keyInput.setEchoMode(QLineEdit.PasswordEchoOnEdit) def getFolder(self): "open file dialog and fill file table" path = QFileDialog(directory=self.path).getExistingDirectory() if not path: return self.path = str(path) self.populateTable(self.path) self.encrypted, self.decrypted = [], [] return def resizeEvent(self, event): self.showFolder(self.path) # update how the folder is shown def splitterChanged(self, pos): self.showFolder(self.path) # likewise def showFolder(self, path): "displays current path, truncating as needed" if not path: return ell, sl = '\u2026', os.path.sep # ellipsis, slash chars lfg, rfg = Qt.ElideLeft, Qt.ElideRight lst, wdh = os.path.basename(path), self.folderLabel.width() path = path.replace(os.path.altsep or '\\', sl) self.folderLabel.setToolTip(path) # truncate folder location fnt = QFontMetrics(self.folderLabel.font()) txt = str(fnt.elidedText(path, lfg, wdh)) if len(txt) <= 1: # label is way too short self.folderLabel.setText('\u22ee' if txt != sl else txt) return # but when would this happen? # truncate some more (don't show part of a folder name) if len(txt) < len(path) and txt[1] != sl: txt = ell + sl + txt.split(sl, 1)[-1] # don't truncate remaining folder name from the left if txt[2:] != lst and len(txt[2:]) < len(lst) + 2: txt = str(fnt.elidedText(ell + sl + lst, rfg, wdh)) # you'd think len(txt) < len(lst) would work, but no; you'd be wrong self.folderLabel.setText(txt) def populateTable(self, path): "fill file table with file names" self.showFolder(path) names = [] for n in os.listdir(path): if os.path.isdir(os.path.join(path, n)): continue # folder names.append(n) self.folderTable.clearContents() self.folderTable.setRowCount(len(names)) self.folderTable.setColumnCount(1) if not names: # no files in this folder, inform user self.setMessage('This folder has no files') return self.folderTable.blockSignals(True) selEnab = Qt.ItemIsSelectable | Qt.ItemIsEnabled for i, n in enumerate(names): item = QTableWidgetItem() item.setText(n) item.setToolTip(n) item.setFlags(selEnab) # color code encrypted/decrypted files if n in self.encrypted: item.setTextColor(QColor(211, 70, 0)) # allowed encrypted filenames to be changed item.setFlags(selEnab | Qt.ItemIsEditable) if n in self.decrypted: item.setForeground(QColor(0, 170, 255)) self.folderTable.setItem(i, 0, item) if len(names) > 5: self.setMessage('{:,} files'.format(len(names)), 7) self.folderTable.blockSignals(False) return def editFileName(self, item): "change file name" new, old = str(item.text()), str(item.toolTip()) result = QMessageBox.question( self, 'Renaming?', ("<p align='center'>Do you wish to rename<br>" + '<span style="color:#d34600;">{}</span>'.format(old) + "<br>to<br>" + '<span style="color:#ef4b00;">{}</span>'.format(new) + '<br>?</p>')) self.folderTable.blockSignals(True) if any(i in new for i in '/?<>:*|"^'): self.setMessage('Invalid character in name', 7) item.setText(old) elif result == QMessageBox.Yes: oold = os.path.join(self.path, old) try: os.rename(oold, os.path.join(self.path, new)) self.encrypted.remove(old) self.encrypted.append(new) item.setToolTip(new) except Exception as err: self.setMessage(str(err), 9) item.setText(old) item.setToolTip(old) self.encrypted.remove(new) self.encrypted.append(old) else: item.setText(old) self.folderTable.blockSignals(False) def setMessage(self, message, secs=4, col=None): "show a message for a few seconds - col must be rgb triplet tuple" if self.timeout: # https://stackoverflow.com/a/21081371 self.timeout.stop() self.timeout.deleteLater() if col is None: color = 'rgb(255, 170, 127)' else: try: color = 'rgb({}, {}, {})'.format(*col) except: color = 'rgb(255, 170, 127)' self.messageLabel.setStyleSheet('background-color: {};'.format(color)) self.messageLabel.setText(message) self.messageLabel.setToolTip(message) self.timeout = QTimer() self.timeout.timeout.connect(self.clearMessage) self.timeout.setSingleShot(True) self.timeout.start(secs * 1000) def clearMessage(self): self.messageLabel.setStyleSheet('') self.messageLabel.setToolTip('') self.messageLabel.setText('') def getName(self): "return file name of selected" items = self.folderTable.selectedItems() names = [str(i.text()) for i in items] if names: return names[0] # only the first selected file else: return '' def showKeyLen(self, string): "displays a tooltip showing length of key" s = len(string) note = '{:,} character{}'.format(s, '' if s == 1 else 's') tip = QToolTip pos = self.genKeyButton.mapToGlobal(QPoint(0, 0)) if s < self.minKeyLen: note = '<span style="color:#c80000;">{}</span>'.format(note) else: note = '<span style="color:#258f22;">{}</span>'.format(note) tip.showText(pos, note) def lock(self, flag=True): "locks buttons if True" stuff = [ self.openButton, self.encryptButton, self.decryptButton, self.genKeyButton, self.hashButton, self.showKeyCB, self.copyButton, self.keyInput, self.keySizeSB, self.folderTable, ] for i in stuff: i.blockSignals(flag) i.setEnabled(not flag) return def _lerp(self, v1, v2, numPts=10): "linearly interpolate from v1 to v2\nFrom Orthallelous" if len(v1) != len(v2): raise ValueError("different dimensions") D, V, n = [], [], abs(numPts) for i, u in enumerate(v1): D.append(v2[i] - u) for i in range(n + 1): vn = [] for j, u in enumerate(v1): vn.append(u + D[j] / float(n + 2) * i) V.append(tuple(vn)) return V def weeeeeee(self): "party time" self.lock() self.setMessage('Party time!', 2.5) a, b, c = self.encryptPbar, self.decryptPbar, self.hashPbar process, sleep = app.processEvents, time.sleep am, bm, cm = a.minimum(), b.minimum(), c.minimum() ax, bx, cx = a.maximum(), b.maximum(), c.maximum() a.reset() b.reset() c.reset() loops = self._lerp((am, bm, cm), (ax, bx, cx), 100) ivops = loops[::-1] # up and up! for i in range(3): for j, k, l in loops: a.setValue(int(j)) b.setValue(int(k)) c.setValue(int(l)) process() sleep(0.01) a.setValue(ax) b.setValue(bx) c.setValue(cx) sleep(0.25) a.setValue(am) b.setValue(bm) c.setValue(cm) # snake! self.setMessage('Snake time!') self.messageLabel.setStyleSheet('background-color: rgb(127,170,255);') for i in range(2): for j, k, l in loops: a.setValue(int(j)) process() sleep(0.002) process() a.setInvertedAppearance(True) process() for j, k, l in ivops: a.setValue(int(j)) process() sleep(0.002) for j, k, l in loops: b.setValue(int(k)) process() sleep(0.002) process() b.setInvertedAppearance(False) process() for j, k, l in ivops: b.setValue(int(k)) process() sleep(0.002) for j, k, l in loops: c.setValue(int(l)) process() sleep(0.002) process() c.setInvertedAppearance(True) process() for j, k, l in ivops: c.setValue(int(l)) process() sleep(0.002) process() b.setInvertedAppearance(True) process() for j, k, l in loops: b.setValue(int(k)) process() sleep(0.002) process() b.setInvertedAppearance(False) process() for j, k, l in ivops: b.setValue(int(k)) process() sleep(0.002) process() a.setInvertedAppearance(False) b.setInvertedAppearance(True) c.setInvertedAppearance(False) for j, k, l in loops: a.setValue(int(j)) process() sleep(0.002) process() a.setInvertedAppearance(True) process() for j, k, l in ivops: a.setValue(int(j)) process() sleep(0.002) # bars sleep(0.5) self.setMessage('Bars!') process() self.messageLabel.setStyleSheet('background-color: rgb(127,255,170);') for i in range(2): a.setValue(ax) time.sleep(0.65) a.setValue(am) sleep(0.25) process() b.setValue(bx) time.sleep(0.65) b.setValue(bm) sleep(0.25) process() c.setValue(cx) time.sleep(0.65) c.setValue(cm) sleep(0.25) process() b.setValue(bx) time.sleep(0.65) b.setValue(bm) sleep(0.25) process() # okay, enough process() a.setValue(ax) b.setValue(bx) c.setValue(cx) #a.setValue(am); b.setValue(bm); c.setValue(cm) a.setInvertedAppearance(False) b.setInvertedAppearance(True) c.setInvertedAppearance(False) self.lock(False) return def windDown(self, note=None): "silly deload on load" if note is None: note = 'Loading...' self.lock() self.setMessage(note) self.messageLabel.setStyleSheet('background-color: rgb(9, 190, 130);') a, b, c = self.encryptPbar, self.decryptPbar, self.hashPbar am, bm, cm = a.minimum(), b.minimum(), c.minimum() ax, bx, cx = a.maximum(), b.maximum(), c.maximum() a.reset() b.reset() c.reset() loops = self._lerp((ax, bx, cx), (am, bm, cm), 100) for j, k, l in loops: a.setValue(int(j)) b.setValue(int(k)) c.setValue(int(l)) app.processEvents() time.sleep(0.02) a.reset() b.reset() c.reset() self.lock(False) self.clearMessage() def genHash(self, action): "generate hash of selected file and display it" name, t0 = self.getName(), time.perf_counter() # mark what hash was used in the drop-down menu for i in self.hashButton.menu().actions(): if i == action: i.setIconVisibleInMenu(True) else: i.setIconVisibleInMenu(False) if str(action.text()) == 'Party': self.weeeeeee() self.windDown('Winding down...') return if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return self.lock() hsh = self.hashFile(os.path.join(self.path, name), getattr(hashlib, str(action.text()))) self.lock(False) #hsh = str(action.text()) + ': ' + hsh self.hashLabel.setText(hsh) self.hashLabel.setToolTip(hsh) self.extraLabel.setText( str(action.text()) + ' hash took ' + self.secs_fmt(time.perf_counter() - t0)) def setCancel(self): "cancel operation" self._requestStop = True def showCancelButton(self, state=False): "show/hide cancel button" self.cancelButton.blockSignals(not state) self.cancelButton.setEnabled(state) if state: self.cancelButton.show() self.keyInput.hide() self.genKeyButton.hide() self.keySizeSB.hide() else: self.cancelButton.hide() self.keyInput.show() self.genKeyButton.show() self.keySizeSB.show() def hashFile(self, fn, hasher): "returns the hash value of a file" hsh, blksize = hasher(), self.blksize fsz, csz = os.path.getsize(fn), 0.0 self.hashPbar.reset() self.showCancelButton(True) prog, title = '(# {:.02%}) {}', self.windowTitle() with open(fn, 'rb') as f: while 1: blk = f.read(blksize) if not blk: break hsh.update(blk) csz += blksize self.hashPbar.setValue(int(round(csz * 100.0 / fsz))) app.processEvents() self.setWindowTitle(prog.format(csz / fsz, title)) if self._requestStop: break self.hashPbar.setValue(self.hashPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Hashing canceled!') self.hashPbar.setValue(self.hashPbar.minimum()) self._requestStop = False return return hsh.hexdigest() def hashKey(self, key, salt=b''): "hashes a key for encrypting/decrypting file" salt = salt.encode() if type(salt) != bytes else salt key = key.encode() if type(key) != bytes else key p = app.processEvents self.setMessage('Key Hashing...', col=(226, 182, 249)) p() key = hashlib.pbkdf2_hmac('sha512', key, salt, 444401) p() self.clearMessage() p() return hashlib.sha3_256(key).digest() # AES requires a 32 char key def encrypt(self): "encrypt selected file with key" name, t0 = self.getName(), time.perf_counter() if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return key = str(self.keyInput.text()) if len(key) < self.minKeyLen: self.setMessage(('Key must be at least ' '{} characters long').format(self.minKeyLen)) return self.lock() gn = self.encryptFile(key, os.path.join(self.path, name)) if not gn: self.lock(False) return self.encrypted.append(os.path.basename(gn)) self.lock(False) self.populateTable(self.path) # repopulate folder list bn, tt = os.path.basename(gn), time.perf_counter() - t0 self.setMessage('Encrypted, saved "{}"'.format(bn, 13)) self.extraLabel.setText('Encrypting took ' + self.secs_fmt(tt)) def encryptFile(self, key, fn): "encrypts a file using AES (MODE_GCM)" chars = ''.join(map(chr, range(256))).encode() chk = AES.block_size sample = random.sample iv = bytes(sample(chars, chk * 2)) salt = bytes(sample(chars * 2, 256)) vault = AES.new(self.hashKey(key, salt), AES.MODE_GCM, iv) fsz = os.path.getsize(fn) del key blksize = self.blksize gn = fn + self.ext fne = os.path.basename(fn).encode() fnz = len(fne) if len(fne) % chk: fne += bytes(sample(chars, chk - len(fne) % chk)) csz = 0.0 # current processed value self.encryptPbar.reset() prog, title = '({:.02%}) {}', self.windowTitle() self.showCancelButton(True) with open(fn, 'rb') as src, open(gn, 'wb') as dst: dst.write(bytes([0] * 16)) # spacer for MAC written at end dst.write(iv) dst.write(salt) # store iv, salt # is it safe to store MAC, iv, salt plain right in file? # can't really store them encrypted, # or elsewhere in this model of single file encryption? # can't have another file for the file to lug around # store file size, file name length dst.write(vault.encrypt(struct.pack('<2Q', fsz, fnz))) dst.write(vault.encrypt(fne)) # store filename while 1: dat = src.read(blksize) if not dat: break elif len(dat) % chk: # add padding fil = chk - len(dat) % chk dat += bytes(sample(chars, fil)) dst.write(vault.encrypt(dat)) csz += blksize # show progress self.encryptPbar.setValue(int(round(csz * 100.0 / fsz))) self.setWindowTitle(prog.format(csz / fsz, title)) app.processEvents() if self._requestStop: break if not self._requestStop: stuf = random.randrange(23) # pack in more stuffing fing = b''.join(bytes(sample(chars, 16)) for i in range(stuf)) dst.write(vault.encrypt(fing)) # and for annoyance dst.seek(0) dst.write(vault.digest()) # write MAC self.hashLabel.setText('MAC: ' + vault.hexdigest()) self.encryptPbar.setValue(self.encryptPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Encryption canceled!') self.encryptPbar.setValue(self.encryptPbar.minimum()) self._requestStop = False os.remove(gn) return return gn def decrypt(self): "encrypt selected file with key" name, t0 = self.getName(), time.perf_counter() if not name: self.setMessage('No file selected') return if not os.path.exists(os.path.join(self.path, name)): self.setMessage('File does not exist') return key = str(self.keyInput.text()) if len(key) < self.minKeyLen: self.setMessage(('Key must be at least ' '{} characters long').format(self.minKeyLen)) return self.lock() gn = self.decryptFile(key, os.path.join(self.path, name)) if not gn: self.lock(False) return self.decrypted.append(os.path.basename(gn)) self.lock(False) self.populateTable(self.path) # repopulate folder list bn, tt = os.path.basename(gn), time.perf_counter() - t0 self.setMessage('Decrypted, saved "{}"'.format(bn, 13)) self.extraLabel.setText('Decrypting took ' + self.secs_fmt(tt)) def decryptFile(self, key, fn): "decrypts a file using AES (MODE_GCM)" blksize = self.blksize gn = hashlib.md5(os.path.basename(fn).encode()).hexdigest() gn = os.path.join(self.path, gn) # temporary name if os.path.exists(gn): self.setMessage('file already exists') return self.decryptPbar.reset() csz = 0.0 # current processed value chk, fnsz = AES.block_size, os.path.getsize(fn) prog, title = '({:.02%}) {}', self.windowTitle() try: with open(fn, 'rb') as src, open(gn, 'wb') as dst: # extract iv, salt MAC = src.read(16) iv = src.read(AES.block_size * 2) salt = src.read(256) vault = AES.new(self.hashKey(key, salt), AES.MODE_GCM, iv) self.showCancelButton(True) # extract file size, file name length sizes = src.read(struct.calcsize('<2Q')) fsz, fnz = struct.unpack('<2Q', vault.decrypt(sizes)) # extract filename; round up fnz to nearest chk rnz = fnz if not fnz % chk else fnz + chk - fnz % chk rfn = vault.decrypt(src.read(rnz))[:fnz].decode() self.setMessage('Found "{}"'.format(rfn), 13, (255, 211, 127)) while 1: dat = src.read(blksize) if not dat: break dst.write(vault.decrypt(dat)) csz += blksize # show progress self.decryptPbar.setValue(int(round(csz * 100.0 / fnsz))) self.setWindowTitle(prog.format(1 - (csz / fnsz), title)) app.processEvents() if self._requestStop: break if not self._requestStop: dst.truncate(fsz) # remove padding if not self._requestStop: vault.verify(MAC) self.hashLabel.setText('') except (ValueError, KeyError) as err: os.remove(gn) self.setMessage('Invalid decryption!') self.setWindowTitle(title) self.showCancelButton(False) return except Exception as err: os.remove(gn) self.setMessage('Invalid key or file!') self.setWindowTitle(title) self.showCancelButton(False) return self.decryptPbar.setValue(self.decryptPbar.maximum()) self.setWindowTitle(title) self.showCancelButton(False) if self._requestStop: self.setMessage('Decryption canceled!') self.decryptPbar.setValue(self.decryptPbar.minimum()) self._requestStop = False os.remove(gn) return # restore original file name name, ext = os.path.splitext(rfn) count = 1 fn = os.path.join(self.path, name + ext) while os.path.exists(fn): fn = os.path.join(self.path, name + '_{}'.format(count) + ext) count += 1 os.rename(gn, fn) # restore original name return fn # saved name def copyKeyHash(self, action): "copies either the key or the hash to clipboard" act = str(action.text()).lower() if 'key' in act: txt = str(self.keyInput.text()) elif 'hash' in act: txt = str(self.hashLabel.text()) else: self.setMessage('Invalid copy selection') return if not txt: self.setMessage('Empty text; Nothing to copy') return if 'key' in act: self.setMessage('Key copied to clipboard') elif 'hash' in act: self.setMessage('Hash copied to clipboard') else: self.setMessage('Invalid copy selection') return self.clipboard.clear() self.clipboard.setText(txt) def secs_fmt(self, s): "6357 -> '1h 45m 57s'" Y, D, H, M = 31556952, 86400, 3600, 60 y = int(s // Y) s -= y * Y d = int(s // D) s -= d * D h = int(s // H) s -= h * H m = int(s // M) s -= m * M r = (str(int(s)) if int(s) == s else str(round(s, 3))) + 's' if m: r = str(m) + 'm ' + r if h: r = str(h) + 'h ' + r if d: r = str(d) + 'd ' + r if y: r = str(y) + 'y ' + r return r.strip() def closeEvent(self, event): self.clipboard.clear() def setup(self): "constructs the gui" Fixed = QSizePolicy() MinimumExpanding = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.minKeyLen = 8 self.maxKeyLen = 4096 self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Horizontal) self.splitter.splitterMoved.connect(self.splitterChanged) # left column self.leftColumn = QWidget() self.vl01 = QVBoxLayout() # left column - first item (0; horizonal layout 0) self.hl00 = QHBoxLayout() self.hl00.setSpacing(5) self.openButton = QPushButton('&Open') self.openButton.setToolTip('Open folder') self.openButton.setMinimumSize(60, 20) self.openButton.setMaximumSize(60, 20) self.openButton.setSizePolicy(Fixed) self.openButton.clicked.connect(self.getFolder) #ico = self.style().standardIcon(QStyle.SP_DirIcon) #self.openButton.setIcon(ico) self.folderLabel = QLabel() self.folderLabel.setMinimumSize(135, 20) self.folderLabel.setMaximumSize(16777215, 20) self.folderLabel.setSizePolicy(MinimumExpanding) self.hl00.insertWidget(0, self.openButton) self.hl00.insertWidget(1, self.folderLabel) # left column - second item (1) self.folderTable = QTableWidget() self.folderTable.setMinimumSize(200, 32) self.folderTable.horizontalHeader().setVisible(False) self.folderTable.horizontalHeader().setStretchLastSection(True) self.folderTable.verticalHeader().setVisible(False) self.folderTable.verticalHeader().setDefaultSectionSize(15) self.folderTable.itemChanged.connect(self.editFileName) # left column - third item (2) self.extraLabel = QLabel() self.extraLabel.setMinimumSize(200, 20) self.extraLabel.setMaximumSize(16777215, 20) self.extraLabel.setSizePolicy(MinimumExpanding) self.extraLabel.setTextInteractionFlags(Qt.LinksAccessibleByMouse) # finalize left column self.vl01.insertLayout(0, self.hl00) self.vl01.insertWidget(1, self.folderTable) self.vl01.insertWidget(2, self.extraLabel) self.leftColumn.setLayout(self.vl01) # right column self.rightColumn = QWidget() self.vl02 = QVBoxLayout() # right column - first item (0) self.messageLabel = QLabel() self.messageLabel.setMinimumSize(290, 20) self.messageLabel.setMaximumSize(16777215, 20) self.messageLabel.setSizePolicy(MinimumExpanding) self.messageLabel.setAlignment(Qt.AlignCenter) # right column - second item (2; horizontal layout 1) self.hl01 = QHBoxLayout() self.hl01.setSpacing(5) self.encryptButton = QPushButton('&Encrypt') #\U0001F512 self.encryptButton.setToolTip('Encrypt selected file') self.encryptButton.setMinimumSize(60, 20) self.encryptButton.setMaximumSize(60, 20) self.encryptButton.setSizePolicy(Fixed) self.encryptButton.clicked.connect(self.encrypt) self.encryptPbar = QProgressBar() self.encryptPbar.setMinimumSize(225, 20) self.encryptPbar.setMaximumSize(16777215, 20) self.encryptPbar.setSizePolicy(MinimumExpanding) self.encryptPbar.setTextVisible(False) palette = self.encryptPbar.palette() # color of progress bar color = QColor(211, 70, 0) palette.setColor(QPalette.Highlight, color) self.encryptPbar.setPalette(palette) self.hl01.insertWidget(0, self.encryptButton) self.hl01.insertWidget(1, self.encryptPbar) # right column - third item (3; horizontal layout 2) self.hl02 = QHBoxLayout() self.hl02.setSpacing(5) self.cancelButton = QPushButton('C&ANCEL') self.cancelButton.setToolTip('Cancels current operation') self.cancelButton.setMinimumSize(70, 24) self.cancelButton.setMaximumSize(70, 24) self.cancelButton.setSizePolicy(Fixed) self.cancelButton.clicked.connect(self.setCancel) font = self.cancelButton.font() font.setBold(True) self.cancelButton.setFont(font) self.cancelButton.blockSignals(True) self.cancelButton.setEnabled(False) self.cancelButton.hide() self._requestStop = False self.keyInput = QLineEdit() self.keyInput.setMinimumSize(225, 20) self.keyInput.setMaximumSize(16777215, 20) self.keyInput.setSizePolicy(MinimumExpanding) self.keyInput.setPlaceholderText('key') self.keyInput.setMaxLength(self.maxKeyLen) self.keyInput.setAlignment(Qt.AlignCenter) self.keyInput.textEdited.connect(self.showKeyLen) self.genKeyButton = QPushButton('&Gen Key') #\U0001F511 self.genKeyButton.setToolTip('Generate a random key') self.genKeyButton.setMinimumSize(60, 20) self.genKeyButton.setMaximumSize(60, 20) self.genKeyButton.setSizePolicy(Fixed) self.genKeyButton.clicked.connect(self.genKey) self.keySizeSB = QSpinBox() self.keySizeSB.setToolTip('Length of key to generate') self.keySizeSB.setRange(32, 1024) self.keySizeSB.setMinimumSize(40, 20) self.keySizeSB.setMaximumSize(40, 20) self.keySizeSB.setSizePolicy(Fixed) self.keySizeSB.setAlignment(Qt.AlignCenter) self.keySizeSB.setButtonSymbols(QSpinBox.NoButtons) self.keySizeSB.setWrapping(True) self.hl02.insertWidget(0, self.cancelButton) self.hl02.insertWidget(1, self.keyInput) self.hl02.insertWidget(2, self.genKeyButton) self.hl02.insertWidget(3, self.keySizeSB) # right column - fourth item (4; horizontal layout 3) self.hl03 = QHBoxLayout() self.hl03.setSpacing(5) self.decryptButton = QPushButton('&Decrypt') #\U0001F513 self.decryptButton.setToolTip('Decrypt selected file') self.decryptButton.setMinimumSize(60, 20) self.decryptButton.setMaximumSize(60, 20) self.decryptButton.setSizePolicy(Fixed) self.decryptButton.clicked.connect(self.decrypt) self.decryptPbar = QProgressBar() self.decryptPbar.setMinimumSize(225, 20) self.decryptPbar.setMaximumSize(16777215, 20) self.decryptPbar.setSizePolicy(MinimumExpanding) self.decryptPbar.setTextVisible(False) self.decryptPbar.setInvertedAppearance(True) palette = self.decryptPbar.palette() # color of progress bar color = QColor(0, 170, 255) palette.setColor(QPalette.Highlight, color) self.decryptPbar.setPalette(palette) self.hl03.insertWidget(0, self.decryptButton) self.hl03.insertWidget(1, self.decryptPbar) # right column - fifth item (7; horizontal layout 4) self.hl04 = QHBoxLayout() self.hl04.setSpacing(5) self.showKeyCB = QCheckBox('&Show Key') self.showKeyCB.setToolTip('Show/Hide key value') self.showKeyCB.setMinimumSize(75, 20) self.showKeyCB.setMaximumSize(75, 20) self.showKeyCB.setSizePolicy(Fixed) self.showKeyCB.clicked.connect(self.showKey) self.showKeyCB.setChecked(True) self.hashPbar = QProgressBar() self.hashPbar.setMinimumSize(150, 20) self.hashPbar.setMaximumSize(16777215, 20) self.hashPbar.setSizePolicy(MinimumExpanding) self.hashPbar.setTextVisible(False) palette = self.hashPbar.palette() # color of progress bar color = QColor(31, 120, 73) palette.setColor(QPalette.Highlight, color) self.hashPbar.setPalette(palette) self.hashButton = QPushButton('&Hash') self.hashButton.setToolTip('Determine file hash') self.hashButton.setMinimumSize(60, 20) self.hashButton.setMaximumSize(60, 20) self.hashButton.setSizePolicy(Fixed) menu = QMenu(self.hashButton) ico = self.style().standardIcon(QStyle.SP_DialogYesButton) for alg in sorted( filter(lambda x: 'shake' not in x, hashlib.algorithms_guaranteed), key=lambda n: (len(n), sorted(hashlib.algorithms_guaranteed).index(n))): menu.addAction( ico, alg ) # drop shake algs as their .hexdigest requires an argument - the rest don't menu.addAction(ico, 'Party') for i in menu.actions(): i.setIconVisibleInMenu(False) self.hashButton.setMenu(menu) menu.triggered.connect(self.genHash) self.hl04.insertWidget(0, self.showKeyCB) self.hl04.insertWidget(1, self.hashPbar) self.hl04.insertWidget(2, self.hashButton) # right column - sixth item (8; horizontal layout 5) self.hl05 = QHBoxLayout() self.hl05.setSpacing(5) self.copyButton = QPushButton('&Copy') #\U0001F4CB self.copyButton.setToolTip('Copy key or hash to clipboard') self.copyButton.setMinimumSize(60, 20) self.copyButton.setMaximumSize(60, 20) self.copyButton.setSizePolicy(Fixed) menu2 = QMenu(self.copyButton) menu2.addAction('Copy Key') menu2.addAction('Copy Hash') self.copyButton.setMenu(menu2) menu2.triggered.connect(self.copyKeyHash) self.hashLabel = QLabel() self.hashLabel.setMinimumSize(225, 20) self.hashLabel.setMaximumSize(16777215, 20) self.hashLabel.setSizePolicy(MinimumExpanding) self.hashLabel.setTextFormat(Qt.PlainText) self.hashLabel.setAlignment(Qt.AlignCenter) self.hashLabel.setTextInteractionFlags(Qt.TextSelectableByMouse) self.hl05.insertWidget(0, self.copyButton) self.hl05.insertWidget(1, self.hashLabel) # finalize right column self.vl02.insertWidget(0, self.messageLabel) self.vl02.insertSpacerItem(1, QSpacerItem(0, 0)) self.vl02.insertLayout(2, self.hl01) self.vl02.insertLayout(3, self.hl02) self.vl02.insertLayout(4, self.hl03) self.vl02.insertSpacerItem(5, QSpacerItem(0, 0)) self.vl02.insertWidget(6, QFrame()) self.vl02.insertLayout(7, self.hl04) self.vl02.insertLayout(8, self.hl05) self.rightColumn.setLayout(self.vl02) # finalize main window self.splitter.insertWidget(0, self.leftColumn) self.splitter.insertWidget(1, self.rightColumn) layout = QHBoxLayout(self) layout.addWidget(self.splitter) self.setLayout(layout) self.setWindowTitle('Simple File Encryptor/Decryptor') self.resize(self.sizeHint())
class ProcessListWidget(QScrollArea): action_requested = Signal(str, str) process_state_changed = Signal(str, str) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.process_data = set() self.process_widget_map = {} # type: Dict[str, ProcessWidget] self.main_widget = QWidget() self.process_layout = QVBoxLayout() self.process_layout.setContentsMargins(2, 2, 2, 2) self.main_widget.setLayout(self.process_layout) self.lbl_default = QLabel(text="No processes found") self.process_layout.addWidget(self.lbl_default) self.process_layout.addStretch() # Configure ScrollArea self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setWidgetResizable(True) self.setWidget(self.main_widget) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def on_action_completed(self, response: ActionResponse): logger.info("Action completed: %s", response) self.process_widget_map[response.uid].on_action_completed( response.action) def update_single_process_state(self, event: StateChangedEvent): self.process_widget_map[event.uid].update_state( event.state, event.exit_code) def update_process_data(self, updated_process_data: Set[ProcessData]): potentially_updated = set() for process_data in updated_process_data: if process_data in self.process_data: potentially_updated.add(process_data) new_processes = updated_process_data - self.process_data unknown_processes = self.process_data - updated_process_data print(potentially_updated, new_processes, unknown_processes) # TODO(mark): update process data sets with new data for known_process in potentially_updated: widget = self.process_widget_map[known_process.uid] widget.on_update_process_data(known_process) for new_process in new_processes: widget = ProcessWidget(new_process) self.process_widget_map[new_process.uid] = widget widget.actionRequested.connect(self.action_requested) widget.process_state_changed.connect(self.process_state_changed) self.process_layout.insertWidget(0, widget) # TODO(mark): unknown processes for process in unknown_processes: widget = self.process_widget_map[process.uid] logger.info("Removing widget: %s", process.uid) self.process_layout.removeWidget(widget) del self.process_widget_map[process.uid] widget.deleteLater() self.process_data = new_processes | potentially_updated self.lbl_default.setVisible(len(self.process_data) == 0) return new_processes, unknown_processes
class ViewWidget(QWidget): exercise_name_label = None exercise_name_line = None scroll_area = None base_widget = None exercises_widget = None return_button = None add_button = None def __init__(self): QWidget.__init__(self) self.file = "" self.setup_widget() def setup_widget(self): self.exercise_name_label = QLabel("Exercise name:", self) self.exercise_name_label.move(5, 5) self.exercise_name_label.resize(125, 25) self.add_button = QPushButton("Add", self) self.add_button.resize(75, 25) self.add_button.clicked.connect(self.add_line) self.exercise_name_line = QLineEdit(self) self.exercise_name_line.move(135, 5) self.exercise_name_line.resize(125, 25) self.scroll_area = QScrollArea(self) self.base_widget = QWidget(self) self.scroll_area.setWidget(self.base_widget) self.exercises_widget = QVBoxLayout() self.exercises_widget.setAlignment(Qt.AlignTop) self.base_widget.setLayout(self.exercises_widget) self.return_button = QPushButton("Return wo save", self) def resizeEvent(self, event): self.scroll_area.move(5, 35) self.scroll_area.resize(self.width() - 165, self.height() - 40) self.add_button.move(self.width() - 160 - 75, 5) self.return_button.move(self.width() - 155, 5) self.return_button.resize(150, 40) self.base_widget.resize(self.scroll_area.width() - 25, self.exercises_widget.count() * 25) def clear_widget(self): while self.exercises_widget.count() > 0: self.exercises_widget.takeAt(0) def open_exercise_file(self, file: str): self.file = file with open(self.file, "r") as json_file: json_data = json.load(json_file) name = json_data['name'] for data in json_data['exercise']: movement = data['name'] description = data['description'] time = data['time'] widget = PanelWidget() widget.set_data(movement, description, time) widget.remove_signal.connect(self.remove_panel_item) widget.move_down_signal.connect(self.move_widget_down) widget.move_up_signal.connect(self.move_widget_up) self.exercises_widget.addWidget(widget) json_file.close() self.base_widget.resize(self.scroll_area.width() - 25, self.exercises_widget.count() * 25) self.exercise_name_line.setText(name) @Slot() def add_line(self): widget = PanelWidget() self.exercises_widget.addWidget(widget) self.base_widget.resize(self.scroll_area.width() - 25, self.exercises_widget.count() * 25) @Slot(QWidget) def move_widget_down(self, widget: QWidget): ind = self.exercises_widget.indexOf(widget) self.exercises_widget.removeWidget(widget) self.exercises_widget.insertWidget((ind + 1), widget) @Slot(QWidget) def move_widget_up(self, widget: QWidget): ind = self.exercises_widget.indexOf(widget) self.exercises_widget.removeWidget(widget) self.exercises_widget.insertWidget((ind - 1), widget) @Slot(QWidget) def remove_panel_item(self, widget: QWidget): self.exercises_widget.removeWidget(widget) self.base_widget.resize(self.scroll_area.width() - 25, self.exercises_widget.count() * 25)
class Central(QFrame): '''Initializes, styles, and connects the various classes''' def __init__(self): super().__init__() # Objects self.overallLayout = QVBoxLayout(self) self.contentLayout = QHBoxLayout() self.dropShadow = QGraphicsDropShadowEffect(self) self.boxManager = BoxManager.BoxManager() self.topBar = TopBar.TopBar() self.selectorArea = QFrame() self.selectorLayout = QVBoxLayout(self.selectorArea) self.folderArea = QFrame() self.folderLayout = QHBoxLayout(self.folderArea) self.folderList = FolderList.FolderList() self.folderBar = ScrollBar.ScrollBar(self.folderList) self.canvas = Canvas.Canvas(self.boxManager) self.imageArea = QFrame() self.imageList = ImageList.ImageList() self.imageLayout = QHBoxLayout(self.imageArea) self.imageBar = ScrollBar.ScrollBar(self.imageList) # Styling self.setStyleSheet('Central { background: transparent; }') self.overallLayout.setMargin(20) self.overallLayout.setSpacing(0) self.dropShadow.setOffset(QPointF(0,4)) self.dropShadow.setColor(QColor(0,0,0,100)) self.dropShadow.setBlurRadius(10) self.setGraphicsEffect(self.dropShadow) self.contentLayout.setAlignment(Qt.AlignCenter) self.contentLayout.setMargin(0) self.contentLayout.setSpacing(0) self.selectorLayout.setMargin(0) self.selectorLayout.setSpacing(0) self.folderLayout.setMargin(0) self.folderLayout.setSpacing(0) self.imageLayout.setMargin(0) self.imageLayout.setSpacing(0) self.folderList.setVerticalScrollBar(self.folderBar) self.imageList.setVerticalScrollBar(self.imageBar) self.selectorArea.setMaximumWidth(400) self.selectorArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Layout self.folderLayout.addWidget(self.folderList) self.folderLayout.addSpacerItem(QSpacerItem(-7, 0)) self.folderLayout.addWidget(self.folderBar) self.imageLayout.addWidget(self.imageList) self.imageLayout.addSpacerItem(QSpacerItem(-7, 0)) self.imageLayout.addWidget(self.imageBar) self.selectorLayout.addWidget(self.folderArea, 15) self.selectorLayout.addWidget(self.imageArea, 85) self.contentLayout.addWidget(self.selectorArea, 30) self.contentLayout.addWidget(self.canvas, 70) self.overallLayout.addLayout(self.contentLayout) self.overallLayout.insertWidget(0, self.topBar) # Connections self.folderList.selectedFolderChanged.connect(self.handleSelectedFolderChanged) self.imageList.selectedImageChanged.connect(self.handleSelectedImageChanged) def handleSelectedFolderChanged(self, folder): self.imageList.populate(folder) self.canvas.changeImage(None) self.canvas.setMessage('Switching Folders - {}'.format(folder.data(role=Qt.DisplayRole))) self.topBar.setSelectedFolder(str(folder.data(role=Qt.UserRole+1))) self.topBar.setSelectedImage('') def handleSelectedImageChanged(self, image): self.canvas.changeImage(image) self.canvas.setMessage('Switching Images - {}'.format(image.data(role=Qt.DisplayRole))) self.topBar.setSelectedImage(str(image.data(role=Qt.DisplayRole)))
class SettingsUI(MayaQWidgetDockableMixin, QWidget): save_prefs = Signal(dict) def __init__(self, parent=None): super(SettingsUI, self).__init__(parent=parent) # log_lvl = sys._global_spore_dispatcher.spore_globals['LOG_LEVEL'] # self.logger = logging_util.SporeLogger(__name__, log_lvl) self.build_ui() self.pref_tracking_dir = {} def build_ui(self): self.setWindowTitle('Spore Preferences') self.setGeometry(250, 250, 400, 150) self.layout = QVBoxLayout() self.setLayout(self.layout) self.layout.addStretch() self.save_btn = QPushButton('Save') self.layout.addWidget(self.save_btn) self.save_btn.clicked.connect(self.about_to_save) def add_pref_wdg(self, name, value): if isinstance(value, bool): wdg = BoolWidget(name, value) self.layout.insertWidget(0, wdg) self.pref_tracking_dir[wdg.name_lbl] = wdg.bool_cbx elif isinstance(value, str) or isinstance(value, unicode): wdg = StringWidget(name, value) self.layout.insertWidget(0, wdg) self.pref_tracking_dir[wdg.name_lbl] = wdg.line_edt elif isinstance(value, int): wdg = IntegerWidget(name, value) self.layout.insertWidget(0, wdg) self.pref_tracking_dir[wdg.name_lbl] = wdg.int_spn elif isinstance(value, float): wdg = FloatWidget(name, value) self.layout.insertWidget(0, wdg) self.pref_tracking_dir[wdg.name_lbl] = wdg.float_spn else: print 'no assignment for', name, value, type(value) pass def about_to_save(self): """ triggered by the save button. emit the save_prefs signal and send the resulting settings """ # self.logger.debug('About to emit save settings.') prefs = {} for attr_lbl, val_wdg in self.pref_tracking_dir.iteritems(): attr = attr_lbl.text() val = None if isinstance(val_wdg, QCheckBox): val = val_wdg.isChecked() elif isinstance(val_wdg, QLineEdit): val = val_wdg.text() elif isinstance(val_wdg, QSpinBox)\ or isinstance(val_wdg, QDoubleSpinBox): val = val_wdg.value() else: # self.logger.error('Unknown widget type: {}'.format(val_wdg)) raise RuntimeError('Impossible') prefs[attr] = val self.save_prefs.emit(prefs) self.close()
class GroupOgen(QGroupBox): def __init__(self, parent): """Groupbox for Ogen module fields filling form.""" super().__init__() self.setTitle('Ogen') self.group_type = 'Ogen' self.groups_list = [] self.vbox1 = QVBoxLayout() self.vbox1.addStretch() self.grid = QGridLayout() self.grid.setColumnStretch(5, 1) self.grid.addLayout(self.vbox1, 2, 0, 1, 6) self.setLayout(self.grid) def add_group(self, group, metadata=None): """Adds group form.""" if metadata is not None: group.write_fields(metadata=metadata) group.form_name.textChanged.connect(self.refresh_children) self.groups_list.append(group) nWidgetsVbox = self.vbox1.count() self.vbox1.insertWidget(nWidgetsVbox - 1, group) # insert before the stretch self.refresh_children(metadata=metadata) def is_referenced(self, grp_unique_name): """Tests if a group is being referenced any other groups. Returns boolean.""" nWidgetsVbox = self.vbox1.count() for i in range(nWidgetsVbox): if self.vbox1.itemAt(i).widget() is not None: other_grp = self.vbox1.itemAt(i).widget() # check if this subgroup has any ComboBox referencing grp_unique_name for ch in other_grp.children(): if isinstance(ch, (CustomComboBox, QComboBox)): if ch.currentText() == grp_unique_name: return True return False def refresh_children(self, metadata=None): """Refreshes references with existing objects in child groups.""" for child in self.groups_list: child.refresh_objects_references(metadata=metadata) def read_fields(self): """Reads fields and returns them structured in a dictionary.""" error = None data = {} # group_type counts, if there are multiple groups of same type, they are saved in a list grp_types = [grp.group_type for grp in self.groups_list] grp_type_count = { value: len(list(freq)) for value, freq in groupby(sorted(grp_types)) } # initiate lists as values for groups keys with count > 1 for k, v in grp_type_count.items(): if v > 1 or k == 'Device' or k == 'OptogeneticStimulusSite' or k == 'OptogeneticSeries': data[k] = [] # iterate over existing groups and copy their metadata for grp in self.groups_list: if grp_type_count[grp.group_type] > 1 or grp.group_type == 'Device' \ or grp.group_type == 'OptogeneticStimulusSite' \ or grp.group_type == 'OptogeneticSeries': data[grp.group_type].append(grp.read_fields()) else: data[grp.group_type] = grp.read_fields() return data, error
class ComponentTreeViewTab(QWidget): def __init__(self, parent=None): super().__init__() self.setLayout(QVBoxLayout()) self.setParent(parent) self.componentsTabLayout = QVBoxLayout() self.component_tree_view = QTreeView() self.componentsTabLayout.addWidget(self.component_tree_view) self.layout().addLayout(self.componentsTabLayout) self.component_tree_view.setDragEnabled(True) self.component_tree_view.setAcceptDrops(True) self.component_tree_view.setDropIndicatorShown(True) self.component_tree_view.header().hide() self.component_tree_view.updateEditorGeometries() self.component_tree_view.updateGeometries() self.component_tree_view.updateGeometry() self.component_tree_view.clicked.connect(self._set_button_state) self.component_tree_view.setSelectionMode(QAbstractItemView.SingleSelection) self.component_tool_bar = QToolBar("Actions", self) self.new_component_action = create_and_add_toolbar_action( "new_component.png", "New Component", self.parent().show_add_component_window, self.component_tool_bar, self, True, ) self.new_translation_action = create_and_add_toolbar_action( "new_translation.png", "New Translation", lambda: self._add_transformation(TransformationType.TRANSLATION), self.component_tool_bar, self, ) self.new_rotation_action = create_and_add_toolbar_action( "new_rotation.png", "New Rotation", lambda: self._add_transformation(TransformationType.ROTATION), self.component_tool_bar, self, ) self.create_link_action = create_and_add_toolbar_action( "create_link.png", "Create Link", self.on_create_link, self.component_tool_bar, self, ) self.duplicate_action = create_and_add_toolbar_action( "duplicate.png", "Duplicate", self.on_duplicate_node, self.component_tool_bar, self, ) self.edit_component_action = create_and_add_toolbar_action( "edit_component.png", "Edit Component", self.parent().show_edit_component_dialog, self.component_tool_bar, self, ) self.delete_action = create_and_add_toolbar_action( "delete.png", "Delete", self.on_delete_item, self.component_tool_bar, self ) self.zoom_action = create_and_add_toolbar_action( "zoom.svg", "Zoom To Component", self.on_zoom_item, self.component_tool_bar, self, ) self.component_tool_bar.insertSeparator(self.zoom_action) self.componentsTabLayout.insertWidget(0, self.component_tool_bar) def set_up_model(self, instrument): self.component_model = ComponentTreeModel(instrument) self.component_delegate = ComponentEditorDelegate( self.component_tree_view, instrument ) self.component_tree_view.setItemDelegate(self.component_delegate) self.component_tree_view.setModel(self.component_model) def _set_button_state(self): set_button_states( self.component_tree_view, self.delete_action, self.duplicate_action, self.new_rotation_action, self.new_translation_action, self.create_link_action, self.zoom_action, self.edit_component_action, ) def on_create_link(self): selected = self.component_tree_view.selectedIndexes() if len(selected) > 0: self.component_model.add_link(selected[0]) self._expand_transformation_list(selected[0]) self._set_button_state() def on_duplicate_node(self): selected = self.component_tree_view.selectedIndexes() if len(selected) > 0: self.component_model.duplicate_node(selected[0]) self._expand_transformation_list(selected[0]) def _expand_transformation_list(self, node: QModelIndex): expand_transformation_list(node, self.component_tree_view, self.component_model) def _add_transformation(self, transformation_type: TransformationType): add_transformation( transformation_type, self.component_tree_view, self.component_model ) def on_delete_item(self): selected = self.component_tree_view.selectedIndexes() for item in selected: self.component_model.remove_node(item) self._set_button_state() def on_zoom_item(self): selected = self.component_tree_view.selectedIndexes()[0] component = selected.internalPointer() self.sceneWidget.zoom_to_component( self.sceneWidget.get_entity(component.name), self.sceneWidget.view.camera() )
class AbsOperationEditor(QWidget): """ Base class for operation editors. Provide editors made of a custom widget and two buttons, one to accept and one to close and reject changes. Pressing of one of these two buttons emits one of two signals: - accept - reject """ # Signal to emit when editing is finished (must be class object) accept = Signal() reject = Signal() # ---------------------------------------------------------------------------- # ---------------------- FINAL METHODS (PLS NO OVERRIDE) --------------------- # ---------------------------------------------------------------------------- def __init__(self, parent: QWidget = None): super().__init__(parent) # Standard options self.errorHandlers: Dict[str, Callable] = dict() self.acceptedTypes: List[Type] = list() self.inputShapes: List[Optional[data.Shape]] = list() self.workbench: 'WorkbenchModel' = None # Set up buttons self._butOk = QPushButton('Ok') butCancel = QPushButton('Cancel') self.butLayout = QHBoxLayout() self.butLayout.addWidget(butCancel, alignment=Qt.AlignLeft) self.butLayout.addWidget(self._butOk, alignment=Qt.AlignRight) self.__helpVerticalLayout = QVBoxLayout() self.__helpLayout = QHBoxLayout() self.__descLabel = QLabel() self.__helpLayout.addWidget(self.__descLabel, 7) self.__descLabel.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) self.__helpVerticalLayout.addLayout(self.__helpLayout) ll = QLabel('<hr>') ll.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.__helpVerticalLayout.addWidget(ll) self.errorLabel = QLabel(self) self.errorLabel.setWordWrap(True) self.errorLabel.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) self._layout = QVBoxLayout() self._layout.addLayout(self.__helpVerticalLayout, 1) self._layout.addWidget(self.errorLabel, 1) ll = QLabel('<hr>') ll.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self._layout.addWidget(ll, 1) self._layout.addLayout(self.butLayout, 1) self.setLayout(self._layout) self.setFocusPolicy(Qt.StrongFocus) self.errorLabel.hide() self.setMinimumWidth(500) self._butOk.pressed.connect(self.onAcceptSlot) butCancel.pressed.connect(self.reject) # emit reject self.__sh: Optional[QSize] = None # Qt sizeHint property self._infoBalloon = None def setDescription(self, short: str, long: str) -> None: self.__descLabel.setText(short) self.__descLabel.setWordWrap(True) if long: whatsThisButton = QPushButton('More') whatsThisButton.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.__helpLayout.addWidget(whatsThisButton, 1) whatsThisButton.clicked.connect(lambda: self.showInfoBalloon(long)) # self.setWhatsThis(long) # whatsThisButton.clicked.connect( # lambda: QWhatsThis.showText(QCursor.pos(), long, self)) @Slot(str) def showInfoBalloon(self, long: str) -> None: if not self._infoBalloon: self._infoBalloon = InfoBalloon(self) self._infoBalloon.setText(long) self._infoBalloon.show() self._infoBalloon.move(QCursor.pos()) def closeEvent(self, event: QCloseEvent) -> None: """ Reject changes and close editor if the close button is pressed """ self.reject.emit() def setUpEditor(self): """ Calls editorBody and add the returned widget """ w = self.editorBody() w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) self._layout.insertWidget(1, w) def handleErrors(self, errors: List[Tuple[str, str]]) -> None: """ Provide a list of readable errors to be shown in the widget :param errors: list of (errorName, errorMessage). If 'errorName' is available in the widget errorHandlers field the corresponding callback will be fired with the 'errorMessage' This is useful to show custom error messages in specific parts of the editor widget """ self.errorLabel.hide() text = '' for (field, message) in errors: handler = self.errorHandlers.get(field, None) if handler: handler(message) else: text += '<br>' + message text = text.strip('<br>') if text: # Default message on bottom self.errorLabel.setText(text) self.errorLabel.setStyleSheet('color: red;') self.errorLabel.show() def disableOkButton(self) -> None: """ Makes the accept button uncheckable. Useful to prevent user from saving invalid changes """ self._butOk.setDisabled(True) def enableOkButton(self) -> None: """ Enable the accept button """ self._butOk.setEnabled(True) @Slot() def onAcceptSlot(self) -> None: self.onAccept() self.accept.emit() def setSizeHint(self, w: int, h: int) -> None: """ Set the editor sizeHint property. This function is provided for convenience: it is equivalent to reimplementing sizeHint() directly in a subclass. """ self.__sh = QSize(w, h) def sizeHint(self) -> QSize: if self.__sh: return self.__sh return super().sizeHint() def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key_Escape: self.reject.emit() else: super().keyPressEvent(event) # ---------------------------------------------------------------------------- # ------------------------------ VIRTUAL METHODS ----------------------------- # ---------------------------------------------------------------------------- @abc.abstractmethod def editorBody(self) -> QWidget: """ Hook method to add widget components. This may include Qt components of every kind, as long as they can belong to a QWidget. This method may add fields to the instance but must return the widget that should be shown in the editor. This method is called after the constructor """ pass @abc.abstractmethod def getOptions(self) -> Iterable: """ Return the arguments read by the editor. Must be an iterable and parameters are passed in the same order. If no options are set return a list with None values :return: the options currently set by the user in the editor """ pass def setOptions(self, *args, **kwargs) -> None: """ Set the data to be visualized in the editor. Useful to show an existing configuration. Does nothing by default :param args: any positional argument. """ pass def onAccept(self) -> None: """ Method called when the user accepts current options (e.g. clicks the Ok button), immediately before emitting the 'accept' signal. Useful to do additional actions over options before setting them in the operation. Does nothing by default """ pass
class InstallingMissingDependencies(QDialog): """ At start, dEaduction checks if missing dependencies are missing. If some indeed are, usr is asked is they want to install them (ReallyWantQuit). If they do, download begins and this dialog is shown showing a logger of the installation. When the download is completed, the 'Start dEAduction' button is activated (method installation_completed) and usr may click on it. If they do, the signal plz_start_deaduction is emitted. Furthermore, the usr may want to quit (by clicking the 'Quit' button). If they do so during the installation, they are asked for confirmation (see __quit method) and if they confirm, the signal plz_quit is emitted. The logger is displayed with the class TextEditLogger. """ plz_start_deaduction = Signal() plz_quit = Signal() def __init__( self, log_format: str = '%(asctime)s - %(levelname)s - %(message)s'): """ Init self with a logger formater (so specify the layout of the log entries, see logging module documentation), e.g. '%(asctime)s - %(levelname)s - %(message)s'. :param log_format: Logger formatter for the log entries. """ super().__init__() self.setModal(True) self.setWindowTitle(f"{_('Installing missing dependencies')}" \ " — d∃∀duction") self.__text_edit_logger = TextEditLogger() self.__confirm_quit = True # Buttons self.__quit_btn = QPushButton(_('Quit')) self.__start_dead_btn = QPushButton(_('Start d∃∀duction')) self.__quit_btn.setAutoDefault(False) self.__start_dead_btn.setEnabled(False) self.__quit_btn.clicked.connect(self.__quit) self.__start_dead_btn.clicked.connect(self.plz_start_deaduction) # Layouts self.__main_layout = QVBoxLayout() btns_layout = QHBoxLayout() btns_layout.addStretch() btns_layout.addWidget(self.__quit_btn) btns_layout.addWidget(self.__start_dead_btn) self.__main_layout.addWidget(self.__text_edit_logger) self.__main_layout.addLayout(btns_layout) self.setLayout(self.__main_layout) # Logging facilities, avoid some segfault and thread-related nastyness self.__text_edit_logger_handler = TextEditLoggerHandler( self.__text_edit_logger, log_format) self.__log_queue = Queue(-1) self.__queue_handler = QueueHandler(self.__log_queue) self.__queue_listener = QueueListener(self.__log_queue, self.__text_edit_logger_handler) def log_attach(self, log_obj: logging.Logger): log_obj.addHandler(self.__queue_handler) def log_dettach(self, log_obj: logging.Logger): log_obj.removeHandler(self.__queue_handler) def log_start(self): self.__queue_listener.start() def log_stop(self): self.__queue_listener.stop() @Slot() def installation_completed(self): """ This function is to be called when dependencies are all downloaded and the installation is completed: - it indicates all went good; - it allows usr to click on the 'Start dEAduction button'; - it sets self.__confirm_quit to False, which implies that if usr wants to quit instead of starting dEAduction, confirmation will *not* be asked. It is recommanded (to comply with Qt's style) to connect this method to a slot and not call it directly. """ self.__confirm_quit = False self.__text_edit_logger.setStyleSheet('background: SpringGreen;') self.__main_layout.insertWidget( 1, QLabel(_('Missing dependencies installed.'))) self.__start_dead_btn.setEnabled(True) self.__start_dead_btn.setDefault(True) self.__start_dead_btn.clicked.connect(self.plz_start_deaduction) @Slot() def __quit(self): """ This slot is called when usr clicks on the 'Quit' button. If downloads and installation are not completed, confirmation is asked by executing ReallyWantQuit dialog. In the end, if usr really wants to quit, the signal plz_quit is emitted (here is not the place to quit). """ if self.__confirm_quit: rwtq = ReallyWantQuit(_('All downloaded data will be lost.')) rwtq.exec_() if rwtq.yes: self.plz_quit.emit() else: self.plz_quit.emit()
class ConfigWindow(QWidget): def __init__(self, current_profile): """ The ConfigWindow is a widget dedicated to reading and editing the OCI config file and provides functionality to create, edit, and switch profiles on the fly, updating the view of the main window. :param current_profile: The profile the ConfigWindow should be initialized with :type current_profile: string """ super().__init__() # self.main_window = None self.setWindowTitle("Profile Settings") self.setMinimumSize(600, 200) #Looks for the config file in '~/.oci/config' and reads it into config self.DEFAULT_LOCATION = os.path.expanduser( os.path.join('~', '.oci', 'config')) self.config = configparser.ConfigParser(interpolation=None) self.config.read(self.DEFAULT_LOCATION) self.current_profile = current_profile #Set up necessary dropdown and LineEdit widgets self.dropdown = self.get_profiles_dropdown() self.tenancy = QLineEdit() self.tenancy.setPlaceholderText("Tenancy OCID") self.region = QLineEdit() self.region.setPlaceholderText("Region") self.user = QLineEdit() self.user.setPlaceholderText("User OCID") self.fingerprint = QLineEdit() self.fingerprint.setPlaceholderText("Fingerprint") self.key_file = QLineEdit() self.key_file.setPlaceholderText("Key File Path") self.passphrase = QLineEdit() self.passphrase.setEchoMode(QLineEdit.Password) self.passphrase.setPlaceholderText("Passphrase") self.save_button = QPushButton('Save') self.save_button.clicked.connect(self.save_signal) #Set the profile to the current_profile passed in upon init self.change_profile(current_profile) self.dropdown.setCurrentText(current_profile) #Add all widgets to a vertical layout self.layout = QVBoxLayout() self.layout.addWidget(self.dropdown) self.layout.addWidget(self.tenancy) self.layout.addWidget(self.region) self.layout.addWidget(self.user) self.layout.addWidget(self.key_file) self.layout.addWidget(self.fingerprint) self.layout.addWidget(self.passphrase) self.layout.addWidget(self.save_button) self.setLayout(self.layout) def get_profiles_dropdown(self): """ :return: A dropdown menu widget that lists all profiles including the default profile from the OCI config file When index changes, it will call the change_profile signal function :rtype: :class: 'Qt.QtWidgets.QComboBox' """ dropdown = QComboBox() dropdown.addItems(['DEFAULT'] + self.config.sections()) dropdown.addItem("Add New Profile...") dropdown.currentIndexChanged.connect(self.change_profile_signal) return dropdown def change_profile_signal(self, item): """ Slot to change profile. If the item index is at 0, then it is the default profile. If it is the last index, then that means create a new profile :param item: The index of the item from the dropdown widget :type item: int """ if item > len(self.config.sections()): self.create_new_profile() elif item == 0: self.change_profile('DEFAULT') else: self.change_profile(self.config.sections()[item - 1]) def change_profile(self, profile_name): """ Changes the profile that the ConfigWindow is set for and also changes it for the MainWindow :param profile_name: the name of the profile to switch to :type profile_name: string TODO: Adhere to signal/slot convention """ self.current_profile = profile_name profile = self.config[profile_name] for line, key in zip([self.tenancy, self.region, self.user, self.fingerprint, self.key_file, self.passphrase],\ ['tenancy', 'region', 'user', 'fingerprint', 'key_file', 'pass_phrase']): if key in profile: line.setText(profile[key]) else: line.setText("") if self.main_window: self.main_window.change_profile(self.current_profile) def create_new_profile(self): """ Layout to create a new profile. Removes the dropdown widget and changes the buttons """ self.layout.removeItem(self.layout.itemAt(0)) self.dropdown.setParent(None) self.new_profile_name = QLineEdit() self.new_profile_name.setPlaceholderText("Profile Name") self.layout.insertWidget(0, self.new_profile_name) self.tenancy.setText("") self.region.setText("") self.user.setText("") self.fingerprint.setText("") self.key_file.setText("") self.passphrase.setText("") self.create_button = QPushButton('Create') self.create_button.clicked.connect(self.create_signal) self.cancel_button = QPushButton('Cancel') self.cancel_button.clicked.connect(self.cancel_signal) self.buttonBox = QDialogButtonBox() self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.addButton(self.create_button, QDialogButtonBox.ActionRole) self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.ActionRole) self.layout.removeItem(self.layout.itemAt(7)) self.save_button.setParent(None) self.layout.addWidget(self.buttonBox) def create_signal(self): """ Create a new profile with the given information in the LineEdit widgets. Saves to the OCI config file """ profile_name = self.new_profile_name.text() self.config[profile_name] = {} self.config[profile_name]['tenancy'] = self.tenancy.text() self.config[profile_name]['region'] = self.region.text() self.config[profile_name]['user'] = self.user.text() self.config[profile_name]['fingerprint'] = self.fingerprint.text() self.config[profile_name]['key_file'] = self.key_file.text() self.config[profile_name]['pass_phrase'] = self.passphrase.text() with open(self.DEFAULT_LOCATION, 'w') as configfile: self.config.write(configfile) self.current_profile = profile_name self.cancel_signal() def save_signal(self): """ Saves edits on a currently existing profile. Saves to the OCI config file """ self.config[self.current_profile]['tenancy'] = self.tenancy.text() self.config[self.current_profile]['region'] = self.region.text() self.config[self.current_profile]['user'] = self.user.text() self.config[ self.current_profile]['fingerprint'] = self.fingerprint.text() self.config[self.current_profile]['key_file'] = self.key_file.text() self.config[ self.current_profile]['pass_phrase'] = self.passphrase.text() with open(self.DEFAULT_LOCATION, 'w') as configfile: self.config.write(configfile) def cancel_signal(self): """ Cancels the creation a new profile and reverts layout to default layout """ self.layout.removeItem(self.layout.itemAt(0)) self.new_profile_name.setParent(None) self.dropdown = self.get_profiles_dropdown() self.layout.insertWidget(0, self.dropdown) self.layout.removeItem(self.layout.itemAt(7)) self.buttonBox.setParent(None) self.change_profile(self.current_profile) self.dropdown.setCurrentText(self.current_profile) self.layout.addWidget(self.save_button)
class Widget(QWidget): def __init__(self): QWidget.__init__(self) self.setWindowTitle("Backend Discord-GUI") self.changeStyle('fusion') palette = QPalette() palette.setColor(QPalette.Window, QColor(53, 53, 53)) palette.setColor(QPalette.WindowText, Qt.white) palette.setColor(QPalette.Text, Qt.white) palette.setColor(QPalette.Button, QColor(60, 60, 60)) palette.setColor(QPalette.ButtonText, Qt.white) palette.setColor(QPalette.Base, QColor(40, 40, 40)) palette.setColor(QPalette.ToolTipBase, QColor(60, 60, 60)) palette.setColor(QPalette.ToolTipText, Qt.white) palette.setColor(QPalette.PlaceholderText, Qt.white) palette.setColor(QPalette.BrightText, Qt.white) palette.setColor(QPalette.Highlight, QColor(106, 13, 173)) palette.setColor(QPalette.HighlightedText, Qt.white) topButtonLayout = QGroupBox("Configurations") topStatsLayout = QGroupBox("Statistics") layoutLeft = QHBoxLayout() botConfigButton = QPushButton("Bot Config") botConfigButton.clicked.connect(lambda: CommentPopup()) serverSettingsButton = QPushButton("Server Settings") settingsButton = QPushButton("Settings") layoutLeft.addWidget(botConfigButton) layoutLeft.addWidget(serverSettingsButton) layoutLeft.addWidget(settingsButton) layoutRight = QVBoxLayout() botReadyLabel = QLabel("Bot_Ready: False") botStatusLabel = QLabel("Bot_Status: Off") # botDatabaseLabel = QLabel("Bot_Database: None") # botStandbyLabel = QLabel("Bot_Standby: False") layoutRight.addWidget(botReadyLabel) layoutRight.addWidget(botStatusLabel) # layoutRight.addWidget(botDatabaseLabel) # layoutRight.addWidget(botStandbyLabel) topButtonLayout.setLayout(layoutLeft) topStatsLayout.setLayout(layoutRight) self.createLeftSide() self.createRightSide() self.createProgressBar() topLayout = QGridLayout() topLayout.addWidget(topButtonLayout, 0, 0) topLayout.addWidget(topStatsLayout, 0, 1) topLayout.setColumnStretch(0, 1) mainLayout = QGridLayout() mainLayout.addLayout(topLayout, 0, 0, 1, 2) mainLayout.addWidget(self.leftSideGB, 1, 0) mainLayout.addWidget(self.topRightGroupBox, 1, 1) mainLayout.addWidget(self.progressBar, 3, 0, 1, 2) mainLayout.setRowStretch(1, 2) mainLayout.setColumnStretch(0, 1) mainLayout.setColumnStretch(1, 2) self.setLayout(mainLayout) QApplication.setPalette(palette) def changeStyle(self, styleName): QApplication.setStyle(QStyleFactory.create(styleName)) def advanceProgressBarLoading(self): curVal = self.progressBar.value() maxVal = self.progressBar.maximum() if curVal != maxVal: num = random.randint(1, 30) self.progressBar.setValue(curVal + num) else: self.timer.stop() change_status('Ready') self.progressBar.setValue(0) def createLeftSide(self): self.leftSideGB = QGroupBox() home_directory = "./app/" palette = QPalette() palette.setColor(QPalette.Window, QColor(30, 30, 30)) model = QDirModel() view = QTreeView() view.setStyleSheet("QTreeView { border: 0px; }") view.setModel(model) view.setRootIndex(model.index(home_directory)) view.setColumnHidden(1, True) view.setColumnHidden(2, True) view.setColumnHidden(3, True) view.show() view.setPalette(palette) runButton = QPushButton("►") stopButton = QPushButton("❚❚") bottomBar = QHBoxLayout() bottomBar.addWidget(runButton) bottomBar.addWidget(stopButton) layout = QVBoxLayout() layout.addWidget(view) layout.addLayout(bottomBar) layout.setStretch(0, 2) self.leftSideGB.setLayout(layout) def createRightSide(self): self.topRightGroupBox = QGroupBox() self.totalLength = 0 self.elems = 0 self.elems_list = [] self.overall_layout = QVBoxLayout() grad = QPalette() gradient = QConicalGradient(QPointF(1100, 150), -190) gradient.setColorAt(0.0, QColor(30, 30, 30)) gradient.setColorAt(0.5, QColor(50, 50, 50)) gradient.setColorAt(0.97, QColor(50, 13, 150)) gradient.setColorAt(1.0, QColor(106, 13, 173)) gradient.setSpread(QGradient.RepeatSpread) grad.setBrush(QPalette.Window, QBrush(gradient)) self.setPalette(grad) self.scrollarea = QScrollArea() self.scrollarea.setWidgetResizable(True) self.widget = QWidget() self.scrollarea.setWidget(self.widget) self.layout = QVBoxLayout(self.widget) self.add_elem = QPushButton("Add Element") if PLATFORM == "darwin": self.add_elem.setToolTip("Shortcut: ⌘E") else: self.add_elem.setToolTip("Shortcut: Ctrl+E") self.add_elem.setStyleSheet( "QToolTip { border: 0px; border-radius: 3px }") self.add_elem.clicked.connect(lambda: ElementPopup()) self.add_elem.setFixedWidth(300) shortcut = QShortcut(QKeySequence("Ctrl+E"), self.add_elem) shortcut.activated.connect(lambda: ElementPopup()) shortcut.setEnabled(True) self.layout.addWidget(self.add_elem) self.layout.setAlignment(self.add_elem, Qt.AlignCenter | Qt.AlignTop) self.overall_layout.addWidget(self.scrollarea) self.topRightGroupBox.setLayout(self.overall_layout) def add_element(self, title, type, isDupe=False, indexForDupe=0, data=""): # open form of widget lists if data != "": title = title + ": " + data elem = create_elem(title, type, data) self.elems_list.append(elem.getElem()) self.elems += 1 self.totalLength += 100 if isDupe: self.layout.insertWidget(indexForDupe + 1, self.elems_list[self.elems - 1]) else: self.layout.insertWidget(self.elems - 1, self.elems_list[self.elems - 1]) if self.totalLength > self.topRightGroupBox.height(): self.scrollarea.verticalScrollBar().setMaximum( self.scrollarea.verticalScrollBar().maximum() + 85) self.scrollarea.verticalScrollBar().setValue( self.scrollarea.verticalScrollBar().maximum()) self.topRightGroupBox.update() def createProgressBar(self): self.progressBar = QProgressBar() self.progressBar.setRange(0, 10000) self.progressBar.setValue(0) self.progressBar.setTextVisible(False) self.progressBar.setFixedHeight(5) # self.timer = QTimer(self) # self.timer.timeout.connect(self.advanceProgressBarLoading) # self.timer.start(10) @Slot() def quit_application(self): QApplication.quit()
class NotificationsList(QWidget): READ_BACKGROUND_COLOR = "white" UNREAD_BACKGROUND_COLOR = "#eeeeee" WIDGET_BACKGROUND_COLOR = "darkGray" def __init__(self, dp, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self._dp = dp self._items = [] self._main_layout = QVBoxLayout(self) self._main_layout.setSpacing(2) self._main_layout.setContentsMargins(0, 0, 0, 0) self._main_layout.addStretch() self.setStyleSheet('background-color: {};'.format( self.READ_BACKGROUND_COLOR)) def show_notifications(self, notifications): items_count = len(self._items) for i, notification in enumerate(notifications): if i < items_count: self._update_n_list_item_widget(notification, self._items[i]) else: self._create_n_list_item_widget(notification) for i in range(items_count - 1, len(notifications) - 1, -1): self._main_layout.removeWidget(self._items[i]) self._items.pop() self.setStyleSheet('background-color: {};'.format( self.WIDGET_BACKGROUND_COLOR)) def loading_needed(self, limit): items_len = len(self._items) if items_len < limit: return True for widget in self._items[-limit:]: if not widget.visibleRegion().isEmpty(): return True return False def _create_n_list_item_widget(self, notification): widget = QWidget(parent=self) # widget.setFixedWidth(self.width()) widget.notification = notification main_layout = QVBoxLayout(widget) main_layout.setSpacing(2) text_label = QLabel(widget) widget.text_label = text_label text_label.setWordWrap(True) text_label.setFont(QFont('Noto Sans', 10 * self._dp)) text_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) text_label.setText(notification.get_text()) main_layout.addWidget(text_label) time_label = QLabel(widget) widget.time_label = time_label time_label.setFont(QFont('Noto Sans', 8 * self._dp)) time_label.setAlignment(Qt.AlignTop | Qt.AlignRight) time_label.setText(notification.get_datetime()) main_layout.addWidget(time_label) self._set_background_color(widget, notification) def clicked(_): widget.notification.read() self._set_background_color(widget, widget.notification) widget.mouseReleaseEvent = clicked widget.text_label.mouseReleaseEvent = clicked widget.time_label.mouseReleaseEvent = clicked self._main_layout.insertWidget(len(self._items), widget) self._items.append(widget) def _update_n_list_item_widget(self, notification, widget): widget.notification = notification widget.text_label.setText(notification.get_text()) widget.time_label.setText(notification.get_datetime()) self._set_background_color(widget, notification) def _set_background_color(self, widget, notification): background_color = self.READ_BACKGROUND_COLOR \ if notification.is_read \ else self.UNREAD_BACKGROUND_COLOR widget.setStyleSheet('background-color: {};'.format(background_color))
class ManagerWindow(MayaQWidgetDockableMixin, QWidget): add_spore_clicked = Signal(str) remove_spore_clicked = Signal() refresh_spore_clicked = Signal() close_event = Signal() def __init__(self, parent=None): super(ManagerWindow, self).__init__(parent=parent) self.setWindowTitle('Spore Manager') self.setGeometry(50, 50, 400, 550) # self.setMinimumSize(350, 400) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.items = [] # list of all item widgets self.build_ui() self.connect_signals() def build_ui(self): layout = QGridLayout(self) layout.setContentsMargins(5, 5, 5, 5) self.setLayout(layout) self.name_edt = QLineEdit() self.name_edt.setPlaceholderText('Create New') self.name_edt.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) layout.addWidget(self.name_edt, 0, 0, 1, 1) self.add_btn = QPushButton() self.add_btn.setIcon(QIcon(QPixmap(':/teAdditive.png'))) self.add_btn.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) layout.addWidget(self.add_btn, 0, 1, 1, 1) self.refresh_btn = QPushButton() self.refresh_btn.setIcon(QIcon(QPixmap(':/teKeyRefresh.png'))) self.refresh_btn.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) layout.addWidget(self.refresh_btn, 0, 2, 1, 1) scroll_wdg = QWidget(self) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet( "QScrollArea { background-color: rgb(57,57,57);}") scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setWidget(scroll_wdg) self.spore_layout = QVBoxLayout() self.spore_layout.setContentsMargins(1, 1, 3, 1) self.spore_layout.setSpacing(0) self.spore_layout.addStretch() scroll_wdg.setLayout(self.spore_layout) layout.addWidget(scroll_area, 1, 0, 1, 3) # self.frame_lay.addWidget(ItemWidget()) # layout.addWidget(btn, 0, 0, 1, 1) def connect_signals(self): self.name_edt.returnPressed.connect( lambda: self.add_spore_clicked.emit(self.name_edt.text())) self.add_btn.clicked.connect( lambda: self.add_spore_clicked.emit(self.name_edt.text())) # self.remove_btn.clicked.connect(self.remove_spore_clicked.emit) self.refresh_btn.clicked.connect(self.refresh_spore_clicked.emit) def append_item(self, item): self.items.append(item) self.spore_layout.insertWidget(0, item) def remove_item(self, item): pass # def clear_items(self): # for item in self.items: # self.spore_layout.removeWidget(item) # self.items.remove(item) # item.delateLater() # del item # # self.spore_layout.update() def clear_layout(self): """ remove all child widgets and layout """ self.name_edt.setText('') del self.items[:] while self.spore_layout.count(): child = self.spore_layout.takeAt(0) if child.widget() is not None: child.widget().deleteLater() # elif child.layout() is not None: # self.clear_layout(child.layout()) self.spore_layout.setSpacing(0) self.spore_layout.addStretch() def closeEvent(self, event): self.close_event.emit() def hideEvent(self, event): self.close_event.emit()
class ComponentTreeViewTab(QWidget): def __init__(self, scene_widget: InstrumentView, parent=None): super().__init__() self.setLayout(QVBoxLayout()) self.setParent(parent) self.componentsTabLayout = QVBoxLayout() self.component_tree_view = QNexusTreeView() self.parameters_widget = ParametersView(parent) self.componentsTabLayout.addWidget(self.parameters_widget) self.componentsTabLayout.addWidget(self.component_tree_view) self.layout().addLayout(self.componentsTabLayout) self.sceneWidget = scene_widget self.component_tree_view.setDragEnabled(True) self.component_tree_view.setAcceptDrops(True) self.component_tree_view.setDropIndicatorShown(True) self.component_tree_view.header().hide() self.component_tree_view.updateEditorGeometries() self.component_tree_view.updateGeometries() self.component_tree_view.updateGeometry() self.component_tree_view.clicked.connect(self._set_button_state) self.component_tree_view.setSelectionMode( QAbstractItemView.SingleSelection) self.component_tool_bar = QToolBar("Actions", self) self.new_component_action = create_and_add_toolbar_action( "new_component.png", "Group", self.parent().show_add_component_dialog, self.component_tool_bar, self, False, ) self.new_translation_action = create_and_add_toolbar_action( "new_translation.png", "Translation", lambda: self._add_transformation(TransformationType.TRANSLATION), self.component_tool_bar, self, ) self.new_rotation_action = create_and_add_toolbar_action( "new_rotation.png", "Rotation", lambda: self._add_transformation(TransformationType.ROTATION), self.component_tool_bar, self, ) self.create_link_action = create_and_add_toolbar_action( "create_link.png", "Link", self.on_create_link, self.component_tool_bar, self, ) self.edit_component_action = create_and_add_toolbar_action( "edit_component.png", "Edit", self.parent().show_edit_component_dialog, self.component_tool_bar, self, ) self.zoom_action = create_and_add_toolbar_action( "zoom.svg", "Zoom", self.on_zoom_item, self.component_tool_bar, self, ) self.component_tool_bar.insertSeparator(self.zoom_action) self.spacer = QWidget() self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self.component_tool_bar.addWidget(self.spacer) self.delete_action = create_and_add_toolbar_action( "delete.png", "Delete", self.on_delete_item, self.component_tool_bar, self) self.component_tool_bar.insertSeparator(self.delete_action) self.componentsTabLayout.insertWidget(0, self.component_tool_bar) def set_up_model(self, model: Model): model.signals.group_edited.connect( self.component_tree_view.collapse_group_in_tree) self.component_model = NexusTreeModel(model) self.component_delegate = ComponentEditorDelegate( self.component_tree_view, model) self.component_tree_view.setItemDelegate(self.component_delegate) self.component_tree_view.setModel(self.component_model) self.parameters_widget.set_up_model(model) def reset_model(self): self.set_up_model(self.component_model.model) def _set_button_state(self): set_button_states( self.component_tree_view, self.new_component_action, self.delete_action, self.new_rotation_action, self.new_translation_action, self.create_link_action, self.zoom_action, self.edit_component_action, ) def on_create_link(self): selected = self.component_tree_view.selectedIndexes() if len(selected) > 0: self.component_model.add_link(selected[0]) self._expand_transformation_list(selected[0]) self._set_button_state() def _expand_transformation_list(self, node: QModelIndex): expand_transformation_list(node, self.component_tree_view, self.component_model) def _add_transformation(self, transformation_type: str): add_transformation(transformation_type, self.component_tree_view, self.component_model) self._set_button_state() def on_delete_item(self): selected = self.component_tree_view.selectedIndexes() if len(selected[0].data().parent_node.children) == 1: new_selection_index = selected[0].parent() elif selected[0].row() > 0: new_selection_index = self.component_model.parent( selected[0]).child(selected[0].row() - 1, 0) elif selected[-1].row() <= len( selected[-1].data().parent_node.children) - 1: new_selection_index = self.component_model.parent( selected[-1]).child(selected[-1].row(), 0) else: new_selection_index = selected[0].parent() for item in selected: self.component_model.remove_node(item) self.component_tree_view.setCurrentIndex(new_selection_index) self._set_button_state() def on_zoom_item(self): selected = self.component_tree_view.selectedIndexes()[0] component = selected.internalPointer() self.sceneWidget.zoom_to_component( self.sceneWidget.get_entity(component.name), self.sceneWidget.view.camera())
class ButtonList(QWidget): # 按钮添加时发送信号 # QPushButton为触发的按钮 signalBtnAdded = Signal(QPushButton) signalBtnDeleted = Signal(QPushButton) signalBtnClicked = Signal(QPushButton) """按钮列表,默认有一个添加按钮的按钮""" def __init__(self, addStr: str, parent: any = None) -> None: '''按钮列表,默认有一个添加按钮的按钮 Args: addStr(str):添加按钮上显示的文字 ''' super().__init__(parent=parent) self.addIterm = QPushButton(addStr, self) self.addIterm.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) # self.addIterm.resize(self.addIterm.sizeHint()) self.addIterm.clicked.connect(self.addBtn_clicked) # 添加按钮布局 self.layDown = QVBoxLayout() self.layDown.addWidget(self.addIterm) # 按钮布局,与addIterm所处的布局分开,防止删除按钮时布局的神秘错乱(我太菜了) self.layUp = QVBoxLayout() # 整体外布局 lay = QVBoxLayout() lay.setMargin(3) # 设置边距 lay.addLayout(self.layUp) lay.addLayout(self.layDown) lay.addStretch(1) # 添加拉伸因子,防止按钮由于父控件大小被纵向拉伸 self.setLayout(lay) # 应用布局 @Slot() def addBtn_clicked(self) -> QPushButton: index = self.layUp.count() # 添加的按钮在布局中的索引位置,起始0 button = QPushButton('btn{}'.format(index)) button.clicked.connect(lambda: self.button_clicked(button)) button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) def on_context_menu(point): # 弹出菜单 menu.exec_(button.mapToGlobal(point)) # 把相对于按钮的位置转为相对于全局的位置 button.setContextMenuPolicy(Qt.CustomContextMenu) # 菜单策略,自定义 button.customContextMenuRequested.connect(on_context_menu) # 触发信号 # 设置右击删除菜单 menu = QMenu(button) delQAction = QAction("删除", button) delQAction.triggered.connect(lambda: self.deleteButton(button)) menu.addAction(delQAction) self.layUp.insertWidget(index, button) # 添加按钮到布局中 self.signalBtnAdded.emit(button) # 发送添加信号 return button @Slot(QPushButton) def deleteButton(self, button: QPushButton): self.signalBtnDeleted.emit(button) # 发送删除信号 # self.layUp.removeWidget(button) # 移除控件 button.deleteLater() # 删除控件 @Slot(QPushButton) def button_clicked(self, button: QPushButton): self.signalBtnClicked.emit(button) # 发送点击信号