def set_combo_with_value(combo: QComboBox, value): """ Searches all items of the given combo for the given value and changes the current index to that one. :param combo: :param value: :return: """ combo.setCurrentIndex(combo.findData(value))
def _create_from_until_combo(self, row: RowWidgets, cur_serial: int, on_func: Callable) -> QComboBox: combo = QComboBox(row.parent) combo.setMinimumWidth(100) combo.addItem('', 0) sorted_date_serials = sorted(x.serial for x in self._contacts_model.iter_dates()) for date_serial in sorted_date_serials: date = self._contacts_model.get_date(date_serial) item_text = self._create_vague_date_combo_text(date) combo.addItem(item_text, date.serial) cur_index = combo.findData(cur_serial) combo.setCurrentIndex(cur_index) combo.currentIndexChanged.connect(on_func) combo.row = row return combo
def _create_val_combo(self, row: RowWidgets) -> QComboBox: combo = QComboBox(row.parent) # completer = QCompleter(word_list, self) # completer.setCaseSensitivity(Qt.CaseInsensitive) # combo.setCompleter(completer) # combo.setEditable(True) ref_map = self._create_ref_map(row.attr) combo.addItem('', 0) current_data = 0 for title in sorted(ref_map.keys()): contact = ref_map[title] if row.fact is not None and contact.serial == int(row.fact.value): current_data = contact.serial combo.addItem(title, contact.serial) cur_index = combo.findData(current_data) combo.setCurrentIndex(cur_index) combo.currentIndexChanged.connect(self.on_val_combo_index_changed) combo.row = row return combo
class ConfigWindow(QDialog): # noinspection SpellCheckingInspection LOCALES = { 'en': 'English', 'nl': 'Nederlands' } def __init__(self, gui_window: mainWindow.MainWindow, device_manager: logic.DeviceManager): QDialog.__init__(self, gui_window) self.setModal(True) self.setWindowTitle(QCoreApplication.translate('configs', 'Configs', 'title')) self._device_manager = device_manager layout = QGridLayout() layout.addWidget(QLabel(QCoreApplication.translate('configs', 'Open on startup'), self), 0, 0) self._open_on_startup = QCheckBox(self) self._open_on_startup.setChecked(device_manager.open_on_start) self._open_on_startup.stateChanged.connect(self._open_on_startup_changed) layout.addWidget(self._open_on_startup, 0, 1) layout.addWidget(QLabel(QCoreApplication.translate('configs', 'Locale'), self), 1, 0) self._locale_select = QComboBox(self) self._locale_select.setToolTip(QCoreApplication.translate('configs', 'Restart required')) for locale_key, locale_value in ConfigWindow.LOCALES.items(): self._locale_select.addItem(locale_value, userData=locale_key) self._locale_select.setCurrentIndex(self._locale_select.findData(device_manager.locale)) self._locale_select.currentIndexChanged.connect(self._locale_changed) layout.addWidget(self._locale_select, 1, 1) self.setLayout(layout) def _open_on_startup_changed(self, state): self._device_manager.open_on_start = state == Qt.Checked def _locale_changed(self, index): self._device_manager.locale = self._locale_select.itemData(index) def closeEvent(self, event): event.ignore() self.hide()
class RequirementEditor: _editor: Union[None, ResourceRequirementEditor, ArrayRequirementEditor, TemplateRequirementEditor] def __init__(self, parent: QWidget, parent_layout: QVBoxLayout, resource_database: ResourceDatabase, *, on_remove=None): self.parent = parent self.parent_layout = parent_layout self.resource_database = resource_database self._editor = None self._last_resource = None self._last_items = () self.line_layout = QHBoxLayout() self.line_layout.setAlignment(Qt.AlignLeft) self.parent_layout.addLayout(self.line_layout) if on_remove is not None: self.remove_button = QtWidgets.QToolButton(parent) self.remove_button.setText("X") self.remove_button.setMaximumWidth(20) self.remove_button.clicked.connect(on_remove) self.line_layout.addWidget(self.remove_button) else: self.remove_button = None self.requirement_type_combo = QComboBox(parent) self.requirement_type_combo.addItem("Resource", ResourceRequirement) self.requirement_type_combo.addItem("Or", RequirementOr) self.requirement_type_combo.addItem("And", RequirementAnd) if resource_database.requirement_template: self.requirement_type_combo.addItem("Template", RequirementTemplate) self.requirement_type_combo.setMaximumWidth(75) self.requirement_type_combo.activated.connect( self._on_change_requirement_type) self.line_layout.addWidget(self.requirement_type_combo) def create_specialized_editor(self, requirement: Requirement): self.requirement_type_combo.setCurrentIndex( self.requirement_type_combo.findData(type(requirement))) if isinstance(requirement, ResourceRequirement): self._editor = ResourceRequirementEditor(self.parent, self.line_layout, self.resource_database, requirement) elif isinstance(requirement, (RequirementOr, RequirementAnd)): self._editor = ArrayRequirementEditor(self.parent, self.parent_layout, self.resource_database, requirement) elif isinstance(requirement, RequirementTemplate): self._editor = TemplateRequirementEditor(self.parent, self.line_layout, self.resource_database, requirement) else: raise RuntimeError( f"Unknown requirement type: {type(requirement)} - {requirement}" ) def _on_change_requirement_type(self): current_requirement = self.current_requirement self._editor.deleteLater() if isinstance(current_requirement, ResourceRequirement): self._last_resource = current_requirement elif isinstance(current_requirement, (RequirementOr, RequirementAnd)): self._last_items = current_requirement.items elif isinstance(current_requirement, RequirementTemplate): pass else: raise RuntimeError( f"Unknown requirement type: {type(current_requirement)} - {current_requirement}" ) new_class = self.requirement_type_combo.currentData() if new_class == ResourceRequirement: if self._last_resource is None: new_requirement = _create_default_resource_requirement( self.resource_database) else: new_requirement = self._last_resource elif new_class == RequirementTemplate: new_requirement = _create_default_template_requirement( self.resource_database) else: new_requirement = new_class(self._last_items) self.create_specialized_editor(new_requirement) def deleteLater(self): if self.remove_button is not None: self.remove_button.deleteLater() self.requirement_type_combo.deleteLater() if self._editor is not None: self._editor.deleteLater() @property def current_requirement(self) -> Requirement: return self._editor.current_requirement
class SimpleSelector(object): # Constants LIST_BOX_HEIGHT = utils.dpiScale(100) EXPRESSION_BUTTON_WIDTH = utils.dpiScale(50) INVERSE_STRING = maya.stringTable['y_simpleSelector.kInverse'] SELECT_STRING = maya.stringTable['y_simpleSelector.kSelect'] CREATE_EXPRESSION_STRING = maya.stringTable[ 'y_simpleSelector.kCreateExpression'] DRAG_DROP_FILTER_STRING = maya.stringTable[ 'y_simpleSelector.kDragDropFilter'] # Corresponding data model type. kModelType = selector.SimpleSelector.kTypeName def __init__(self, selector): self.expression = "" self._blockChangeMessages = False self._selector = selector self.filterType = None self.addToCollectionGroupBoxLayout = None self.includeExpressionWidgets = None self.includeExpression = None self.customFilterEdit = None self.filtersGroupBoxLayout = None self.staticSelector = None def build(self, layout, populate=True): self._setupFilterGroupBox(layout) self._setupAddToCollectionGroupBox(layout) if populate: self.populateFields() def displayType(self): """Return the user-visible display type string. By default this is the same for all objects of a selector class.""" return self.kDisplayType def _setupExpression(self, expressionLabel, expressionChangedCB, expressionFinishedCB): expressionWidget = QWidget() expressionLayout = QHBoxLayout() expressionLayout.setContentsMargins(0, 0, 0, 0) expressionLayout.setSpacing(utils.dpiScale(2)) expressionWidget.setLayout(expressionLayout) expression = QLineEdit() tip = maya.stringTable['y_simpleSelector.kExpressionTooltipStr'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr1'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr2'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr3'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr4'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr5'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr6'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr7'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr8'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr9'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr10'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr11'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr12'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr13'] tip += maya.stringTable['y_simpleSelector.kExpressionTooltipStr14'] expression.setToolTip(tip) expression.textChanged.connect(expressionChangedCB) # editingFinished is triggered for both enter key pressed and focused lost. expression.editingFinished.connect(expressionFinishedCB) expression.returnPressed.connect(lambda: expression.clearFocus()) expression.setPlaceholderText(self.CREATE_EXPRESSION_STRING) expressionLayout.addWidget(expression) selectButton = QPushButton(self.SELECT_STRING) selectButton.setToolTip( maya.stringTable['y_simpleSelector.kExpressionSelectTooltipStr']) selectButton.setMinimumWidth(self.EXPRESSION_BUTTON_WIDTH) selectButton.clicked.connect(self.selectIncludeExpression) expressionLayout.addWidget(selectButton) self.addToCollectionGroupBoxLayout.addRow(expressionLabel, expressionWidget) return expressionWidget, expression def _setupIncludeExpression(self): includeExpressionLabel = IncludeExpressionLabel( maya.stringTable['y_simpleSelector.kInclude']) includeExpressionWidget, self.includeExpression = \ self._setupExpression(includeExpressionLabel, self.includeExpressionChanged, self.includeExpressionEntered) self.includeExpressionWidgets = [includeExpressionWidget] self.expression = self._selector.getPattern() @abstractmethod def _getFilterEnum(self): pass def _setupFilterUI(self, layout): filterUIWidget = QWidget() filterUILayout = QHBoxLayout() filterUILayout.setContentsMargins(0, 0, 0, 0) filterUILayout.setSpacing(utils.dpiScale(2)) filterUIWidget.setLayout(filterUILayout) #setup and add the filter combo box. self.filterType = QComboBox() self.filterType.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch( 10) # stronger than the spacer added below self.filterType.setSizePolicy(sizePolicy) self.filterType.setMinimumContentsLength(0) self.filterType.setMaximumWidth(200) self.filterType.setToolTip( maya.stringTable['y_simpleSelector.kFiltersToolTip']) for ftype in self._selector.getAvailableFilters(): self.filterType.addItem( QIcon(RenderSetupDelegate.getFilterIcon(ftype)), selector.Filters.filterUIName(ftype), ftype) # get the current filter value and set the index of the combo box appropriately. filter = self._selector.getFilterType() self.filterType.currentIndexChanged[str].connect( self._filterTypeChanged) filterUILayout.addWidget(self.filterType) filterUILayout.addStretch() filterUILayout.addSpacing(4) self.customFilterEdit = CollectionFilterLineEdit() self.customFilterEdit.returnPressed.connect( lambda: self.customFilterEdit.clearFocus()) self.customFilterEdit.setPlaceholderText(self.DRAG_DROP_FILTER_STRING) self.customFilterEdit.editingFinished.connect(self.customFilterEntered) showCustomFilter = (filter == selector.Filters.kCustom) layout.addRow("", filterUIWidget) layout.addRow(maya.stringTable['y_simpleSelector.kByTypeFilter'], self.customFilterEdit) @ifNotBlockChangeMessages def _filterTypeChanged(self, value): filterNameMap = { selector.Filters.filterUIName(ftype): ftype for ftype in self._selector.getAvailableFilters() } self._selector.setFilterType(filterNameMap[value]) def _setupStaticSelector(self): staticSelectionWidget = QWidget() staticSelectionLayout = QHBoxLayout() staticSelectionLayout.setContentsMargins(0, 0, 0, 0) staticSelectionLayout.setSpacing(utils.dpiScale(2)) self.staticSelector = CollectionStaticSelectionWidget(self._selector) self.staticSelector.setFixedHeight(self.LIST_BOX_HEIGHT) staticSelectionLayout.addWidget(self.staticSelector) # Add the drag/drop buttons dragDropButtonLayout = QVBoxLayout() dragDropButtonLayout.setSpacing(utils.dpiScale(2)) dragDropButtonLayout.setContentsMargins(0, 0, 0, 0) addButton = QPushButton(maya.stringTable['y_simpleSelector.kAdd']) addButton.setToolTip( maya.stringTable['y_simpleSelector.kStaticAddTooltipStr']) addButton.setMinimumWidth(self.EXPRESSION_BUTTON_WIDTH) addButton.clicked.connect(self.staticSelector.addEntry) dragDropButtonLayout.addWidget(addButton) removeButton = QPushButton( maya.stringTable['y_simpleSelector.kRemove']) removeButton.setToolTip( maya.stringTable['y_simpleSelector.kStaticRemoveTooltipStr']) removeButton.setMinimumWidth(self.EXPRESSION_BUTTON_WIDTH) removeButton.clicked.connect(self.staticSelector.removeEntry) dragDropButtonLayout.addWidget(removeButton) dragDropButtonLayout.addStretch(1) selectButton = QPushButton(self.SELECT_STRING) selectButton.setToolTip( maya.stringTable['y_simpleSelector.kStaticSelectTooltipStr']) selectButton.setMinimumWidth(self.EXPRESSION_BUTTON_WIDTH) selectButton.clicked.connect(self.selectStaticEntries) dragDropButtonLayout.addWidget(selectButton) dragDropButtonWidget = QWidget() staticSelectionLayout.addWidget(dragDropButtonWidget) dragDropButtonWidget.setLayout(dragDropButtonLayout) staticSelectionWidget.setLayout(staticSelectionLayout) self.addToCollectionGroupBoxLayout.addRow("", staticSelectionWidget) def _setupSelectAllButton(self): selectAllButtonWidget = QWidget() selectAllButtonLayout = QHBoxLayout() selectAllButtonLayout.setContentsMargins(0, 0, 0, 0) selectAllButtonLayout.setSpacing(utils.dpiScale(2)) selectAllButton = QPushButton( maya.stringTable['y_simpleSelector.kSelectAll']) selectAllButtonLayout.addStretch(1) selectAllButtonLayout.addWidget(selectAllButton) selectAllButtonLayout.addStretch(1) selectAllButtonLayout.addSpacing(self.EXPRESSION_BUTTON_WIDTH) selectAllButtonWidget.setLayout(selectAllButtonLayout) self.addToCollectionGroupBoxLayout.addRow("", selectAllButtonWidget) def _setupFilterGroupBox(self, layout): filterGroupBox = QGroupBox( maya.stringTable['y_simpleSelector.kCollectionFilters']) font = QFont() font.setBold(True) filterGroupBox.setFont(font) filterGroupBox.setContentsMargins(0, utils.dpiScale(12), 0, 0) self.filtersGroupBoxLayout = Layout() self.filtersGroupBoxLayout.setVerticalSpacing(utils.dpiScale(2)) self._setupFilterUI(self.filtersGroupBoxLayout) filterGroupBox.setLayout(self.filtersGroupBoxLayout) layout.addWidget(filterGroupBox) def _setupAddToCollectionGroupBox(self, layout): addToCollectionGroupBox = QGroupBox( maya.stringTable['y_simpleSelector.kAddToCollection']) font = QFont() font.setBold(True) addToCollectionGroupBox.setFont(font) addToCollectionGroupBox.setContentsMargins(0, utils.dpiScale(12), 0, 0) self.addToCollectionGroupBoxLayout = Layout() self.addToCollectionGroupBoxLayout.setVerticalSpacing( utils.dpiScale(2)) self._setupIncludeExpression() self._setupStaticSelector() addToCollectionGroupBox.setLayout(self.addToCollectionGroupBoxLayout) layout.addWidget(addToCollectionGroupBox) def includeExpressionChanged(self, text): self.expression = text @ifNotBlockChangeMessages def includeExpressionEntered(self): self._selector.setPattern(self.expression) @ifNotBlockChangeMessages def customFilterEntered(self): customFilterText = self.customFilterEdit.text() self._selector.setCustomFilterValue(customFilterText) @guard.member('_blockChangeMessages', True) def populateFields(self): filter = self._selector.getFilterType() filterIndex = self.filterType.findData(filter) self.filterType.setCurrentIndex(filterIndex) showCustomFilter = (filter == selector.Filters.kCustom) customFilterText = self._selector.getCustomFilterValue() self.customFilterEdit.setText(customFilterText) self.customFilterEdit.setVisible(showCustomFilter) self.filtersGroupBoxLayout.labelForField( self.customFilterEdit).setVisible(showCustomFilter) expressionText = self._selector.getPattern() self.includeExpression.setText(expressionText) self.staticSelector.populate() def selectIncludeExpression(self): cmds.select(list(self._selector.getDynamicNames()), add=False, noExpand=True) def selectStaticEntries(self): self.staticSelector.selectMembers() cmds.select(list(self._selector.getStaticNames()), add=False, noExpand=True) def highlight(self, names): dynamicNames = self._selector.getDynamicNames() if next((name for name in names if name in dynamicNames), None) is not None: self.includeExpression.setSelection( 0, len(self.includeExpression.text())) staticNames = self._selector.getStaticNames() self.staticSelector.highlight( set(name for name in names if name in staticNames))
class QLayerView(QTableView): """ Display the stack of image layers. """ def __init__(self, parent): super().__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed slot def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return useThumb = (state == Qt.Checked) if useThumb == self.img.useThumb: return self.img.useThumb = useThumb window.updateStatus() self.img.cacheInvalidate() try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor already called by applytostack QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() finally: QApplication.restoreOverrideCursor() QApplication.processEvents() self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider self.maskLabel = QLabel('Mask Color') maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # masks are disbled by default self.maskLabel.setEnabled(False) self.maskSlider.setEnabled(False) self.maskValue.setEnabled(False) # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value() * 255.0 / 100.0) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion), # Type of previous modes is QPainter.CompositionMode (Shiboken enum-type). # Next additional modes are not implemented by QPainter: ('Luminosity', -1), ('color', -2) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item changed slot def g(ind): layer = self.img.getActiveLayer() s = self.blendingModeCombo.currentText() newMode = self.compositionModeDict[str(s)] if newMode == layer.compositionMode: return layer.compositionMode = newMode layer.applyToStack() self.img.onImageChanged() self.blendingModeCombo.currentIndexChanged.connect(g) # layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(self.maskLabel) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it to in blue.py, after loading the main form. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b><br> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Other colors correspond to partially masked pixels.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis def closeAdjustForms(self, delete=False): """ Close all windows associated with image layers. If delete is True (default False), the windows and their dock containers are deleted. @param delete: @type delete: boolean """ if self.img is None: return stack = self.img.layersStack for layer in stack: layer.closeView(delete=delete) if delete: self.currentWin = None gc.collect() def clear(self, delete=True): """ Reset LayerView and clear the back links to image. """ self.closeAdjustForms(delete=delete) self.img = None self.currentWin = None # model = layerModel() # model.setColumnCount(3) # TODO removed 21/01/20 validate self.setModel(None) def setLayers(self, mImg, delete=False): """ Displays the layer stack of a mImage instance. @param mImg: image @type mImg: mImage @param delete: @type delete: """ # close open adjustment windows # self.closeAdjustForms() self.clear(delete=delete) mImg.layerView = self # back link to image self.img = weakProxy(mImg) model = layerModel() model.setColumnCount(3) l = len(mImg.layersStack) # dataChanged event handler : enables edition of layer name def f(index1, index2): # index1 and index2 should be equal # only layer name should be editable # dropEvent emit dataChanged when setting item values. f must # return immediately from these calls, as they are possibly made with unconsistent data : # dragged rows are already removed from layersStack # and not yet removed from model. if l != self.model().rowCount(): return # only name is editable if index1.column() != 1: return row = index1.row() stackIndex = l - row - 1 mImg.layersStack[stackIndex].name = index1.data() model.dataChanged.connect(f) for r, lay in enumerate(reversed(mImg.layersStack)): try: lay.maskSettingsChanged.sig.disconnect() except RuntimeError: pass lay.maskSettingsChanged.sig.connect(self.updateRows) items = [] # col 0 : visibility icon if lay.visible: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon.png"), "") else: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon-strike.png"), "") items.append(item_visible) # col 1 : image icon (for non-adjustment layer only) and name if len(lay.name) <= 30: name = lay.name else: name = lay.name[:28] + '...' if hasattr(lay, 'inputImg'): item_name = QStandardItem(name) else: # icon with very small dim causes QPainter error # QPixmap.fromImage bug ? smallImg = lay.resized(50, 50) w, h = smallImg.width(), smallImg.height() if w < h / 5 or h < w / 5: item_name = QStandardItem(name) else: item_name = QStandardItem( QIcon(QPixmap.fromImage(smallImg)), name) # set tool tip to full name item_name.setToolTip(lay.name) items.append(item_name) item_mask = QStandardItem('m') items.append(item_mask) model.appendRow(items) self.setModel(model) self.horizontalHeader().hide() self.verticalHeader().hide() header = self.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # lay out the graphic forms into right pane forms = [ item.view for item in mImg.layersStack if getattr(item, 'view', None) is not None ] for dock in forms: if TABBING: dockedForms = [item for item in forms if not item.isFloating()] dock.show() # needed to get working tabbing if dock not in dockedForms and dock.tabbed: if dockedForms: window.tabifyDockWidget(dockedForms[-1], dock) else: window.addDockWidget(Qt.RightDockWidgetArea, dock) dock.setFloating(False) # select active layer self.selectRow(len(mImg.layersStack) - 1 - mImg.activeLayerIndex) activeLayer = mImg.getActiveLayer() layerview = activeLayer.view if layerview is not None: layerview.show() if TABBING: layerview.raise_() # lay out subcontrols of activeLayer form = layerview.widget() for dk in form.subControls: dk.setVisible(form.options[dk.widget().optionName]) # clean up: we (re)dock all sucontrols dk.setFloating(False) # emit topLevelChanged signal self.opacitySlider.setSliderPosition(int(activeLayer.opacity * 100)) self.maskSlider.setSliderPosition( int(activeLayer.colorMaskOpacity * 100.0 / 255.0)) ind = self.blendingModeCombo.findData(activeLayer.compositionMode) self.blendingModeCombo.setCurrentIndex(ind) self.previewOptionBox.setChecked(activeLayer.parentImage.useThumb) #activeLayer.maskColor self.updateForm() """ # TODO removed 25/01/20 useless validate for item in self.img.layersStack: if hasattr(item, 'sourceIndex'): combo = item.getGraphicsForm().sourceCombo currentText = combo.currentText() combo.clear() for i, x in enumerate(self.img.layersStack): item.view.widget().sourceCombo.addItem(x.name, i) combo.setCurrentIndex(combo.findText(currentText)) """ def updateForm(self): activeLayer = self.img.getActiveLayer() if hasattr(activeLayer, 'view'): self.currentWin = activeLayer.view if self.currentWin is not None: self.currentWin.show() self.currentWin.activateWindow() def updateRow(self, row): minInd, maxInd = self.model().index(row, 0), self.model().index(row, 3) self.model().dataChanged.emit(minInd, maxInd) def updateRows(self): """ Update all rows. """ count = self.model().rowCount() minInd, maxInd = self.model().index(0, 0), self.model().index( count - 1, 3) self.model().dataChanged.emit(minInd, maxInd) def dropEvent(self, event): """ drop event handler : moving layer @param event: @type event: Qevent """ if event.source() is not self: return # get selected rows and layers rows = set([mi.row() for mi in self.selectedIndexes()]) rStack = self.img.layersStack[::-1] layers = [rStack[i] for i in rows] # get target row and layer targetRow = self.indexAt(event.pos()).row() targetLayer = rStack[targetRow] # remove target from selection if targetRow in rows: rows.discard(targetRow) rows = sorted(rows) if not rows: return # if target is below last row insert at the last position if targetRow == -1: targetRow = self.model().rowCount() # mapping of src (row) indices to target indices rowMapping = dict() for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx # update layerStack using rowMapping # insert None items for _ in range(len(rows)): rStack.insert(targetRow, None) # copy moved items to their final place for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items rStack[tgtRow] = rStack[srcRow] # remove moved items from their initial place for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys rStack.pop(row) self.img.layersStack = rStack[::-1] # update model # insert empty rows for _ in range(len(rows)): result = self.model().insertRow(targetRow, QModelIndex()) # copy moved rows to their final place colCount = self.model().columnCount() for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items for col in range(0, colCount): # CAUTION : setItem calls the data changed event handler (cf. setLayers above) self.model().setItem(tgtRow, col, self.model().takeItem(srcRow, col)) # remove moved rows from their initial place and keep track of moved items movedDict = rowMapping.copy() for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys self.model().removeRow(row) for s, t in rowMapping.items(): if t > row: movedDict[s] -= 1 ######################################### sanity check for r in range(self.model().rowCount()): id = self.model().index(r, 1) if id.data() != rStack[r].name: raise ValueError('Drop Error') ######################################## # reselect moved rows sel = sorted(movedDict.values()) selectionModel = QtCore.QItemSelectionModel(self.model()) self.setSelectionModel(selectionModel) index1 = self.model().index(sel[0], 1) index2 = self.model().index(sel[-1], 1) itemSelection = QtCore.QItemSelection(index1, index2) self.selectionModel().select( itemSelection, QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) # multiple selection : display no window if len(sel) > 1: self.currentWin.hide() self.currentWin = None elif len(sel) == 1: self.img.setActiveLayer(len(self.img.layersStack) - sel[0] - 1) # update stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() def select(self, row, col): """ select item in view @param row: @type row: @param col: @type col: @return: @rtype: """ model = self.model() self.viewClicked(model.index(row, col)) def viewClicked(self, clickedIndex): """ Mouse clicked event handler. @param clickedIndex: @type clickedIndex: QModelIndex """ row = clickedIndex.row() rows = set([mi.row() for mi in self.selectedIndexes()]) # multiple selection : go to top of selection m = min(rows) if row != m: clickedIndex = self.model().index(m, clickedIndex.column()) layer = self.img.layersStack[-1 - row] self.actionDup.setEnabled(not layer.isAdjustLayer()) # toggle layer visibility if clickedIndex.column() == 0: # background layer is always visible if row == len(self.img.layersStack) - 1: return # layer.visible = not(layer.visible) layer.setVisible(not layer.visible) if self.currentWin is not None: self.currentWin.setVisible(layer.visible) if not layer.visible: self.currentWin = None if layer.tool is not None: layer.tool.setVisible(layer.visible) # update stack if layer.visible: layer.applyToStack() else: i = layer.getUpperVisibleStackIndex() if i >= 0: layer.parentImage.layersStack[i].applyToStack() else: # top layer : update only the presentation layer layer.parentImage.prLayer.execute(l=None, pool=None) self.img.onImageChanged() # update displayed window and active layer activeStackIndex = len(self.img.layersStack) - 1 - row activeLayer = self.img.setActiveLayer(activeStackIndex) # update color mask slider and label self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(activeLayer.maskIsSelected) self.maskValue.setEnabled(activeLayer.maskIsSelected) if self.currentWin is not None: # hide sucontrols for dk in self.currentWin.widget().subControls: dk.hide() if self.currentWin.isFloating(): self.currentWin.hide() self.currentWin = None if hasattr(activeLayer, "view"): self.currentWin = activeLayer.view if self.currentWin is not None and activeLayer.visible: self.currentWin.show() self.currentWin.raise_() # display subcontrols for dk in self.currentWin.widget().subControls: dk.setVisible( self.currentWin.widget().options[dk.widget().optionName]) # make self.currentWin the active window self.currentWin.activateWindow() # update opacity and composition mode for current layer opacity = int(layer.opacity * 100) self.opacityValue.setText(str('%d ' % opacity)) self.opacitySlider.setSliderPosition(opacity) compositionMode = layer.compositionMode ind = self.blendingModeCombo.findData(compositionMode) self.blendingModeCombo.setCurrentIndex(ind) # draw the right rectangle window.label.repaint() def initContextMenu(self): """ Context menu initialization @return: @rtype: QMenu """ menu = QMenu() # menu.actionReset = QAction('Reset To Default', None) # menu.actionLoadImage = QAction('Load New Image', None) # multiple selections menu.actionMerge = QAction('Merge Lower', None) # merge only adjustment layer with image layer menu.actionRepositionLayer = QAction('Reposition Layer(s)', None) menu.actionColorMaskEnable = QAction('Color', None) menu.actionOpacityMaskEnable = QAction('Opacity', None) menu.actionClippingMaskEnable = QAction('Clipping', None) menu.actionMaskDisable = QAction('Disabled', None) menu.actionMaskUndo = QAction('Undo Mask', None) menu.actionMaskRedo = QAction('Redo Mask', None) menu.actionMaskInvert = QAction('Invert Mask', None) menu.actionMaskReset_UM = QAction('Unmask All', None) menu.actionMaskReset_M = QAction('Mask All', None) menu.actionMaskCopy = QAction('Copy Mask to Clipboard', None) menu.actionImageCopy = QAction('Copy Image to Clipboard', None) menu.actionMaskPaste = QAction('Paste Mask', None) menu.actionImagePaste = QAction('Paste Image', None) menu.actionMaskDilate = QAction('Dilate Mask', None) menu.actionMaskErode = QAction('Erode Mask', None) menu.actionMaskSmooth = QAction('Smooth Mask', None) menu.actionMaskBright1 = QAction('Bright 1 Mask', None) menu.actionMaskBright2 = QAction('Bright 2 Mask', None) menu.actionMaskBright3 = QAction('Bright 3 Mask', None) menu.actionMaskDark1 = QAction('Dark 1 Mask', None) menu.actionMaskDark2 = QAction('Dark 2 Mask', None) menu.actionMaskDark3 = QAction('Dark 3 Mask', None) menu.actionMaskMid1 = QAction('Mid 1 Mask', None) menu.actionMaskMid2 = QAction('Mid 2 Mask', None) menu.actionMaskMid3 = QAction('Mid 3 Mask', None) menu.actionMergingFlag = QAction('Merged Layer', None) menu.actionMergingFlag.setCheckable(True) menu.actionColorMaskEnable.setCheckable(True) menu.actionOpacityMaskEnable.setCheckable(True) menu.actionClippingMaskEnable.setCheckable(True) menu.actionMaskDisable.setCheckable(True) #################### # Build menu ################### menu.addAction(menu.actionRepositionLayer) menu.addSeparator() # layer menu.addAction(menu.actionImageCopy) menu.addAction(menu.actionImagePaste) menu.addAction(menu.actionMergingFlag) menu.addSeparator() # mask menu.subMenuEnable = menu.addMenu('Mask...') menu.subMenuEnable.addAction(menu.actionColorMaskEnable) menu.subMenuEnable.addAction(menu.actionOpacityMaskEnable) menu.subMenuEnable.addAction(menu.actionClippingMaskEnable) menu.subMenuEnable.addAction(menu.actionMaskDisable) menu.addAction(menu.actionMaskUndo) menu.addAction(menu.actionMaskRedo) menu.addAction(menu.actionMaskInvert) menu.subMenuLum = menu.addMenu('Luminosity Mask...') for a in [ menu.actionMaskBright1, menu.actionMaskBright2, menu.actionMaskBright3, menu.actionMaskDark1, menu.actionMaskDark2, menu.actionMaskDark3, menu.actionMaskMid1, menu.actionMaskMid2, menu.actionMaskMid3 ]: menu.subMenuLum.addAction(a) menu.addAction(menu.actionMaskReset_UM) menu.addAction(menu.actionMaskReset_M) menu.addAction(menu.actionMaskCopy) menu.addAction(menu.actionMaskPaste) menu.addAction(menu.actionMaskDilate) menu.addAction(menu.actionMaskErode) menu.addAction(menu.actionMaskSmooth) menu.addSeparator() # miscellaneous # menu.addAction(menu.actionLoadImage) # to link actionDup with a shortcut, # it must be set in __init__ menu.addAction(self.actionDup) menu.addAction(menu.actionMerge) # menu.addAction(menu.actionReset) return menu def contextMenuEvent(self, event): """ context menu handler @param event @type event: QContextMenuEvent """ selection = self.selectedIndexes() if not selection: return # get a fresh context menu without connected actions # and with state corresponding to the currently clicked layer self.cMenu = self.initContextMenu() # get current selection rows = set([mi.row() for mi in selection]) rStack = self.img.layersStack[::-1] layers = [rStack[r] for r in rows] # get current position index = self.indexAt(event.pos()) layerStackIndex = len(self.img.layersStack) - 1 - index.row() layer = self.img.layersStack[layerStackIndex] lowerVisible = self.img.layersStack[layer.getLowerVisibleStackIndex()] lower = self.img.layersStack[layerStackIndex - 1] # case index == 0 doesn't matter # toggle actions self.cMenu.actionMergingFlag.setChecked(layer.mergingFlag) self.cMenu.actionMerge.setEnabled(not ( hasattr(layer, 'inputImg') or hasattr(lowerVisible, 'inputImg'))) self.actionDup.setEnabled(not layer.isAdjustLayer()) self.cMenu.actionColorMaskEnable.setChecked(layer.maskIsSelected and layer.maskIsEnabled) self.cMenu.actionOpacityMaskEnable.setChecked( (not layer.maskIsSelected) and layer.maskIsEnabled) self.cMenu.actionClippingMaskEnable.setChecked( layer.isClipping and (layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskDisable.setChecked(not ( layer.isClipping or layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskUndo.setEnabled(layer.historyListMask.canUndo()) self.cMenu.actionMaskRedo.setEnabled(layer.historyListMask.canRedo()) self.cMenu.subMenuEnable.setEnabled(len(rows) == 1) self.cMenu.actionMaskPaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionImagePaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionMergingFlag.setEnabled(layer.isImageLayer()) # Event handlers def RepositionLayer(): layer.xOffset, layer.yOffset = 0, 0 layer.Zoom_coeff = 1.0 layer.AltZoom_coeff = 1.0 layer.xAltOffset, layer.yAltOffset = 0, 0 layer.updatePixmap() self.img.onImageChanged() def merge(): layer.merge_with_layer_immediately_below() def testUpperVisibility(): pos = self.img.getStackIndex(layer) upperVisible = False for i in range(len(self.img.layersStack) - pos - 1): if self.img.layersStack[pos + 1 + i].visible: upperVisible = True break if upperVisible: dlgWarn("Upper visible layers slow down mask edition") return True return False def colorMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = True self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.applyToStack() self.img.onImageChanged() def opacityMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.applyToStack() self.img.onImageChanged() def clippingMaskEnable(): layer.maskIsEnabled = True layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.isClipping = True layer.applyToStack() self.img.onImageChanged() def maskDisable(): layer.maskIsEnabled = False layer.maskIsSelected = False self.maskLabel.setEnabled(layer.maskIsSelected) self.maskSlider.setEnabled(layer.maskIsSelected) self.maskValue.setEnabled(layer.maskIsSelected) layer.isClipping = False layer.applyToStack() self.img.onImageChanged() def undoMask(): try: layer.mask = layer.historyListMask.undo( saveitem=layer.mask.copy()) layer.applyToStack() self.img.onImageChanged() except ValueError: pass def redoMask(): try: layer.mask = layer.historyListMask.redo() layer.applyToStack() self.img.onImageChanged() except ValueError: pass def maskInvert(): layer.invertMask() # update mask stack layer.applyToStack() self.img.onImageChanged() def maskReset_UM(): layer.resetMask(maskAll=False) # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def maskReset_M(): layer.resetMask(maskAll=True) # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def maskCopy(): QApplication.clipboard().setImage(layer.mask) def imageCopy(): QApplication.clipboard().setImage(layer.getCurrentMaskedImage()) def maskPaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): img = cb.image() if img.size() == layer.mask.size(): layer.mask = img else: layer.mask = img.scaled(layer.mask.size()) layer.applyToStack() self.img.prLayer.execute(l=None, pool=None) self.img.onImageChanged() def imagePaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): srcImg = cb.image() if srcImg.size() == layer.size(): layer.setImage(srcImg) else: layer.setImage(srcImg.scaled(layer.size())) layer.applyToStack() self.img.onImageChanged() def maskDilate(): """ Increase the masked part of the image """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskDilate(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskErode(): """ Reduce the masked part of the image """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskErode(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskSmooth(): """ Smooth the mask boundary """ buf = QImageBuffer(layer.mask) buf[:, :, 2] = vImage.maskSmooth(buf[:, :, 2]) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright1(): layer.setMaskLuminosity(min=128, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright2(): layer.setMaskLuminosity(min=192, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskBright3(): layer.setMaskLuminosity(min=224, max=255) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskDark1(): layer.setMaskLuminosity(min=0, max=128) def maskDark2(): layer.setMaskLuminosity(min=0, max=64) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskDark3(): layer.setMaskLuminosity(min=0, max=32) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid1(): layer.setMaskLuminosity(min=64, max=192) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid2(): layer.setMaskLuminosity(min=96, max=160) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def maskMid3(): layer.setMaskLuminosity(min=112, max=144) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.prLayer.update() self.img.onImageChanged() def mergingFlag(flag): layer.mergingFlag = flag self.cMenu.actionRepositionLayer.triggered.connect(RepositionLayer) # self.cMenu.actionLoadImage.triggered.connect(loadImage) self.cMenu.actionMerge.triggered.connect(merge) self.cMenu.actionColorMaskEnable.triggered.connect(colorMaskEnable) self.cMenu.actionOpacityMaskEnable.triggered.connect(opacityMaskEnable) self.cMenu.actionClippingMaskEnable.triggered.connect( clippingMaskEnable) self.cMenu.actionMaskDisable.triggered.connect(maskDisable) self.cMenu.actionMaskUndo.triggered.connect(undoMask) self.cMenu.actionMaskRedo.triggered.connect(redoMask) self.cMenu.actionMaskInvert.triggered.connect(maskInvert) self.cMenu.actionMaskReset_UM.triggered.connect(maskReset_UM) self.cMenu.actionMaskReset_M.triggered.connect(maskReset_M) self.cMenu.actionMaskCopy.triggered.connect(maskCopy) self.cMenu.actionMaskPaste.triggered.connect(maskPaste) self.cMenu.actionImageCopy.triggered.connect(imageCopy) self.cMenu.actionImagePaste.triggered.connect(imagePaste) self.cMenu.actionMaskDilate.triggered.connect(maskDilate) self.cMenu.actionMaskErode.triggered.connect(maskErode) self.cMenu.actionMaskSmooth.triggered.connect(maskSmooth) self.cMenu.actionMaskBright1.triggered.connect(maskBright1) self.cMenu.actionMaskBright2.triggered.connect(maskBright2) self.cMenu.actionMaskBright3.triggered.connect(maskBright3) self.cMenu.actionMaskDark1.triggered.connect(maskDark1) self.cMenu.actionMaskDark2.triggered.connect(maskDark2) self.cMenu.actionMaskDark3.triggered.connect(maskDark3) self.cMenu.actionMaskMid1.triggered.connect(maskMid1) self.cMenu.actionMaskMid2.triggered.connect(maskMid2) self.cMenu.actionMaskMid3.triggered.connect(maskMid3) self.cMenu.actionMergingFlag.toggled.connect(mergingFlag) self.cMenu.exec_(event.globalPos() - QPoint(400, 0)) # update table for row in rows: self.updateRow(row)
class QDisasmStatusBar(QFrame): """ Status and control bar for disassembly views """ def __init__(self, disasm_view, parent=None): super().__init__(parent) self.disasm_view = disasm_view # widgets self._nav_toolbar: Optional[NavToolbar] = None self._function_label: QLabel = None self._options_menu: DisasmOptionsMenu = None self._view_combo: QComboBox = None # information self._function = None self._init_menu() self._init_widgets() @property def function(self): return self._function @function.setter def function(self, f): self._function = f self._update_function_address() @property def function_address(self): if self._function is None: return None return self._function.addr # # Initialization # def _init_widgets(self): self._nav_toolbar = NavToolbar( self.disasm_view.jump_history, self.disasm_view.jump_back, self.disasm_view.jump_forward, self.disasm_view.jump_to_history_position, True, self) # current function self._function_label = QLabel() self._view_combo = QComboBox(self) self._view_combo.addItem("Linear Disassembly", QLinearDisassembly) self._view_combo.addItem("Graph Disassembly", QDisassemblyGraph) self._view_combo.activated.connect(self._view_combo_changed) self.disasm_view.view_visibility_changed.connect( self._update_view_combo) self._update_view_combo() self._disasm_level_combo = QComboBox(self) self._disasm_level_combo.addItem("Machine Code", DisassemblyLevel.MachineCode) self._disasm_level_combo.addItem("Lifter IR", DisassemblyLevel.LifterIR) self._disasm_level_combo.addItem("AIL", DisassemblyLevel.AIL) self._disasm_level_combo.activated.connect( self._disasm_level_combo_changed) self.disasm_view.disassembly_level_changed.connect( self._update_disasm_level_combo) self._update_disasm_level_combo() # options button option_btn = QPushButton() option_btn.setText('Options') option_btn.setMenu(self._options_menu.qmenu()) # Save image button saveimage_btn = QPushButton() saveimage_btn.setText('Save image...') saveimage_btn.clicked.connect(self._on_saveimage_btn_clicked) layout = QHBoxLayout() layout.setContentsMargins(2, 2, 2, 2) layout.addWidget(self._nav_toolbar.qtoolbar()) layout.addWidget(self._function_label) layout.addStretch(0) layout.addWidget(saveimage_btn) layout.addWidget(self._view_combo) layout.addWidget(self._disasm_level_combo) layout.addWidget(option_btn) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def _init_menu(self): self._options_menu = DisasmOptionsMenu(self.disasm_view) # # Private methods # def _view_combo_changed(self, index: int): { QLinearDisassembly: self.disasm_view.display_linear_viewer, QDisassemblyGraph: self.disasm_view.display_disasm_graph }[self._view_combo.itemData(index)]() def _update_view_combo(self): graph_type = type(self.disasm_view.current_graph) index = self._view_combo.findData(graph_type) self._view_combo.setCurrentIndex(index) def _disasm_level_combo_changed(self, index: int): new_level = self._disasm_level_combo.itemData(index) self.disasm_view.set_disassembly_level(new_level) def _update_disasm_level_combo(self): new_level = self.disasm_view.disassembly_level index = self._disasm_level_combo.findData(new_level) self._disasm_level_combo.setCurrentIndex(index) def _update_function_address(self): if self.function_address is not None: self._function_label.setText("Function %x" % self.function_address) def _on_saveimage_btn_clicked(self): filename, folder = QFileDialog.getSaveFileName( self, 'Save image...', '', 'PNG Files (*.png);;Bitmaps (*.bmp)') if not filename or not folder: return self.disasm_view.save_image_to(os.path.join(folder, filename))
class QLayerView(QTableView): """ Display the stack of image layers. """ def __init__(self, parent): super(QLayerView, self).__init__(parent) self.img = None # graphic form to show : it # should correspond to the currently selected layer self.currentWin = None # mouse click event self.clicked.connect(self.viewClicked) # set behavior and styles self.setSelectionBehavior(QAbstractItemView.SelectRows) delegate = itemDelegate(parent=self) self.setItemDelegate(delegate) ic1 = QImage(":/images/resources/eye-icon.png") ic2 = QImage(":/images/resources/eye-icon-strike.png") delegate.px1 = QPixmap.fromImage(ic1) delegate.px2 = QPixmap.fromImage(ic2) ic1.invertPixels() ic2.invertPixels() delegate.inv_px1 = QPixmap.fromImage(ic1) delegate.inv_px2 = QPixmap.fromImage(ic2) self.setIconSize(QSize(20, 15)) self.verticalHeader().setMinimumSectionSize(-1) self.verticalHeader().setDefaultSectionSize( self.verticalHeader().minimumSectionSize()) self.horizontalHeader().setMinimumSectionSize(40) self.horizontalHeader().setDefaultSectionSize(40) # drag and drop self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDragDropOverwriteMode(False) self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) ################################ # layer property GUI : # preview, blending mode, opacity, mask color ################################ # Preview option # We should use a QListWidget or a custom optionsWidget # (cf. utils.py) : adding it to QVBoxLayout with mode # Qt.AlignBottom does not work. self.previewOptionBox = QCheckBox('Preview') self.previewOptionBox.setMaximumSize(100, 30) # View/Preview changed event handler def m(state): # state : Qt.Checked Qt.UnChecked if self.img is None: return self.img.useThumb = (state == Qt.Checked) window.updateStatus() self.img.cacheInvalidate() for layer in self.img.layersStack: layer.autoclone = True # auto update cloning layers layer.knitted = False try: QApplication.setOverrideCursor( Qt.WaitCursor ) # TODO 18/04/18 waitcursor is called by applytostack? QApplication.processEvents() # update the whole stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() # TODO added 30/11/18 validate finally: for layer in self.img.layersStack: layer.autoclone = False # reset flags layer.knitted = False QApplication.restoreOverrideCursor() QApplication.processEvents() # window.label.repaint() # TODO removed 30/11/18 replaced by onImageChange above self.previewOptionBox.stateChanged.connect(m) self.previewOptionBox.setChecked(True) # m is not triggered # title titleLabel = QLabel('Layer') titleLabel.setMaximumSize(100, 30) # opacity slider self.opacitySlider = QbLUeSlider(Qt.Horizontal) self.opacitySlider.setStyleSheet( QbLUeSlider.bLueSliderDefaultBWStylesheet) self.opacitySlider.setTickPosition(QSlider.TicksBelow) self.opacitySlider.setRange(0, 100) self.opacitySlider.setSingleStep(1) self.opacitySlider.setSliderPosition(100) self.opacityValue = QLabel() font = self.opacityValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.opacityValue.setMinimumSize(w, h) self.opacityValue.setMaximumSize(w, h) self.opacityValue.setText('100 ') # opacity value changed event handler def f1(): self.opacityValue.setText(str('%d ' % self.opacitySlider.value())) # opacity slider released event handler def f2(): try: layer = self.img.getActiveLayer() layer.setOpacity(self.opacitySlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.opacitySlider.valueChanged.connect(f1) self.opacitySlider.sliderReleased.connect(f2) # mask color slider maskSlider = QbLUeSlider(Qt.Horizontal) maskSlider.setStyleSheet(QbLUeSlider.bLueSliderDefaultBWStylesheet) maskSlider.setTickPosition(QSlider.TicksBelow) maskSlider.setRange(0, 100) maskSlider.setSingleStep(1) maskSlider.setSliderPosition(100) self.maskSlider = maskSlider self.maskValue = QLabel() font = self.maskValue.font() metrics = QFontMetrics(font) w = metrics.width("100 ") h = metrics.height() self.maskValue.setMinimumSize(w, h) self.maskValue.setMaximumSize(w, h) self.maskValue.setText('100 ') # mask value changed event handler def g1(): self.maskValue.setText(str('%d ' % self.maskSlider.value())) # mask slider released event handler def g2(): try: layer = self.img.getActiveLayer() layer.setColorMaskOpacity(self.maskSlider.value()) layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.maskSlider.valueChanged.connect(g1) self.maskSlider.sliderReleased.connect(g2) # blending mode combo box compLabel = QLabel() compLabel.setText("Blend") self.compositionModeDict = OrderedDict([ ('Normal', QPainter.CompositionMode_SourceOver), ('Plus', QPainter.CompositionMode_Plus), ('Multiply', QPainter.CompositionMode_Multiply), ('Screen', QPainter.CompositionMode_Screen), ('Overlay', QPainter.CompositionMode_Overlay), ('Darken', QPainter.CompositionMode_Darken), ('Lighten', QPainter.CompositionMode_Lighten), ('Color Dodge', QPainter.CompositionMode_ColorDodge), ('Color Burn', QPainter.CompositionMode_ColorBurn), ('Hard Light', QPainter.CompositionMode_HardLight), ('Soft Light', QPainter.CompositionMode_SoftLight), ('Difference', QPainter.CompositionMode_Difference), ('Exclusion', QPainter.CompositionMode_Exclusion) ]) self.blendingModeCombo = QComboBox() for key in self.compositionModeDict: self.blendingModeCombo.addItem(key, self.compositionModeDict[key]) # combo box item chosen event handler def g(ind): s = self.blendingModeCombo.currentText() try: layer = self.img.getActiveLayer() layer.compositionMode = self.compositionModeDict[str(s)] layer.applyToStack() self.img.onImageChanged() except AttributeError: return self.blendingModeCombo.currentIndexChanged.connect(g) # self.blendingModeCombo.activated.connect(g) # TODO activated changed to currentIndexChanged 08/10/18 validate #layout l = QVBoxLayout() l.setAlignment(Qt.AlignTop) hl0 = QHBoxLayout() hl0.addWidget(titleLabel) hl0.addStretch(1) hl0.addWidget(self.previewOptionBox) l.addLayout(hl0) hl = QHBoxLayout() hl.addWidget(QLabel('Opacity')) hl.addWidget(self.opacityValue) hl.addWidget(self.opacitySlider) l.addLayout(hl) hl1 = QHBoxLayout() hl1.addWidget(QLabel('Mask Color')) hl1.addWidget(self.maskValue) hl1.addWidget(self.maskSlider) l.addLayout(hl1) l.setContentsMargins(0, 0, 10, 0) # left, top, right, bottom hl2 = QHBoxLayout() hl2.addWidget(compLabel) hl2.addWidget(self.blendingModeCombo) l.addLayout(hl2) for layout in [hl, hl1, hl2]: layout.setContentsMargins(5, 0, 0, 0) # this layout must be added to the propertyWidget object loaded from blue.ui : # we postpone it after loading of the main form, in blue.py. self.propertyLayout = l # shortcut actions self.actionDup = QAction('Duplicate layer', None) self.actionDup.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_J)) self.addAction(self.actionDup) def dup(): row = self.selectedIndexes()[0].row() # Stack index index = len(self.img.layersStack) - row - 1 layer = self.img.layersStack[index] if layer.isAdjustLayer(): return # add new layer to stack and set it to active self.img.dupLayer(index=index) # update layer view self.setLayers(self.img) self.actionDup.triggered.connect(dup) self.setWhatsThis("""<b>Layer Stack</b> To <b>toggle the layer visibility</b> click on the Eye icon.<br> To <b>add a mask</b> use the context menu to enable it and paint pixels with the Mask/Unmask tools in the left pane.<br> For <b>color mask<b/b>: <br> green pixels are masked,<br> red pixels are unmasked.<br> Note that upper visible layers slow down mask edition.<br> """) # end of setWhatsThis """ def setEnabled(self, value): # TODO removed 30/11/18 super(QLayerView, self).setEnabled(value) if not self.isEnabled(): self.setStatusTip('Close adjustment form %s to enable Layers' % self.currentWin.windowTitle()) else: self.setStatusTip('') """ def closeAdjustForms(self, delete=False): """ Close all layer forms. If delete is True (default False), the forms and their dock containers are deleted. @param delete: @type delete: boolean """ if self.img is None: return stack = self.img.layersStack for layer in stack: if hasattr(layer, "view"): if layer.view is not None: dock = layer.view if delete: form = dock.widget() # remove back link form.layer = None # QtGui1.window.removeDockWidget(dock) form.setAttribute(Qt.WA_DeleteOnClose) form.close() dock.setAttribute(Qt.WA_DeleteOnClose) dock.close() layer.view = None elif not TABBING: # tabbed forms should not be closed dock.close() if delete: self.currentWin = None gc.collect() def clear(self, delete=True): """ Reset LayerView and clear the back links to image. """ self.closeAdjustForms(delete=delete) self.img = None self.currentWin = None model = layerModel() model.setColumnCount(3) self.setModel(None) def setLayers(self, mImg, delete=False): """ Displays the layer stack of mImg @param mImg: image @type mImg: mImage """ # close open adjustment windows #self.closeAdjustForms() self.clear(delete=delete) mImg.layerView = self # back link to image self.img = weakProxy(mImg) model = layerModel() model.setColumnCount(3) l = len(mImg.layersStack) # dataChanged event handler : enables edition of layer name def f(index1, index2): # index1 and index2 should be equal # only layer name should be editable # dropEvent emit dataChanged when setting item values. f must # return immediately from these calls, as they are possibly made with unconsistent data : # dragged rows are already removed from layersStack # and not yet removed from model. if l != self.model().rowCount(): return # only name is editable if index1.column() != 1: return row = index1.row() stackIndex = l - row - 1 mImg.layersStack[stackIndex].name = index1.data() model.dataChanged.connect(f) for r, lay in enumerate(reversed(mImg.layersStack)): items = [] # col 0 : visibility icon if lay.visible: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon.png"), "") else: item_visible = QStandardItem( QIcon(":/images/resources/eye-icon-strike.png"), "") items.append(item_visible) # col 1 : image icon (for non-adjustment layeronly) and name if len(lay.name) <= 30: name = lay.name else: name = lay.name[:28] + '...' if hasattr(lay, 'inputImg'): item_name = QStandardItem(name) else: # icon with very small dim causes QPainter error # QPixmap.fromImage bug ? smallImg = lay.resize(50 * 50) w, h = smallImg.width(), smallImg.height() if w < h / 5 or h < w / 5: item_name = QStandardItem(name) else: item_name = QStandardItem( QIcon(QPixmap.fromImage(smallImg)), name) # set tool tip to full name item_name.setToolTip(lay.name) items.append(item_name) item_mask = QStandardItem('m') items.append(item_mask) model.appendRow(items) self.setModel(model) self.horizontalHeader().hide() self.verticalHeader().hide() header = self.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # select active layer self.selectRow(len(mImg.layersStack) - 1 - mImg.activeLayerIndex) layerview = mImg.getActiveLayer().view # TODO added 25/11/18 if layerview is not None: layerview.show() if TABBING: layerview.raise_() self.updateForm() for item in self.img.layersStack: if hasattr(item, 'sourceIndex'): combo = item.getGraphicsForm().sourceCombo currentText = combo.currentText() combo.clear() for i, x in enumerate(self.img.layersStack): item.view.widget().sourceCombo.addItem(x.name, i) combo.setCurrentIndex(combo.findText(currentText)) def updateForm(self): activeLayer = self.img.getActiveLayer() if hasattr(activeLayer, 'view'): self.currentWin = activeLayer.view if self.currentWin is not None: self.currentWin.show() self.currentWin.activateWindow() def updateRow(self, row): minInd, maxInd = self.model().index(row, 0), self.model().index(row, 3) self.model().dataChanged.emit(minInd, maxInd) def dropEvent(self, event): """ drop event handler : moving layer @param event: @type event: Qevent """ if event.source() is not self: return # get selected rows and layers rows = set([mi.row() for mi in self.selectedIndexes()]) rStack = self.img.layersStack[::-1] layers = [rStack[i] for i in rows] linked = any(l.group for l in layers) if linked and len(rows) > 1: return # get target row and layer targetRow = self.indexAt(event.pos()).row() targetLayer = rStack[targetRow] if linked: if layers[0].group is not targetLayer.group: return if bool(targetLayer.group) != linked: return # remove target from selection if targetRow in rows: rows.discard(targetRow) rows = sorted(rows) if not rows: return # if target is below last row insert at the last position if targetRow == -1: targetRow = self.model().rowCount() # mapping of src (row) indices to target indices rowMapping = dict() for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx # update layerStack using rowMapping # insert None items for _ in range(len(rows)): rStack.insert(targetRow, None) # copy moved items to their final place for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items rStack[tgtRow] = rStack[srcRow] # remove moved items from their initial place for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys rStack.pop(row) self.img.layersStack = rStack[::-1] # update model # insert empty rows for _ in range(len(rows)): result = self.model().insertRow(targetRow, QModelIndex()) # copy moved rows to their final place colCount = self.model().columnCount() for srcRow, tgtRow in sorted( rowMapping.items()): # python 3 iteritems->items for col in range(0, colCount): # CAUTION : setItem calls the data changed event handler (cf. setLayers above) self.model().setItem(tgtRow, col, self.model().takeItem(srcRow, col)) # remove moved rows from their initial place and keep track of moved items movedDict = rowMapping.copy() for row in reversed(sorted( rowMapping.keys())): # python 3 iterkeys -> keys self.model().removeRow(row) for s, t in rowMapping.items(): if t > row: movedDict[s] -= 1 ######################################### sanity check for r in range(self.model().rowCount()): id = self.model().index(r, 1) if id.data() != rStack[r].name: raise ValueError('Drop Error') ######################################## # reselect moved rows sel = sorted(movedDict.values()) selectionModel = QtCore.QItemSelectionModel(self.model()) self.setSelectionModel(selectionModel) index1 = self.model().index(sel[0], 1) index2 = self.model().index(sel[-1], 1) itemSelection = QtCore.QItemSelection(index1, index2) self.selectionModel().select( itemSelection, QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.Select) # multiple selection : display no window if len(sel) > 1: self.currentWin.hide() self.currentWin = None elif len(sel) == 1: self.img.setActiveLayer(len(self.img.layersStack) - sel[0] - 1) # update stack self.img.layersStack[0].applyToStack() self.img.onImageChanged() def select(self, row, col): """ select item in view @param row: @type row: @param col: @type col: @return: @rtype: """ model = self.model() self.viewClicked(model.index(row, col)) def viewClicked(self, clickedIndex): """ Mouse clicked event handler. @param clickedIndex: @type clickedIndex: QModelIndex """ row = clickedIndex.row() rows = set([mi.row() for mi in self.selectedIndexes()]) #multiple selection : go to top of selection m = min(rows) if row != m: clickedIndex = self.model().index(m, clickedIndex.column()) layer = self.img.layersStack[-1 - row] self.actionDup.setEnabled(not layer.isAdjustLayer()) # toggle layer visibility if clickedIndex.column() == 0: # background layer is always visible if row == len(self.img.layersStack) - 1: return #layer.visible = not(layer.visible) layer.setVisible(not (layer.visible)) if self.currentWin is not None: self.currentWin.setVisible(layer.visible) if not layer.visible: self.currentWin = None if layer.tool is not None: layer.tool.setVisible(layer.visible) # update stack if layer.visible: layer.applyToStack() else: i = layer.getUpperVisibleStackIndex() if i >= 0: layer.parentImage.layersStack[i].applyToStack() else: # top layer : update only the presentation layer layer.parentImage.prLayer.execute(l=None, pool=None) self.img.onImageChanged() # update displayed window and active layer activeStackIndex = len(self.img.layersStack) - 1 - row activeLayer = self.img.setActiveLayer(activeStackIndex) if self.currentWin is not None: if not self.currentWin.isFloating(): #self.currentWin.hide() self.currentWin = None if hasattr(self.img.layersStack[activeStackIndex], "view"): self.currentWin = self.img.layersStack[activeStackIndex].view if self.currentWin is not None and activeLayer.visible: self.currentWin.show() self.currentWin.raise_() # make self.currentWin the active window self.currentWin.activateWindow() # update opacity and composition mode for current layer opacity = int(layer.opacity * 100) self.opacityValue.setText(str('%d ' % opacity)) self.opacitySlider.setSliderPosition(opacity) compositionMode = layer.compositionMode ind = self.blendingModeCombo.findData(compositionMode) self.blendingModeCombo.setCurrentIndex(ind) # draw the right rectangle window.label.repaint() def initContextMenu(self): """ return the context menu @return: @rtype: QMenu """ menu = QMenu() menu.actionReset = QAction('Reset To Default', None) menu.actionLoadImage = QAction('Load New Image', None) menu.actionGroupSelection = QAction('Group Selection', None) menu.actionAdd2Group = QAction('Add to Group', None) # Active layer is not in a group or right clicked layer is in a group menu.actionUnGroup = QAction('Ungroup', None) # multiple selections menu.actionMerge = QAction('Merge Lower', None) # merge only adjustment layer with image layer # don't dup adjustment layers menu.actionUnselect = QAction('Unselect All', None) menu.actionRepositionLayer = QAction('Reposition Layer(s)', None) menu.actionColorMaskEnable = QAction('Color Mask', None) menu.actionOpacityMaskEnable = QAction('Opacity Mask', None) menu.actionClippingMaskEnable = QAction('Clipping Mask', None) menu.actionMaskDisable = QAction('Disable Mask', None) menu.actionMaskInvert = QAction('Invert Mask', None) menu.actionMaskReset = QAction('Clear Mask', None) menu.actionMaskCopy = QAction('Copy Mask to Clipboard', None) menu.actionImageCopy = QAction('Copy Image to Clipboard', None) menu.actionMaskPaste = QAction('Paste Mask', None) menu.actionImagePaste = QAction('Paste Image', None) menu.actionMaskDilate = QAction('Dilate Mask', None) menu.actionMaskErode = QAction('Erode Mask', None) menu.actionColorMaskEnable.setCheckable(True) menu.actionOpacityMaskEnable.setCheckable(True) menu.actionClippingMaskEnable.setCheckable(True) menu.actionMaskDisable.setCheckable(True) #################### # Build menu ################### # group/ungroup menu.addAction(menu.actionAdd2Group) menu.addAction(menu.actionGroupSelection) menu.addAction(menu.actionUnGroup) menu.addSeparator() menu.addAction(menu.actionUnselect) menu.addSeparator() menu.addAction(menu.actionRepositionLayer) menu.addSeparator() # layer menu.addAction(menu.actionImageCopy) menu.addAction(menu.actionImagePaste) menu.addSeparator() # mask menu.subMenuEnable = menu.addMenu('Mask...') menu.subMenuEnable.addAction(menu.actionColorMaskEnable) menu.subMenuEnable.addAction(menu.actionOpacityMaskEnable) menu.subMenuEnable.addAction(menu.actionClippingMaskEnable) menu.subMenuEnable.addAction(menu.actionMaskDisable) menu.addAction(menu.actionMaskInvert) menu.addAction(menu.actionMaskReset) menu.addAction(menu.actionMaskCopy) menu.addAction(menu.actionMaskPaste) menu.addAction(menu.actionMaskDilate) menu.addAction(menu.actionMaskErode) menu.addSeparator() # miscellaneous menu.addAction(menu.actionLoadImage) # to link actionDup with a shortcut, # it must be set in __init__ menu.addAction(self.actionDup) menu.addAction(menu.actionMerge) menu.addAction(menu.actionReset) return menu def contextMenuEvent(self, event): """ context menu handler @param event @type event: QContextMenuEvent """ selection = self.selectedIndexes() if not selection: return # get fresh context menu self.cMenu = self.initContextMenu() # get current selection rows = set([mi.row() for mi in selection]) rStack = self.img.layersStack[::-1] layers = [rStack[r] for r in rows] group = [] # TODO added 5/11/18 validate if layers: group = layers[0].group for l in layers: # different groups if l.group and group: if l.group is not group: dlgWarn("Select a single group") return # get current position index = self.indexAt(event.pos()) layerStackIndex = len(self.img.layersStack) - 1 - index.row() layer = self.img.layersStack[layerStackIndex] lowerVisible = self.img.layersStack[layer.getLowerVisibleStackIndex()] lower = self.img.layersStack[layerStackIndex - 1] # case index == 0 doesn't matter # toggle actions self.cMenu.actionGroupSelection.setEnabled(not (len(rows) < 2 or any( l.group for l in layers))) self.cMenu.actionAdd2Group.setEnabled(not (group or layer.group)) self.cMenu.actionUnGroup.setEnabled(bool(layer.group)) self.cMenu.actionMerge.setEnabled(not ( hasattr(layer, 'inputImg') or hasattr(lowerVisible, 'inputImg'))) self.actionDup.setEnabled(not layer.isAdjustLayer()) self.cMenu.actionColorMaskEnable.setChecked(layer.maskIsSelected and layer.maskIsEnabled) self.cMenu.actionOpacityMaskEnable.setChecked( (not layer.maskIsSelected) and layer.maskIsEnabled) self.cMenu.actionClippingMaskEnable.setChecked( layer.isClipping and (layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionMaskDisable.setChecked(not ( layer.isClipping or layer.maskIsSelected or layer.maskIsEnabled)) self.cMenu.actionUnselect.setEnabled(layer.rect is None) self.cMenu.subMenuEnable.setEnabled(len(rows) == 1) self.cMenu.actionMaskPaste.setEnabled( not QApplication.clipboard().image().isNull()) self.cMenu.actionImagePaste.setEnabled( not QApplication.clipboard().image().isNull()) # Event handlers def f(): self.opacitySlider.show() def unselectAll(): layer.rect = None def RepositionLayer(): layer.xOffset, layer.yOffset = 0, 0 layer.Zoom_coeff = 1.0 layer.AltZoom_coeff = 1.0 layer.xAltOffset, layer.yAltOffset = 0, 0 layer.updatePixmap() self.img.onImageChanged() def loadImage(): return # TODO 26/06/18 action to remove from menu? replaced by new image layer filename = openDlg(window) img = QImage(filename) layer.thumb = None layer.setImage(img) def add2Group(): layer.group = group layer.mask = group[0].mask layer.maskIsEnabled = True layer.maskIsSelected = True def groupSelection(): layers = [rStack[i] for i in sorted(rows)] if any(l.group for l in layers): dlgWarn("Some layers are already grouped. Ungroup first") return mask = layers[0].mask for l in layers: l.group = layers l.mask = mask l.maskIsEnabled = True l.maskIsSelected = False def unGroup(): group = layer.group.copy() for l in group: l.unlinkMask() def merge(): layer.merge_with_layer_immediately_below() def testUpperVisibility(): pos = self.img.getStackIndex(layer) upperVisible = False for i in range(len(self.img.layersStack) - pos - 1): if self.img.layersStack[pos + 1 + i].visible: upperVisible = True break if upperVisible: dlgWarn("Upper visible layers slow down mask edition") return True return False def colorMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = True layer.applyToStack() self.img.onImageChanged() def opacityMaskEnable(): testUpperVisibility() layer.maskIsEnabled = True layer.maskIsSelected = False layer.applyToStack() self.img.onImageChanged() def clippingMaskEnable(): layer.maskIsEnabled = True layer.maskIsSelected = False layer.isClipping = True layer.applyToStack() self.img.onImageChanged() def maskDisable(): layer.maskIsEnabled = False layer.maskIsSelected = False layer.isClipping = False # TODO added 28/11/18 layer.applyToStack() self.img.onImageChanged() def maskInvert(): layer.invertMask() # update mask stack layer.applyToStack() #for l in self.img.layersStack: #l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskReset(): layer.resetMask() # update mask stack for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskCopy(): QApplication.clipboard().setImage(layer.mask) def imageCopy(): QApplication.clipboard().setImage(layer.getCurrentMaskedImage()) def maskPaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): img = cb.image() if img.size() == layer.mask.size(): layer.mask = img else: layer.mask = img.scaled(layer.mask.size()) layer.applyToStack() self.img.onImageChanged() def imagePaste(): """ Pastes clipboard to mask and updates the stack. The clipboard image is scaled if its size does not match the size of the mask """ cb = QApplication.clipboard() if not cb.image().isNull(): srcImg = cb.image() if srcImg.size() == layer.size(): layer.setImage(srcImg) else: layer.setImage(srcImg.scaled(layer.size())) layer.applyToStack() self.img.onImageChanged() def maskDilate(): kernel = np.ones((5, 5), np.uint8) buf = QImageBuffer(layer.mask) # CAUTION erode decreases values (min filter), so it extends the masked part of the image buf[:, :, 2] = cv2.erode(buf[:, :, 2], kernel, iterations=1) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def maskErode(): kernel = np.ones((5, 5), np.uint8) buf = QImageBuffer(layer.mask) # CAUTION dilate increases values (max filter), so it reduces the masked part of the image buf[:, :, 2] = cv2.dilate(buf[:, :, 2], kernel, iterations=1) for l in self.img.layersStack: l.updatePixmap(maskOnly=True) self.img.onImageChanged() def layerReset(): view = layer.getGraphicsForm() if hasattr(view, 'reset'): view.reset() self.cMenu.actionRepositionLayer.triggered.connect(RepositionLayer) self.cMenu.actionUnselect.triggered.connect(unselectAll) self.cMenu.actionLoadImage.triggered.connect(loadImage) self.cMenu.actionAdd2Group.triggered.connect(add2Group) self.cMenu.actionGroupSelection.triggered.connect(groupSelection) self.cMenu.actionUnGroup.triggered.connect(unGroup) self.cMenu.actionMerge.triggered.connect(merge) self.cMenu.actionColorMaskEnable.triggered.connect(colorMaskEnable) self.cMenu.actionOpacityMaskEnable.triggered.connect(opacityMaskEnable) self.cMenu.actionClippingMaskEnable.triggered.connect( clippingMaskEnable) self.cMenu.actionMaskDisable.triggered.connect(maskDisable) self.cMenu.actionMaskInvert.triggered.connect(maskInvert) self.cMenu.actionMaskReset.triggered.connect(maskReset) self.cMenu.actionMaskCopy.triggered.connect(maskCopy) self.cMenu.actionMaskPaste.triggered.connect(maskPaste) self.cMenu.actionImageCopy.triggered.connect(imageCopy) self.cMenu.actionImagePaste.triggered.connect(imagePaste) self.cMenu.actionMaskDilate.triggered.connect(maskDilate) self.cMenu.actionMaskErode.triggered.connect(maskErode) self.cMenu.actionReset.triggered.connect(layerReset) self.cMenu.exec_(event.globalPos()) # update table for row in rows: self.updateRow(row)
def _init_load_options_tab(self, tab): arch_layout = QHBoxLayout() arch_caption = QLabel(self) arch_caption.setText('Architecture:') arch_caption.setSizePolicy( QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) arch_layout.addWidget(arch_caption) arch_combo = QComboBox(self) for arch in archinfo.all_arches: addendum = ' (P-code Engine)' if hasattr(arch, 'pcode_arch') else '' arch_combo.addItem( f'{arch.bits}b {arch.name} ({arch.memory_endness[-2:]}){addendum}', str(arch)) index = arch_combo.findData(str(self.arch)) arch_combo.setCurrentIndex(index) arch_layout.addWidget(arch_combo) self.option_widgets['arch'] = arch_combo if self.is_blob: blob_layout = QGridLayout() # load address base_addr_caption = QLabel(self) base_addr_caption.setText('Base Address:') blob_layout.addWidget(base_addr_caption, 1, 0) base_addr = QLineEdit(self) base_addr.setText('0') blob_layout.addWidget(base_addr, 1, 1) self.option_widgets['base_addr'] = base_addr # entry address entry_addr_caption = QLabel(self) entry_addr_caption.setText('Entry Address:') blob_layout.addWidget(entry_addr_caption, 2, 0) entry_addr = QLineEdit(self) entry_addr.setText('0') blob_layout.addWidget(entry_addr, 2, 1) self.option_widgets['entry_addr'] = entry_addr # load debug symbols load_debug_info = QCheckBox() load_debug_info.setText("Load debug information if available") load_debug_info.setChecked(True) self.option_widgets['load_debug_info'] = load_debug_info # auto load libs auto_load_libs = QCheckBox() auto_load_libs.setText( "Automatically load all libraries (slow, not recommended)") auto_load_libs.setChecked(False) self.option_widgets['auto_load_libs'] = auto_load_libs # dependencies list dep_group = QGroupBox("Dependencies") dep_list = QListWidget() self.option_widgets['dep_list'] = dep_list sublayout = QVBoxLayout() sublayout.addWidget(dep_list) dep_group.setLayout(sublayout) layout = QVBoxLayout() if self.is_blob: layout.addLayout(blob_layout) layout.addLayout(arch_layout) layout.addWidget(load_debug_info) layout.addWidget(auto_load_libs) layout.addWidget(dep_group) layout.addStretch(0) frame = QFrame(self) frame.setLayout(layout) tab.addTab(frame, "Loading Options")
class TallyDock(PlotterDock): def __init__(self, model, font_metric, parent=None): super().__init__(model, font_metric, parent) self.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) # Dock maps for tally information self.tally_map = {} self.filter_map = {} self.score_map = {} self.nuclide_map = {} # Tally selector self.tallySelectorLayout = QFormLayout() self.tallySelector = QComboBox(self) self.tallySelector.currentTextChanged[str].connect( self.main_window.editSelectedTally) self.tallySelectorLayout.addRow(self.tallySelector) self.tallySelectorLayout.setLabelAlignment(QtCore.Qt.AlignLeft) self.tallySelectorLayout.setFieldGrowthPolicy( QFormLayout.AllNonFixedFieldsGrow) # Add selector to its own box self.tallyGroupBox = QGroupBox('Selected Tally') self.tallyGroupBox.setLayout(self.tallySelectorLayout) # Create submit button self.applyButton = QPushButton("Apply Changes") self.applyButton.setMinimumHeight(self.font_metric.height() * 1.6) self.applyButton.clicked.connect(self.main_window.applyChanges) # Color options section self.tallyColorForm = ColorForm(self.model, self.main_window, 'tally') self.scoresGroupBox = Expander(title="Scores:") self.scoresListWidget = QListWidget() self.nuclidesListWidget = QListWidget() # Main layout self.dockLayout = QVBoxLayout() self.dockLayout.addWidget(QLabel("Tallies")) self.dockLayout.addWidget(HorizontalLine()) self.dockLayout.addWidget(self.tallyGroupBox) self.dockLayout.addStretch() self.dockLayout.addWidget(HorizontalLine()) self.dockLayout.addWidget(self.tallyColorForm) self.dockLayout.addWidget(HorizontalLine()) self.dockLayout.addWidget(self.applyButton) # Create widget for dock and apply main layout self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.widget = QWidget() self.widget.setLayout(self.dockLayout) self.scroll.setWidget(self.widget) self.setWidget(self.scroll) def _createFilterTree(self, spatial_filters): av = self.model.activeView tally = self.model.statepoint.tallies[av.selectedTally] filters = tally.filters # create a tree for the filters self.treeLayout = QVBoxLayout() self.filterTree = QTreeWidget() self.treeLayout.addWidget(self.filterTree) self.treeExpander = Expander("Filters:", layout=self.treeLayout) self.treeExpander.expand() # start with filters expanded header = QTreeWidgetItem(["Filters"]) self.filterTree.setHeaderItem(header) self.filterTree.setItemHidden(header, True) self.filterTree.setColumnCount(1) self.filterTree.itemChanged.connect(self.updateFilters) self.filter_map = {} self.bin_map = {} for tally_filter in filters: filter_label = str(type(tally_filter)).split(".")[-1][:-2] filter_item = QTreeWidgetItem(self.filterTree, (filter_label,)) self.filter_map[tally_filter] = filter_item # make checkable if not spatial_filters: filter_item.setFlags(QtCore.Qt.ItemIsUserCheckable) filter_item.setToolTip(0, "Only tallies with spatial filters are viewable.") else: filter_item.setFlags(filter_item.flags() | QtCore.Qt.ItemIsTristate | QtCore.Qt.ItemIsUserCheckable) filter_item.setCheckState(0, QtCore.Qt.Unchecked) # all mesh bins are selected by default and not shown in the dock if isinstance(tally_filter, openmc.MeshFilter): filter_item.setCheckState(0, QtCore.Qt.Checked) filter_item.setFlags(QtCore.Qt.ItemIsUserCheckable) filter_item.setToolTip(0, "All Mesh bins are selected automatically") continue def _bin_sort_val(bin): if isinstance(bin, Iterable) and all([isinstance(val, float) for val in bin]): return np.sum(bin) else: return bin for bin in sorted(tally_filter.bins, key=_bin_sort_val): item = QTreeWidgetItem(filter_item, [str(bin),]) if not spatial_filters: item.setFlags(QtCore.Qt.ItemIsUserCheckable) item.setToolTip(0, "Only tallies with spatial filters are viewable.") else: item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) item.setCheckState(0, QtCore.Qt.Unchecked) bin = bin if not isinstance(bin, Iterable) else tuple(bin) self.bin_map[tally_filter, bin] = item # start with all filters selected if spatial filters are present if spatial_filters: filter_item.setCheckState(0, QtCore.Qt.Checked) def selectFromModel(self): cv = self.model.currentView self.selectedTally(cv.selectedTally) def selectTally(self, tally_label=None): # using active view to populate tally options live av = self.model.activeView # reset form layout for i in reversed(range(self.tallySelectorLayout.count())): self.tallySelectorLayout.itemAt(i).widget().setParent(None) # always re-add the tally selector to the layout self.tallySelectorLayout.addRow(self.tallySelector) self.tallySelectorLayout.addRow(HorizontalLine()) if tally_label is None or tally_label == "None" or tally_label == "": av.selectedTally = None self.score_map = None self.nuclide_map = None self.filter_map = None av.tallyValue = "Mean" else: # get the tally tally = self.model.statepoint.tallies[av.selectedTally] # populate filters filter_types = {type(f) for f in tally.filters} spatial_filters = bool(filter_types.intersection(_SPATIAL_FILTERS)) if not spatial_filters: self.filter_description = QLabel("(No Spatial Filters)") self.tallySelectorLayout.addRow(self.filter_description) self._createFilterTree(spatial_filters) self.tallySelectorLayout.addRow(self.treeExpander) self.tallySelectorLayout.addRow(HorizontalLine()) # value selection self.tallySelectorLayout.addRow(QLabel("Value:")) self.valueBox = QComboBox(self) self.values = tuple(_TALLY_VALUES.keys()) for value in self.values: self.valueBox.addItem(value) self.tallySelectorLayout.addRow(self.valueBox) self.valueBox.currentTextChanged[str].connect( self.main_window.editTallyValue) self.updateTallyValue() if not spatial_filters: self.valueBox.setEnabled(False) self.valueBox.setToolTip("Only tallies with spatial filters are viewable.") # scores self.score_map = {} self.scoresListWidget.itemClicked.connect( self.main_window.updateScores) self.score_map.clear() self.scoresListWidget.clear() sorted_scores = sorted(tally.scores) # always put total first if present if 'total' in sorted_scores: idx = sorted_scores.index('total') sorted_scores.insert(0, sorted_scores.pop(idx)) for score in sorted_scores: ql = QListWidgetItem() ql.setText(score.capitalize()) ql.setCheckState(QtCore.Qt.Unchecked) if not spatial_filters: ql.setFlags(QtCore.Qt.ItemIsUserCheckable) else: ql.setFlags(ql.flags() | QtCore.Qt.ItemIsUserCheckable) ql.setFlags(ql.flags() & ~QtCore.Qt.ItemIsSelectable) self.score_map[score] = ql self.scoresListWidget.addItem(ql) # select the first score item by default for item in self.score_map.values(): item.setCheckState(QtCore.Qt.Checked) break self.updateScores() self.scoresGroupBoxLayout = QVBoxLayout() self.scoresGroupBoxLayout.addWidget(self.scoresListWidget) self.scoresGroupBox = Expander("Scores:", layout=self.scoresGroupBoxLayout) self.tallySelectorLayout.addRow(self.scoresGroupBox) # nuclides self.nuclide_map = {} self.nuclidesListWidget.itemClicked.connect(self.main_window.updateNuclides) self.nuclide_map.clear() self.nuclidesListWidget.clear() sorted_nuclides = sorted(tally.nuclides) # always put total at the top if 'total' in sorted_nuclides: idx = sorted_nuclides.index('total') sorted_nuclides.insert(0, sorted_nuclides.pop(idx)) for nuclide in sorted_nuclides: ql = QListWidgetItem() ql.setText(nuclide.capitalize()) ql.setCheckState(QtCore.Qt.Unchecked) if not spatial_filters: ql.setFlags(QtCore.Qt.ItemIsUserCheckable) else: ql.setFlags(ql.flags() | QtCore.Qt.ItemIsUserCheckable) ql.setFlags(ql.flags() & ~QtCore.Qt.ItemIsSelectable) self.nuclide_map[nuclide] = ql self.nuclidesListWidget.addItem(ql) # select the first nuclide item by default for item in self.nuclide_map.values(): item.setCheckState(QtCore.Qt.Checked) break self.updateNuclides() self.nuclidesGroupBoxLayout = QVBoxLayout() self.nuclidesGroupBoxLayout.addWidget(self.nuclidesListWidget) self.nuclidesGroupBox = Expander("Nuclides:", layout=self.nuclidesGroupBoxLayout) self.tallySelectorLayout.addRow(self.nuclidesGroupBox) def updateMinMax(self): self.tallyColorForm.updateMinMax() def updateTallyValue(self): cv = self.model.currentView idx = self.valueBox.findText(cv.tallyValue) self.valueBox.setCurrentIndex(idx) def updateSelectedTally(self): cv = self.model.currentView idx = 0 if cv.selectedTally: idx = self.tallySelector.findData(cv.selectedTally) self.tallySelector.setCurrentIndex(idx) def updateFilters(self): applied_filters = defaultdict(tuple) for f, f_item in self.filter_map.items(): if type(f) == openmc.MeshFilter: continue filter_checked = f_item.checkState(0) if filter_checked != QtCore.Qt.Unchecked: selected_bins = [] for idx, b in enumerate(f.bins): b = b if not isinstance(b, Iterable) else tuple(b) bin_checked = self.bin_map[(f, b)].checkState(0) if bin_checked == QtCore.Qt.Checked: selected_bins.append(idx) applied_filters[f] = tuple(selected_bins) self.model.appliedFilters = applied_filters def updateScores(self): applied_scores = [] for score, score_box in self.score_map.items(): if score_box.checkState() == QtCore.Qt.CheckState.Checked: applied_scores.append(score) self.model.appliedScores = tuple(applied_scores) if not applied_scores: # if no scores are selected, enable all scores again for score, score_box in self.score_map.items(): sunits = _SCORE_UNITS.get(score, _REACTION_UNITS) empty_item = QListWidgetItem() score_box.setFlags(empty_item.flags() | QtCore.Qt.ItemIsUserCheckable) score_box.setFlags(empty_item.flags() & ~QtCore.Qt.ItemIsSelectable) elif 'total' in applied_scores: self.model.appliedScores = ('total',) # if total is selected, disable all other scores for score, score_box in self.score_map.items(): if score != 'total': score_box.setFlags(QtCore.Qt.ItemIsUserCheckable) score_box.setToolTip("De-select 'total' to enable other scores") else: # get units of applied scores selected_units = _SCORE_UNITS.get(applied_scores[0], _REACTION_UNITS) # disable scores with incompatible units for score, score_box in self.score_map.items(): sunits = _SCORE_UNITS.get(score, _REACTION_UNITS) if sunits != selected_units: score_box.setFlags(QtCore.Qt.ItemIsUserCheckable) score_box.setToolTip("Score is incompatible with currently selected scores") else: score_box.setFlags(score_box.flags() | QtCore.Qt.ItemIsUserCheckable) score_box.setFlags(score_box.flags() & ~QtCore.Qt.ItemIsSelectable) def updateNuclides(self): applied_nuclides = [] for nuclide, nuclide_box in self.nuclide_map.items(): if nuclide_box.checkState() == QtCore.Qt.CheckState.Checked: applied_nuclides.append(nuclide) self.model.appliedNuclides = tuple(applied_nuclides) if 'total' in applied_nuclides: self.model.appliedNuclides = ['total',] for nuclide, nuclide_box in self.nuclide_map.items(): if nuclide != 'total': nuclide_box.setFlags(QtCore.Qt.ItemIsUserCheckable) nuclide_box.setToolTip("De-select 'total' to enable other nuclides") elif not applied_nuclides: # if no nuclides are selected, enable all nuclides again for nuclide, nuclide_box in self.nuclide_map.items(): empty_item = QListWidgetItem() nuclide_box.setFlags(empty_item.flags() | QtCore.Qt.ItemIsUserCheckable) nuclide_box.setFlags(empty_item.flags() & ~QtCore.Qt.ItemIsSelectable) def update(self): # update the color form self.tallyColorForm.update() if self.model.statepoint: self.tallySelector.clear() self.tallySelector.setEnabled(True) self.tallySelector.addItem("None") for idx, tally in enumerate(self.model.statepoint.tallies.values()): if tally.name == "": self.tallySelector.addItem('Tally {}'.format(tally.id), userData=tally.id) else: self.tallySelector.addItem('Tally {} "{}"'.format(tally.id, tally.name), userData=tally.id) self.tally_map[idx] = tally self.updateSelectedTally() self.updateMinMax() else: self.tallySelector.clear() self.tallySelector.setDisabled(True)
class WidgetConfig(QGroupBox): def __init__(self): super(WidgetConfig, self).__init__() HEIGHT = 30 grid = QGridLayout() # 使用默认摄像头复选框 self.check_camera = QCheckBox('Use default camera') self.check_camera.setChecked(True) self.check_camera.stateChanged.connect(self.slot_check_camera) grid.addWidget(self.check_camera, 0, 0, 1, 3) # 一行三列 # 选择视频文件 label_video = QLabel('Source') self.line_video = QLineEdit() self.line_video.setFixedHeight(HEIGHT) self.line_video.setEnabled(False) self.line_video.setText(GLOBAL.config.get('video', '')) self.line_video.editingFinished.connect( lambda: GLOBAL.record_config({'video': self.line_video.text()})) self.btn_video = QPushButton('...') self.btn_video.setFixedWidth(40) self.btn_video.setFixedHeight(HEIGHT) self.btn_video.setEnabled(False) self.btn_video.clicked.connect(self.choose_video_file) self.slot_check_camera() grid.addWidget(label_video, 1, 0) grid.addWidget(self.line_video, 1, 1) grid.addWidget(self.btn_video, 1, 2) # 选择权重文件 label_weights = QLabel('Weights') self.line_weights = QLineEdit() self.line_weights.setFixedHeight(HEIGHT) self.line_weights.setText(GLOBAL.config.get('weights', '')) self.line_weights.editingFinished.connect(lambda: GLOBAL.record_config( {'weights': self.line_weights.text()} )) self.btn_weights = QPushButton('...') self.btn_weights.setFixedWidth(40) self.btn_weights.setFixedHeight(HEIGHT) self.btn_weights.clicked.connect(self.choose_weights_file) grid.addWidget(label_weights, 2, 0) grid.addWidget(self.line_weights, 2, 1) grid.addWidget(self.btn_weights, 2, 2) # 是否使用GPU label_device = QLabel('CUDA device') self.line_device = QLineEdit('cpu') self.line_device.setText(GLOBAL.config.get('device', 'cpu')) self.line_device.setPlaceholderText('cpu or 0 or 0,1,2,3') self.line_device.setFixedHeight(HEIGHT) self.line_device.editingFinished.connect(lambda: GLOBAL.record_config( {'device': self.line_device.text()} )) grid.addWidget(label_device, 3, 0) grid.addWidget(self.line_device, 3, 1, 1, 2) # 设置图像大小 label_size = QLabel('Img Size') self.combo_size = QComboBox() self.combo_size.setFixedHeight(HEIGHT) self.combo_size.setStyleSheet( 'QAbstractItemView::item {height: 40px;}') self.combo_size.setView(QListView()) self.combo_size.addItem('320', 320) self.combo_size.addItem('416', 416) self.combo_size.addItem('480', 480) self.combo_size.addItem('544', 544) self.combo_size.addItem('640', 640) self.combo_size.setCurrentIndex( self.combo_size.findData(GLOBAL.config.get('img_size', 480))) self.combo_size.currentIndexChanged.connect(lambda: GLOBAL.record_config( {'img_size': self.combo_size.currentData()})) grid.addWidget(label_size, 4, 0) grid.addWidget(self.combo_size, 4, 1, 1, 2) # 设置置信度阈值 label_conf = QLabel('Confidence') self.spin_conf = QDoubleSpinBox() self.spin_conf.setFixedHeight(HEIGHT) self.spin_conf.setDecimals(1) self.spin_conf.setRange(0.1, 0.9) self.spin_conf.setSingleStep(0.1) self.spin_conf.setValue(GLOBAL.config.get('conf_thresh', 0.5)) self.spin_conf.valueChanged.connect(lambda: GLOBAL.record_config( {'conf_thresh': round(self.spin_conf.value(), 1)} )) grid.addWidget(label_conf, 5, 0) grid.addWidget(self.spin_conf, 5, 1, 1, 2) # 设置IOU阈值 label_iou = QLabel('IOU') self.spin_iou = QDoubleSpinBox() self.spin_iou.setFixedHeight(HEIGHT) self.spin_iou.setDecimals(1) self.spin_iou.setRange(0.1, 0.9) self.spin_iou.setSingleStep(0.1) self.spin_iou.setValue(GLOBAL.config.get('iou_thresh', 0.5)) self.spin_iou.valueChanged.connect(lambda: GLOBAL.record_config( {'iou_thresh': round(self.spin_iou.value(), 1)} )) grid.addWidget(label_iou, 6, 0) grid.addWidget(self.spin_iou, 6, 1, 1, 2) # class-agnostic NMS self.check_agnostic = QCheckBox('Agnostic') self.check_agnostic.setChecked(GLOBAL.config.get('agnostic', True)) self.check_agnostic.stateChanged.connect(lambda: GLOBAL.record_config( {'agnostic': self.check_agnostic.isChecked()} )) grid.addWidget(self.check_agnostic, 7, 0, 1, 3) # 一行三列 # augmented inference self.check_augment = QCheckBox('Augment') self.check_augment.setChecked(GLOBAL.config.get('augment', True)) self.check_augment.stateChanged.connect(lambda: GLOBAL.record_config( {'augment': self.check_augment.isChecked()} )) grid.addWidget(self.check_augment, 8, 0, 1, 3) # 一行三列 # 视频录制 self.check_record = QCheckBox('Record video') grid.addWidget(self.check_record, 9, 0, 1, 3) # 一行三列 self.setLayout(grid) # 设置布局 def slot_check_camera(self): check = self.check_camera.isChecked() GLOBAL.record_config({'use_camera': check}) # 保存配置 if check: self.line_video.setEnabled(False) self.btn_video.setEnabled(False) else: self.line_video.setEnabled(True) self.btn_video.setEnabled(True) def choose_weights_file(self): """从系统中选择权重文件""" file = QFileDialog.getOpenFileName(self, "Pre-trained YOLOv5 Weights", "./", "Weights Files (*.pt);;All Files (*)") if file[0] != '': self.line_weights.setText(file[0]) GLOBAL.record_config({'weights': file[0]}) def choose_video_file(self): """从系统中选择视频文件""" file = QFileDialog.getOpenFileName(self, "Video Files", "./", "Video Files (*)") if file[0] != '': self.line_video.setText(file[0]) GLOBAL.record_config({'video': file[0]}) def save_config(self): """保存当前的配置到配置文件""" config = { 'use_camera': self.check_camera.isChecked(), 'video': self.line_video.text(), 'weights': self.line_weights.text(), 'device': self.line_device.text(), 'img_size': self.combo_size.currentData(), 'conf_thresh': round(self.spin_conf.value(), 1), 'iou_thresh': round(self.spin_iou.value(), 1), 'agnostic': self.check_agnostic.isChecked(), 'augment': self.check_augment.isChecked() } GLOBAL.record_config(config)