def __init__(self): super(FindToolBar, self).__init__() self._line_edit = QLineEdit() self._line_edit.setClearButtonEnabled(True) self._line_edit.setPlaceholderText("Find...") self._line_edit.setMaximumWidth(300) self._line_edit.returnPressed.connect(self._find_next) self.addWidget(self._line_edit) self._previous_button = QToolButton() self._previous_button.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png')) self._previous_button.clicked.connect(self._find_previous) self.addWidget(self._previous_button) self._next_button = QToolButton() self._next_button.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/down-32.png')) self._next_button.clicked.connect(self._find_next) self.addWidget(self._next_button) self._case_sensitive_checkbox = QCheckBox('Case Sensitive') self.addWidget(self._case_sensitive_checkbox) self._hideButton = QToolButton() self._hideButton.setShortcut(QKeySequence(Qt.Key_Escape)) self._hideButton.setIcon(QIcon(':/qt-project.org/styles/macstyle/images/closedock-16.png')) self._hideButton.clicked.connect(self.hide) self.addWidget(self._hideButton)
class FindToolBar(QToolBar): find = QtCore.Signal(str, QWebEnginePage.FindFlags) def __init__(self): super(FindToolBar, self).__init__() self._line_edit = QLineEdit() self._line_edit.setClearButtonEnabled(True) self._line_edit.setPlaceholderText("Find...") self._line_edit.setMaximumWidth(300) self._line_edit.returnPressed.connect(self._find_next) self.addWidget(self._line_edit) self._previous_button = QToolButton() self._previous_button.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png')) self._previous_button.clicked.connect(self._find_previous) self.addWidget(self._previous_button) self._next_button = QToolButton() self._next_button.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/down-32.png')) self._next_button.clicked.connect(self._find_next) self.addWidget(self._next_button) self._case_sensitive_checkbox = QCheckBox('Case Sensitive') self.addWidget(self._case_sensitive_checkbox) self._hideButton = QToolButton() self._hideButton.setShortcut(QKeySequence(Qt.Key_Escape)) self._hideButton.setIcon(QIcon(':/qt-project.org/styles/macstyle/images/closedock-16.png')) self._hideButton.clicked.connect(self.hide) self.addWidget(self._hideButton) def focus_find(self): self._line_edit.setFocus() def _emit_find(self, backward): needle = self._line_edit.text().strip() if needle: flags = QWebEnginePage.FindFlags() if self._case_sensitive_checkbox.isChecked(): flags |= QWebEnginePage.FindCaseSensitively if backward: flags |= QWebEnginePage.FindBackward self.find.emit(needle, flags) def _find_next(self): self._emit_find(False) def _find_previous(self): self._emit_find(True)
class TableWidget(QWidget): def __init__(self, table, headers, bold=True, mono=True, tooltips=None, align=False, search=True, parent=None): super(TableWidget, self).__init__(parent) self.table_widget = QTableWidget(len(table), len(table[0])) for i, row in enumerate(table): for j, item in enumerate(row): if item is not None: self.table_widget.setItem(i, j, QTableWidgetItem(str(item))) if tooltips is not None: self.table_widget.setToolTip(tooltips[i][j]) modify_font(self.table_widget.item(i, j), bold=bold and j == 0, mono=mono) if align: self.table_widget.item(i, j).setTextAlignment( Qt.AlignRight) self.table_headers = headers self.table_widget.setHorizontalHeaderLabels(self.table_headers) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.resizeColumnsToContents() self.table_widget.setAlternatingRowColors(True) self.table_widget.itemDoubleClicked.connect(self.copy) search_layout = QHBoxLayout() search_layout.addWidget(QLabel(self.tr('Search:'))) self.search_edit = QLineEdit() self.search_edit.textChanged.connect(self.start) self.search_edit.returnPressed.connect(self.next) search_layout.addWidget(self.search_edit) clear_button = QToolButton() clear_button.setIcon(QIcon('icons/clear.svg')) clear_button.setShortcut(QKeySequence.DeleteCompleteLine) clear_button.setToolTip(self.tr('Clear pattern')) clear_button.clicked.connect(self.search_edit.clear) search_layout.addWidget(clear_button) self.case_button = QToolButton() self.case_button.setText(self.tr('Aa')) self.case_button.setCheckable(True) self.case_button.toggled.connect(self.start) self.case_button.setToolTip(self.tr('Case sensitive')) search_layout.addWidget(self.case_button) self.word_button = QToolButton() self.word_button.setText(self.tr('W')) self.word_button.setCheckable(True) self.word_button.toggled.connect(self.start) self.word_button.setToolTip(self.tr('Whole words')) search_layout.addWidget(self.word_button) self.regex_button = QToolButton() self.regex_button.setText(self.tr('.*')) self.regex_button.setCheckable(True) self.regex_button.toggled.connect(self.start) self.regex_button.setToolTip(self.tr('Regular expression')) search_layout.addWidget(self.regex_button) prev_button = QToolButton() prev_button.setIcon(QIcon('icons/up.svg')) prev_button.setShortcut(QKeySequence.FindPrevious) prev_button.clicked.connect(self.previous) prev_button.setToolTip(self.tr('Previous occurence')) search_layout.addWidget(prev_button) next_button = QToolButton() next_button.setIcon(QIcon('icons/down.svg')) next_button.setShortcut(QKeySequence.FindNext) next_button.clicked.connect(self.next) next_button.setToolTip(self.tr('Next occurence')) search_layout.addWidget(next_button) self.matches_label = QLabel() search_layout.addWidget(self.matches_label) search_layout.addStretch() export_button = QToolButton() export_button.setText(self.tr('Export...')) export_button.clicked.connect(self.export) search_layout.addWidget(export_button) main_layout = QVBoxLayout() main_layout.addWidget(self.table_widget) if search: main_layout.addLayout(search_layout) self.setLayout(main_layout) def start(self): self.search(self.search_edit.text(), -1, -1, 1) def next(self): row = self.table_widget.currentRow() col = self.table_widget.currentColumn() + 1 if col == self.table_widget.columnCount(): row += 1 col = 0 self.search(self.search_edit.text(), row, col, +1) def previous(self): row = self.table_widget.currentRow() col = self.table_widget.currentColumn() - 1 if col == -1: row -= 1 col = self.table_widget.columnCount() - 1 self.search(self.search_edit.text(), row, col, -1) def search(self, pattern, row, col, direction): nocase = not self.case_button.isChecked() word = self.word_button.isChecked() regex = self.regex_button.isChecked() matches = 0 index = 0 if direction > 0: row_range = range(self.table_widget.rowCount() - 1, -1, -1) col_range = range(self.table_widget.columnCount() - 1, -1, -1) else: row_range = range(self.table_widget.rowCount()) col_range = range(self.table_widget.columnCount()) for i in row_range: for j in col_range: item = self.table_widget.item(i, j) if item is not None: text = item.text() if regex: match = QRegularExpression(pattern).match( text).hasMatch() else: if nocase: text = text.lower() pattern = pattern.lower() if word: match = text == pattern else: match = pattern in text if match and pattern: self.table_widget.item(i, j).setBackground(Qt.yellow) if (direction > 0 and (i > row or i == row and j > col)) or \ (direction < 0 and (i < row or i == row and j < col)): self.table_widget.setCurrentCell(i, j) index = matches matches += 1 else: self.table_widget.item(i, j).setBackground(Qt.transparent) if pattern: self.matches_label.setVisible(True) if matches > 0: match = matches - index if direction > 0 else index + 1 self.matches_label.setText( self.tr('match #{}/{}'.format(match, matches))) self.matches_label.setStyleSheet('color: #000000') modify_font(self.matches_label, bold=True) else: self.matches_label.setText(self.tr('not found!')) self.matches_label.setStyleSheet('color: #FF0000') modify_font(self.matches_label, italic=True) else: self.matches_label.setText('') def export(self): settings = QSettings() filename = QFileDialog.getSaveFileName(self, self.tr('Export metadata'), settings.value('save_folder'), self.tr('CSV files (*.csv)'))[0] if not filename: return if not filename.endswith('.csv'): filename += '.csv' settings.setValue('save_folder', QFileInfo(filename).absolutePath()) rows = self.table_widget.rowCount() cols = self.table_widget.columnCount() table = [[None for _ in range(cols)] for __ in range(rows)] for i in range(rows): for j in range(cols): item = self.table_widget.item(i, j) if item is not None: table[i][j] = item.text() with open(filename, 'w') as file: writer = csv.writer(file) writer.writerow(self.table_headers) writer.writerows(table) self.info_message.emit(self.tr('Table contents exported to disk')) def copy(self, item): QApplication.clipboard().setText(item.text()) QToolTip.showText(QCursor.pos(), self.tr('Cell contents copied to clipboard'), self, QRect(), 3000)
class SoundBoard(QDialog): def __init__(self): super(SoundBoard, self).__init__() self.title = '=== SoundBoard ===' # positionnement de la fenêtre à l'ouverture self.left = 50 self.top = 50 # initialisation de la largeur et hauteur par défaut self.width = 500 self.height = 500 self.currFileName = "" self.pbPosToModify = -1 self.initUI() def initUI(self): self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) self.windowLayout = QHBoxLayout() self.tableWidget = QTableWidget() self.tableWidget.horizontalHeader().hide() self.tableWidget.verticalHeader().hide() self.initIcons() self.initMenu() self.initColorPicker() self.initButtons() self.windowLayout.setStretch(1, 0) self.setLayout(self.windowLayout) self.show() def initIcons(self): self.iEdit = QIcon() self.iEdit.addPixmap(QPixmap("./icons/edit.png"), QIcon.Normal, QIcon.Off) self.iPlus = QIcon() self.iPlus.addPixmap(QPixmap("./icons/plus.png"), QIcon.Normal, QIcon.Off) self.iMinus = QIcon() self.iMinus.addPixmap(QPixmap("./icons/minus.png"), QIcon.Normal, QIcon.Off) self.iParam = QIcon() self.iParam.addPixmap(QPixmap("./icons/cog.png"), QIcon.Normal, QIcon.Off) def initMenu(self): layout = QVBoxLayout() hlayout = QHBoxLayout() # bouton ajout self.tbPlus = QToolButton() self.tbPlus.setGeometry(QRect(0, 0, 32, 32)) self.tbPlus.setIcon(self.iPlus) self.tbPlus.setObjectName("tbPlus") hlayout.addWidget(self.tbPlus) self.tbPlus.clicked.connect(self.add) # bouton suppression self.tbMinus = QToolButton() self.tbMinus.setGeometry(QRect(0, 0, 32, 32)) self.tbMinus.setIcon(self.iMinus) self.tbMinus.setObjectName("tbMinus") hlayout.addWidget(self.tbMinus) self.tbMinus.clicked.connect(self.delete) # bouton édition self.tbEdit = QToolButton() self.tbEdit.setGeometry(QRect(0, 0, 32, 32)) self.tbEdit.setIcon(self.iEdit) self.tbEdit.setObjectName("tbEdit") hlayout.addWidget(self.tbEdit) self.tbEdit.clicked.connect(self.editBtn) # bouton paramètres self.tbParam = QToolButton() self.tbParam.setGeometry(QRect(0, 0, 32, 32)) self.tbParam.setIcon(self.iParam) self.tbParam.setObjectName("tbParam") hlayout.addWidget(self.tbParam) self.tbParam.clicked.connect(self.settings) layout.addLayout(hlayout) self.pbStop = QPushButton("Don't STOP\n\nthe\n\nSoundBoard") self.pbStop.setStyleSheet("font-weight: bold;") self.pbStop.setMinimumSize(QSize(100, 100)) self.pbStop.setGeometry(QRect(0, 0, 100, 100)) layout.addWidget(self.pbStop) self.pbStop.clicked.connect(self.stop) spacerMenu = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) layout.addItem(spacerMenu) self.windowLayout.addLayout(layout) def startInitButtons(self): self.tableWidget.clear() self.tableWidget.clearSpans() self.tableWidget.setColumnWidth(0, 100) self.tableWidget.setColumnWidth(2, 100) self.cdColorPicker.setVisible(False) self.tableWidget.horizontalHeader().hide() # import des informations boutons contenues dans le json with open('buttons.json', encoding='utf-8') as json_file: self.data_buttons = json.load(json_file) # stockage de la position la plus élevée pour le cadrage self.positions = [p['position'] for p in self.data_buttons['buttons']] self.max_pos = max(self.positions) # calcul du nombre de boutons par hauteur et largeur self.BtnH = self.data_buttons['buttons_grid']['height'] self.BtnW = self.data_buttons['buttons_grid']['width'] self.setGeometry(self.left, self.top, 140 + self.BtnW * 100, 175 if self.BtnH * 31 < 175 else 25 + self.BtnH * 30) self.tableWidget.setColumnCount(self.BtnW) self.tableWidget.setRowCount(self.BtnH) def endInitButtons(self): buttonsLayout = QVBoxLayout() buttonsLayout.setStretch(0, 1) buttonsLayout.addWidget(self.tableWidget) self.windowLayout.addLayout(buttonsLayout) self.setGeometry(self.left, self.top, 140 + self.BtnW * 100, 175 if self.BtnH * 31 < 175 else 25 + self.BtnH * 30) def initButtons(self): self.startInitButtons() # positionnement des boutons en fonction des positions du json for ligne in range(self.BtnH): for colonne in range(self.BtnW): if (ligne * self.BtnW) + (colonne + 1) in self.positions: for b in self.data_buttons['buttons']: if b['position'] == (ligne * self.BtnW) + (colonne + 1): pb = QPushButton(b['name'][:9]) pb.setProperty('pbPos', b['position']) # si fond clair, font noire, si sombre, font blanche if (b['r'] * 0.299 + b['g'] * 0.587 + b['b'] * 0.114) > 186: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #000000;" ) else: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #ffffff;" ) self.tableWidget.setCellWidget(ligne, colonne, pb) pb.clicked.connect(self.play) else: pb = QPushButton('Nouveau') calcPos = self.BtnW * ligne + colonne + 1 pb.setProperty('pbPos', f"nouveau,{calcPos}") pb.clicked.connect(self.add) self.tableWidget.setCellWidget(ligne, colonne, pb) colonne += 1 ligne += 1 self.endInitButtons() def initColorPicker(self): self.lColorPicker = QVBoxLayout() self.cdColorPicker = QColorDialog() self.cdColorPicker.setOption(self.cdColorPicker.NoButtons, True) self.colorSelected = self.cdColorPicker.currentColor() self.lColorPicker.addWidget(self.cdColorPicker) self.cdColorPicker.setVisible(False) self.cdColorPicker.currentColorChanged.connect(self.colorChanged) self.windowLayout.addLayout(self.lColorPicker) def play(self): pb = self.sender() pbPos = pb.property('pbPos') for b in self.data_buttons['buttons']: if pbPos == b['position']: pbFile = b['file'] if (p.get_state() == vlc.State.Playing): p.stop() media = instance.media_new(soundRep + pbFile) if (self.currFileName != pbFile): p.set_media(media) p.play() self.currFileName = pbFile else: media = instance.media_new(soundRep + pbFile) p.set_media(media) p.play() self.currFileName = pbFile def stop(self): p.stop() def add(self): self.cdColorPicker.setVisible(True) self.tableWidget.clear() self.tableWidget.clearSpans() self.tableWidget.setColumnWidth(2, 100) self.tableWidget.setColumnCount(6) self.tableWidget.setRowCount(len(self.data_buttons['buttons']) + 1) self.tableWidget.horizontalHeader().show() self.tableWidget.setHorizontalHeaderItem(0, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(0).setText('Nom') self.tableWidget.setHorizontalHeaderItem(1, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(1).setText('Fichier') self.tableWidget.setHorizontalHeaderItem(2, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(2).setText('') self.tableWidget.setColumnWidth(2, 22) self.tableWidget.setHorizontalHeaderItem(3, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(3).setText('Position') self.tableWidget.setHorizontalHeaderItem(4, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(4).setText('Couleur') self.tableWidget.setHorizontalHeaderItem(5, QTableWidgetItem()) self.tableWidget.horizontalHeaderItem(5).setText('') # nom self.leName = QLineEdit() self.leName.setPlaceholderText('Nom (10 max.)') self.tableWidget.setCellWidget(0, 0, self.leName) # fichier self.leFile = QLineEdit() self.leFile.setPlaceholderText('Fichier') self.tableWidget.setCellWidget(0, 1, self.leFile) # browse pbBrowser = QPushButton('...') pbBrowser.setMinimumSize(QSize(21, 21)) pbBrowser.clicked.connect(self.browseMedia) self.tableWidget.setCellWidget(0, 2, pbBrowser) # position self.lePos = QLineEdit() self.lePos.setPlaceholderText('Position') self.tableWidget.setCellWidget(0, 3, self.lePos) # couleur self.leColor = QLineEdit() self.leColor.setPlaceholderText('255,255,255') self.leColor.setText( str(self.colorSelected.red()) + "," + str(self.colorSelected.green()) + "," + str(self.colorSelected.blue())) self.tableWidget.setCellWidget(0, 4, self.leColor) # validation pbValid = QPushButton('Valider') pbValid.clicked.connect(self.addValid) self.tableWidget.setCellWidget(0, 5, pbValid) pb = self.sender() pbPos = pb.property('pbPos') if pbPos is not None: if str(pbPos)[:8] == 'nouveau,': self.lePos.setText(pbPos[8:]) def sortByPos(val): return val['position'] self.data_buttons['buttons'].sort(key=sortByPos) for ligne, b in enumerate(self.data_buttons['buttons'], start=1): self.tableWidget.setSpan(ligne, 1, 1, 2) self.tableWidget.setCellWidget(ligne, 0, QLabel(b['name'])) self.tableWidget.setCellWidget(ligne, 1, QLabel(b['file'])) self.tableWidget.setCellWidget(ligne, 3, QLabel(str(b['position']))) self.tableWidget.setCellWidget(ligne, 4, QLabel('Couleur')) # 530 color picker width self.setGeometry(self.left, self.top, 690 + 530, 300) def addValid(self): gName = self.leName.text() self.leName.setStyleSheet("color: rgb(0,0,0);") gFile = self.leFile.text() self.leFile.setStyleSheet("color: rgb(0,0,0);") gPos = self.lePos.text() self.lePos.setStyleSheet("color: rgb(0,0,0);") gColor = self.leColor.text() self.leColor.setStyleSheet("color: rgb(0,0,0);") # si champs vides if ((gName == '' or gName == 'Obligatoire !') or (gFile == '' or gFile == 'Obligatoire !') or (gPos == '' or gColor == 'Obligatoire !') or (gColor == '' or gColor == 'Obligatoire !')): if gName == '' or gName == 'Obligatoire !': self.leName.setText('Obligatoire !') self.leName.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") if gFile == '' or gFile == 'Obligatoire !': self.leFile.setText('Obligatoire !') self.leFile.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") if gPos == '' or gColor == 'Obligatoire !': self.lePos.setText('Obligatoire !') self.lePos.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") if gColor == '' or gColor == 'Obligatoire !': self.leColor.setText('Obligatoire !') self.leColor.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") else: # vérif si champ position est un nombre try: flag = 0 flag = int(gPos) except ValueError: self.lePos.setText(f"{str(gPos)} n'est pas un nombre") self.lePos.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") # si position est un nombre if flag != 0: # si position hors grille if int(gPos) < 0 or int(gPos) > self.data_buttons[ 'buttons_grid']['height'] * self.data_buttons[ 'buttons_grid']['width']: self.lePos.setText(f"{str(gPos)} hors grille") self.lePos.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") else: dictToAppend = { "name": gName, "file": gFile, "position": int(gPos), "r": self.colorSelected.red(), "g": self.colorSelected.green(), "b": self.colorSelected.blue() } # si c'est une modification if self.pbPosToModify != -1: for b in self.data_buttons['buttons']: if b['position'] == self.pbPosToModify: self.data_buttons['buttons'].remove(b) self.data_buttons['buttons'].append(dictToAppend) with open('buttons.json', 'w', encoding='utf-8') as outfile: json.dump(self.data_buttons, outfile, indent=4) self.initButtons() else: # si position déjà prise if int(gPos) in self.positions: self.lePos.setText(f"{str(gPos)} déjà prise") self.lePos.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") else: self.data_buttons['buttons'].append(dictToAppend) with open('buttons.json', 'w', encoding='utf-8') as outfile: json.dump(self.data_buttons, outfile, indent=4) self.initButtons() def delete(self): self.startInitButtons() # positionnement des boutons en fonction des positions du json for ligne in range(self.BtnH): for colonne in range(self.BtnW): if (ligne * self.BtnW) + (colonne + 1) in self.positions: for b in self.data_buttons['buttons']: if b['position'] == (ligne * self.BtnW) + (colonne + 1): pb = QPushButton(b['name'][:9]) pb.setProperty('pbPos', b['position']) pb.setIcon(self.iMinus) # si fond clair, font noire, si sombre, font blanche if (b['r'] * 0.299 + b['g'] * 0.587 + b['b'] * 0.114) > 186: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #000000;" ) else: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #ffffff;" ) self.tableWidget.setCellWidget(ligne, colonne, pb) pb.clicked.connect(self.deleteTw) else: pb = QPushButton('Nouveau') calcPos = self.BtnW * ligne + colonne + 1 pb.setProperty('pbPos', f"nouveau,{calcPos}") pb.clicked.connect(self.add) self.tableWidget.setCellWidget(ligne, colonne, pb) colonne += 1 ligne += 1 self.endInitButtons() def deleteTw(self): pb = self.sender() pbPos = pb.property('pbPos') for b in self.data_buttons['buttons']: if b['position'] == pbPos: self.data_buttons['buttons'].remove(b) with open('buttons.json', 'w', encoding='utf-8') as outfile: json.dump(self.data_buttons, outfile, indent=4) self.delete() def editBtn(self): self.startInitButtons() # positionnement des boutons en fonction des positions du json for ligne in range(self.BtnH): for colonne in range(self.BtnW): if (ligne * self.BtnW) + (colonne + 1) in self.positions: for b in self.data_buttons['buttons']: if b['position'] == (ligne * self.BtnW) + (colonne + 1): pb = QPushButton(b['name'][:9]) pb.setProperty('pbPos', b['position']) pb.setIcon(self.iEdit) # si fond clair, font noire, si sombre, font blanche if (b['r'] * 0.299 + b['g'] * 0.587 + b['b'] * 0.114) > 186: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #000000;" ) else: pb.setStyleSheet( f"background-color: rgb({b['r']},{b['g']},{b['b']}); color: #ffffff;" ) self.tableWidget.setCellWidget(ligne, colonne, pb) pb.clicked.connect(self.editTw) else: pb = QPushButton('Nouveau') pb.setIcon(self.iEdit) calcPos = self.BtnW * ligne + colonne + 1 pb.setProperty('pbPos', f"nouveau,{calcPos}") pb.clicked.connect(self.add) self.tableWidget.setCellWidget(ligne, colonne, pb) colonne += 1 ligne += 1 self.endInitButtons() def editTw(self): pb = self.sender() pbPos = pb.property('pbPos') self.pbPosToModify = pbPos self.add() for b in self.data_buttons['buttons']: if b['position'] == pbPos: self.leName.setText(b['name']) self.leFile.setText(b['file']) self.lePos.setText(str(b['position'])) self.cdColorPicker.setCurrentColor( QColor(b['r'], b['g'], b['b'])) def settings(self): self.tableWidget.clear() self.tableWidget.clearSpans() self.tableWidget.setColumnWidth(2, 100) self.cdColorPicker.setVisible(False) self.tableWidget.setColumnCount(2) self.tableWidget.setRowCount(4) self.tableWidget.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.tableWidget.horizontalHeader().hide() # bouton validation pb = QPushButton('Valider') self.tableWidget.setCellWidget(3, 0, pb) pb.clicked.connect(self.saveSettings) # bouton annulation pb = QPushButton('Annuler') self.tableWidget.setCellWidget(3, 1, pb) pb.clicked.connect(self.refreshUI) # parameters self.tableWidget.setSpan(0, 0, 1, 2) self.lAlert = QLabel("La modification de ces valeurs entrainera la " "modification de position des boutons") self.lAlert.setStyleSheet("font-weight: bold;") self.tableWidget.setCellWidget(0, 0, self.lAlert) self.tableWidget.setCellWidget(1, 0, QLabel('Nombre de boutons en Hauteur')) self.leH = QLineEdit(str(self.data_buttons['buttons_grid']['height'])) self.tableWidget.setCellWidget(1, 1, self.leH) self.tableWidget.setCellWidget(2, 0, QLabel('Nombre de boutons en Largeur')) self.leW = QLineEdit(str(self.data_buttons['buttons_grid']['width'])) self.tableWidget.setCellWidget(2, 1, self.leW) settingsLayout = QVBoxLayout() settingsLayout.setStretch(0, 1) settingsLayout.addWidget(self.tableWidget) self.windowLayout.addLayout(settingsLayout) self.setGeometry(self.left, self.top, 600, 300) def saveSettings(self): h = int(self.leH.text()) w = int(self.leW.text()) if h * w < self.max_pos: self.lAlert.setText(f"Le bouton à la position {str(self.max_pos)} " f"est en dehors de la grille {h} x {w}") self.lAlert.setStyleSheet( "color: rgb(255,0,0); font-weight: bold;") else: self.data_buttons['buttons_grid']['height'] = int(self.leH.text()) self.data_buttons['buttons_grid']['width'] = int(self.leW.text()) with open('buttons.json', 'w', encoding='utf-8') as outfile: json.dump(self.data_buttons, outfile, indent=4) self.initButtons() def refreshUI(self): self.initButtons() def browseMedia(self): self.openFile = QFileDialog.getOpenFileName( self, "Sélectionner un média...", "./sons", "Image Files (*.avi *.mp3 *.wav)") filenameSplitted = self.openFile[0].split('/') self.leFile.setText(filenameSplitted[-1]) def colorChanged(self): self.colorSelected = self.cdColorPicker.currentColor() self.leColor.setText( str(self.colorSelected.red()) + "," + str(self.colorSelected.green()) + "," + str(self.colorSelected.blue()))
class _HeaderWithButton(QHeaderView): """Class that reimplements the QHeaderView section paint event to draw a button that is used to display and change the type of that column or row. """ def __init__(self, orientation, parent=None): super().__init__(orientation, parent) self.setHighlightSections(True) self.setSectionsClickable(True) self.setDefaultAlignment(Qt.AlignLeft) self.sectionResized.connect(self._section_resize) self.sectionMoved.connect(self._section_move) self._font = QFont('Font Awesome 5 Free Solid') self._display_all = True self._display_sections = [] self._margin = Margin(left=0, right=0, top=0, bottom=0) self._menu = _create_allowed_types_menu(self, self._menu_pressed) self._button = QToolButton(parent=self) self._button.setMenu(self._menu) self._button.setPopupMode(QToolButton.InstantPopup) self._button.setFont(self._font) self._button.hide() self._render_button = QToolButton(parent=self) self._render_button.setFont(self._font) self._render_button.hide() self._button_logical_index = None self.setMinimumSectionSize(self.minimumSectionSize() + self.widget_width()) @property def display_all(self): return self._display_all @display_all.setter def display_all(self, display_all): self._display_all = display_all self.viewport().update() @property def sections_with_buttons(self): return self._display_sections @sections_with_buttons.setter def sections_with_buttons(self, sections): self._display_sections = set(sections) self.viewport().update() @Slot("QAction") def _menu_pressed(self, action): """Sets the data type of a row or column according to menu action.""" type_str = action.text() self.set_data_types(self._button_logical_index, type_str, update_viewport=False) def set_data_types(self, sections, type_str, update_viewport=True): """ Sets the data types of given sections (rows, columns). Args: sections (Iterable or int or NoneType): row/column index type_str (str): data type name update_viewport (bool): True if the buttons need repaint """ if type_str == "integer sequence datetime": dialog = NewIntegerSequenceDateTimeConvertSpecDialog() if dialog.exec_(): convert_spec = dialog.get_spec() else: return else: convert_spec = value_to_convert_spec(type_str) if not isinstance(sections, Iterable): sections = [sections] orientation = self.orientation() for section in sections: self.model().set_type(section, convert_spec, orientation) if update_viewport: self.viewport().update() def widget_width(self): """Width of widget Returns: [int] -- Width of widget """ if self.orientation() == Qt.Horizontal: return self.height() return self.sectionSize(0) def widget_height(self): """Height of widget Returns: [int] -- Height of widget """ if self.orientation() == Qt.Horizontal: return self.height() return self.sectionSize(0) def mouseMoveEvent(self, mouse_event): """Moves the button to the correct section so that interacting with the button works. """ log_index = self.logicalIndexAt(mouse_event.x(), mouse_event.y()) if not self._display_all and log_index not in self._display_sections: self._button_logical_index = None self._button.hide() super().mouseMoveEvent(mouse_event) return if self._button_logical_index != log_index: self._button_logical_index = log_index self._set_button_geometry(self._button, log_index) self._button.show() super().mouseMoveEvent(mouse_event) def mousePressEvent(self, mouse_event): """Move the button to the pressed location and show or hide it if button should not be shown. """ log_index = self.logicalIndexAt(mouse_event.x(), mouse_event.y()) if not self._display_all and log_index not in self._display_sections: self._button_logical_index = None self._button.hide() super().mousePressEvent(mouse_event) return if self._button_logical_index != log_index: self._button_logical_index = log_index self._set_button_geometry(self._button, log_index) self._button.show() super().mousePressEvent(mouse_event) def leaveEvent(self, event): """Hide button """ self._button_logical_index = None self._button.hide() super().leaveEvent(event) def _set_button_geometry(self, button, index): """Sets a buttons geometry depending on the index. Arguments: button {QWidget} -- QWidget that geometry should be set index {int} -- logical_index to set position and geometry to. """ margin = self._margin if self.orientation() == Qt.Horizontal: button.setGeometry( self.sectionViewportPosition(index) + margin.left, margin.top, self.widget_width() - self._margin.left - self._margin.right, self.widget_height() - margin.top - margin.bottom, ) else: button.setGeometry( margin.left, self.sectionViewportPosition(index) + margin.top, self.widget_width() - self._margin.left - self._margin.right, self.widget_height() - margin.top - margin.bottom, ) def _section_resize(self, i): """When a section is resized. Arguments: i {int} -- logical index to section being resized """ self._button.hide() if i == self._button_logical_index: self._set_button_geometry(self._button, self._button_logical_index) def paintSection(self, painter, rect, logical_index): """Paints a section of the QHeader view. Works by drawing a pixmap of the button to the left of the original paint rectangle. Then shifts the original rect to the right so these two doesn't paint over each other. """ if not self._display_all and logical_index not in self._display_sections: super().paintSection(painter, rect, logical_index) return # get the type of the section. type_spec = self.model().get_type(logical_index, self.orientation()) if type_spec is None: type_spec = "string" else: type_spec = type_spec.DISPLAY_NAME font_str = _TYPE_TO_FONT_AWESOME_ICON[type_spec] # set data for both interaction button and render button. self._button.setText(font_str) self._render_button.setText(font_str) self._set_button_geometry(self._render_button, logical_index) # get pixmap from render button and draw into header section. rw = self._render_button.grab() if self.orientation() == Qt.Horizontal: painter.drawPixmap(self.sectionViewportPosition(logical_index), 0, rw) else: painter.drawPixmap(0, self.sectionViewportPosition(logical_index), rw) # shift rect that super class should paint in to the right so it doesn't # paint over the button rect.adjust(self.widget_width(), 0, 0, 0) super().paintSection(painter, rect, logical_index) def sectionSizeFromContents(self, logical_index): """Add the button width to the section so it displays right. Arguments: logical_index {int} -- logical index of section Returns: [QSize] -- Size of section """ org_size = super().sectionSizeFromContents(logical_index) org_size.setWidth(org_size.width() + self.widget_width()) return org_size def _section_move(self, logical, old_visual_index, new_visual_index): """Section beeing moved. Arguments: logical {int} -- logical index of section beeing moved. old_visual_index {int} -- old visual index of section new_visual_index {int} -- new visual index of section """ self._button.hide() if self._button_logical_index is not None: self._set_button_geometry(self._button, self._button_logical_index) def fix_widget_positions(self): """Update position of interaction button """ if self._button_logical_index is not None: self._set_button_geometry(self._button, self._button_logical_index) def set_margins(self, margins): """Sets the header margins.""" self._margin = margins
class ManageRelationshipsDialog(AddOrManageRelationshipsDialog): """A dialog to query user's preferences for managing relationships. """ def __init__(self, parent, db_mngr, *db_maps, relationship_class_key=None): """ Args: parent (SpineDBEditor): data store widget db_mngr (SpineDBManager): the manager to do the removal *db_maps: DiffDatabaseMapping instances relationship_class_key (str, optional): relationships class name, object_class name list string. """ super().__init__(parent, db_mngr, *db_maps) self.setWindowTitle("Manage relationships") self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.remove_rows_button.setToolButtonStyle(Qt.ToolButtonIconOnly) self.remove_rows_button.setToolTip( "<p>Remove selected relationships.</p>") self.remove_rows_button.setIconSize(QSize(24, 24)) self.db_map = db_maps[0] self.relationship_ids = dict() layout = self.header_widget.layout() self.db_combo_box = QComboBox(self) layout.addSpacing(32) layout.addWidget(QLabel("Database")) layout.addWidget(self.db_combo_box) self.splitter = QSplitter(self) self.add_button = QToolButton(self) self.add_button.setToolTip( "<p>Add relationships by combining selected available objects.</p>" ) self.add_button.setIcon(QIcon(":/icons/menu_icons/cubes_plus.svg")) self.add_button.setIconSize(QSize(24, 24)) self.add_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.add_button.setText(">>") label_available = QLabel("Available objects") label_existing = QLabel("Existing relationships") self.layout().addWidget(self.header_widget, 0, 0, 1, 4, Qt.AlignHCenter) self.layout().addWidget(label_available, 1, 0) self.layout().addWidget(label_existing, 1, 2) self.layout().addWidget(self.splitter, 2, 0) self.layout().addWidget(self.add_button, 2, 1) self.layout().addWidget(self.table_view, 2, 2) self.layout().addWidget(self.remove_rows_button, 2, 3) self.layout().addWidget(self.button_box, 3, 0, -1, -1) self.hidable_widgets = [ self.add_button, label_available, label_existing, self.table_view, self.remove_rows_button, ] for widget in self.hidable_widgets: widget.hide() self.existing_items_model = MinimalTableModel(self, lazy=False) self.new_items_model = MinimalTableModel(self, lazy=False) self.model.sub_models = [ self.new_items_model, self.existing_items_model ] self.db_combo_box.addItems([db_map.codename for db_map in db_maps]) self.reset_relationship_class_combo_box(db_maps[0].codename, relationship_class_key) self.connect_signals() def make_model(self): return CompoundTableModel(self) def splitter_widgets(self): return [self.splitter.widget(i) for i in range(self.splitter.count())] def connect_signals(self): """Connect signals to slots.""" super().connect_signals() self.db_combo_box.currentTextChanged.connect( self.reset_relationship_class_combo_box) self.add_button.clicked.connect(self.add_relationships) @Slot(str) def reset_relationship_class_combo_box(self, database, relationship_class_key=None): self.db_map = self.keyed_db_maps[database] self.relationship_class_keys = list( self.db_map_rel_cls_lookup[self.db_map]) self.rel_cls_combo_box.addItems( [f"{name}" for name, _ in self.relationship_class_keys]) try: current_index = self.relationship_class_keys.index( relationship_class_key) self.reset_model(current_index) self._handle_model_reset() except ValueError: current_index = -1 self.rel_cls_combo_box.setCurrentIndex(current_index) @Slot(bool) def add_relationships(self, checked=True): object_names = [[item.text(0) for item in wg.selectedItems()] for wg in self.splitter_widgets()] candidate = list(product(*object_names)) existing = self.new_items_model._main_data + self.existing_items_model._main_data to_add = list(set(candidate) - set(existing)) count = len(to_add) self.new_items_model.insertRows(0, count) self.new_items_model._main_data[0:count] = to_add self.model.refresh() @Slot(int) def reset_model(self, index): """Setup model according to current relationship_class selected in combobox. """ self.class_name, self.object_class_name_list = self.relationship_class_keys[ index] object_class_name_list = self.object_class_name_list.split(",") self.model.set_horizontal_header_labels(object_class_name_list) self.existing_items_model.set_horizontal_header_labels( object_class_name_list) self.new_items_model.set_horizontal_header_labels( object_class_name_list) self.relationship_ids.clear() for db_map in self.db_maps: relationship_classes = self.db_map_rel_cls_lookup[db_map] rel_cls = relationship_classes.get( (self.class_name, self.object_class_name_list), None) if rel_cls is None: continue for relationship in self.db_mngr.get_items_by_field( db_map, "relationship", "class_id", rel_cls["id"]): key = tuple(relationship["object_name_list"].split(",")) self.relationship_ids[key] = relationship["id"] existing_items = list(self.relationship_ids) self.existing_items_model.reset_model(existing_items) self.model.refresh() self.model.modelReset.emit() for wg in self.splitter_widgets(): wg.deleteLater() for name in object_class_name_list: tree_widget = QTreeWidget(self) tree_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) tree_widget.setColumnCount(1) tree_widget.setIndentation(0) header_item = QTreeWidgetItem([name]) header_item.setTextAlignment(0, Qt.AlignHCenter) tree_widget.setHeaderItem(header_item) objects = self.db_mngr.get_items_by_field(self.db_map, "object", "class_name", name) items = [QTreeWidgetItem([obj["name"]]) for obj in objects] tree_widget.addTopLevelItems(items) tree_widget.resizeColumnToContents(0) self.splitter.addWidget(tree_widget) sizes = [wg.columnWidth(0) for wg in self.splitter_widgets()] self.splitter.setSizes(sizes) for widget in self.hidable_widgets: widget.show() def resize_window_to_columns(self, height=None): table_view_width = (self.table_view.frameWidth() * 2 + self.table_view.verticalHeader().width() + self.table_view.horizontalHeader().length()) self.table_view.setMinimumWidth(table_view_width) self.table_view.setMinimumHeight( self.table_view.verticalHeader().defaultSectionSize() * 16) margins = self.layout().contentsMargins() if height is None: height = self.sizeHint().height() self.resize( margins.left() + margins.right() + table_view_width + self.add_button.width() + self.splitter.width(), height, ) @Slot() def accept(self): """Collect info from dialog and try to add items.""" keys_to_remove = set(self.relationship_ids) - set( self.existing_items_model._main_data) to_remove = [self.relationship_ids[key] for key in keys_to_remove] self.db_mngr.remove_items({self.db_map: {"relationship": to_remove}}) to_add = [[self.class_name, object_name_list] for object_name_list in self.new_items_model._main_data] self.db_mngr.import_data({self.db_map: { "relationships": to_add }}, command_text="Add relationships") super().accept()
def __init__(self, parent, object_class_item, db_mngr, *db_maps): """ Args: parent (SpineDBEditor): data store widget object_class_item (ObjectClassItem) db_mngr (SpineDBManager) *db_maps: database mappings """ super().__init__(parent) self.object_class_item = object_class_item self.db_mngr = db_mngr self.db_maps = db_maps self.db_map = db_maps[0] self.db_maps_by_codename = { db_map.codename: db_map for db_map in db_maps } self.db_combo_box = QComboBox(self) self.header_widget = QWidget(self) self.group_name_line_edit = QLineEdit(self) header_layout = QHBoxLayout(self.header_widget) header_layout.addWidget(QLabel(f"Group name: ")) header_layout.addWidget(self.group_name_line_edit) header_layout.addSpacing(32) header_layout.addWidget(QLabel("Database")) header_layout.addWidget(self.db_combo_box) self.non_members_tree = QTreeWidget(self) self.non_members_tree.setHeaderLabel("Non members") self.non_members_tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.non_members_tree.setColumnCount(1) self.non_members_tree.setIndentation(0) self.members_tree = QTreeWidget(self) self.members_tree.setHeaderLabel("Members") self.members_tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.members_tree.setColumnCount(1) self.members_tree.setIndentation(0) self.add_button = QToolButton() self.add_button.setToolTip("<p>Add selected non-members.</p>") self.add_button.setIcon(QIcon(":/icons/menu_icons/cube_plus.svg")) self.add_button.setIconSize(QSize(24, 24)) self.add_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.add_button.setText(">>") self.remove_button = QToolButton() self.remove_button.setToolTip("<p>Remove selected members.</p>") self.remove_button.setIcon(QIcon(":/icons/menu_icons/cube_minus.svg")) self.remove_button.setIconSize(QSize(24, 24)) self.remove_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.remove_button.setText("<<") self.vertical_button_widget = QWidget() vertical_button_layout = QVBoxLayout(self.vertical_button_widget) vertical_button_layout.addStretch() vertical_button_layout.addWidget(self.add_button) vertical_button_layout.addWidget(self.remove_button) vertical_button_layout.addStretch() self.button_box = QDialogButtonBox(self) self.button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) layout = QGridLayout(self) layout.addWidget(self.header_widget, 0, 0, 1, 3, Qt.AlignHCenter) layout.addWidget(self.non_members_tree, 1, 0) layout.addWidget(self.vertical_button_widget, 1, 1) layout.addWidget(self.members_tree, 1, 2) layout.addWidget(self.button_box, 2, 0, 1, 3) self.setAttribute(Qt.WA_DeleteOnClose) self.db_combo_box.addItems(list(self.db_maps_by_codename)) self.db_map_object_ids = { db_map: { x["name"]: x["id"] for x in self.db_mngr.get_items_by_field( self.db_map, "object", "class_id", self.object_class_item.db_map_id(db_map)) } for db_map in db_maps } self.reset_list_widgets(db_maps[0].codename) self.connect_signals()
def Button(self): Styles = [ QStyle.SP_TitleBarMinButton, QStyle.SP_TitleBarMenuButton, QStyle.SP_TitleBarMaxButton, QStyle.SP_TitleBarCloseButton, QStyle.SP_TitleBarNormalButton, QStyle.SP_TitleBarShadeButton, QStyle.SP_TitleBarUnshadeButton, QStyle.SP_TitleBarContextHelpButton, QStyle.SP_MessageBoxInformation, QStyle.SP_MessageBoxWarning, QStyle.SP_MessageBoxCritical, QStyle.SP_MessageBoxQuestion, QStyle.SP_DesktopIcon, QStyle.SP_TrashIcon, QStyle.SP_ComputerIcon, QStyle.SP_DriveFDIcon, QStyle.SP_DriveHDIcon, QStyle.SP_DriveCDIcon, QStyle.SP_DriveDVDIcon, QStyle.SP_DriveNetIcon, QStyle.SP_DirHomeIcon, QStyle.SP_DirOpenIcon, QStyle.SP_DirClosedIcon, QStyle.SP_DirIcon, QStyle.SP_DirLinkIcon, QStyle.SP_DirLinkOpenIcon, QStyle.SP_FileIcon, QStyle.SP_FileLinkIcon, QStyle.SP_FileDialogStart, QStyle.SP_FileDialogEnd, QStyle.SP_FileDialogToParent, QStyle.SP_FileDialogNewFolder, QStyle.SP_FileDialogDetailedView, QStyle.SP_FileDialogInfoView, QStyle.SP_FileDialogContentsView, QStyle.SP_FileDialogListView, QStyle.SP_FileDialogBack, QStyle.SP_DockWidgetCloseButton, QStyle.SP_ToolBarHorizontalExtensionButton, QStyle.SP_ToolBarVerticalExtensionButton, QStyle.SP_DialogOkButton, QStyle.SP_DialogCancelButton, QStyle.SP_DialogHelpButton, QStyle.SP_DialogOpenButton, QStyle.SP_DialogSaveButton, QStyle.SP_DialogCloseButton, QStyle.SP_DialogApplyButton, QStyle.SP_DialogResetButton, QStyle.SP_DialogDiscardButton, QStyle.SP_DialogYesButton, QStyle.SP_DialogNoButton, QStyle.SP_ArrowUp, QStyle.SP_ArrowDown, QStyle.SP_ArrowLeft, QStyle.SP_ArrowRight, QStyle.SP_ArrowBack, QStyle.SP_ArrowForward, QStyle.SP_CommandLink, QStyle.SP_VistaShield, QStyle.SP_BrowserReload, QStyle.SP_BrowserStop, QStyle.SP_MediaPlay, QStyle.SP_MediaStop, QStyle.SP_MediaPause, QStyle.SP_MediaSkipForward, QStyle.SP_MediaSkipBackward, QStyle.SP_MediaSeekForward, QStyle.SP_MediaSeekBackward, QStyle.SP_MediaVolume, QStyle.SP_MediaVolumeMuted, QStyle.SP_LineEditClearButton, #QStyle.SP_DialogYesToAllButton, #QStyle.SP_DialogNoToAllButton, #QStyle.SP_DialogSaveAllButton, #QStyle.SP_DialogAbortButton, #QStyle.SP_DialogRetryButton, #QStyle.SP_DialogIgnoreButton, #QStyle.SP_RestoreDefaultsButton, #QStyle.SP_CustomBase, ] StylesText = [ 'SP_TitleBarMinButton', 'SP_TitleBarMenuButton', 'SP_TitleBarMaxButton', 'SP_TitleBarCloseButton', 'SP_TitleBarNormalButton', 'SP_TitleBarShadeButton', 'SP_TitleBarUnshadeButton', 'SP_TitleBarContextHelpButton', 'SP_MessageBoxInformation', 'SP_MessageBoxWarning', 'SP_MessageBoxCritical', 'SP_MessageBoxQuestion', 'SP_DesktopIcon', 'SP_TrashIcon', 'SP_ComputerIcon', 'SP_DriveFDIcon', 'SP_DriveHDIcon', 'SP_DriveCDIcon', 'SP_DriveDVDIcon', 'SP_DriveNetIcon', 'SP_DirHomeIcon', 'SP_DirOpenIcon', 'SP_DirClosedIcon', 'SP_DirIcon', 'SP_DirLinkIcon', 'SP_DirLinkOpenIcon', 'SP_FileIcon', 'SP_FileLinkIcon', 'SP_FileDialogStart', 'SP_FileDialogEnd', 'SP_FileDialogToParent', 'SP_FileDialogNewFolder', 'SP_FileDialogDetailedView', 'SP_FileDialogInfoView', 'SP_FileDialogContentsView', 'SP_FileDialogListView', 'SP_FileDialogBack', 'SP_DockWidgetCloseButton', 'SP_ToolBarHorizontalExtensionButton', 'SP_ToolBarVerticalExtensionButton', 'SP_DialogOkButton', 'SP_DialogCancelButton', 'SP_DialogHelpButton', 'SP_DialogOpenButton', 'SP_DialogSaveButton', 'SP_DialogCloseButton', 'SP_DialogApplyButton', 'SP_DialogResetButton', 'SP_DialogDiscardButton', 'SP_DialogYesButton', 'SP_DialogNoButton', 'SP_ArrowUp', 'SP_ArrowDown', 'SP_ArrowLeft', 'SP_ArrowRight', 'SP_ArrowBack', 'SP_ArrowForward', 'SP_CommandLink', 'SP_VistaShield', 'SP_BrowserReload', 'SP_BrowserStop', 'SP_MediaPlay', 'SP_MediaStop', 'SP_MediaPause', 'SP_MediaSkipForward', 'SP_MediaSkipBackward', 'SP_MediaSeekForward', 'SP_MediaSeekBackward', 'SP_MediaVolume', 'SP_MediaVolumeMuted', 'SP_LineEditClearButton', #'SP_DialogYesToAllButton', #'SP_DialogNoToAllButton', #'SP_DialogSaveAllButton', #'SP_DialogAbortButton', #'SP_DialogRetryButton', #'SP_DialogIgnoreButton', #'SP_RestoreDefaultsButton', #'SP_CustomBase', ] btn = [QToolButton(self) for i in range(len(Styles))] self.myHLayout = QGridLayout() j = 0 k = 0 style = self.style() for i in range(len(Styles)): btn[i].setText("%s" % (StylesText[i])) btn[i].setToolButtonStyle(Qt.ToolButtonTextUnderIcon) icon = style.standardIcon(Styles[i]) btn[i].setIcon(QIcon(icon)) self.myHLayout.addWidget(btn[i], j, k) if i == 0: pass elif 0 == i % 5: j += 1 k = 0 else: k += 1 self.setLayout(self.myHLayout)
class camera_view(QWidget): """docstring for camera_view.""" def __init__(self, camera): super(camera_view, self).__init__() self.camera = camera self.image_camera = np.zeros((1,1)) #self.image = QImage() self.button_up = QToolButton() self.button_up.setArrowType(Qt.UpArrow) self.button_down = QToolButton() self.button_down.setArrowType(Qt.DownArrow) self.button_left = QToolButton() self.button_left.setArrowType(Qt.LeftArrow) self.button_right = QToolButton() self.button_right.setArrowType(Qt.RightArrow) self.box_button = QVBoxLayout() self.box_button_centre = QHBoxLayout() self.box_button_centre.addWidget(self.button_left) self.box_button_centre.addWidget(self.button_right) self.box_button_centre.addStretch(1) self.box_button.addWidget(self.button_up) self.box_button.addLayout(self.box_button_centre) self.box_button.addWidget(self.button_down) self.box_button.addStretch(1) self.button_affichage = QPushButton("Affichage Vidéo") self.button_affichage.clicked.connect(self.Affichage) self.button_stop = QPushButton("Stop retour Vidéo") self.button_stop.clicked.connect(self.fin_video) self.box = QVBoxLayout() self.box.addWidget(self.button_affichage) self.box.addWidget(self.button_stop) self.box_panel = QHBoxLayout() self.box_panel.addLayout(self.box) self.box_panel.addLayout(self.box_button) self.image2 = QLabel() self.layout = QVBoxLayout() self.layout.addWidget(self.image2) self.layout.addLayout(self.box_panel) self.setLayout(self.layout) self.movie_thread = MovieThread(self.camera) self.movie_thread.changePixmap.connect(self.setImage) self.movie_thread.setTerminationEnabled(True) # self.button_up.clicked.connect(self.up) self.button_down.clicked.connect(self.down) self.button_right.clicked.connect(self.right) self.button_left.clicked.connect(self.left) #box enregistrement + photo self.button_debut_video = QPushButton("Début enregistrement Vidéo") self.button_fin_video = QPushButton("Fin enregistrement Vidéo") self.button_photo = QPushButton("Photo") self.box_video_photo = QVBoxLayout() self.box_video_photo.addWidget(self.button_debut_video) self.box_video_photo.addWidget(self.button_fin_video) self.box_video_photo.addWidget(self.button_photo) self.box_panel.addLayout(self.box_video_photo) self.enregistrement = Record(self.camera) self.enregistrement.setTerminationEnabled(True) self.button_debut_video.clicked.connect(self.video_debut) self.button_fin_video.clicked.connect(self.video_fin) self.button_photo.clicked.connect(self.photo) self.compteur_photo = 0 self.enregistrement.start() self.timer_enregistrement = QTimer() self.timer_enregistrement.setInterval((1/24)*1000) self.timer_enregistrement.timeout.connect(self.video) #self.Timer.setInterval(500) def video(self): self.enregistrement.video() pass def photo(self): cv2.imwrite("./photo"+str(self.compteur_photo)+".png",self.camera.last_frame[1]) self.compteur_photo += 1 pass def video_debut(self): self.timer_enregistrement.start() pass def video_fin(self): print("nombre photo ",self.enregistrement.nombre) self.enregistrement.nombre = 0 self.timer_enregistrement.stop() self.enregistrement.stop() pass def up(self): self.client.envoie('up') print('up') pass def down(self): self.client.envoie('down') print('down') pass def right(self): self.client.envoie('right') print('right') pass def left(self): self.client.envoie('left') print('left') pass def Affichage(self): self.client = Client("192.168.1.62",35351) self.movie_thread.start() def fin_video(self): self.movie_thread.stop() self.client.envoie('q') def setImage(self,image): self.image2.setPixmap(QPixmap.fromImage(image))
def add_execute_buttons(self): icon_size = 24 self.addSeparator() self.addWidget(QLabel("Execution")) execute_project_icon = QIcon( ":/icons/project_item_icons/play-circle-solid.svg").pixmap( icon_size, icon_size) execute_project = QToolButton(self) execute_project.setIcon(execute_project_icon) execute_project.clicked.connect(self.execute_project) execute_project.setToolTip("Execute project") self.addWidget(execute_project) execute_selected_icon = QIcon( ":/icons/project_item_icons/play-circle-regular.svg").pixmap( icon_size, icon_size) execute_selected = QToolButton(self) execute_selected.setIcon(execute_selected_icon) execute_selected.clicked.connect(self.execute_selected) execute_selected.setToolTip("Execute selection") self.addWidget(execute_selected) stop_icon = QIcon( ":/icons/project_item_icons/stop-circle-regular.svg").pixmap( icon_size, icon_size) stop = QToolButton(self) stop.setIcon(stop_icon) stop.clicked.connect(self.stop_execution) stop.setToolTip("Stop execution") self.addWidget(stop)
class MyDockWidget(cutter.CutterDockWidget): def __init__(self, parent, action): super(MyDockWidget, self).__init__(parent, action) self.setObjectName("Capa explorer") self.setWindowTitle("Capa explorer") self._config = CutterBindings.Configuration.instance() self.model_data = CapaExplorerDataModel() self.range_model_proxy = CapaExplorerRangeProxyModel() self.range_model_proxy.setSourceModel(self.model_data) self.search_model_proxy = CapaExplorerSearchProxyModel() self.search_model_proxy.setSourceModel(self.range_model_proxy) self.create_view_tabs() self.create_menu() self.create_tree_tab_ui() self.create_view_attack() self.connect_signals() self.setWidget(self.tabs) self.show() def create_view_tabs(self): # Create tabs container self.tabs = QTabWidget() # Create the tabs self.tab_attack = QWidget(self.tabs) self.tab_tree_w_model = QWidget(self.tabs) self.tabs.addTab(self.tab_tree_w_model, "Tree View") self.tabs.addTab(self.tab_attack, "MITRE") def create_menu(self): # Define menu actions # Text, tooltip, function, enabled before file load self.disabled_menu_items = [] menu_actions = [ ("Load JSON file", '', self.cma_load_file, True), (), ("Auto rename functions", 'Auto renames functions according to capa detections, can result in very long function names.', self.cma_analyze_and_rename, False), ("Create flags", 'Creates flagspaces and flags from capa detections.', self.cma_create_flags, False), (), ("About", '', self.cma_display_about, True), ] self.capa_menu = QMenu() self.capa_menu.setToolTipsVisible(True) # Create qactions for action in menu_actions: if not len(action): # Create separator on empty self.capa_menu.addSeparator() continue a = QAction(self) a.setText(action[0]) a.setToolTip(action[1]) a.triggered.connect(action[2]) a.setEnabled(action[3]) if not action[3]: self.disabled_menu_items.append(a) self.capa_menu.addAction(a) # Create menu button font = QFont() font.setBold(True) self.btn_menu = QToolButton() self.btn_menu.setText('...') self.btn_menu.setFont(font) self.btn_menu.setPopupMode(QToolButton.InstantPopup) self.btn_menu.setMenu(self.capa_menu) self.btn_menu.setStyleSheet( 'QToolButton::menu-indicator { image: none; }') self.tabs.setCornerWidget(self.btn_menu, corner=Qt.TopRightCorner) def create_tree_tab_ui(self): self.capa_tree_view_layout = QVBoxLayout() self.capa_tree_view_layout.setAlignment(Qt.AlignTop) self.chk_fcn_scope = QCheckBox("Limit to Current function") #TODO: reset state on load file self.chk_fcn_scope.setChecked(False) self.chk_fcn_scope.stateChanged.connect( self.slot_checkbox_limit_by_changed) self.input_search = QLineEdit() self.input_search.setStyleSheet("margin:0px; padding:0px;") self.input_search.setPlaceholderText("search...") self.input_search.textChanged.connect( self.slot_limit_results_to_search) self.filter_controls_container = QGroupBox() self.filter_controls_container.setObjectName("scope") self.filter_controls_container.setFlat(True) self.filter_controls_container.setStyleSheet( "#scope{border:0px; padding:0px; margin:0px;subcontrol-origin: padding; subcontrol-position: left top;}" ) self.filter_controls_layout = QHBoxLayout( self.filter_controls_container) self.filter_controls_layout.setContentsMargins(0, 0, 0, 0) self.filter_controls_layout.addWidget(self.input_search) self.filter_controls_layout.addWidget(self.chk_fcn_scope) self.view_tree = CapaExplorerQtreeView(self.search_model_proxy) self.view_tree.setModel(self.search_model_proxy) # Make it look a little nicer when no results are loaded self.view_tree.header().setStretchLastSection(True) self.capa_tree_view_layout.addWidget(self.filter_controls_container) self.capa_tree_view_layout.addWidget(self.view_tree) self.tab_tree_w_model.setLayout(self.capa_tree_view_layout) def create_view_attack(self): table_headers = [ "ATT&CK Tactic", "ATT&CK Technique ", ] table = QTableWidget() table.setColumnCount(len(table_headers)) table.verticalHeader().setVisible(False) table.setSortingEnabled(False) table.setEditTriggers(QAbstractItemView.NoEditTriggers) table.setFocusPolicy(Qt.NoFocus) table.setSelectionMode(QAbstractItemView.NoSelection) table.setHorizontalHeaderLabels(table_headers) table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) table.horizontalHeader().setStretchLastSection(True) table.setShowGrid(False) table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeToContents) table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) #table.setStyleSheet("QTableWidget::item { padding: 25px; }") attack_view_layout = QVBoxLayout() attack_view_layout.setAlignment(Qt.AlignTop) self.attack_table = table attack_view_layout.addWidget(self.attack_table) self.tab_attack.setLayout(attack_view_layout) return table def connect_signals(self): QObject.connect(cutter.core(), SIGNAL("functionRenamed(RVA, QString)"), self.model_data.refresh_function_names) QObject.connect(cutter.core(), SIGNAL("functionsChanged()"), self.model_data.refresh_function_names) QObject.connect(cutter.core(), SIGNAL("seekChanged(RVA)"), self.signal_shim_slot_checkbox_limit_by_changed) def render_new_table_header_item(self, text): """create new table header item with our style @param text: header text to display """ item = QTableWidgetItem(text) item.setForeground(self._config.getColor("graph.true")) font = QFont() font.setBold(True) item.setFont(font) return item def fill_attack_table(self, rules): tactics = collections.defaultdict(set) for key, rule in rules.items(): if not rule["meta"].get("att&ck"): continue for attack in rule["meta"]["att&ck"]: tactic, _, rest = attack.partition("::") if "::" in rest: technique, _, rest = rest.partition("::") subtechnique, _, id = rest.rpartition(" ") tactics[tactic].add((technique, subtechnique, id)) else: technique, _, id = rest.rpartition(" ") tactics[tactic].add((technique, id)) column_one = [] column_two = [] for (tactic, techniques) in sorted(tactics.items()): column_one.append(tactic.upper()) # add extra space when more than one technique column_one.extend(["" for i in range(len(techniques) - 1)]) for spec in sorted(techniques): if len(spec) == 2: technique, id = spec column_two.append("%s %s" % (technique, id)) elif len(spec) == 3: technique, subtechnique, id = spec column_two.append("%s::%s %s" % (technique, subtechnique, id)) else: raise RuntimeError("unexpected ATT&CK spec format") self.attack_table.setRowCount(max(len(column_one), len(column_two))) for (row, value) in enumerate(column_one): self.attack_table.setItem(row, 0, self.render_new_table_header_item(value)) for (row, value) in enumerate(column_two): self.attack_table.setItem(row, 1, QTableWidgetItem(value)) def enable_menu_items_after_load(self): # enables menu actions after file is loaded for action in self.disabled_menu_items: action.setEnabled(True) def slot_limit_results_to_search(self, text): """limit tree view results to search matches reset view after filter to maintain level 1 expansion """ self.search_model_proxy.set_query(text) self.view_tree.reset_ui(should_sort=False) def signal_shim_slot_checkbox_limit_by_changed(self): if self.chk_fcn_scope.isChecked(): self.slot_checkbox_limit_by_changed(Qt.Checked) def slot_checkbox_limit_by_changed(self, state): """slot activated if checkbox clicked if checked, configure function filter if screen location is located in function, otherwise clear filter @param state: checked state """ invoke_reset = True if state == Qt.Checked: minbound, maxbound = util.get_function_boundries_at_current_location( ) if self.range_model_proxy.min_ea == minbound and self.range_model_proxy.max_ea == maxbound: # Seek only changed within current function, avoid resetting tree invoke_reset = False self.limit_results_to_function((minbound, maxbound)) else: self.range_model_proxy.reset_address_range_filter() if invoke_reset: self.view_tree.reset_ui() def limit_results_to_function(self, f): """add filter to limit results to current function adds new address range filter to include function bounds, allowing basic blocks matched within a function to be included in the results @param f: (tuple (maxbound, minbound)) """ if f: self.range_model_proxy.add_address_range_filter(f[0], f[1]) else: # if function not exists don't display any results (assume address never -1) self.range_model_proxy.add_address_range_filter(-1, -1) # --- Menu Actions def cma_analyze_and_rename(self): message_box = QMessageBox() message_box.setStyleSheet("QLabel{min-width: 370px;}") message_box.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) message_box.setEscapeButton(QMessageBox.Cancel) message_box.setDefaultButton(QMessageBox.Ok) message_box.setWindowTitle('Warning') message_box.setText( 'Depending on the size of the binary and the' ' amount of \n' 'capa matches this feature can take some time to \n' 'complete and might make the UI freeze temporarily.') message_box.setInformativeText('Are you sure you want to proceed ?') ret = message_box.exec_() # Ok = 1024 if ret == 1024: self.model_data.auto_rename_functions() def cma_create_flags(self): self.model_data.create_flags() def cma_display_about(self): c = CAPAExplorerPlugin() info_text = ("{description}\n\n" "https://github.com/ninewayhandshake/capa-explorer\n\n" "Version: {version}\n" "Author: {author}\n" "License: Apache License 2.0\n").format( version=c.version, author=c.author, description=c.description, ) text = CAPAExplorerPlugin().name message_box = QMessageBox() message_box.setStyleSheet("QLabel{min-width: 370px;}") message_box.setWindowTitle('About') message_box.setText(text) message_box.setInformativeText(info_text) message_box.setStandardButtons(QMessageBox.Close) for i in message_box.findChildren(QLabel): i.setFocusPolicy(Qt.NoFocus) message_box.exec_() def cma_load_file(self): filename = QFileDialog.getOpenFileName() path = filename[0] if len(path): try: data = util.load_capa_json(path) self.fill_attack_table(data['rules']) self.model_data.clear() self.model_data.render_capa_doc(data) # Restore ability to scroll on last column self.view_tree.header().setStretchLastSection(False) self.view_tree.slot_resize_columns_to_content() self.enable_menu_items_after_load() except Exception as e: util.log('Could not load json file.') else: util.log('No file selected.')
class CadastroPadrao(QWidget): """ Classe padrão para cadastros Alguns métodos devem ser reimplementados chamando super(ClasseFilha, self).metodo() Métodos: void cadastrar() void editar() void excluir() int localizar() void limpar_dados() void receber_dados(dict()) void cancela() str', 'int valida_obrigatorios() bool confirma() bool fechar bool nao_esta_em_modo_edicao() bool esta_em_modo_edicao() void entrar_modo_edicao() void sair_modo_edicao() void define_permite_editar() """ def __init__(self, parent=None, **kwargs): super(CadastroPadrao, self).__init__(parent) self.dados_formatados = None self.db = None self.window_list = None self.dialog = kwargs.get('dialog') if self.dialog: self.setWindowFlags(Qt.Dialog) else: self.setWindowFlags(Qt.Window) # Configuracoes self.localizar_campos = None self.dados = None self.modo_edicao = False self.novo_cadastro = True self.view_busca = None self.colunas_busca = None self.filtro_adicional = None self.campos_obrigatorios = dict() # QFrame self.frame_menu = None self.frame_buttons = None self.lineEdit_id = None self.pushButton_cadastrar = None self.pushButton_editar = None self.pushButton_excluir = None self.pushButton_localizar = None # QWidget self.frame_contents = None self.parent_window = None self.icone_insert = None self.icone_update = None self.icone_delete = None self.icone_find = None self.ultimo_id = '' #self.define_icones() def define_icones(self): self.icone_insert = QIcon( os.path.join('Resources', 'icons', 'insert.png')) self.icone_update = QIcon( os.path.join('Resources', 'icons', 'update.png')) self.icone_delete = QIcon( os.path.join('Resources', 'icons', 'delete.png')) self.icone_find = QIcon(os.path.join('Resources', 'icons', 'find.png')) if self.pushButton_cadastrar is not None: self.pushButton_cadastrar.setIcon(self.icone_insert) if self.pushButton_editar is not None: self.pushButton_editar.setIcon(self.icone_update) if self.pushButton_excluir is not None: self.pushButton_excluir.setIcon(self.icone_delete) if self.pushButton_localizar is not None: self.pushButton_localizar.setIcon(self.icone_find) self.translate_ui() def translate_ui(self): self.buttonBox.button(QDialogButtonBox.Ok).setText('Salvar') self.buttonBox.button(QDialogButtonBox.Cancel).setText('Cancelar') def adiciona_help(self, texto="Teste ToolButton", layout=None): from PySide2.QtWidgets import QToolButton self.toolButton_help = QToolButton(self.frame_buttons) self.toolButton_help.setToolTipDuration(5000) self.toolButton_help.setText("?") self.toolButton_help.setToolTip( texto + '\n\nCampos destacados em vermelho são obrigatórios.') self.toolButton_help.setStyleSheet("background: rgb(38, 183, 212);\n" "color: white;\n" "border: 10px;\n" "border-radius: 5px;") self.toolButton_help.setObjectName("toolButton_help") self.horizontalLayout_3.addWidget(self.toolButton_help) # Reimplementar chamando super def cadastrar(self): self.entrar_modo_edicao() self.novo_cadastro = True self.lineEdit_id.setText('') # Reimplementar chamando super def editar(self): self.entrar_modo_edicao() self.novo_cadastro = False # Reimplementar chamando super def excluir(self, validar=True): if self.nao_esta_em_modo_edicao(): if validar: dialog = ConfirmDialog(parent=self) dialog.definir_mensagem( "Tem certeza que deseja realizar a exclusão desse registro?" ) cancelar = dialog.exec() else: cancelar = True return self.db.call_procedure(self.db.schema, self.dados) if cancelar else False else: return False def localizar(self, parent=None): localizar = LocalizarDialog(db=self.db, campos=self.localizar_campos, tabela=self.view_busca, colunas=self.colunas_busca, filtro=self.filtro_adicional, parent=parent) localizar.retorno_dados.connect(self.receber_dados) modal = localizar.exec() return modal if modal > 0 else None # Reimplementar chamando super def limpar_dados(self): # limpa todos os campos self.ultimo_id = self.lineEdit_id.text() self.lineEdit_id.setText('') def receber_dados(self, dados): self.dados = dados # Reimplementar chamando super e limpar_dados def cancela(self): if self.modo_edicao: dialog = ConfirmDialog(parent=self) dialog.definir_mensagem( "Tem certeza que deseja cancelar? Todas as alterações serão perdidas." ) cancelar = dialog.exec() if cancelar: self.sair_modo_edicao() return True return False # Reimplementar chamando super def valida_obrigatorios(self): if len(self.campos_obrigatorios) > 0: vermelho = "247, 192, 188" style = "border: 0.5px solid red; background: rgb({});".format( vermelho) print(style) for campo, valor in self.campos_obrigatorios.items(): valor.setStyleSheet("QLineEdit { background: white; }") try: if valor.text() == '': valor.setStyleSheet(style) dialog = StatusDialog(status='ALERTA', mensagem='O campo ' + campo + ' é obrigatório.', parent=self.parent_window) #return dialog.exec() return False except AttributeError as attr: if valor.currentText() == '': valor.setStyleSheet(style) dialog = StatusDialog(status='ALERTA', mensagem='O campo ' + campo + ' é obrigatório.', parent=self.parent_window) #return dialog.exec() return False except Exception as e: dialog = StatusDialog( status='ERRO', mensagem='Erro ao verificar campos obrigatórios.', exception=e, parent=self.parent_window) return dialog.exec() self.marca_obrigatorios() return 'OK' def marca_obrigatorios(self): if len(self.campos_obrigatorios) > 0: for campo, valor in self.campos_obrigatorios.items(): valor.setStyleSheet("\nborder: 0.5px solid red") def limpa_obrigatorios(self): if len(self.campos_obrigatorios) > 0: for campo, valor in self.campos_obrigatorios.items(): valor.setStyleSheet("border: 0px solid black") valor.setStyleSheet("QLineEdit { background: white; }") # Reimplementar chamando super def confirma(self): if self.esta_em_modo_edicao(): if self.valida_obrigatorios() != 'OK': return False # pega os dados tela e envia pro banco prc = self.db.call_procedure(self.db.schema, self.dados) if prc[0]: if self.novo_cadastro: acao = 'realizado' else: acao = 'atualizado' dialog = StatusDialog(status='OK', mensagem='Cadastro ' + acao + ' com sucesso!', parent=self.parent_window) self.sair_modo_edicao() else: dialog = StatusDialog( status='ALERTA', mensagem='Não foi possível salvar os dados.', exception=prc, parent=self.parent_window) self.modo_edicao = True dialog.exec() return prc[0], prc[1][0] # Não deve ser reimplementado na tela #verifica se tem alguma alteracao pendente e pergunta se deseja fechar def fechar(self): if self.modo_edicao: dialog = ConfirmDialog(parent=self) dialog.definir_mensagem( "Tem certeza que deseja fechar? Todas as alterações serão perdidas." ) fechar = dialog.exec() else: fechar = True return fechar def nao_esta_em_modo_edicao(self): if self.modo_edicao: dialog = StatusDialog( status='ERRO', mensagem='A interface está em modo de edição!', parent=self.parent_window) return dialog.exec() else: logging.info('[CadastroPadrao] Não está em modo de edição.') return True def esta_em_modo_edicao(self): if not self.modo_edicao: dialog = StatusDialog( status='ERRO', mensagem='A interface não está em modo de edição!', parent=self.parent_window) return dialog.exec() else: logging.info('[CadastroPadrao] Está em modo de edição.') return True def entrar_modo_edicao(self): if self.nao_esta_em_modo_edicao(): self.modo_edicao = True self.frame_menu.setDisabled(True) self.frame_buttons.setDisabled(False) self.frame_contents.setDisabled(False) self.marca_obrigatorios() logging.info('[CadastroPadrao] Entrando em modo edição') def sair_modo_edicao(self): if self.esta_em_modo_edicao(): self.modo_edicao = False self.frame_menu.setDisabled(False) self.frame_buttons.setDisabled(True) self.frame_contents.setDisabled(True) self.limpa_obrigatorios() self.define_permite_editar() logging.info('[CadastroPadrao] Saindo do modo edição') def entrar_modo_visualizacao(self): if self.nao_esta_em_modo_edicao(): self.modo_edicao = False self.frame_menu.setDisabled(False) self.frame_buttons.setDisabled(True) self.frame_contents.setDisabled(False) def define_permite_editar(self): logging.info('[CadastroPadrao] Editar: ' + str(self.lineEdit_id.text() != '')) self.pushButton_editar.setDisabled(self.lineEdit_id.text() == '') self.pushButton_excluir.setDisabled(self.lineEdit_id.text() == '') def formatar_numero(self, numero): try: numero = str(numero) if len(numero.split(',')) > 1: numero = '%.2f' % float(numero.replace(',', '.')) elif len(numero.split('.')) > 1: numero = '%.2f' % float(numero) numero = numero.replace('.', ',') return str(numero) except ValueError: return numero # Override PySide2.QtGui.QCloseEvent def closeEvent(self, event): if self.fechar(): if not self.dialog: try: self.window_list.remove(self) except ValueError as ve: logging.debug( '[CadastroPadrao] Não foi possível remover a janela ' + str(self) + ' da lista de janelas abertas.') else: try: pass # self.parent().refresh() except Exception as e: logging.debug( '[CadastroPadrao] Não foi possível recarregar a janela <parent>: ' + str(self.parent())) event.accept() else: event.ignore()
def add_project_item_spec_list_view(self): icon_size = 16 self.addSeparator() self.addWidget(QLabel("Specific items")) self.addWidget(self.project_item_spec_list_view) remove_spec = QToolButton(self) remove_spec_icon = QIcon(":/icons/wrench_minus.svg").pixmap( icon_size, icon_size) remove_spec.setIcon(remove_spec_icon) remove_spec.clicked.connect( self._toolbox.remove_selected_specification) remove_spec.setToolTip( "<html><head/><body><p>Remove selected specific item from the project</p></body></html>" ) self.addWidget(remove_spec) add_spec = QToolButton(self) add_spec_icon = QIcon(":/icons/wrench_plus.svg").pixmap( icon_size, icon_size) add_spec.setIcon(add_spec_icon) add_spec.setMenu(self._toolbox.add_specification_popup_menu) add_spec.setPopupMode(QToolButton.InstantPopup) add_spec.setToolTip( "<html><head/><body><p>Add new specific item to the project</p></body></html>" ) self.addWidget(add_spec)
def __init__(self, parent, name, data, debug_state): if not type(data) == BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRun.setIcon(load_icon('run.svg')) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionRestart.setIcon(load_icon('restart.svg')) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionQuit.setIcon(load_icon('cancel.svg')) self.actionAttach = QAction("Attach", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionAttach.setIcon(load_icon('connect.svg')) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionDetach.setIcon(load_icon('disconnect.svg')) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionPause.setIcon(load_icon('pause.svg')) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionResume.setIcon(load_icon('resume.svg')) self.actionStepIntoAsm = QAction("Step Into (Assembly)", self) self.actionStepIntoAsm.triggered.connect( lambda: self.perform_step_into_asm()) self.actionStepIntoAsm.setIcon(load_icon('stepinto.svg')) self.actionStepIntoIL = QAction("Step Into", self) self.actionStepIntoIL.triggered.connect( lambda: self.perform_step_into_il()) self.actionStepIntoIL.setIcon(load_icon('stepinto.svg')) self.actionStepOverAsm = QAction("Step Over (Assembly)", self) self.actionStepOverAsm.triggered.connect( lambda: self.perform_step_over_asm()) self.actionStepOverAsm.setIcon(load_icon('stepover.svg')) self.actionStepOverIL = QAction("Step Over", self) self.actionStepOverIL.triggered.connect( lambda: self.perform_step_over_il()) self.actionStepOverIL.setIcon(load_icon('stepover.svg')) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) self.actionStepReturn.setIcon(load_icon('stepout.svg')) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.stepIntoMenu = QMenu("Step Into", self) self.stepIntoMenu.addAction(self.actionStepIntoIL) self.stepIntoMenu.addAction(self.actionStepIntoAsm) self.stepOverMenu = QMenu("Step Over", self) self.stepOverMenu.addAction(self.actionStepOverIL) self.stepOverMenu.addAction(self.actionStepOverAsm) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.btnPauseResume = QToolButton(self) self.btnPauseResume.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnPauseResume.setDefaultAction(self.actionPause) self.addWidget(self.btnPauseResume) #self.addAction(self.actionPause) #self.addAction(self.actionResume) self.btnStepInto = QToolButton(self) self.btnStepInto.setMenu(self.stepIntoMenu) self.btnStepInto.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepInto.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepInto.setDefaultAction(self.actionStepIntoIL) self.addWidget(self.btnStepInto) self.btnStepOver = QToolButton(self) self.btnStepOver.setMenu(self.stepOverMenu) self.btnStepOver.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepOver.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepOver.setDefaultAction(self.actionStepOverIL) self.addWidget(self.btnStepOver) # TODO: Step until returning from current function self.btnStepReturn = QToolButton(self) self.btnStepReturn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepReturn.setDefaultAction(self.actionStepReturn) self.addWidget(self.btnStepReturn) #self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=self.can_exec(), Restart=False, Quit=False, Attach=self.can_connect(), Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") self.set_default_process_action( "Attach" if self.can_connect() else "Run")
class DebugControlsWidget(QToolBar): def __init__(self, parent, name, data, debug_state): if not type(data) == BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRun.setIcon(load_icon('run.svg')) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionRestart.setIcon(load_icon('restart.svg')) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionQuit.setIcon(load_icon('cancel.svg')) self.actionAttach = QAction("Attach", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionAttach.setIcon(load_icon('connect.svg')) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionDetach.setIcon(load_icon('disconnect.svg')) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionPause.setIcon(load_icon('pause.svg')) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionResume.setIcon(load_icon('resume.svg')) self.actionStepIntoAsm = QAction("Step Into (Assembly)", self) self.actionStepIntoAsm.triggered.connect( lambda: self.perform_step_into_asm()) self.actionStepIntoAsm.setIcon(load_icon('stepinto.svg')) self.actionStepIntoIL = QAction("Step Into", self) self.actionStepIntoIL.triggered.connect( lambda: self.perform_step_into_il()) self.actionStepIntoIL.setIcon(load_icon('stepinto.svg')) self.actionStepOverAsm = QAction("Step Over (Assembly)", self) self.actionStepOverAsm.triggered.connect( lambda: self.perform_step_over_asm()) self.actionStepOverAsm.setIcon(load_icon('stepover.svg')) self.actionStepOverIL = QAction("Step Over", self) self.actionStepOverIL.triggered.connect( lambda: self.perform_step_over_il()) self.actionStepOverIL.setIcon(load_icon('stepover.svg')) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) self.actionStepReturn.setIcon(load_icon('stepout.svg')) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.stepIntoMenu = QMenu("Step Into", self) self.stepIntoMenu.addAction(self.actionStepIntoIL) self.stepIntoMenu.addAction(self.actionStepIntoAsm) self.stepOverMenu = QMenu("Step Over", self) self.stepOverMenu.addAction(self.actionStepOverIL) self.stepOverMenu.addAction(self.actionStepOverAsm) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.btnPauseResume = QToolButton(self) self.btnPauseResume.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnPauseResume.setDefaultAction(self.actionPause) self.addWidget(self.btnPauseResume) #self.addAction(self.actionPause) #self.addAction(self.actionResume) self.btnStepInto = QToolButton(self) self.btnStepInto.setMenu(self.stepIntoMenu) self.btnStepInto.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepInto.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepInto.setDefaultAction(self.actionStepIntoIL) self.addWidget(self.btnStepInto) self.btnStepOver = QToolButton(self) self.btnStepOver.setMenu(self.stepOverMenu) self.btnStepOver.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepOver.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepOver.setDefaultAction(self.actionStepOverIL) self.addWidget(self.btnStepOver) # TODO: Step until returning from current function self.btnStepReturn = QToolButton(self) self.btnStepReturn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepReturn.setDefaultAction(self.actionStepReturn) self.addWidget(self.btnStepReturn) #self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=self.can_exec(), Restart=False, Quit=False, Attach=self.can_connect(), Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") self.set_default_process_action( "Attach" if self.can_connect() else "Run") def __del__(self): # TODO: Move this elsewhere # This widget is tasked with cleaning up the state after the view is closed # binjaplug.delete_state(self.bv) pass # ------------------------------------------------------------------------- # Helpers # ------------------------------------------------------------------------- def can_exec(self): return DebugAdapter.ADAPTER_TYPE.use_exec( self.debug_state.adapter_type) def can_connect(self): return DebugAdapter.ADAPTER_TYPE.use_connect( self.debug_state.adapter_type) def alert_need_install(self, proc): message = "Cannot start debugger: {} not found on {}.".format( proc, "the target machine" if self.can_connect() else "your machine") adapter_type = self.debug_state.adapter_type # TODO: detect remote os correctly, as gdb/lldb are compatible with both macos and linux if adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_GDB or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_GDB: remote_os = "Linux" elif adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_LLDB or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_LLDB: remote_os = "Darwin" elif adapter_type == DebugAdapter.ADAPTER_TYPE.LOCAL_DBGENG or adapter_type == DebugAdapter.ADAPTER_TYPE.REMOTE_DBGENG: remote_os = "Windows" else: # Uncertain remote_os = platform.system() if remote_os == "Linux": message += "\nYou can find this in your package manager or build it from source." elif remote_os == "Darwin": if proc == "lldb": message += "\nYou need to install it by running the following command in Terminal:\nxcode-select --install" elif proc == "gdbserver": message += "\nYou can find this in your package manager or build it from source." else: message += "\nYou need to install this manually." elif remote_os == "Windows": # TODO: dbgeng does not currently throw this message += "\nYou need to reinstall the debugger plugin." else: message += "\nYou need to install this manually." show_message_box("Cannot Start Debugger", message, icon=MessageBoxIcon.ErrorIcon) # ------------------------------------------------------------------------- # UIActions # ------------------------------------------------------------------------- def perform_run(self): def perform_run_thread(): while True: try: self.debug_state.run() execute_on_main_thread_and_wait(perform_run_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Connection Refused')) except DebugAdapter.NotExecutableError as e: fpath = e.args[0] if platform.system() != 'Windows': msg = '%s is not executable, would you like to set +x and retry?' % fpath res = show_message_box( 'Error', msg, MessageBoxButtonSet.YesNoButtonSet, MessageBoxIcon.ErrorIcon) if res == MessageBoxButtonResult.YesButton: os.chmod(fpath, os.stat(fpath).st_mode | 0o100) continue execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: Target Not Executable')) except DebugAdapter.NotInstalledError as e: execute_on_main_thread_and_wait( lambda: self.alert_need_install(e.args[0])) execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: Debugger Not Installed')) except DebugAdapter.PermissionDeniedError as e: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Permission denied')) if platform.system() == 'Darwin': res = show_message_box( 'Error', 'Developer tools need to be enabled to debug programs. This can be authorized either from here or by starting a debugger in Xcode.', MessageBoxButtonSet.OKButtonSet, MessageBoxIcon.ErrorIcon) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_run_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) break def perform_run_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_run_error(e): self.state_error(e) self.state_starting('STARTING') threading.Thread(target=perform_run_thread).start() def perform_restart(self): def perform_restart_thread(): try: self.debug_state.restart() execute_on_main_thread_and_wait(perform_restart_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_restart_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_restart_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_restart_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_restart_error(e): self.state_error(e) self.state_starting('RESTARTING') threading.Thread(target=perform_restart_thread).start() def perform_quit(self): self.debug_state.quit() self.state_inactive() self.debug_state.ui.on_step() def perform_attach(self): def perform_attach_thread(): try: self.debug_state.attach() execute_on_main_thread_and_wait(perform_attach_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except TimeoutError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_attach_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_attach_error(e): self.state_error(e) self.state_starting('ATTACHING') threading.Thread(target=perform_attach_thread).start() def perform_detach(self): self.debug_state.detach() self.state_inactive() self.debug_state.ui.on_step() def perform_settings(self): def settings_finished(): if self.debug_state.running: self.state_running() elif self.debug_state.connected: local_rip = self.debug_state.local_ip if self.debug_state.bv.read(local_rip, 1) and len( self.debug_state.bv.get_functions_containing( local_rip)) > 0: self.state_stopped() else: self.state_stopped_extern() else: self.state_inactive() dialog = AdapterSettingsDialog.AdapterSettingsDialog(self, self.bv) dialog.show() dialog.finished.connect(settings_finished) def perform_pause(self): self.debug_state.pause() # Don't update state here-- one of the other buttons is running in a thread and updating for us def perform_resume(self): def perform_resume_thread(): (reason, data) = self.debug_state.go() execute_on_main_thread_and_wait( lambda: perform_resume_after(reason, data)) def perform_resume_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_running() threading.Thread(target=perform_resume_thread).start() def perform_step_into_asm(self): def perform_step_into_asm_thread(): (reason, data) = self.debug_state.step_into() execute_on_main_thread_and_wait( lambda: perform_step_into_asm_after(reason, data)) def perform_step_into_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_asm_thread).start() def perform_step_into_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_into_il_thread(): (reason, data) = self.debug_state.step_into(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_into_il_after(reason, data)) def perform_step_into_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_il_thread).start() def perform_step_over_asm(self): def perform_step_over_asm_thread(): (reason, data) = self.debug_state.step_over() execute_on_main_thread_and_wait( lambda: perform_step_over_asm_after(reason, data)) def perform_step_over_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_asm_thread).start() def perform_step_over_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_over_il_thread(): (reason, data) = self.debug_state.step_over(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_over_il_after(reason, data)) def perform_step_over_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_il_thread).start() def perform_step_return(self): def perform_step_return_thread(): (reason, data) = self.debug_state.step_return() execute_on_main_thread_and_wait( lambda: perform_step_return_after(reason, data)) def perform_step_return_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_return_thread).start() # ------------------------------------------------------------------------- # Control state setters # ------------------------------------------------------------------------- def set_actions_enabled(self, **kwargs): def enable_step_into(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) def enable_step_over(e): self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) def enable_starting(e): self.actionRun.setEnabled(e and self.can_exec()) self.actionAttach.setEnabled(e and self.can_connect()) def enable_stopping(e): self.actionRestart.setEnabled(e) self.actionQuit.setEnabled(e) self.actionDetach.setEnabled(e) def enable_stepping(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) self.actionStepReturn.setEnabled(e) actions = { "Run": lambda e: self.actionRun.setEnabled(e), "Restart": lambda e: self.actionRestart.setEnabled(e), "Quit": lambda e: self.actionQuit.setEnabled(e), "Attach": lambda e: self.actionAttach.setEnabled(e), "Detach": lambda e: self.actionDetach.setEnabled(e), "Pause": lambda e: self.actionPause.setEnabled(e), "Resume": lambda e: self.actionResume.setEnabled(e), "StepInto": enable_step_into, "StepOver": enable_step_over, "StepReturn": lambda e: self.actionStepReturn.setEnabled(e), "Threads": lambda e: self.btnThreads.setEnabled(e), "Starting": enable_starting, "Stopping": enable_stopping, "Stepping": enable_stepping, } for (action, enabled) in kwargs.items(): actions[action](enabled) def set_default_process_action(self, action): actions = { "Run": self.actionRun, "Restart": self.actionRestart, "Quit": self.actionQuit, "Attach": self.actionAttach, "Detach": self.actionDetach, } self.btnControl.setDefaultAction(actions[action]) def set_resume_pause_action(self, action): lookup = {'Resume': self.actionResume, 'Pause': self.actionPause} self.btnPauseResume.setDefaultAction(lookup[action]) def set_thread_list(self, threads): def select_thread_fn(tid): def select_thread(tid): stateObj = binjaplug.get_state(self.bv) if stateObj.connected and not stateObj.running: stateObj.threads.current = tid stateObj.ui.context_display() stateObj.ui.on_step() else: print('cannot set thread in current state') return lambda: select_thread(tid) self.threadMenu.clear() if len(threads) > 0: for thread in threads: item_name = "Thread {} at {}".format(thread['tid'], hex(thread['ip'])) action = self.threadMenu.addAction( item_name, select_thread_fn(thread['tid'])) if thread['selected']: self.btnThreads.setDefaultAction(action) else: defaultThreadAction = self.threadMenu.addAction("Thread List") defaultThreadAction.setEnabled(False) self.btnThreads.setDefaultAction(defaultThreadAction) # ------------------------------------------------------------------------- # State handling # ------------------------------------------------------------------------- def state_starting(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=False, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_inactive(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=True, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_stopped(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_stopped_extern(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, StepReturn=False, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_running(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_busy(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_error(self, msg=None): self.editStatus.setText(msg or 'ERROR') self.set_actions_enabled(Starting=True, Stopping=False, Pause=False, Resume=False, Stepping=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Resume") def handle_stop_return(self, reason, data): if reason == DebugAdapter.STOP_REASON.STDOUT_MESSAGE: self.state_stopped('stdout: ' + data) elif reason == DebugAdapter.STOP_REASON.PROCESS_EXITED: self.debug_state.quit() self.state_inactive('process exited, return code=%d' % data) elif reason == DebugAdapter.STOP_REASON.BACKEND_DISCONNECTED: self.debug_state.quit() self.state_inactive('backend disconnected (process exited?)')
def __init__(self, original, processed, title=None, parent=None): super(ImageViewer, self).__init__(parent) if original is None and processed is None: raise ValueError( self.tr('ImageViewer.__init__: Empty image received')) if original is None and processed is not None: original = processed self.original = original self.processed = processed if self.original is not None and self.processed is None: self.view = DynamicView(self.original) else: self.view = DynamicView(self.processed) # view_label = QLabel(self.tr('View:')) self.original_radio = QRadioButton(self.tr('Original')) self.original_radio.setToolTip( self.tr('Show the original image for comparison')) self.process_radio = QRadioButton(self.tr('Processed')) self.process_radio.setToolTip( self.tr('Show result of the current processing')) self.zoom_label = QLabel() full_button = QToolButton() full_button.setText(self.tr('100%')) fit_button = QToolButton() fit_button.setText(self.tr('Fit')) height, width, _ = self.original.shape size_label = QLabel(self.tr('[{}x{} px]'.format(height, width))) export_button = QToolButton() export_button.setText(self.tr('Export...')) tool_layout = QHBoxLayout() if processed is not None: # tool_layout.addWidget(view_label) tool_layout.addWidget(self.original_radio) tool_layout.addWidget(self.process_radio) tool_layout.addStretch() tool_layout.addWidget(QLabel(self.tr('Zoom:'))) tool_layout.addWidget(self.zoom_label) # tool_layout.addWidget(full_button) # tool_layout.addWidget(fit_button) tool_layout.addStretch() tool_layout.addWidget(size_label) if processed is not None: tool_layout.addWidget(export_button) self.original_radio.setChecked(False) self.process_radio.setChecked(True) self.toggle_mode(False) vert_layout = QVBoxLayout() if title is not None: title_label = QLabel(title) modify_font(title_label, bold=True) title_label.setAlignment(Qt.AlignCenter) vert_layout.addWidget(title_label) vert_layout.addWidget(self.view) vert_layout.addLayout(tool_layout) self.setLayout(vert_layout) self.original_radio.toggled.connect(self.toggle_mode) fit_button.clicked.connect(self.view.zoom_fit) full_button.clicked.connect(self.view.zoom_full) export_button.clicked.connect(self.export_image) self.view.view_changed.connect(self.forward_changed)
def __init__(self, table, headers, bold=True, mono=True, tooltips=None, align=False, search=True, parent=None): super(TableWidget, self).__init__(parent) self.table_widget = QTableWidget(len(table), len(table[0])) for i, row in enumerate(table): for j, item in enumerate(row): if item is not None: self.table_widget.setItem(i, j, QTableWidgetItem(str(item))) if tooltips is not None: self.table_widget.setToolTip(tooltips[i][j]) modify_font(self.table_widget.item(i, j), bold=bold and j == 0, mono=mono) if align: self.table_widget.item(i, j).setTextAlignment( Qt.AlignRight) self.table_headers = headers self.table_widget.setHorizontalHeaderLabels(self.table_headers) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.resizeColumnsToContents() self.table_widget.setAlternatingRowColors(True) self.table_widget.itemDoubleClicked.connect(self.copy) search_layout = QHBoxLayout() search_layout.addWidget(QLabel(self.tr('Search:'))) self.search_edit = QLineEdit() self.search_edit.textChanged.connect(self.start) self.search_edit.returnPressed.connect(self.next) search_layout.addWidget(self.search_edit) clear_button = QToolButton() clear_button.setIcon(QIcon('icons/clear.svg')) clear_button.setShortcut(QKeySequence.DeleteCompleteLine) clear_button.setToolTip(self.tr('Clear pattern')) clear_button.clicked.connect(self.search_edit.clear) search_layout.addWidget(clear_button) self.case_button = QToolButton() self.case_button.setText(self.tr('Aa')) self.case_button.setCheckable(True) self.case_button.toggled.connect(self.start) self.case_button.setToolTip(self.tr('Case sensitive')) search_layout.addWidget(self.case_button) self.word_button = QToolButton() self.word_button.setText(self.tr('W')) self.word_button.setCheckable(True) self.word_button.toggled.connect(self.start) self.word_button.setToolTip(self.tr('Whole words')) search_layout.addWidget(self.word_button) self.regex_button = QToolButton() self.regex_button.setText(self.tr('.*')) self.regex_button.setCheckable(True) self.regex_button.toggled.connect(self.start) self.regex_button.setToolTip(self.tr('Regular expression')) search_layout.addWidget(self.regex_button) prev_button = QToolButton() prev_button.setIcon(QIcon('icons/up.svg')) prev_button.setShortcut(QKeySequence.FindPrevious) prev_button.clicked.connect(self.previous) prev_button.setToolTip(self.tr('Previous occurence')) search_layout.addWidget(prev_button) next_button = QToolButton() next_button.setIcon(QIcon('icons/down.svg')) next_button.setShortcut(QKeySequence.FindNext) next_button.clicked.connect(self.next) next_button.setToolTip(self.tr('Next occurence')) search_layout.addWidget(next_button) self.matches_label = QLabel() search_layout.addWidget(self.matches_label) search_layout.addStretch() export_button = QToolButton() export_button.setText(self.tr('Export...')) export_button.clicked.connect(self.export) search_layout.addWidget(export_button) main_layout = QVBoxLayout() main_layout.addWidget(self.table_widget) if search: main_layout.addLayout(search_layout) self.setLayout(main_layout)
class TabBarPlus(QTabBar): """Tab bar that has a plus button floating to the right of the tabs.""" plus_clicked = Signal() def __init__(self, parent): """ Args: parent (MultiSpineDBEditor) """ super().__init__(parent) self._parent = parent self._plus_button = QToolButton(self) self._plus_button.setIcon(QIcon(CharIconEngine("\uf067"))) self._plus_button.clicked.connect( lambda _=False: self.plus_clicked.emit()) self._move_plus_button() self.setShape(QTabBar.RoundedNorth) self.setTabsClosable(True) self.setMovable(True) self.setElideMode(Qt.ElideLeft) self.drag_index = None self._tab_hot_spot_x = None self._hot_spot_y = None def resizeEvent(self, event): """Sets the dimension of the plus button. Also, makes the tab bar as wide as the parent.""" super().resizeEvent(event) self.setFixedWidth(self.parent().width()) self.setMinimumHeight(self.height()) self._move_plus_button() extent = max(0, self.height() - 2) self._plus_button.setFixedSize(extent, extent) self.setExpanding(False) def tabLayoutChange(self): super().tabLayoutChange() self._move_plus_button() def _move_plus_button(self): """Places the plus button at the right of the last tab.""" left = sum([self.tabRect(i).width() for i in range(self.count())]) top = self.geometry().top() + 1 self._plus_button.move(left, top) def mousePressEvent(self, event): """Registers the position of the press, in case we need to detach the tab.""" super().mousePressEvent(event) tab_rect = self.tabRect(self.tabAt(event.pos())) self._tab_hot_spot_x = event.pos().x() - tab_rect.x() self._hot_spot_y = event.pos().y() - tab_rect.y() def mouseMoveEvent(self, event): """Detaches a tab either if the user moves beyond the limits of the tab bar, or if it's the only one.""" self._plus_button.hide() if self.count() == 1: self._send_release_event(event.pos()) hot_spot = QPoint(event.pos().x(), self._hot_spot_y) self._parent.start_drag(hot_spot) return if self.count() > 1 and not self.geometry().contains(event.pos()): self._send_release_event(event.pos()) hot_spot_x = event.pos().x() hot_spot = QPoint(event.pos().x(), self._hot_spot_y) index = self.tabAt(hot_spot) if index == -1: index = self.count() - 1 self._parent.detach(index, hot_spot, hot_spot_x - self._tab_hot_spot_x) return super().mouseMoveEvent(event) def _send_release_event(self, pos): """Sends a mouse release event at given position in local coordinates. Called just before detaching a tab. Args: pos (QPoint) """ self.drag_index = None release_event = QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QApplication.sendEvent(self, release_event) def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) self._plus_button.show() self.update() self.releaseMouse() if self.drag_index is not None: # Pass it to parent event.ignore() def start_dragging(self, index): """Stars dragging the given index. This happens when a detached tab is reattached to this bar. Args: index (int) """ self.drag_index = index press_pos = self.tabRect(self.drag_index).center() press_event = QMouseEvent(QEvent.MouseButtonPress, press_pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QApplication.sendEvent(self, press_event) QApplication.processEvents() move_pos = self.mapFromGlobal(QCursor.pos()) if self.geometry().contains(move_pos): move_event = QMouseEvent(QEvent.MouseMove, move_pos, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QApplication.sendEvent(self, move_event) self.grabMouse() def index_under_mouse(self): """Returns the index under the mouse cursor, or None if the cursor isn't over the tab bar. Used to check for drop targets. Returns: int or NoneType """ pos = self.mapFromGlobal(QCursor.pos()) if not self.geometry().contains(pos): return None index = self.tabAt(pos) if index == -1: index = self.count() return index def _show_plus_button_context_menu(self, global_pos): toolbox = self._parent.db_mngr.parent() if toolbox is None: return ds_urls = { ds.name: ds.project_item.sql_alchemy_url() for ds in toolbox.project_item_model.items("Data Stores") } if not ds_urls: return menu = QMenu(self) for name, url in ds_urls.items(): action = menu.addAction(name, lambda name=name, url=url: self._parent. add_new_tab({url: name})) action.setEnabled(bool(url)) menu.popup(global_pos) menu.aboutToHide.connect(menu.deleteLater) def contextMenuEvent(self, event): index = self.tabAt(event.pos()) if self._plus_button.underMouse(): self._show_plus_button_context_menu(event.globalPos()) return if self.tabButton(index, QTabBar.RightSide).underMouse(): return db_editor = self._parent.tab_widget.widget(index) if db_editor is None: return menu = QMenu(self) others = self._parent.others() if others: move_tab_menu = menu.addMenu("Move tab to another window") move_tab_to_new_window = move_tab_menu.addAction( "New window", lambda _=False, index=index: self._parent.move_tab( index, None)) for other in others: move_tab_menu.addAction( other.name(), lambda _=False, index=index, other=other: self._parent. move_tab(index, other)) else: move_tab_to_new_window = menu.addAction( "Move tab to new window", lambda _=False, index=index: self._parent.move_tab( index, None)) move_tab_to_new_window.setEnabled(self.count() > 1) menu.addSeparator() menu.addAction(db_editor.url_toolbar.reload_action) db_url_codenames = db_editor.db_url_codenames menu.addAction( QIcon(CharIconEngine("\uf24d")), "Duplicate", lambda _=False, index=index + 1, db_url_codenames=db_url_codenames: self._parent.insert_new_tab(index, db_url_codenames), ) menu.popup(event.globalPos()) menu.aboutToHide.connect(menu.deleteLater) event.accept()
class QtViewer(QWidget): """Full featured, stand-alone viewer suitable for displaying a :class:`vpype.Document` to a user.""" def __init__( self, document: Optional[vp.Document] = None, view_mode: ViewMode = ViewMode.PREVIEW, show_pen_up: bool = False, show_points: bool = False, parent=None, ): super().__init__(parent) self.setWindowTitle("vpype viewer") self.setStyleSheet(""" QToolButton:pressed { background-color: rgba(0, 0, 0, 0.2); } """) self._viewer_widget = QtViewerWidget(parent=self) # setup toolbar self._toolbar = QToolBar() self._icon_size = QSize(32, 32) self._toolbar.setIconSize(self._icon_size) view_mode_grp = QActionGroup(self._toolbar) if _DEBUG_ENABLED: act = view_mode_grp.addAction("None") act.setCheckable(True) act.setChecked(view_mode == ViewMode.NONE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.NONE)) act = view_mode_grp.addAction("Outline Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE)) act = view_mode_grp.addAction("Outline Mode (Colorful)") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE_COLORFUL) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE_COLORFUL)) act = view_mode_grp.addAction("Preview Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.PREVIEW) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.PREVIEW)) self.set_view_mode(view_mode) # VIEW MODE # view modes view_mode_btn = QToolButton() view_mode_menu = QMenu(view_mode_btn) act = view_mode_menu.addAction("View Mode:") act.setEnabled(False) view_mode_menu.addActions(view_mode_grp.actions()) view_mode_menu.addSeparator() # show pen up act = view_mode_menu.addAction("Show Pen-Up Trajectories") act.setCheckable(True) act.setChecked(show_pen_up) act.toggled.connect(self.set_show_pen_up) self._viewer_widget.engine.show_pen_up = show_pen_up # show points act = view_mode_menu.addAction("Show Points") act.setCheckable(True) act.setChecked(show_points) act.toggled.connect(self.set_show_points) self._viewer_widget.engine.show_points = show_points # preview mode options view_mode_menu.addSeparator() act = view_mode_menu.addAction("Preview Mode Options:") act.setEnabled(False) # pen width pen_width_menu = view_mode_menu.addMenu("Pen Width") act_grp = PenWidthActionGroup(0.3, parent=pen_width_menu) act_grp.triggered.connect(self.set_pen_width_mm) pen_width_menu.addActions(act_grp.actions()) self.set_pen_width_mm(0.3) # pen opacity pen_opacity_menu = view_mode_menu.addMenu("Pen Opacity") act_grp = PenOpacityActionGroup(0.8, parent=pen_opacity_menu) act_grp.triggered.connect(self.set_pen_opacity) pen_opacity_menu.addActions(act_grp.actions()) self.set_pen_opacity(0.8) # debug view if _DEBUG_ENABLED: act = view_mode_menu.addAction("Debug View") act.setCheckable(True) act.toggled.connect(self.set_debug) view_mode_btn.setMenu(view_mode_menu) view_mode_btn.setIcon(load_icon("eye-outline.svg")) view_mode_btn.setText("View") view_mode_btn.setPopupMode(QToolButton.InstantPopup) view_mode_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(view_mode_btn) # LAYER VISIBILITY self._layer_visibility_btn = QToolButton() self._layer_visibility_btn.setIcon( load_icon("layers-triple-outline.svg")) self._layer_visibility_btn.setText("Layer") self._layer_visibility_btn.setMenu(QMenu(self._layer_visibility_btn)) self._layer_visibility_btn.setPopupMode(QToolButton.InstantPopup) self._layer_visibility_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(self._layer_visibility_btn) # FIT TO PAGE fit_act = self._toolbar.addAction(load_icon("fit-to-page-outline.svg"), "Fit") fit_act.triggered.connect(self._viewer_widget.engine.fit_to_viewport) # RULER # TODO: not implemented yet # self._toolbar.addAction(load_icon("ruler-square.svg"), "Units") # MOUSE COORDINATES> self._mouse_coord_lbl = QLabel("") font = self._mouse_coord_lbl.font() font.setPointSize(11) self._mouse_coord_lbl.setMargin(6) # self._mouse_coord_lbl.setStyleSheet("QLabel { background-color : red }") self._mouse_coord_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self._mouse_coord_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self._mouse_coord_lbl.setFont(font) self._toolbar.addWidget(self._mouse_coord_lbl) # noinspection PyUnresolvedReferences self._viewer_widget.mouse_coords.connect( self.set_mouse_coords) # type: ignore # setup layout layout = QVBoxLayout() layout.setSpacing(0) layout.setMargin(0) layout.addWidget(self._toolbar) layout.addWidget(self._viewer_widget) self.setLayout(layout) if document is not None: self.set_document(document) def set_document(self, document: vp.Document) -> None: self._viewer_widget.set_document(document) self._update_layer_menu() def _update_layer_menu(self): layer_menu = QMenu(self._layer_visibility_btn) for layer_id in sorted(self._viewer_widget.document().layers): action = layer_menu.addAction(f"Layer {layer_id}") action.setCheckable(True) action.setChecked(True) # TODO: set color icon action.triggered.connect( functools.partial( self._viewer_widget.engine.toggle_layer_visibility, layer_id)) self._layer_visibility_btn.setMenu(layer_menu) def set_mouse_coords(self, txt: str) -> None: self._mouse_coord_lbl.setText(txt) def set_view_mode(self, mode: ViewMode) -> None: self._viewer_widget.engine.view_mode = mode def set_show_pen_up(self, show_pen_up: bool) -> None: self._viewer_widget.engine.show_pen_up = show_pen_up def set_show_points(self, show_points: bool) -> None: self._viewer_widget.engine.show_points = show_points def set_pen_width_mm(self, value: Union[float, QAction]) -> None: if isinstance(value, QAction): value = value.data() self._viewer_widget.engine.pen_width = value / 25.4 * 96.0 def set_pen_opacity(self, value: Union[float, QAction]) -> None: if isinstance(value, QAction): value = value.data() self._viewer_widget.engine.pen_opacity = value def set_debug(self, debug: bool) -> None: self._viewer_widget.engine.debug = debug
class ModernWindow(QDialog): """ Implements a modern window-frame. Notes: * Except for macOS, the OS will not add a shadow to the window. On Windows and Linux this class will add QGraphicsDropShadowEffect to the entire window. """ __double_clicked = Signal() def __init__(self, window: Any, parent: Optional[Any], style: Optional[Any], title_bar: bool, transparent_window: bool, titlebar_height: Optional[int], titlebar_color: Optional[QColor], titlebar_nofocus_color: Optional[QColor], titlebar_text_color: Optional[QColor], titlebar_widget: Optional[Union[QWidget, QTabWidget]], window_buttons_position: Optional[str]) -> None: """ Constructor. Parameters: * window: Qt widget that should be wrapped * window_style: choose from one of the pre-defined styles, e.g. 'APL', 'WOW' * title_bar: display a traditional titlebar or not * titlebar_height: height of the titlebar in pixel * titlebar_color: override background color of the titlebar * titlebar_nofocus_color: override background color of the titlebar when the window is out of focus * titlebar_text_color: override color for the window title text * window_button_position: positions the window close/min/max buttons left (macOS) or right of the window title (Windows, Gnome, KDE, etc.) * transparent_window: turns off window transparency. This ensures compatibility with certain widgets and draw modes, such as QMediaWidget on Windows. Drawbacks: when transparent_window is False, window drop shadow effects will be disabled, except for operating systems that automatically add a drop shadow to all windows. """ def expose_msgbox_methods() -> None: """ensure Qt methods of wrapped children are exposed""" assert self.__window is not None self.setText = self.__window.setText self.setInformativeText = self.__window.setInformativeText self.setDetailedText = self.__window.setDetailedText def add_window_buttons() -> None: """create window widget buttons""" button_size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) if not isinstance(self.__window, QDIALOG_TYPES): self._minimize_button = QToolButton(self) self._minimize_button.setObjectName('btnMinimize') self._minimize_button.setSizePolicy(button_size_policy) self._minimize_button.clicked.connect( self.__on_minimize_button_clicked) # pylint: disable=no-member self._restore_button = QToolButton(self) self._restore_button.setObjectName('btnRestore') self._restore_button.setSizePolicy(button_size_policy) self._restore_button.clicked.connect( self.__on_restore_button_clicked) # pylint: disable=no-member self._maximize_button = QToolButton(self) self._maximize_button.setObjectName('btnMaximize') self._maximize_button.setSizePolicy(button_size_policy) self._maximize_button.clicked.connect( self.__on_maximize_button_clicked) # pylint: disable=no-member self._close_button = QToolButton(self) self._close_button.setObjectName('btnClose') self._close_button.setSizePolicy(button_size_policy) self._close_button.clicked.connect(self.__on_close_button_clicked) # pylint: disable=no-member self.__move_window_buttons() # place buttons def set_window_properties(titlebar_height: int) -> None: """Sets Qt Properties for the wrapper window""" assert self.__window is not None self.setStyleSheet(self.__style.get_window_stylesheet()) self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint) if sys.platform not in ["darwin"] and self.__transparent_window: self.setAttribute(Qt.WA_TranslucentBackground, True) # no need for translucency on macOS if sys.platform == 'win32': self.setAttribute( Qt.WA_OpaquePaintEvent, True) # avoid flickering on window resize on Windows else: self.setAttribute(Qt.WA_OpaquePaintEvent, False) self.setAttribute(Qt.WA_NoSystemBackground, True) self.setWindowTitle(self.__window.windowTitle()) self.setGeometry(self.__window.geometry()) height = titlebar_height if self.__use_shadow: height += self.__style.window.SHADOW_RADIUS_PX * 2 self.setMinimumHeight(self.__window.minimumSizeHint().height() + height) self.setMinimumWidth(self.__window.minimumSizeHint().width()) def add_window_drop_shadow() -> None: """Adds a drop-shadow behind the window""" if self.__use_shadow: self.layout().setMargin(self.__style.window.SHADOW_RADIUS_PX) drop_shadow_effect = QGraphicsDropShadowEffect(self) drop_shadow_effect.setEnabled(True) drop_shadow_effect.setBlurRadius( self.__style.window.SHADOW_RADIUS_PX) color = QColor(self.__style.window.SHADOW_COLOR_RGB) color.setAlpha(self.__style.window.SHADOW_OPACITY_HEX) drop_shadow_effect.setColor(color) drop_shadow_effect.setOffset(0) self.setGraphicsEffect(drop_shadow_effect) def adjust_wrapped_window_object() -> None: """Adding attribute to clean up the parent window when the child is closed""" assert self.__window is not None self.__window.wrapper = self self.__window.setAttribute(Qt.WA_DeleteOnClose, True) self.__window.destroyed.connect(self.__child_was_closed) def add_resizers() -> None: """Adds resizer widgets, which act as resize handles, to the window wrapper""" if isinstance(window, QDIALOG_TYPES): assert self.__window is not None self.__window.installEventFilter(self) else: self.resizer_top = Resizer( self, Resizer.TOP, self.__style.window.SHADOW_RADIUS_PX) self.resizer_bot = Resizer( self, Resizer.BOTTOM, self.__style.window.SHADOW_RADIUS_PX) self.resizer_lef = Resizer( self, Resizer.LEFT, self.__style.window.SHADOW_RADIUS_PX) self.resizer_rig = Resizer( self, Resizer.RIGHT, self.__style.window.SHADOW_RADIUS_PX) self.resizer_tl = Resizer(self, Resizer.TOP_LEFT, self.__style.window.SHADOW_RADIUS_PX) self.resizer_tr = Resizer(self, Resizer.TOP_RIGHT, self.__style.window.SHADOW_RADIUS_PX) self.resizer_br = Resizer(self, Resizer.BOTTOM_RIGHT, self.__style.window.SHADOW_RADIUS_PX) self.resizer_bl = Resizer(self, Resizer.BOTTOM_LEFT, self.__style.window.SHADOW_RADIUS_PX) def get_window_frame_widget( window_buttons_position: str) -> WindowFrame: """Returns a widget which acts as Qt 'windowFrame' element""" window_frame_widget = WindowFrame( titlebar_height=self.__titlebar_height, titlebar_color=self.__titlebar_color, background_color=self.__style.window.WINDOW_BACKGROUND_RGB, corner_radius=self.__style.window.WINDOW_CORNER_RADIUS_PX, parent=self) if not isinstance(window, QDIALOG_TYPES): window_frame_widget.double_clicked.connect( self.__on_title_bar_double_clicked) # type: ignore window_frame_widget.setObjectName('windowFrame') self.__vbox_frame_layout = QVBoxLayout(window_frame_widget) self.__vbox_frame_layout.setContentsMargins(0, 0, 0, 0) self.__vbox_frame_layout.setSpacing(0) if self.__title_bar: # add optional titlebar tcolor = QColor(self.__style.window.TITLE_BAR_FONT_COLOR_RGB) if titlebar_text_color is not None: tcolor = titlebar_text_color self.__title_widget = WindowTitleLabel( text='', height=self.__titlebar_height, color=tcolor, window_buttons_position=window_buttons_position, margin=self.__style.window. TITLE_BAR_TITLE_TEXT_RIGHT_MARGIN_PX, button_bar_width=self.window_buttons_width + self.window_buttons_margin, minimum_width=self.__style.window. TITLE_LABEL_MINIMUM_WIDTH_PX, parent=None) self.__title_widget.setMinimumHeight(self.__titlebar_height) self.__vbox_frame_layout.addWidget(self.__title_widget) else: # no title-bar; add dummy widget to create a margin self.__title_widget = QWidget() self.__title_widget.setGeometry( 0, 0, 1, self.__style.window.TITLE_BAR_TOP_MARGIN_PX) self.__title_widget.setMinimumHeight( self.__style.window.TITLE_BAR_TOP_MARGIN_PX) self.__vbox_frame_layout.addWidget(self.__title_widget) assert self.__window is not None self.__vbox_frame_layout.addWidget(self.__window) return window_frame_widget QDialog.__init__(self, parent) self.__style: Any = style if titlebar_height is None: titlebar_height = self.__style.window.TITLE_BAR_HEIGHT_PX assert window_buttons_position in [ None, WINDOW_BUTTONS_LEFT, WINDOW_BUTTONS_RIGHT ] if window_buttons_position is None: window_buttons_position = WINDOW_BUTTONS_LEFT if sys.platform == 'darwin' else WINDOW_BUTTONS_RIGHT self.__window: Optional[QWidget] = window self.__title_bar: bool = True if isinstance( self.__window, QDIALOG_TYPES) else title_bar self.__titlebar_height: int = titlebar_height self.__transparent_window: bool = transparent_window # True if window uses WA_TranslucentBackground self.__use_shadow: bool = sys.platform != 'darwin' and transparent_window self.__maximized: bool = False self.__drag_move_enabled: bool = True self.__mouse_pressed: bool = False self.__mouse_pos: Optional[QPoint] = None self.__window_pos: Optional[QPoint] = None self.__window_buttons_width: int = 0 self.__window_buttons_position: str = window_buttons_position # create main contaner layout self.__vbox_master_layout = QGridLayout(self) self.__vbox_master_layout.setContentsMargins(0, 0, 0, 0) if titlebar_color is None: self.__titlebar_color: QColor = QColor( self.__style.window.TITLE_BAR_COLOR_RGB) else: self.__titlebar_color = titlebar_color if titlebar_nofocus_color is None: self.__titlebar_nofocus_color: QColor = QColor( self.__style.window.TITLE_BAR_NOFOCUS_COLOR_RGB) else: self.__titlebar_nofocus_color = titlebar_nofocus_color self.__window_frame_widget: WindowFrame = get_window_frame_widget( window_buttons_position=self.__window_buttons_position) self.__vbox_master_layout.addWidget(self.__window_frame_widget, 0, 0) # run window initialization methods add_resizers() if isinstance(self.__window, QMessageBox): expose_msgbox_methods() adjust_wrapped_window_object() add_window_drop_shadow() add_window_buttons() set_window_properties(self.__titlebar_height) # connect slot to detect if window/app loses focus app = QApplication.instance() app.focusChanged.connect(self.__app_focus_changed_slot) self.setFocus() self.layout().setSizeConstraint( QLayout.SetMinimumSize ) # ensure widgets cannot be resized below their min size # attributes for title-bar tab widget self.__tab_widget_dummy_close_button: Optional[QWidget] = None self.__tab_widget_filter: Optional[TabFilter] = None self.__tab_widget: Optional[QTabWidget] = None if titlebar_widget is not None: self.__adjust_title_tabwidget(titlebar_widget) def keyPressEvent(self, event: QKeyEvent) -> None: """Ignore Escape key as it closes the window""" if event.key() != Qt.Key_Escape: super().keyPressEvent(event) # def keyPressEvent(self, e: QKeyPressEvent) -> None: # """ # Example code for moving a window that has no titlebar between 2 screens with different # scaling factors on Windows without any graphical artifacts. # """ # scr = QApplication.instance().screens() # if e.key() == Qt.Key_1: # s = scr[0].availableSize() # o = 0 # elif e.key() == Qt.Key_2: # s = scr[1].availableSize() # o = scr[1].availableSize().width() # else: # return # size = self.size() # pos = QPoint(s.width()/2 - self.width()/2 + o, s.height()/2 - self.height()/2) # self.showMinimized() # self.setGeometry(pos.x(), pos.y(), size.width(), size.height()) # self.showNormal() # self.resize(size) def setWindowIcon(self, icon: Union[QIcon, QPixmap]) -> None: """Sets the window's window icon""" super().setWindowIcon(icon) if sys.platform == 'win32': # make icon show up in taskbar on Windows import ctypes # pylint: disable=import-outside-toplevel myappid: str = 'mycompany.qtmodern.redux.version' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( myappid) # ############################################################ # Properties # ############################################################ def get_style(self) -> Any: """returns the class (static) which holds all the style's properties""" return self.__style @property def use_shadow(self) -> bool: """returns true if the window features a drop-shadow not generated by the OS or window manager""" return self.__use_shadow @property def maximized(self) -> bool: """Returns true if the window is maximzed""" return self.__maximized @property def titlebar_height(self) -> int: """Returns the title bar's height in pixels""" return self.__titlebar_height @property def window_buttons_width(self) -> int: """Returns the width of the area taken up by the titlebar buttons""" diameter: int = self.__style.window.TITLE_BAR_BUTTON_DIAMETER_PX spacing: int = self.__style.window.TITLE_BAR_BUTTON_SPACING_PX - diameter if not isinstance(self.__window, QDIALOG_TYPES): return spacing * 2 + diameter * 3 return diameter @property def window_buttons_margin(self) -> int: """ Returns the distance of the titlebar buttons from the left window edge. The maximum value is the distance of the buttons from the top edge, the minimum distance is defined in the window style. This value changes depending on the size of the window. """ margin_y: int = int( (self.__titlebar_height - self.__style.window.TITLE_BAR_BUTTON_DIAMETER_PX) / 2) min_margin: int = round( self.__style.window.TITLE_BAR_BUTTON_MIN_X_MARGIN_PX if margin_y < self.__style.window.TITLE_BAR_BUTTON_MIN_X_MARGIN_PX else margin_y) return min_margin # ############################################################ # Overloaded Qt methods # ############################################################ def resizeEvent(self, r: QResizeEvent) -> None: """ Qt Resize Event - does two things: 1) applies rounded corners if window transparency has been disabled 2) ensures resizer widgets and titlebar buttons stay in place TODO: the 'rounded_corner_px' value is harcoded. It relates to in '.TitleTabBar::tab' in widgetstyle/tabwidget_titlebar.py (border-width and border-image QSS attribute). Action: investigate a way so that this value doesn't have to be hard-coded. """ # if transparency is turned off, set a mask for some basic rounded corners: if not self.__transparent_window and self.__style.window.WINDOW_CORNER_RADIUS_PX > 0: path = QPainterPath() path.addRoundedRect( QRectF(self.rect()), self.__style.window.WINDOW_CORNER_RADIUS_PX + 1, # add 1 to avoid drawing artifacts self.__style.window.WINDOW_CORNER_RADIUS_PX + 1) reg = QRegion(path.toFillPolygon().toPolygon()) self.setMask(reg) # adjust window button positions if not isinstance(self.__window, QDIALOG_TYPES): self.resizer_bl.adjust_resizers(self.geometry( )) # adjusting one resizer adjusts all other resizers too if self.__window_buttons_position == WINDOW_BUTTONS_RIGHT: self.__move_window_buttons() # if a titlebar tab widget is set, mask it so that the empty area can be used to drag the window if self.__tab_widget is not None: width = 0 tab_bar = self.__tab_widget.tabBar() for i in range(0, tab_bar.count() - 1): # don't count invisible tab at end if tab_bar.isTabVisible(i): width += tab_bar.tabRect(i).width() rounder_corner_px = 8 # TODO # related to hardcoded border-image value in widgetstyle/tabwidget_titlebar.py r = QRect(0, 0, width + rounder_corner_px, self.__titlebar_height) self.__tab_widget.tabBar().setMask(r) def showEvent(self, _: QShowEvent) -> None: """ Qt Show Event: * ensures resizers stay in place * ensures window is centered on screen or relative to their parent window """ if not isinstance(self.__window, QDIALOG_TYPES): self.resizer_bl.adjust_resizers(self.geometry( )) # adjusting one resizer adjusts all other resizers too parent = self.parent() # center MainWindow on screen if CENTER_MAINWINDOW: if parent is None: self.move( QApplication.desktop().screenGeometry(self).center() - self.rect().center()) # type: ignore # center dialogs relative to parent window if ALIGN_CHILD_WINDOWS and parent is not None: try: pos = parent.wrapper.pos( ) # parent is wrapped in a QtModernRedux window width = parent.wrapper.width() height = parent.wrapper.height() except AttributeError: pos = parent.pos() # parent isn't wrapped width = parent.width() height = parent.height() x = pos.x() + (width / 2 - self.width() / 2) y = pos.y() + (height / 2 - self.height() / 2) self.move(x, y) def closeEvent(self, event: QCloseEvent) -> None: """Qt Close Event: Window close & cleanup""" if not self.__window: event.accept() else: self.__window.close() event.setAccepted(self.__window.isHidden()) def eventFilter(self, target: QObject, event: QEvent) -> bool: """Qt Event Filter: This event filter is used when display QDialogs, such as message boxes.""" # FOR MESSAGEBOX: if isinstance(event, QResizeEvent): assert self.__window is not None geometry = self.__window.geometry() if sys.platform in ['darwin']: self.setFixedSize( geometry.width() + self.__style.window.SHADOW_RADIUS_PX * 2, # macOS, Windows (?) geometry.height() + self.__style.window.SHADOW_RADIUS_PX * 2) else: self.setFixedSize( geometry.width() + self.__style.window.SHADOW_RADIUS_PX * 2, geometry.height() + self.__style.window.SHADOW_RADIUS_PX * 2 + self.__titlebar_height) return True return QObject.eventFilter(self, target, event) def setIcon(self, icon: Any) -> None: """Qt setIcon: sets custom icons from the theme for QMessageBox""" if isinstance(self.__window, QMessageBox) and icon != QMessageBox.NoIcon: icons = { QMessageBox.Information: self.__style.window.MSGBOX_ICON_INFORMATION, QMessageBox.Question: self.__style.window.MSGBOX_ICON_QUESTION, QMessageBox.Warning: self.__style.window.MSGBOX_ICON_WARNING, QMessageBox.Critical: self.__style.window.MSGBOX_ICON_CRITICAL, } self.__window.setIconPixmap(self.__load_svg(icons[icon])) def setWindowTitle(self, title: str) -> None: """Qt setWindowTitle: ensures the titlebar displays the window title""" super().setWindowTitle(title) if self.__title_bar: self.__title_widget.setWindowTitle(title) def setWindowFlag(self, flag: Any, on: bool = True) -> None: """Qt setWindowFlag""" button_hints = [ Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint, Qt.WindowMaximizeButtonHint ] if flag in button_hints: self.__set_window_button_state(flag, on) else: QWidget.setWindowFlag(self, flag, on) def setWindowFlags(self, flags: Any) -> None: """Qt setWindowFlags""" button_hints = [ Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint, Qt.WindowMaximizeButtonHint ] for hint in button_hints: self.__set_window_button_state(hint, bool(flags & hint)) QWidget.setWindowFlags(self, flags) def mousePressEvent(self, event: QMouseEvent) -> None: """Qt Mouse-Press Event: Window dragging""" if not self.__drag_move_enabled: return radius = self.__style.window.SHADOW_RADIUS_PX if self.__use_shadow else 0 ep = event.pos() if radius <= ep.y() <= self.__titlebar_height + radius + 1: epx = ep.x() if radius < epx < self.width() - radius: self.__mouse_pressed = True self.__mouse_pos = event.globalPos() self.__window_pos = self.pos() # self.__os = self.size() # use when dragging between screens with different scale factors def mouseMoveEvent(self, event: QMouseEvent) -> None: """Qt Mouse-Move Event: Window dragging""" if self.__mouse_pressed and self.__drag_move_enabled: # CODE TO DETECT DISPLAY CHANGE # display = self.screen().virtualSiblingAt(event.globalPos()).name() # if display != self.__old_display: # print('DISPLAY CHANGED TO %s' % display, self.__old_size) # self.__old_display = display self.move(self.__window_pos + (event.globalPos() - self.__mouse_pos)) def mouseReleaseEvent(self, _: QMouseEvent) -> None: """Qt Mouse-Release Event: Window dragging""" self.__mouse_pressed = False # self.resize(self.__os) # use when dragging between screens with different scale factors # ############################################################ # PRIVATE Custom methods # ############################################################ def __move_window_buttons(self) -> None: """ MacOS: * buttons are on the right side of the titlebar * order (left to right): close, minimize/restore, maximize Windows and most Linux: * buttons are on the left side of the titlebar * order (left to right): minimize/restore, maximize, close """ # determine shadow radius radius: int = self.__style.window.SHADOW_RADIUS_PX if self.__use_shadow is False or self.__maximized: radius = 0 # determine combined button width diameter = self.__style.window.TITLE_BAR_BUTTON_DIAMETER_PX spacing = self.__style.window.TITLE_BAR_BUTTON_SPACING_PX - diameter buttons_width: int = self.window_buttons_width # determine individual button positions margin_y: int = int((self.__titlebar_height - diameter) / 2) min_margin = self.window_buttons_margin if self.__window_buttons_position == WINDOW_BUTTONS_RIGHT: ofs_min = 0 ofs_max = diameter + spacing if not isinstance(self.__window, QDIALOG_TYPES): ofs_close = (diameter + spacing) * 2 else: ofs_close = 0 margin_x: int = self.width() - buttons_width - min_margin - radius else: ofs_close = 0 ofs_min = diameter + spacing ofs_max = (diameter + spacing) * 2 margin_x = min_margin + radius # move buttons self._close_button.move(margin_x + ofs_close, margin_y + radius) if not isinstance(self.__window, QDIALOG_TYPES ): # no additional window buttons for message-boxes self._minimize_button.move(margin_x + ofs_min, margin_y + radius) self._restore_button.move(margin_x + ofs_max, margin_y + radius) self._maximize_button.move(margin_x + ofs_max, margin_y + radius) def __load_svg(self, svg_file: str) -> QPixmap: """Loads a SVG file and scales it correctly for High-DPI screens""" svg_renderer = QSvgRenderer(svg_file) pixmap = QPixmap(svg_renderer.defaultSize() * self.devicePixelRatio()) pixmap.fill(Qt.transparent) painter = QPainter() painter.begin(pixmap) svg_renderer.render(painter) painter.end() pixmap.setDevicePixelRatio(self.devicePixelRatio()) return pixmap def __show_resizers(self, show: bool) -> None: """Shows / hides the resizer widgets""" self.resizer_top.setVisible(show) self.resizer_bot.setVisible(show) self.resizer_lef.setVisible(show) self.resizer_rig.setVisible(show) self.resizer_tl.setVisible(show) self.resizer_tr.setVisible(show) self.resizer_bl.setVisible(show) self.resizer_br.setVisible(show) def __child_was_closed(self) -> None: """Wrapped window was closed""" self.__window = None # The child was deleted, remove the reference to it and close the parent window self.close() def __set_window_button_state(self, hint: Any, state: Any) -> None: # pylint: disable=too-many-branches """Adjusts button state (enabled/disabled) based on window status""" buttons = {Qt.WindowCloseButtonHint: self._close_button} if not isinstance(self.__window, QDIALOG_TYPES): buttons[Qt.WindowMinimizeButtonHint] = self._minimize_button buttons[Qt.WindowMaximizeButtonHint] = self._maximize_button button = buttons.get(hint) maximized = bool(self.windowState() & Qt.WindowMaximized) if not isinstance(self.__window, QDIALOG_TYPES): if button == self._maximize_button: # special rules for max/restore self._restore_button.setEnabled(state) self._maximize_button.setEnabled(state) if maximized: self._restore_button.setVisible(state) self._maximize_button.setVisible(False) else: self._maximize_button.setVisible(state) self._restore_button.setVisible(False) elif button is not None: button.setEnabled(state) elif button is not None: button.setEnabled(state) all_buttons = [self._close_button] if not isinstance(self.__window, QDIALOG_TYPES): all_buttons.append(self._minimize_button) all_buttons.append(self._maximize_button) all_buttons.append(self._restore_button) if True in [button.isEnabled() for button in all_buttons]: for button in all_buttons: button.setVisible(True) if not isinstance(self.__window, QDIALOG_TYPES): if maximized: self._maximize_button.setVisible(False) else: self._restore_button.setVisible(False) else: for button in all_buttons: button.setVisible(False) @Slot() # type: ignore def __on_minimize_button_clicked(self) -> None: """ macOS workaround: Frameless windows cannot be minimized on macOS, therefore we need to reinstate the titlebar before we minimize the window. Once the window minimize command has been issued, we can hide the titlebar again. """ if sys.platform == 'darwin': self.setWindowFlags(Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint) self.show() self.setWindowState(Qt.WindowMinimized) self._minimize_button.setAttribute( Qt.WA_UnderMouse, False) # prevent hover state form getting stuck if sys.platform == 'darwin': self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint) self.show() @Slot() # type: ignore def __on_restore_button_clicked(self) -> None: """Restore button clicked""" if self._maximize_button.isEnabled() or self._restore_button.isEnabled( ): self._restore_button.setVisible(False) self._restore_button.setEnabled(False) self._maximize_button.setVisible(True) self._maximize_button.setEnabled(True) self.__maximized = False if self.__use_shadow: self.layout().setMargin(self.__style.window.SHADOW_RADIUS_PX ) # adjust window for drop-shadow margin self.__show_resizers(True) self.__drag_move_enabled = True self.__move_window_buttons() # adjust window for drop-shadow margin self.setWindowState(Qt.WindowNoState) self._maximize_button.setAttribute( Qt.WA_UnderMouse, False) # prevent hover state form getting stuck self._restore_button.setAttribute(Qt.WA_UnderMouse, False) @Slot() # type: ignore def __on_maximize_button_clicked(self) -> None: """Maximize button clicked""" if self._maximize_button.isEnabled() or self._restore_button.isEnabled( ): self._restore_button.setVisible(True) self._restore_button.setEnabled(True) self._maximize_button.setVisible(False) self._maximize_button.setEnabled(False) self.__maximized = True if self.__use_shadow: self.layout().setMargin(0) self.__show_resizers(False) self.__drag_move_enabled = False self.__move_window_buttons() # adjust window for drop-shadow margin self.setWindowState( Qt.WindowMaximized) # adjust window for drop-shadow margin self._maximize_button.setAttribute( Qt.WA_UnderMouse, False) # prevent hover state form getting stuck self._restore_button.setAttribute(Qt.WA_UnderMouse, False) @Slot() # type: ignore def __on_close_button_clicked(self) -> None: """Close button clicked""" self.close() @Slot() # type: ignore def __on_title_bar_double_clicked(self) -> None: """Maximize / Restore window on titlebar double click""" if not bool(self.windowState() & Qt.WindowMaximized): self.__on_maximize_button_clicked() else: self.__on_restore_button_clicked() def __get_title_tabwidget_style(self, background: QColor) -> str: """Gets the QSS for the TabBar widget and adjusts margins""" margin_top_px = self.__style.window.TITLE_BAR_TOP_MARGIN_PX top_bottom_border_px = ( self.__style.window.TITLE_BAR_TAB_CSS_TOP_BORDER_PX + self.__style.window.TITLE_BAR_TAB_CSS_BOTTOM_BORDER_PX) height_px = self.titlebar_height - margin_top_px - top_bottom_border_px style: str = self.__style.get_title_tabwidget_stylesheet() style = style.replace('{TITLEBAR_HEIGHT}', str(height_px)) button_margin = str( self.window_buttons_margin + self.window_buttons_width + self.__style.window.TITLE_BAR_BUTTON_MIN_X_MARGIN_PX) if self.__window_buttons_position == WINDOW_BUTTONS_RIGHT: style = style.replace('{WINDOW_BUTTON_MARGIN_LEFT}', str(0)) style = style.replace('{WINDOW_BUTTON_MARGIN_RIGHT}', button_margin) else: style = style.replace('{WINDOW_BUTTON_MARGIN_LEFT}', button_margin) style = style.replace('{WINDOW_BUTTON_MARGIN_RIGHT}', str(0)) style = style.replace('{BACKGROUND_COLOR}', background.name()) return style @Slot() # type: ignore def __app_focus_changed_slot(self) -> None: """Changes the titlebar's background color when the window acquires/loses focus""" if self.isActiveWindow(): self.__window_frame_widget.set_background_color( self.__titlebar_color) else: self.__window_frame_widget.set_background_color( self.__titlebar_nofocus_color) self.__window_frame_widget.update() if self.__tab_widget is not None: if self.isActiveWindow(): self.__tab_widget.setStyleSheet( self.__get_title_tabwidget_style(self.__titlebar_color)) else: self.__tab_widget.setStyleSheet( self.__get_title_tabwidget_style( self.__titlebar_nofocus_color)) def __adjust_title_tabwidget(self, tab_widget: QTabWidget) -> None: """ Adjusts a TabWidget to work as a Google Chrome style tab bar in the window's title bar. TODO: to properly center the Widget (doesn't apply to TabWidgets) a bottom margin of 2px is needed; why is that? Action: track down source for this offset in the QSS definitions and figure out the proper calculation of this value. """ # add properties to ensure style is only applied tabwidget in titlebar if isinstance(tab_widget, QTabWidget): self.__tab_widget = tab_widget tab_widget.setProperty("class", "TitleTabWidget") # type: ignore tab_widget.tabBar().setProperty("class", "TitleTabBar") # type: ignore tab_widget.setStyleSheet( self.__get_title_tabwidget_style(self.__titlebar_color)) # insert empty disabled tab for rounded effect on last tab idx = tab_widget.addTab(QWidget(), "") tab_widget.setTabEnabled(idx, False) self.__tab_widget_dummy_close_button = QWidget() self.__tab_widget_dummy_close_button.setGeometry(0, 0, 1, 1) # remove close button for last widget tab_widget.tabBar().setTabButton( idx, QTabBar.RightSide, self.__tab_widget_dummy_close_button) self.__tab_widget_filter = TabFilter(tab_widget.tabBar()) tab_widget.tabBar().installEventFilter(self.__tab_widget_filter) tab_widget.tabBar().setDocumentMode(True) tab_widget.tabBar().setExpanding(False) else: margin_top_px: int = self.__style.window.TITLE_BAR_TOP_MARGIN_PX # type: ignore margin_bottom_px: int = self.__style.window.TITLE_BAR_TAB_CSS_BOTTOM_BORDER_PX height_px: int = self.titlebar_height - margin_top_px - margin_bottom_px button_margin: int = ( self.window_buttons_margin + self.window_buttons_width + self.__style.window.TITLE_BAR_BUTTON_MIN_X_MARGIN_PX) non_button_margin: int = self.__style.window.TITLE_BAR_BUTTON_MIN_X_MARGIN_PX if self.__window_buttons_position == WINDOW_BUTTONS_RIGHT: tab_widget.layout().setContentsMargins(non_button_margin, 2, button_margin, 0) # TODO: why 2? else: tab_widget.layout().setContentsMargins(button_margin, 2, non_button_margin, 0) # TODO: why 2? tab_widget.setMaximumHeight(height_px) tab_widget.setMinimumHeight(height_px)
def __init__(self, identifier, area, menu=None, parent=None): """ Args: identifier (str) area (str): either "rows", "columns", or "frozen" menu (FilterMenu, optional) parent (QWidget, optional): Parent widget """ super().__init__(parent=parent) self._identifier = identifier self._area = area layout = QHBoxLayout(self) button = QToolButton(self) button.setPopupMode(QToolButton.InstantPopup) button.setStyleSheet("QToolButton {border: none;}") button.setEnabled(menu is not None) if menu: self.menu = menu button.setMenu(self.menu) self.menu.anchor = self self.drag_start_pos = None label = QLabel(identifier) layout.addWidget(label) layout.addWidget(button) layout.setContentsMargins(self._H_MARGIN, 0, self._H_MARGIN, 0) if area == "rows": h_alignment = Qt.AlignLeft layout.insertSpacing(1, self._SPACING) button.setArrowType(Qt.DownArrow) elif area == "columns": h_alignment = Qt.AlignRight layout.insertSpacing(0, self._SPACING) button.setArrowType(Qt.RightArrow) elif area == "frozen": h_alignment = Qt.AlignHCenter label.setAlignment(h_alignment | Qt.AlignVCenter) label.setStyleSheet("QLabel {font-weight: bold;}") self.setAttribute(Qt.WA_DeleteOnClose) self.setAutoFillBackground(True) self.setFrameStyle(QFrame.Raised) self.setFrameShape(QFrame.Panel) self.setStyleSheet("QFrame {background: " + PIVOT_TABLE_HEADER_COLOR + ";}") self.setAcceptDrops(True) self.setToolTip( "<p>This is a draggable header. </p>" "<p>Drag-and-drop it onto another header to pivot the table, " "or onto the Frozen table to freeze this dimension.</p>") self.adjustSize() self.setMinimumWidth(self.size().width())
def __init__(self, original, processed, title=None, parent=None, export=False): super(ImageViewer, self).__init__(parent) if original is None and processed is None: raise ValueError( self.tr("ImageViewer.__init__: Empty image received")) if original is None and processed is not None: original = processed self.original = original self.processed = processed if self.original is not None and self.processed is None: self.view = DynamicView(self.original) else: self.view = DynamicView(self.processed) # view_label = QLabel(self.tr('View:')) self.original_radio = QRadioButton(self.tr("Original")) self.original_radio.setToolTip( self. tr("Show the original image for comparison (press SPACE to toggle)" )) self.process_radio = QRadioButton(self.tr("Processed")) self.process_radio.setToolTip( self.tr( "Show result of the current processing (press SPACE to toggle)" )) self.zoom_label = QLabel() full_button = QToolButton() full_button.setText(self.tr("100%")) fit_button = QToolButton() fit_button.setText(self.tr("Fit")) height, width, _ = self.original.shape size_label = QLabel(self.tr(f"[{height}x{width} px]")) export_button = QToolButton() export_button.setToolTip(self.tr("Export processed image")) # export_button.setText(self.tr('Export...')) export_button.setIcon(QIcon("icons/export.svg")) tool_layout = QHBoxLayout() tool_layout.addWidget(QLabel(self.tr("Zoom:"))) tool_layout.addWidget(self.zoom_label) # tool_layout.addWidget(full_button) # tool_layout.addWidget(fit_button) tool_layout.addStretch() if processed is not None: # tool_layout.addWidget(view_label) tool_layout.addWidget(self.original_radio) tool_layout.addWidget(self.process_radio) tool_layout.addStretch() tool_layout.addWidget(size_label) if export or processed is not None: tool_layout.addWidget(export_button) if processed is not None: self.original_radio.setChecked(False) self.process_radio.setChecked(True) self.toggle_mode(False) vert_layout = QVBoxLayout() if title is not None: self.title_label = QLabel(title) modify_font(self.title_label, bold=True) self.title_label.setAlignment(Qt.AlignCenter) vert_layout.addWidget(self.title_label) else: self.title_label = None vert_layout.addWidget(self.view) vert_layout.addLayout(tool_layout) self.setLayout(vert_layout) self.original_radio.toggled.connect(self.toggle_mode) fit_button.clicked.connect(self.view.zoom_fit) full_button.clicked.connect(self.view.zoom_full) export_button.clicked.connect(self.export_image) self.view.viewChanged.connect(self.forward_changed)
def __init__(self, camera): super(camera_view, self).__init__() self.camera = camera self.image_camera = np.zeros((1,1)) #self.image = QImage() self.button_up = QToolButton() self.button_up.setArrowType(Qt.UpArrow) self.button_down = QToolButton() self.button_down.setArrowType(Qt.DownArrow) self.button_left = QToolButton() self.button_left.setArrowType(Qt.LeftArrow) self.button_right = QToolButton() self.button_right.setArrowType(Qt.RightArrow) self.box_button = QVBoxLayout() self.box_button_centre = QHBoxLayout() self.box_button_centre.addWidget(self.button_left) self.box_button_centre.addWidget(self.button_right) self.box_button_centre.addStretch(1) self.box_button.addWidget(self.button_up) self.box_button.addLayout(self.box_button_centre) self.box_button.addWidget(self.button_down) self.box_button.addStretch(1) self.button_affichage = QPushButton("Affichage Vidéo") self.button_affichage.clicked.connect(self.Affichage) self.button_stop = QPushButton("Stop retour Vidéo") self.button_stop.clicked.connect(self.fin_video) self.box = QVBoxLayout() self.box.addWidget(self.button_affichage) self.box.addWidget(self.button_stop) self.box_panel = QHBoxLayout() self.box_panel.addLayout(self.box) self.box_panel.addLayout(self.box_button) self.image2 = QLabel() self.layout = QVBoxLayout() self.layout.addWidget(self.image2) self.layout.addLayout(self.box_panel) self.setLayout(self.layout) self.movie_thread = MovieThread(self.camera) self.movie_thread.changePixmap.connect(self.setImage) self.movie_thread.setTerminationEnabled(True) # self.button_up.clicked.connect(self.up) self.button_down.clicked.connect(self.down) self.button_right.clicked.connect(self.right) self.button_left.clicked.connect(self.left) #box enregistrement + photo self.button_debut_video = QPushButton("Début enregistrement Vidéo") self.button_fin_video = QPushButton("Fin enregistrement Vidéo") self.button_photo = QPushButton("Photo") self.box_video_photo = QVBoxLayout() self.box_video_photo.addWidget(self.button_debut_video) self.box_video_photo.addWidget(self.button_fin_video) self.box_video_photo.addWidget(self.button_photo) self.box_panel.addLayout(self.box_video_photo) self.enregistrement = Record(self.camera) self.enregistrement.setTerminationEnabled(True) self.button_debut_video.clicked.connect(self.video_debut) self.button_fin_video.clicked.connect(self.video_fin) self.button_photo.clicked.connect(self.photo) self.compteur_photo = 0 self.enregistrement.start() self.timer_enregistrement = QTimer() self.timer_enregistrement.setInterval((1/24)*1000) self.timer_enregistrement.timeout.connect(self.video)
def __init__(self, image, parent=None): super(CloningWidget, self).__init__(parent) self.detector_combo = QComboBox() self.detector_combo.addItems( [self.tr('BRISK'), self.tr('ORB'), self.tr('AKAZE')]) self.detector_combo.setCurrentIndex(0) self.detector_combo.setToolTip( self.tr('Algorithm used for localization and description')) self.response_spin = QSpinBox() self.response_spin.setRange(0, 100) self.response_spin.setSuffix(self.tr('%')) self.response_spin.setValue(90) self.response_spin.setToolTip( self.tr('Maximum keypoint response to perform matching')) self.matching_spin = QSpinBox() self.matching_spin.setRange(1, 100) self.matching_spin.setSuffix(self.tr('%')) self.matching_spin.setValue(20) self.matching_spin.setToolTip( self.tr('Maximum metric difference to accept matching')) self.distance_spin = QSpinBox() self.distance_spin.setRange(1, 100) self.distance_spin.setSuffix(self.tr('%')) self.distance_spin.setValue(15) self.distance_spin.setToolTip( self.tr('Maximum distance between matches in the same cluster')) self.cluster_spin = QSpinBox() self.cluster_spin.setRange(1, 20) self.cluster_spin.setValue(5) self.cluster_spin.setToolTip( self.tr('Minimum number of keypoints to create a new cluster')) self.kpts_check = QCheckBox(self.tr('Show keypoints')) self.kpts_check.setToolTip(self.tr('Show keypoint coverage')) self.nolines_check = QCheckBox(self.tr('Hide lines')) self.nolines_check.setToolTip(self.tr('Disable match line drawing')) self.process_button = QToolButton() self.process_button.setText(self.tr('Process')) self.process_button.setToolTip(self.tr('Perform automatic detection')) modify_font(self.process_button, bold=True) self.status_label = QLabel() self.mask_label = QLabel() self.mask_button = QToolButton() self.mask_button.setText(self.tr('Load mask...')) self.mask_button.setToolTip( self.tr('Load an image to be used as mask')) self.onoff_button = QToolButton() self.onoff_button.setText(self.tr('OFF')) self.onoff_button.setCheckable(True) self.onoff_button.setToolTip(self.tr('Toggle keypoint detection mask')) self.image = image self.viewer = ImageViewer(self.image, self.image) self.gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) self.total = self.kpts = self.desc = self.matches = self.clusters = self.mask = None self.canceled = False self.detector_combo.currentIndexChanged.connect(self.update_detector) self.response_spin.valueChanged.connect(self.update_detector) self.matching_spin.valueChanged.connect(self.update_matching) self.distance_spin.valueChanged.connect(self.update_cluster) self.cluster_spin.valueChanged.connect(self.update_cluster) self.nolines_check.stateChanged.connect(self.process) self.kpts_check.stateChanged.connect(self.process) self.process_button.clicked.connect(self.process) self.mask_button.clicked.connect(self.load_mask) self.onoff_button.toggled.connect(self.toggle_mask) self.onoff_button.setEnabled(False) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr('Detector:'))) top_layout.addWidget(self.detector_combo) top_layout.addWidget(QLabel(self.tr('Response:'))) top_layout.addWidget(self.response_spin) top_layout.addWidget(QLabel(self.tr('Matching:'))) top_layout.addWidget(self.matching_spin) top_layout.addWidget(QLabel(self.tr('Distance:'))) top_layout.addWidget(self.distance_spin) top_layout.addWidget(QLabel(self.tr('Cluster:'))) top_layout.addWidget(self.cluster_spin) top_layout.addWidget(self.nolines_check) top_layout.addWidget(self.kpts_check) top_layout.addStretch() bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.process_button) bottom_layout.addWidget(self.status_label) bottom_layout.addStretch() bottom_layout.addWidget(self.mask_button) bottom_layout.addWidget(self.onoff_button) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(bottom_layout) main_layout.addWidget(self.viewer) self.setLayout(main_layout)
class AddOrManageObjectGroupDialog(QDialog): def __init__(self, parent, object_class_item, db_mngr, *db_maps): """ Args: parent (SpineDBEditor): data store widget object_class_item (ObjectClassItem) db_mngr (SpineDBManager) *db_maps: database mappings """ super().__init__(parent) self.object_class_item = object_class_item self.db_mngr = db_mngr self.db_maps = db_maps self.db_map = db_maps[0] self.db_maps_by_codename = { db_map.codename: db_map for db_map in db_maps } self.db_combo_box = QComboBox(self) self.header_widget = QWidget(self) self.group_name_line_edit = QLineEdit(self) header_layout = QHBoxLayout(self.header_widget) header_layout.addWidget(QLabel(f"Group name: ")) header_layout.addWidget(self.group_name_line_edit) header_layout.addSpacing(32) header_layout.addWidget(QLabel("Database")) header_layout.addWidget(self.db_combo_box) self.non_members_tree = QTreeWidget(self) self.non_members_tree.setHeaderLabel("Non members") self.non_members_tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.non_members_tree.setColumnCount(1) self.non_members_tree.setIndentation(0) self.members_tree = QTreeWidget(self) self.members_tree.setHeaderLabel("Members") self.members_tree.setSelectionMode(QTreeWidget.ExtendedSelection) self.members_tree.setColumnCount(1) self.members_tree.setIndentation(0) self.add_button = QToolButton() self.add_button.setToolTip("<p>Add selected non-members.</p>") self.add_button.setIcon(QIcon(":/icons/menu_icons/cube_plus.svg")) self.add_button.setIconSize(QSize(24, 24)) self.add_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.add_button.setText(">>") self.remove_button = QToolButton() self.remove_button.setToolTip("<p>Remove selected members.</p>") self.remove_button.setIcon(QIcon(":/icons/menu_icons/cube_minus.svg")) self.remove_button.setIconSize(QSize(24, 24)) self.remove_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.remove_button.setText("<<") self.vertical_button_widget = QWidget() vertical_button_layout = QVBoxLayout(self.vertical_button_widget) vertical_button_layout.addStretch() vertical_button_layout.addWidget(self.add_button) vertical_button_layout.addWidget(self.remove_button) vertical_button_layout.addStretch() self.button_box = QDialogButtonBox(self) self.button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) layout = QGridLayout(self) layout.addWidget(self.header_widget, 0, 0, 1, 3, Qt.AlignHCenter) layout.addWidget(self.non_members_tree, 1, 0) layout.addWidget(self.vertical_button_widget, 1, 1) layout.addWidget(self.members_tree, 1, 2) layout.addWidget(self.button_box, 2, 0, 1, 3) self.setAttribute(Qt.WA_DeleteOnClose) self.db_combo_box.addItems(list(self.db_maps_by_codename)) self.db_map_object_ids = { db_map: { x["name"]: x["id"] for x in self.db_mngr.get_items_by_field( self.db_map, "object", "class_id", self.object_class_item.db_map_id(db_map)) } for db_map in db_maps } self.reset_list_widgets(db_maps[0].codename) self.connect_signals() def connect_signals(self): """Connect signals to slots.""" self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) self.db_combo_box.currentTextChanged.connect(self.reset_list_widgets) self.add_button.clicked.connect(self.add_members) self.remove_button.clicked.connect(self.remove_members) def reset_list_widgets(self, database): self.db_map = self.db_maps_by_codename[database] object_ids = self.db_map_object_ids[self.db_map] members = [] non_members = [] for obj_name, obj_id in object_ids.items(): if obj_id in self.initial_member_ids(): members.append(obj_name) elif obj_id != self.initial_entity_id(): non_members.append(obj_name) member_items = [QTreeWidgetItem([obj_name]) for obj_name in members] non_member_items = [ QTreeWidgetItem([obj_name]) for obj_name in non_members ] self.members_tree.addTopLevelItems(member_items) self.non_members_tree.addTopLevelItems(non_member_items) def initial_member_ids(self): raise NotImplementedError() def initial_entity_id(self): raise NotImplementedError() @Slot(bool) def add_members(self, checked=False): indexes = sorted([ self.non_members_tree.indexOfTopLevelItem(x) for x in self.non_members_tree.selectedItems() ], reverse=True) items = [ self.non_members_tree.takeTopLevelItem(ind) for ind in indexes ] self.members_tree.addTopLevelItems(items) @Slot(bool) def remove_members(self, checked=False): indexes = sorted([ self.members_tree.indexOfTopLevelItem(x) for x in self.members_tree.selectedItems() ], reverse=True) items = [self.members_tree.takeTopLevelItem(ind) for ind in indexes] self.non_members_tree.addTopLevelItems(items) def _check_validity(self): if not self.members_tree.topLevelItemCount(): self.parent().msg_error.emit( "Please select at least one member object.") return False return True
def _toggle_box_visibility(toggle_button: QToolButton, box: QGroupBox): box.setVisible(not box.isVisible()) toggle_button.setText("-" if box.isVisible() else "+")
class CloningWidget(ToolWidget): def __init__(self, image, parent=None): super(CloningWidget, self).__init__(parent) self.detector_combo = QComboBox() self.detector_combo.addItems( [self.tr('BRISK'), self.tr('ORB'), self.tr('AKAZE')]) self.detector_combo.setCurrentIndex(0) self.detector_combo.setToolTip( self.tr('Algorithm used for localization and description')) self.response_spin = QSpinBox() self.response_spin.setRange(0, 100) self.response_spin.setSuffix(self.tr('%')) self.response_spin.setValue(90) self.response_spin.setToolTip( self.tr('Maximum keypoint response to perform matching')) self.matching_spin = QSpinBox() self.matching_spin.setRange(1, 100) self.matching_spin.setSuffix(self.tr('%')) self.matching_spin.setValue(20) self.matching_spin.setToolTip( self.tr('Maximum metric difference to accept matching')) self.distance_spin = QSpinBox() self.distance_spin.setRange(1, 100) self.distance_spin.setSuffix(self.tr('%')) self.distance_spin.setValue(15) self.distance_spin.setToolTip( self.tr('Maximum distance between matches in the same cluster')) self.cluster_spin = QSpinBox() self.cluster_spin.setRange(1, 20) self.cluster_spin.setValue(5) self.cluster_spin.setToolTip( self.tr('Minimum number of keypoints to create a new cluster')) self.kpts_check = QCheckBox(self.tr('Show keypoints')) self.kpts_check.setToolTip(self.tr('Show keypoint coverage')) self.nolines_check = QCheckBox(self.tr('Hide lines')) self.nolines_check.setToolTip(self.tr('Disable match line drawing')) self.process_button = QToolButton() self.process_button.setText(self.tr('Process')) self.process_button.setToolTip(self.tr('Perform automatic detection')) modify_font(self.process_button, bold=True) self.status_label = QLabel() self.mask_label = QLabel() self.mask_button = QToolButton() self.mask_button.setText(self.tr('Load mask...')) self.mask_button.setToolTip( self.tr('Load an image to be used as mask')) self.onoff_button = QToolButton() self.onoff_button.setText(self.tr('OFF')) self.onoff_button.setCheckable(True) self.onoff_button.setToolTip(self.tr('Toggle keypoint detection mask')) self.image = image self.viewer = ImageViewer(self.image, self.image) self.gray = cv.cvtColor(self.image, cv.COLOR_BGR2GRAY) self.total = self.kpts = self.desc = self.matches = self.clusters = self.mask = None self.canceled = False self.detector_combo.currentIndexChanged.connect(self.update_detector) self.response_spin.valueChanged.connect(self.update_detector) self.matching_spin.valueChanged.connect(self.update_matching) self.distance_spin.valueChanged.connect(self.update_cluster) self.cluster_spin.valueChanged.connect(self.update_cluster) self.nolines_check.stateChanged.connect(self.process) self.kpts_check.stateChanged.connect(self.process) self.process_button.clicked.connect(self.process) self.mask_button.clicked.connect(self.load_mask) self.onoff_button.toggled.connect(self.toggle_mask) self.onoff_button.setEnabled(False) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr('Detector:'))) top_layout.addWidget(self.detector_combo) top_layout.addWidget(QLabel(self.tr('Response:'))) top_layout.addWidget(self.response_spin) top_layout.addWidget(QLabel(self.tr('Matching:'))) top_layout.addWidget(self.matching_spin) top_layout.addWidget(QLabel(self.tr('Distance:'))) top_layout.addWidget(self.distance_spin) top_layout.addWidget(QLabel(self.tr('Cluster:'))) top_layout.addWidget(self.cluster_spin) top_layout.addWidget(self.nolines_check) top_layout.addWidget(self.kpts_check) top_layout.addStretch() bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.process_button) bottom_layout.addWidget(self.status_label) bottom_layout.addStretch() bottom_layout.addWidget(self.mask_button) bottom_layout.addWidget(self.onoff_button) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(bottom_layout) main_layout.addWidget(self.viewer) self.setLayout(main_layout) def toggle_mask(self, checked): self.onoff_button.setText('ON' if checked else 'OFF') if checked: self.viewer.update_processed( cv.merge([c * self.mask for c in cv.split(self.image)])) else: self.viewer.update_processed(self.image) self.update_detector() def load_mask(self): filename, basename, mask = load_image(self) if filename is None: return if self.image.shape[:-1] != mask.shape[:-1]: QMessageBox.critical( self, self.tr('Error'), self.tr('Both image and mask must have the same size!')) return _, self.mask = cv.threshold(cv.cvtColor(mask, cv.COLOR_BGR2GRAY), 0, 1, cv.THRESH_BINARY) self.onoff_button.setEnabled(True) self.onoff_button.setChecked(True) self.mask_button.setText('"{}"'.format(splitext(basename)[0])) self.mask_button.setToolTip(self.tr('Current detection mask image')) def update_detector(self): self.total = self.kpts = self.desc = self.matches = self.clusters = None self.status_label.setText('') self.process_button.setEnabled(True) def update_matching(self): self.matches = self.clusters = None self.process_button.setEnabled(True) def update_cluster(self): self.clusters = None self.process_button.setEnabled(True) def cancel(self): self.canceled = True self.status_label.setText(self.tr('Processing interrupted!')) modify_font(self.status_label, bold=False, italic=False) def process(self): start = time() self.canceled = False self.status_label.setText(self.tr('Processing, please wait...')) algorithm = self.detector_combo.currentIndex() response = 100 - self.response_spin.value() matching = self.matching_spin.value() / 100 * 255 distance = self.distance_spin.value() / 100 cluster = self.cluster_spin.value() modify_font(self.status_label, bold=False, italic=True) QCoreApplication.processEvents() if self.kpts is None: if algorithm == 0: detector = cv.BRISK_create() elif algorithm == 1: detector = cv.ORB_create() elif algorithm == 2: detector = cv.AKAZE_create() else: return mask = self.mask if self.onoff_button.isChecked() else None self.kpts, self.desc = detector.detectAndCompute(self.gray, mask) self.total = len(self.kpts) responses = np.array([k.response for k in self.kpts]) strongest = (cv.normalize(responses, None, 0, 100, cv.NORM_MINMAX) >= response).flatten() self.kpts = list(compress(self.kpts, strongest)) if len(self.kpts) > 30000: QMessageBox.warning( self, self.tr('Warning'), self. tr('Too many keypoints found ({}), please reduce response value' .format(self.total))) self.kpts = self.desc = None self.total = 0 self.status_label.setText('') return self.desc = self.desc[strongest] if self.matches is None: matcher = cv.BFMatcher_create(cv.NORM_HAMMING, True) self.matches = matcher.radiusMatch(self.desc, self.desc, matching) if self.matches is None: self.status_label.setText( self.tr('No keypoint match found with current settings')) modify_font(self.status_label, italic=False, bold=True) return self.matches = [ item for sublist in self.matches for item in sublist ] self.matches = [ m for m in self.matches if m.queryIdx != m.trainIdx ] if not self.matches: self.clusters = [] elif self.clusters is None: self.clusters = [] min_dist = distance * np.min(self.gray.shape) / 2 kpts_a = np.array([p.pt for p in self.kpts]) ds = np.linalg.norm([ kpts_a[m.queryIdx] - kpts_a[m.trainIdx] for m in self.matches ], axis=1) self.matches = [ m for i, m in enumerate(self.matches) if ds[i] > min_dist ] total = len(self.matches) progress = QProgressDialog(self.tr('Clustering matches...'), self.tr('Cancel'), 0, total, self) progress.canceled.connect(self.cancel) progress.setWindowModality(Qt.WindowModal) for i in range(total): match0 = self.matches[i] d0 = ds[i] query0 = match0.queryIdx train0 = match0.trainIdx group = [match0] for j in range(i + 1, total): match1 = self.matches[j] query1 = match1.queryIdx train1 = match1.trainIdx if query1 == train0 and train1 == query0: continue d1 = ds[j] if np.abs(d0 - d1) > min_dist: continue a0 = np.array(self.kpts[query0].pt) b0 = np.array(self.kpts[train0].pt) a1 = np.array(self.kpts[query1].pt) b1 = np.array(self.kpts[train1].pt) aa = np.linalg.norm(a0 - a1) bb = np.linalg.norm(b0 - b1) ab = np.linalg.norm(a0 - b1) ba = np.linalg.norm(b0 - a1) if not (0 < aa < min_dist and 0 < bb < min_dist or 0 < ab < min_dist and 0 < ba < min_dist): continue for g in group: if g.queryIdx == train1 and g.trainIdx == query1: break else: group.append(match1) if len(group) >= cluster: self.clusters.append(group) progress.setValue(i) if self.canceled: self.update_detector() return progress.close() output = np.copy(self.image) hsv = np.zeros((1, 1, 3)) nolines = self.nolines_check.isChecked() show_kpts = self.kpts_check.isChecked() if show_kpts: for kpt in self.kpts: cv.circle(output, (int(kpt.pt[0]), int(kpt.pt[1])), 2, (250, 227, 72)) angles = [] for c in self.clusters: for m in c: ka = self.kpts[m.queryIdx] pa = tuple(map(int, ka.pt)) sa = int(np.round(ka.size)) kb = self.kpts[m.trainIdx] pb = tuple(map(int, kb.pt)) sb = int(np.round(kb.size)) angle = np.arctan2(pb[1] - pa[1], pb[0] - pa[0]) if angle < 0: angle += np.pi angles.append(angle) hsv[0, 0, 0] = angle / np.pi * 180 hsv[0, 0, 1] = 255 hsv[0, 0, 2] = m.distance / matching * 255 rgb = cv.cvtColor(hsv.astype(np.uint8), cv.COLOR_HSV2BGR) rgb = tuple([int(x) for x in rgb[0, 0]]) cv.circle(output, pa, sa, rgb, 1, cv.LINE_AA) cv.circle(output, pb, sb, rgb, 1, cv.LINE_AA) if not nolines: cv.line(output, pa, pb, rgb, 1, cv.LINE_AA) regions = 0 if angles: angles = np.reshape(np.array(angles, dtype=np.float32), (len(angles), 1)) if np.std(angles) < 0.1: regions = 1 else: criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0) attempts = 10 flags = cv.KMEANS_PP_CENTERS compact = [ cv.kmeans(angles, k, None, criteria, attempts, flags)[0] for k in range(1, 11) ] compact = cv.normalize(np.array(compact), None, 0, 1, cv.NORM_MINMAX) regions = np.argmax(compact < 0.005) + 1 self.viewer.update_processed(output) self.process_button.setEnabled(False) modify_font(self.status_label, italic=False, bold=True) self.status_label.setText( self. tr('Keypoints: {} --> Filtered: {} --> Matches: {} --> Clusters: {} --> Regions: {}' .format(self.total, len(self.kpts), len(self.matches), len(self.clusters), regions))) self.info_message.emit( self.tr('Copy-Move Forgery = {}'.format(elapsed_time(start))))
class DebugControlsWidget(QToolBar): def __init__(self, parent, name, data, debug_state): if not type(data) == binaryninja.binaryview.BinaryView: raise Exception('expected widget data to be a BinaryView') self.bv = data self.debug_state = debug_state QToolBar.__init__(self, parent) # TODO: Is there a cleaner way to do this? self.setStyleSheet(""" QToolButton{padding: 4px 14px 4px 14px; font-size: 14pt;} QToolButton:disabled{color: palette(alternate-base)} """) self.actionRun = QAction("Run", self) self.actionRun.triggered.connect(lambda: self.perform_run()) self.actionRestart = QAction("Restart", self) self.actionRestart.triggered.connect(lambda: self.perform_restart()) self.actionQuit = QAction("Quit", self) self.actionQuit.triggered.connect(lambda: self.perform_quit()) self.actionAttach = QAction("Attach", self) self.actionAttach.triggered.connect(lambda: self.perform_attach()) self.actionDetach = QAction("Detach", self) self.actionDetach.triggered.connect(lambda: self.perform_detach()) self.actionSettings = QAction("Settings...", self) self.actionSettings.triggered.connect(lambda: self.perform_settings()) self.actionPause = QAction("Pause", self) self.actionPause.triggered.connect(lambda: self.perform_pause()) self.actionResume = QAction("Resume", self) self.actionResume.triggered.connect(lambda: self.perform_resume()) self.actionStepIntoAsm = QAction("Step Into (Assembly)", self) self.actionStepIntoAsm.triggered.connect( lambda: self.perform_step_into_asm()) self.actionStepIntoIL = QAction("Step Into", self) self.actionStepIntoIL.triggered.connect( lambda: self.perform_step_into_il()) self.actionStepOverAsm = QAction("Step Over (Assembly)", self) self.actionStepOverAsm.triggered.connect( lambda: self.perform_step_over_asm()) self.actionStepOverIL = QAction("Step Over", self) self.actionStepOverIL.triggered.connect( lambda: self.perform_step_over_il()) self.actionStepReturn = QAction("Step Return", self) self.actionStepReturn.triggered.connect( lambda: self.perform_step_return()) # session control menu self.controlMenu = QMenu("Process Control", self) self.controlMenu.addAction(self.actionRun) self.controlMenu.addAction(self.actionRestart) self.controlMenu.addAction(self.actionQuit) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionAttach) self.controlMenu.addAction(self.actionDetach) self.controlMenu.addSeparator() self.controlMenu.addAction(self.actionSettings) self.stepIntoMenu = QMenu("Step Into", self) self.stepIntoMenu.addAction(self.actionStepIntoIL) self.stepIntoMenu.addAction(self.actionStepIntoAsm) self.stepOverMenu = QMenu("Step Over", self) self.stepOverMenu.addAction(self.actionStepOverIL) self.stepOverMenu.addAction(self.actionStepOverAsm) self.btnControl = QToolButton(self) self.btnControl.setMenu(self.controlMenu) self.btnControl.setPopupMode(QToolButton.MenuButtonPopup) self.btnControl.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnControl.setDefaultAction(self.actionRun) self.addWidget(self.btnControl) # execution control buttons self.addAction(self.actionPause) self.addAction(self.actionResume) self.btnStepInto = QToolButton(self) self.btnStepInto.setMenu(self.stepIntoMenu) self.btnStepInto.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepInto.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepInto.setDefaultAction(self.actionStepIntoIL) self.addWidget(self.btnStepInto) self.btnStepOver = QToolButton(self) self.btnStepOver.setMenu(self.stepOverMenu) self.btnStepOver.setPopupMode(QToolButton.MenuButtonPopup) self.btnStepOver.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.btnStepOver.setDefaultAction(self.actionStepOverIL) self.addWidget(self.btnStepOver) # TODO: Step until returning from current function self.addAction(self.actionStepReturn) self.threadMenu = QMenu("Threads", self) self.btnThreads = QToolButton(self) self.btnThreads.setMenu(self.threadMenu) self.btnThreads.setPopupMode(QToolButton.InstantPopup) self.btnThreads.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addWidget(self.btnThreads) self.set_thread_list([]) self.editStatus = QLineEdit('INACTIVE', self) self.editStatus.setReadOnly(True) self.editStatus.setAlignment(QtCore.Qt.AlignCenter) self.addWidget(self.editStatus) # disable buttons self.set_actions_enabled(Run=self.can_exec(), Restart=False, Quit=False, Attach=self.can_connect(), Detach=False, Pause=False, Resume=False, StepInto=False, StepOver=False, StepReturn=False) self.set_resume_pause_action("Pause") self.set_default_process_action( "Attach" if self.can_connect() else "Run") def __del__(self): # TODO: Move this elsewhere # This widget is tasked with cleaning up the state after the view is closed # binjaplug.delete_state(self.bv) pass def can_exec(self): return DebugAdapter.ADAPTER_TYPE.use_exec( self.debug_state.adapter_type) def can_connect(self): return DebugAdapter.ADAPTER_TYPE.use_connect( self.debug_state.adapter_type) def perform_run(self): def perform_run_thread(): try: self.debug_state.run() execute_on_main_thread_and_wait(perform_run_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_run_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_run_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_run_error(e): self.state_error(e) self.state_starting('STARTING') threading.Thread(target=perform_run_thread).start() def perform_restart(self): def perform_restart_thread(): try: self.debug_state.restart() execute_on_main_thread_and_wait(perform_restart_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_restart_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait(lambda: perform_restart_error( 'ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_restart_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_restart_error(e): self.state_error(e) self.state_starting('RESTARTING') threading.Thread(target=perform_restart_thread).start() def perform_quit(self): self.debug_state.quit() self.state_inactive() self.debug_state.ui.on_step() def perform_attach(self): def perform_attach_thread(): try: self.debug_state.attach() execute_on_main_thread_and_wait(perform_attach_after) except ConnectionRefusedError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except TimeoutError: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: Connection Refused')) except Exception as e: execute_on_main_thread_and_wait( lambda: perform_attach_error('ERROR: ' + ' '.join(e.args))) traceback.print_exc(file=sys.stderr) def perform_attach_after(): self.state_stopped() self.debug_state.ui.on_step() def perform_attach_error(e): self.state_error(e) self.state_starting('ATTACHING') threading.Thread(target=perform_attach_thread).start() def perform_detach(self): self.debug_state.detach() self.state_inactive() self.debug_state.ui.on_step() def perform_settings(self): def settings_finished(): if self.debug_state.running: self.state_running() elif self.debug_state.connected: local_rip = self.debug_state.local_ip if self.debug_state.bv.read(local_rip, 1) and len( self.debug_state.bv.get_functions_containing( local_rip)) > 0: self.state_stopped() else: self.state_stopped_extern() else: self.state_inactive() dialog = AdapterSettingsDialog.AdapterSettingsDialog(self, self.bv) dialog.show() dialog.finished.connect(settings_finished) def perform_pause(self): self.debug_state.pause() # Don't update state here-- one of the other buttons is running in a thread and updating for us def perform_resume(self): def perform_resume_thread(): (reason, data) = self.debug_state.go() execute_on_main_thread_and_wait( lambda: perform_resume_after(reason, data)) def perform_resume_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_running() threading.Thread(target=perform_resume_thread).start() def perform_step_into_asm(self): def perform_step_into_asm_thread(): (reason, data) = self.debug_state.step_into() execute_on_main_thread_and_wait( lambda: perform_step_into_asm_after(reason, data)) def perform_step_into_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_asm_thread).start() def perform_step_into_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_into_il_thread(): (reason, data) = self.debug_state.step_into(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_into_il_after(reason, data)) def perform_step_into_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_into_il_thread).start() def perform_step_over_asm(self): def perform_step_over_asm_thread(): (reason, data) = self.debug_state.step_over() execute_on_main_thread_and_wait( lambda: perform_step_over_asm_after(reason, data)) def perform_step_over_asm_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_asm_thread).start() def perform_step_over_il(self): disasm = self.debug_state.ui.debug_view.binary_editor.getDisassembly() graph_type = disasm.getGraphType() def perform_step_over_il_thread(): (reason, data) = self.debug_state.step_over(graph_type) execute_on_main_thread_and_wait( lambda: perform_step_over_il_after(reason, data)) def perform_step_over_il_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_over_il_thread).start() def perform_step_return(self): def perform_step_return_thread(): (reason, data) = self.debug_state.step_return() execute_on_main_thread_and_wait( lambda: perform_step_return_after(reason, data)) def perform_step_return_after(reason, data): self.handle_stop_return(reason, data) self.debug_state.ui.on_step() self.state_busy("STEPPING") threading.Thread(target=perform_step_return_thread).start() def set_actions_enabled(self, **kwargs): def enable_step_into(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) def enable_step_over(e): self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) def enable_starting(e): self.actionRun.setEnabled(e and self.can_exec()) self.actionAttach.setEnabled(e and self.can_connect()) def enable_stopping(e): self.actionRestart.setEnabled(e) self.actionQuit.setEnabled(e) self.actionDetach.setEnabled(e) def enable_stepping(e): self.actionStepIntoAsm.setEnabled(e) self.actionStepIntoIL.setEnabled(e) self.actionStepOverAsm.setEnabled(e) self.actionStepOverIL.setEnabled(e) self.actionStepReturn.setEnabled(e) actions = { "Run": lambda e: self.actionRun.setEnabled(e), "Restart": lambda e: self.actionRestart.setEnabled(e), "Quit": lambda e: self.actionQuit.setEnabled(e), "Attach": lambda e: self.actionAttach.setEnabled(e), "Detach": lambda e: self.actionDetach.setEnabled(e), "Pause": lambda e: self.actionPause.setEnabled(e), "Resume": lambda e: self.actionResume.setEnabled(e), "StepInto": enable_step_into, "StepOver": enable_step_over, "StepReturn": lambda e: self.actionStepReturn.setEnabled(e), "Threads": lambda e: self.btnThreads.setEnabled(e), "Starting": enable_starting, "Stopping": enable_stopping, "Stepping": enable_stepping, } for (action, enabled) in kwargs.items(): actions[action](enabled) def set_default_process_action(self, action): actions = { "Run": self.actionRun, "Restart": self.actionRestart, "Quit": self.actionQuit, "Attach": self.actionAttach, "Detach": self.actionDetach, } self.btnControl.setDefaultAction(actions[action]) def set_resume_pause_action(self, action): self.actionResume.setVisible(action == "Resume") self.actionPause.setVisible(action == "Pause") def set_thread_list(self, threads): def select_thread_fn(tid): def select_thread(tid): stateObj = binjaplug.get_state(self.bv) if stateObj.state == 'STOPPED': adapter = stateObj.adapter adapter.thread_select(tid) self.debug_state.ui.context_display() else: print('cannot set thread in state %s' % stateObj.state) return lambda: select_thread(tid) self.threadMenu.clear() if len(threads) > 0: for thread in threads: item_name = "Thread {} at {}".format(thread['tid'], hex(thread['ip'])) action = self.threadMenu.addAction( item_name, select_thread_fn(thread['tid'])) if thread['selected']: self.btnThreads.setDefaultAction(action) else: defaultThreadAction = self.threadMenu.addAction("Thread List") defaultThreadAction.setEnabled(False) self.btnThreads.setDefaultAction(defaultThreadAction) def state_starting(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=False, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_inactive(self, msg=None): self.editStatus.setText(msg or 'INACTIVE') self.set_actions_enabled(Starting=True, Stopping=False, Stepping=False, Pause=False, Resume=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Pause") def state_stopped(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_stopped_extern(self, msg=None): self.editStatus.setText(msg or 'STOPPED') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=True, StepReturn=False, Pause=True, Resume=True, Threads=True) self.set_default_process_action("Quit") self.set_resume_pause_action("Resume") def state_running(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_busy(self, msg=None): self.editStatus.setText(msg or 'RUNNING') self.set_actions_enabled(Starting=False, Stopping=True, Stepping=False, Pause=True, Resume=False, Threads=False) self.set_default_process_action("Quit") self.set_resume_pause_action("Pause") def state_error(self, msg=None): self.editStatus.setText(msg or 'ERROR') self.set_actions_enabled(Starting=True, Stopping=False, Pause=False, Resume=False, Stepping=False, Threads=False) self.set_default_process_action( "Attach" if self.can_connect() else "Run") self.set_thread_list([]) self.set_resume_pause_action("Resume") def handle_stop_return(self, reason, data): if reason == DebugAdapter.STOP_REASON.STDOUT_MESSAGE: self.state_stopped('stdout: ' + data) elif reason == DebugAdapter.STOP_REASON.PROCESS_EXITED: self.debug_state.quit() self.state_inactive('process exited, return code=%d' % data) elif reason == DebugAdapter.STOP_REASON.BACKEND_DISCONNECTED: self.debug_state.quit() self.state_inactive('backend disconnected (process exited?)')
def __init__( self, document: Optional[vp.Document] = None, view_mode: ViewMode = ViewMode.PREVIEW, show_pen_up: bool = False, show_points: bool = False, parent=None, ): super().__init__(parent) self._settings = QSettings() self._settings.beginGroup("viewer") self.setWindowTitle("vpype viewer") self.setStyleSheet(""" QToolButton:pressed { background-color: rgba(0, 0, 0, 0.2); } """) self._viewer_widget = QtViewerWidget(parent=self) # setup toolbar self._toolbar = QToolBar() self._icon_size = QSize(32, 32) self._toolbar.setIconSize(self._icon_size) view_mode_grp = QActionGroup(self._toolbar) if _DEBUG_ENABLED: act = view_mode_grp.addAction("None") act.setCheckable(True) act.setChecked(view_mode == ViewMode.NONE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.NONE)) act = view_mode_grp.addAction("Outline Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE)) act = view_mode_grp.addAction("Outline Mode (Colorful)") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE_COLORFUL) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE_COLORFUL)) act = view_mode_grp.addAction("Preview Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.PREVIEW) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.PREVIEW)) self.set_view_mode(view_mode) # VIEW MODE # view modes view_mode_btn = QToolButton() view_mode_menu = QMenu(view_mode_btn) act = view_mode_menu.addAction("View Mode:") act.setEnabled(False) view_mode_menu.addActions(view_mode_grp.actions()) view_mode_menu.addSeparator() # show pen up act = view_mode_menu.addAction("Show Pen-Up Trajectories") act.setCheckable(True) act.setChecked(show_pen_up) act.toggled.connect(self.set_show_pen_up) self._viewer_widget.engine.show_pen_up = show_pen_up # show points act = view_mode_menu.addAction("Show Points") act.setCheckable(True) act.setChecked(show_points) act.toggled.connect(self.set_show_points) self._viewer_widget.engine.show_points = show_points # preview mode options view_mode_menu.addSeparator() act = view_mode_menu.addAction("Preview Mode Options:") act.setEnabled(False) # pen width pen_width_menu = view_mode_menu.addMenu("Pen Width") act_grp = PenWidthActionGroup(0.3, parent=pen_width_menu) act_grp.triggered.connect(self.set_pen_width_mm) pen_width_menu.addActions(act_grp.actions()) self.set_pen_width_mm(0.3) # pen opacity pen_opacity_menu = view_mode_menu.addMenu("Pen Opacity") act_grp = PenOpacityActionGroup(0.8, parent=pen_opacity_menu) act_grp.triggered.connect(self.set_pen_opacity) pen_opacity_menu.addActions(act_grp.actions()) self.set_pen_opacity(0.8) # debug view if _DEBUG_ENABLED: act = view_mode_menu.addAction("Debug View") act.setCheckable(True) act.toggled.connect(self.set_debug) # rulers view_mode_menu.addSeparator() act = view_mode_menu.addAction("Show Rulers") act.setCheckable(True) val = bool(self._settings.value("show_rulers", True)) act.setChecked(val) act.toggled.connect(self.set_show_rulers) self._viewer_widget.engine.show_rulers = val # units units_menu = view_mode_menu.addMenu("Units") unit_action_grp = QActionGroup(units_menu) unit_type = UnitType(self._settings.value("unit_type", UnitType.METRIC)) act = unit_action_grp.addAction("Metric") act.setCheckable(True) act.setChecked(unit_type == UnitType.METRIC) act.setData(UnitType.METRIC) act = unit_action_grp.addAction("Imperial") act.setCheckable(True) act.setChecked(unit_type == UnitType.IMPERIAL) act.setData(UnitType.IMPERIAL) act = unit_action_grp.addAction("Pixel") act.setCheckable(True) act.setChecked(unit_type == UnitType.PIXELS) act.setData(UnitType.PIXELS) unit_action_grp.triggered.connect(self.set_unit_type) units_menu.addActions(unit_action_grp.actions()) self._viewer_widget.engine.unit_type = unit_type view_mode_btn.setMenu(view_mode_menu) view_mode_btn.setIcon(load_icon("eye-outline.svg")) view_mode_btn.setText("View") view_mode_btn.setPopupMode(QToolButton.InstantPopup) view_mode_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(view_mode_btn) # LAYER VISIBILITY self._layer_visibility_btn = QToolButton() self._layer_visibility_btn.setIcon( load_icon("layers-triple-outline.svg")) self._layer_visibility_btn.setText("Layer") self._layer_visibility_btn.setMenu(QMenu(self._layer_visibility_btn)) self._layer_visibility_btn.setPopupMode(QToolButton.InstantPopup) self._layer_visibility_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(self._layer_visibility_btn) # FIT TO PAGE fit_act = self._toolbar.addAction(load_icon("fit-to-page-outline.svg"), "Fit") fit_act.triggered.connect(self._viewer_widget.engine.fit_to_viewport) # RULER # TODO: not implemented yet # self._toolbar.addAction(load_icon("ruler-square.svg"), "Units") # MOUSE COORDINATES> self._mouse_coord_lbl = QLabel("") self._mouse_coord_lbl.setMargin(6) self._mouse_coord_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self._mouse_coord_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self._toolbar.addWidget(self._mouse_coord_lbl) # noinspection PyUnresolvedReferences self._viewer_widget.mouse_coords.connect( self.set_mouse_coords) # type: ignore # setup horizontal layout for optional side widgets self._hlayout = QHBoxLayout() self._hlayout.setSpacing(0) self._hlayout.setMargin(0) self._hlayout.addWidget(self._viewer_widget) widget = QWidget() widget.setLayout(self._hlayout) # setup global vertical layout layout = QVBoxLayout() layout.setSpacing(0) layout.setMargin(0) layout.addWidget(self._toolbar) layout.addWidget(widget) self.setLayout(layout) if document is not None: self.set_document(document)
def __init__(self): super(SideBar, self).__init__() # read StyleSheet with open('QSS/sideBar.qss', 'r') as f: self.setStyleSheet(f.read()) f.close() # Vertical Layout self.verticalLayout = QVBoxLayout() self.verticalLayout.setContentsMargins(0, 0, 0, 0) # Icons camera_icon = ico.Icon('font-awesome:solid:camera-retro', QColor('#E91E63')) database_icon = ico.Icon('font-awesome:solid:database', QColor('#03A9F4')) about_icon = ico.Icon('font-awesome:solid:exclamation-triangle', QColor('#FDD835')) # logo image logo = QLabel() logo.setFixedSize(110, 110) logo_img = QPixmap('resources/qt.png') logo.setPixmap(logo_img) logo.setContentsMargins(20, 20, 20, 20) # bottom label like spacer bottom_label = QLabel() # Create Buttons btn_width = 110 btn_height = 40 self.Camera = QToolButton() self.Camera.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.Camera.setObjectName('Camera_btn') self.Camera.setText(' Camera') self.Camera.setFixedSize(btn_width, btn_height) self.Camera.setIcon(camera_icon) self.Camera.setIconSize(QSize(20, 20)) self.DataBase = QToolButton() self.DataBase.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.DataBase.setObjectName('customers_btn') self.DataBase.setText(' DataBase') self.DataBase.setFixedSize(btn_width, btn_height) self.DataBase.setIcon(database_icon) self.DataBase.setIconSize(QSize(20, 20)) self.About = QToolButton() self.About.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.About.setObjectName('About_btn') self.About.setText(' About') self.About.setFixedSize(btn_width, btn_height) self.About.setIcon(about_icon) self.About.setIconSize(QSize(20, 20)) # add widget self.verticalLayout.setSpacing(0) self.verticalLayout.addWidget(logo) self.verticalLayout.addWidget(self.Camera) self.verticalLayout.addWidget(self.DataBase) self.verticalLayout.addWidget(self.About) self.verticalLayout.addWidget(bottom_label) self.setLayout(self.verticalLayout)
def _create_ammo_pickup_boxes(self, size_policy, item_database: ItemDatabase): """ Creates the GroupBox with SpinBoxes for selecting the pickup count of all the ammo :param item_database: :return: """ self._ammo_maximum_spinboxes = collections.defaultdict(list) self._ammo_pickup_widgets = {} resource_database = default_prime2_resource_database() for ammo in item_database.ammo.values(): title_layout = QHBoxLayout() title_layout.setObjectName(f"{ammo.name} Title Horizontal Layout") expand_ammo_button = QToolButton(self.ammo_box) expand_ammo_button.setGeometry(QRect(20, 30, 24, 21)) expand_ammo_button.setText("+") title_layout.addWidget(expand_ammo_button) category_label = QLabel(self.ammo_box) category_label.setSizePolicy(size_policy) category_label.setText(ammo.name + "s") title_layout.addWidget(category_label) pickup_box = QGroupBox(self.ammo_box) pickup_box.setSizePolicy(size_policy) layout = QGridLayout(pickup_box) layout.setObjectName(f"{ammo.name} Box Layout") current_row = 0 for ammo_item in ammo.items: item = resource_database.get_by_type_and_index(ResourceType.ITEM, ammo_item) target_count_label = QLabel(pickup_box) target_count_label.setText(f"{item.long_name} Target" if len(ammo.items) > 1 else "Target count") maximum_spinbox = QSpinBox(pickup_box) maximum_spinbox.setMaximum(ammo.maximum) maximum_spinbox.valueChanged.connect(partial(self._on_update_ammo_maximum_spinbox, ammo_item)) self._ammo_maximum_spinboxes[ammo_item].append(maximum_spinbox) layout.addWidget(target_count_label, current_row, 0) layout.addWidget(maximum_spinbox, current_row, 1) current_row += 1 count_label = QLabel(pickup_box) count_label.setText("Pickup Count") count_label.setToolTip("How many instances of this expansion should be placed.") pickup_spinbox = QSpinBox(pickup_box) pickup_spinbox.setMaximum(AmmoState.maximum_pickup_count()) pickup_spinbox.valueChanged.connect(partial(self._on_update_ammo_pickup_spinbox, ammo)) layout.addWidget(count_label, current_row, 0) layout.addWidget(pickup_spinbox, current_row, 1) current_row += 1 if ammo.temporaries: require_major_item_check = QCheckBox(pickup_box) require_major_item_check.setText("Requires the major item to work?") require_major_item_check.stateChanged.connect(partial(self._on_update_ammo_require_major_item, ammo)) layout.addWidget(require_major_item_check, current_row, 0, 1, 2) current_row += 1 else: require_major_item_check = None expected_count = QLabel(pickup_box) expected_count.setWordWrap(True) expected_count.setText(_EXPECTED_COUNT_TEXT_TEMPLATE) expected_count.setToolTip("Some expansions may provide 1 extra, even with no variance, if the total count " "is not divisible by the pickup count.") layout.addWidget(expected_count, current_row, 0, 1, 2) current_row += 1 self._ammo_pickup_widgets[ammo] = (pickup_spinbox, expected_count, expand_ammo_button, category_label, pickup_box, require_major_item_check) expand_ammo_button.clicked.connect(partial(_toggle_box_visibility, expand_ammo_button, pickup_box)) pickup_box.setVisible(False) self.ammo_layout.addLayout(title_layout) self.ammo_layout.addWidget(pickup_box)
class GeneratedCurvePage(DetailsPageBase): def __init__(self, parent=None): super().__init__(parent, 'synthetic curve') self.gen_curve: Optional[WdGeneratedValues] = None self.segments_edit_semaphore = Lock() layout = QVBoxLayout() group_segments = QGroupBox('Segments') layout_segments = QVBoxLayout() layout_segments_hdr = QHBoxLayout() layout_segments_hdr_labels = QVBoxLayout() layout_segments_hdr_labels.addWidget( QLabel('Segments are calculated in parallel')) layout_segments_hdr_labels.addWidget( QLabel('and may have different density (PHIN)')) layout_segments_hdr.addLayout(layout_segments_hdr_labels) layout_segments_hdr.addStretch() self.add_segment_button = QToolButton() self.add_segment_button.setText('+') self.del_segment_button = QToolButton() self.del_segment_button.setText('-') layout_segments_hdr.addWidget(self.add_segment_button) layout_segments_hdr.addWidget(self.del_segment_button) layout_segments.addLayout(layout_segments_hdr) self.table_segments = QTableWidget(1, 3) self.table_segments.setHorizontalHeaderLabels( ['phase from', 'phase to', 'PHIN phase incr']) layout_segments.addWidget(self.table_segments) group_segments.setLayout(layout_segments) layout.addWidget(group_segments) self.setLayout(layout) self.table_segments.itemActivated.connect(self._on_segment_activated) self.table_segments.itemChanged.connect(self._on_segment_item_changed) self.add_segment_button.clicked.connect(self._on_segment_add_clicked) self.del_segment_button.clicked.connect(self._on_segment_del_clicked) def is_active_for_item( self, item: Optional[Container]) -> (bool, Optional[Container]): """Returns pair: should_be_active and item (may be e.g. parent of provided item)""" try: if isinstance(item, CurveGeneratedContainer): return True, item elif isinstance(item.parent(), CurveGeneratedContainer): return True, item.parent() else: return False, None except AttributeError: return False, None @Slot(QTableWidgetItem) def _on_segment_activated(self, item: QTableWidgetItem): self.del_segment_button.setEnabled(self.table_segments.rowCount() > 1) self.add_segment_button.setEnabled( self.table_segments.rowCount() <= 20) @Slot(QTableWidgetItem) def _on_segment_item_changed(self, item: QTableWidgetItem): """when user enters new value to segments table""" if self.segments_edit_semaphore.locked( ): # item is being updated from code rather than by user return val = float(item.text()) logger().info(f'Segment value changed: {val}') if item.column() == 0: self.gen_curve.segment_set_range(segment=item.row(), from_value=val) elif item.column() == 1: self.gen_curve.segment_set_range(segment=item.row(), to_value=val) elif item.column() == 2: self.gen_curve.segment_update_data(segment=item.row(), data={'PHIN': val}) self.populate_segments() @Slot(bool) def _on_segment_add_clicked(self, selected: bool): """on delete segment button""" row = self.table_segments.currentRow() if row is None: return self.gen_curve.segment_split(row) self.populate_segments() @Slot(bool) def _on_segment_del_clicked(self, selected: bool): """on add new segment button""" row = self.table_segments.currentRow() if self.table_segments.rowCount() < 2 or row is None: return self.gen_curve.segment_delete(row) self.populate_segments() def item_changed(self, previous_item: Container): """when curved item for which details are displayed hve been changed""" super().item_changed(previous_item) if self.enabled: try: if isinstance(self.item.content, WdGeneratedValues): self.gen_curve = self.item.content else: self.gen_curve = None except AttributeError: self.gen_curve = None self.populate_segments() # try: # df = self.item.get_df() # except AttributeError: # df = None # self.pandas.setDataFrame(df) def populate_segments(self): if self.gen_curve is None: self.table_segments.setRowCount(0) return self.table_segments.setRowCount(self.gen_curve.segments_count()) for n in range(self.gen_curve.segments_count()): start, stop = self.gen_curve.segment_range(n) phin = self.gen_curve.segment_get_data(n, 'PHIN') self.set_segment_text(n, 0, start) self.set_segment_text(n, 1, stop) self.set_segment_text(n, 2, phin) self.table_segments.resizeRowsToContents() def set_segment_text(self, row, col, txt): with self.segments_edit_semaphore: item = self.table_segments.item(row, col) if item is None: item = QTableWidgetItem() self.table_segments.setItem(row, col, item) item.setText(str(txt))