def create_field(self, mode): self.buttons = [] for x in range(mode[0]): buttons_row = [] for y in range(mode[1]): button = QToolButton(self) button.installEventFilter(self) button.setObjectName(str(x) + ' ' + str(y)) palette = QPalette() brush = QBrush(QColor(155, 155, 155)) brush.setStyle(Qt.SolidPattern) palette.setBrush(QPalette.Active, QPalette.Button, brush) brush = QBrush(QColor(255, 255, 255)) brush.setStyle(Qt.SolidPattern) palette.setBrush(QPalette.Disabled, QPalette.Button, brush) button.setPalette(palette) button.clicked.connect(self.take_a_step) # if self.mines_map[x][y] == 1: # button.setIcon(QIcon('mine.png')) size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) size_policy.setHorizontalStretch(1) size_policy.setHorizontalStretch(1) button.setSizePolicy(size_policy) self.grid_layout.addWidget(button, x, y) buttons_row.append(button) self.buttons.append(buttons_row) self.close_btn = QPushButton('close', self) self.vertical_layout.addItem(self.grid_layout) self.vertical_layout.addWidget(self.close_btn) self.show()
class _CompactToolButton(QFrame): def __init__(self, action: QAction, menu: QMenu, parent): super(_CompactToolButton, self).__init__(parent) self.overlay = _TTOverlayToolButton(self) iconsize = int( get_pixelmetric(QStyle.PM_LargeIconSize) * get_scalefactor(self)) self.upButton = QToolButton(self) self.upButton.setProperty("TTInternal", QtCore.QVariant(True)) self.upButton.setAutoRaise(True) self.upButton.setDefaultAction(action) self.upButton.setIconSize(QtCore.QSize(iconsize, iconsize)) self.upButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) self.upButton.setStyle(TTToolButtonStyle()) self.upButton.setMaximumHeight(iconsize + 5) self.vlayout = QVBoxLayout(self) self.vlayout.setContentsMargins(0, 0, 0, 0) self.vlayout.setSpacing(0) self.vlayout.setDirection(QBoxLayout.TopToBottom) self.upButton.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) self.upButton.setPopupMode(QToolButton.DelayedPopup) self.vlayout.addWidget(self.upButton) self.downButton = QToolButton(self) self.downButton.setProperty("TTInternal", QtCore.QVariant(True)) self.downButton.setAutoRaise(True) self.downButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly) self.downButton.setPopupMode(QToolButton.InstantPopup) self.downButton.setMinimumHeight(25) self.downButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) self.downButton.setText(action.text()) self.downButton.setToolTip(action.toolTip()) self.downButton.setStyle(TTToolButtonStyle()) if menu: self.downButton.setMenu(menu) menu.aboutToHide.connect(lambda: self.set_hover(False)) self.vlayout.addWidget(self.downButton) self.setLayout(self.vlayout) self.hover = _TTHover(self, self.upButton, self.downButton) self.upButton.installEventFilter(self.hover) self.downButton.installEventFilter(self.hover) def set_hover(self, hover: bool): self.overlay.paint = hover self.update()
class ComboBoxItem(QWidget): itemOpSignal = pyqtSignal(str) def __init__(self, qq, username, user_icon): super().__init__() self.username = username self.qq = qq self.user_icon = user_icon self.initUi() def initUi(self): lb_username = QLabel(self.username, self) lb_qq = QLabel(self.qq, self) lb_icon = QLabel(self) lb_icon.setPixmap(QPixmap(self.user_icon)) self.bt_close = QToolButton(self) self.bt_close.setIcon(QIcon("res/close.png")) self.bt_close.setAutoRaise(True) vlayout = QVBoxLayout() vlayout.addWidget(lb_username) vlayout.addWidget(lb_qq) hlayout = QHBoxLayout() hlayout.addWidget(lb_icon) hlayout.addLayout(vlayout) hlayout.addStretch(1) hlayout.addWidget(self.bt_close) hlayout.setContentsMargins(5, 5, 5, 5) hlayout.setSpacing(5) self.setLayout(hlayout) self.bt_close.installEventFilter(self) self.installEventFilter(self) def eventFilter(self, object, event): if object is self: if event.type() == QEvent.Enter: self.setStyleSheet("QWidget{color:white}") elif event.type() == QEvent.Leave: self.setStyleSheet("QWidget{color:black}") elif object is self.bt_close: if event.type() == QEvent.MouseButtonPress: self.itemOpSignal.emit(self.qq) return QWidget.eventFilter(self, object, event)
class QCustomTitleBar(QFrame): onMousePressEvent = pyqtSignal(QEvent) onMouseMoveEvent = pyqtSignal(QEvent) onMouseReleaseEvent = pyqtSignal(QEvent) onCloseButtonClickedEvent = pyqtSignal(QWidget) def __init__(self, parent): super(QCustomTitleBar, self).__init__(parent) # print ( "[create] QCustomTitleBar for parent", parent ) self.dragging = False self.createFromDraging = False self.mouseStartPos = QCursor.pos() if parent.metaObject().indexOfSignal( QMetaObject.normalizedSignature("contentsChanged")) != -1: parent.contentsChanged.connect(self.onFrameContentsChanged) myLayout = QHBoxLayout(self) myLayout.setContentsMargins(0, 0, 0, 0) myLayout.setSpacing(0) self.setLayout(myLayout) self.caption = QLabel(self) self.caption.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) self.caption.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.sysMenuButton = QToolButton(self) self.onIconChange() self.sysMenuButton.setObjectName("sysMenu") self.sysMenuButton.setFocusPolicy(QtCore.Qt.NoFocus) self.sysMenuButton.installEventFilter(self) myLayout.addWidget(self.sysMenuButton) myLayout.addWidget(self.caption, 1) self.minimizeButton = QToolButton(self) self.minimizeButton.setObjectName("minimizeButton") self.minimizeButton.setFocusPolicy(QtCore.Qt.NoFocus) # self.minimizeButton.clicked.connect ( parent.showMinimized ) self.minimizeButton.clicked.connect( lambda: parent.setWindowState(Qt.WindowMinimized)) myLayout.addWidget(self.minimizeButton) self.maximizeButton = QToolButton(self) self.maximizeButton.setObjectName("maximizeButton") self.maximizeButton.setFocusPolicy(QtCore.Qt.NoFocus) self.maximizeButton.clicked.connect(self.toggleMaximizedParent) myLayout.addWidget(self.maximizeButton) self.closeButton = QToolButton(self) self.closeButton.setObjectName("closeButton") self.closeButton.setFocusPolicy(QtCore.Qt.NoFocus) self.closeButton.clicked.connect(self.onCloseButtonClicked) myLayout.addWidget(self.closeButton) parent.windowTitleChanged.connect(self.caption.setText) parent.windowIconChanged.connect(self.onIconChange) self.onFrameContentsChanged(parent) # self.setMouseTracking ( True ) def updateWindowStateButtons(self): if self.maximizeButton != None: if self.parentWidget().windowState() & QtCore.Qt.WindowMaximized: self.maximizeButton.setObjectName("restoreButton") else: self.maximizeButton.setObjectName("maximizeButton") self.style().unpolish(self.maximizeButton) self.style().polish(self.maximizeButton) def setActive(self, active): self.caption.setObjectName("active" if active else "inactive") self.style().unpolish(self.caption) self.style().polish(self.caption) def toggleMaximizedParent(self): if self.parentWidget().windowState() & QtCore.Qt.WindowMaximized: self.parentWidget().showNormal() else: self.parentWidget().showMaximized() self.updateWindowStateButtons() def toggleMinimizedParent(self): flags = self.parentWidget().windowFlags() self.parentWidget().setWindowFlags(flags | QtCore.Qt.CustomizeWindowHint & ~QtCore.Qt.WindowTitleHint) # self.setWindowFlags ( flags | QtCore.Qt.CustomizeWindowHint & ~QtCore.Qt.WindowTitleHint ) self.parentWidget().showMinimized() self.parentWidget().setWindowFlags( self.parentWidget().windowFlags() & (~QtCore.Qt.CustomContextMenu & ~QtCore.Qt.WindowTitleHint) | QtCore.Qt.FramelessWindowHint) def showSystemMenu(self, p): pass def onCloseButtonClicked(self): # print ( "[QCustomTitleBar] close requested. %s" % self.parentWidget () ) self.onCloseButtonClickedEvent.emit(self) self.parentWidget().close() def onIconChange(self): icon = self.parentWidget().windowIcon() if icon is None: icon = qApp.windowIcon() if icon is None: pass self.sysMenuButton.setIcon(icon) def onFrameContentsChanged(self, newContents): flags = self.parentWidget().windowFlags() self.minimizeButton.setVisible( flags & QtCore.Qt.WindowMinimizeButtonHint or flags & QtCore.Qt.MSWindowsFixedSizeDialogHint) self.maximizeButton.setVisible( flags & QtCore.Qt.WindowMaximizeButtonHint or flags & QtCore.Qt.MSWindowsFixedSizeDialogHint) self.closeButton.setVisible(flags & QtCore.Qt.WindowCloseButtonHint) winTitle = self.parentWidget().windowTitle() if len(winTitle) == 0: self.caption.setText( "[QCustomTitleBar] onFrameContentsChanged NoneWindowTitle") return self.caption.setText(winTitle) def onBeginDrag(self): self.dragging = True def mousePressEvent(self, e): self.onMousePressEvent.emit(e) if e.button() == QtCore.Qt.LeftButton and qApp.widgetAt( QCursor.pos()) != self.sysMenuButton: self.onBeginDrag() self.mouseStartPos = e.globalPos() # self.windowHandle ().startSystemMove () # qWarning ( "QCustomTitleBar::onBeginDrag" ) super().mousePressEvent(e) def mouseMoveEvent(self, e): self.onMouseMoveEvent.emit(e) if self.dragging: # self.window ().move ( e.globalPos () - self.mouseStartPos ) offset = self.mouseStartPos - e.globalPos() self.mouseStartPos = e.globalPos() self.window().move(self.window().pos() - offset) e.accept() # qWarning ( "QCustomTitleBar::mouseMoveEvent" ) else: super().mouseMoveEvent(e) def mouseReleaseEvent(self, e): self.onMouseReleaseEvent.emit(e) if not self.dragging: if e.button() == QtCore.Qt.RightButton and self.rect().contains( self.mapFromGlobal(QCursor.pos())): e.accept() self.showSystemMenu(QCursor.pos()) else: e.ignore() return self.dragging = False if self.createFromDraging: self.releaseMouse() self.createFromDraging = False # qWarning ( "QCustomTitleBar::mouseReleaseEvent" ) super().mouseReleaseEvent(e) def mouseDoubleClickEvent(self, e): e.accept() self.toggleMaximizedParent() def eventFilter(self, o, e): if o == self.sysMenuButton: if e.type() == QEvent.MouseButtonPress: if e.button() == QtCore.Qt.LeftButton and qApp.widgetAt( QCursor.pos()) == self.sysMenuButton: self.showSystemMenu( self.mapToGlobal(self.rect().bottomLeft())) return True if e.type() == QEvent.MouseButtonDblClick: if e.button() == QtCore.Qt.LeftButton and qApp.widgetAt( QCursor.pos()) == self.sysMenuButton: self.parentWidget().close() return True return super().eventFilter(o, e)
class SearchWidget(QFrame): """Widget, appeared, when Ctrl+F pressed. Has different forms for different search modes """ Normal = 'normal' Good = 'good' Bad = 'bad' Incorrect = 'incorrect' visibilityChanged = pyqtSignal(bool) """ visibilityChanged(visible) **Signal** emitted, when widget has been shown or hidden """ # pylint: disable=W0105 searchInDirectoryStartPressed = pyqtSignal(type(re.compile('')), list, str) """ searchInDirectoryStartPressed(regEx, mask, path) **Signal** emitted, when 'search in directory' button had been pressed """ # pylint: disable=W0105 searchInDirectoryStopPressed = pyqtSignal() """ searchInDirectoryStopPressed() **Signal** emitted, when 'stop search in directory' button had been pressed """ # pylint: disable=W0105 replaceCheckedStartPressed = pyqtSignal(str) """ replaceCheckedStartPressed(replText) **Signal** emitted, when 'replace checked' button had been pressed """ # pylint: disable=W0105 replaceCheckedStopPressed = pyqtSignal() """ replaceCheckedStartPressed() **Signal** emitted, when 'stop replacing checked' button had been pressed """ # pylint: disable=W0105 searchRegExpChanged = pyqtSignal(type(re.compile(''))) """ searchRegExpValidStateChanged(regEx) **Signal** emitted, when search regexp has been changed. If reg exp is invalid - regEx object contains empty pattern """ # pylint: disable=W0105 searchNext = pyqtSignal() """ searchNext() **Signal** emitted, when 'Search Next' had been pressed """ # pylint: disable=W0105 searchPrevious = pyqtSignal() """ searchPrevious() **Signal** emitted, when 'Search Previous' had been pressed """ # pylint: disable=W0105 replaceFileOne = pyqtSignal(str) """ replaceFileOne(replText) **Signal** emitted, when 'Replace' had been pressed """ # pylint: disable=W0105 replaceFileAll = pyqtSignal(str) """ replaceFileAll(replText) **Signal** emitted, when 'Replace All' had been pressed """ # pylint: disable=W0105 def __init__(self, plugin): QFrame.__init__(self, core.workspace()) self._mode = None self.plugin = plugin uic.loadUi(os.path.join(os.path.dirname(__file__), 'SearchWidget.ui'), self) self.cbSearch.setCompleter(None) self.cbReplace.setCompleter(None) self.cbMask.setCompleter(None) self.fsModel = QDirModel(self.cbPath.lineEdit()) self.fsModel.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot) self.cbPath.lineEdit().setCompleter(QCompleter(self.fsModel, self.cbPath.lineEdit())) self._pathBackspaceShortcut = QShortcut(QKeySequence("Ctrl+Backspace"), self.cbPath, self._onPathBackspace) self._pathBackspaceShortcut.setContext(Qt.WidgetWithChildrenShortcut) # TODO QDirModel is deprecated but QCompleter does not yet handle # QFileSystemodel - please update when possible.""" self.cbSearch.setCompleter(None) self.pbSearchStop.setVisible(False) self.pbReplaceCheckedStop.setVisible(False) self._progress = QProgressBar(self) self._progress.setAlignment(Qt.AlignCenter) self._progress.setToolTip(self.tr("Search in progress...")) self._progress.setMaximumSize(QSize(80, 16)) core.mainWindow().statusBar().insertPermanentWidget(1, self._progress) self._progress.setVisible(False) # cd up action self.tbCdUp = QToolButton(self.cbPath.lineEdit()) self.tbCdUp.setIcon(QIcon(":/enkiicons/go-up.png")) self.tbCdUp.setCursor(Qt.ArrowCursor) self.tbCdUp.installEventFilter(self) # for drawing button self.cbSearch.installEventFilter(self) # for catching Tab and Shift+Tab self.cbReplace.installEventFilter(self) # for catching Tab and Shift+Tab self.cbPath.installEventFilter(self) # for catching Tab and Shift+Tab self.cbMask.installEventFilter(self) # for catching Tab and Shift+Tab self._closeShortcut = QShortcut(QKeySequence("Esc"), self) self._closeShortcut.setContext(Qt.WidgetWithChildrenShortcut) self._closeShortcut.activated.connect(self.hide) # connections self.cbSearch.lineEdit().textChanged.connect(self._onSearchRegExpChanged) self.cbSearch.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbReplace.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbPath.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbMask.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbRegularExpression.stateChanged.connect(self._onSearchRegExpChanged) self.cbCaseSensitive.stateChanged.connect(self._onSearchRegExpChanged) self.cbWholeWord.stateChanged.connect(self._onSearchRegExpChanged) self.tbCdUp.clicked.connect(self._onCdUpPressed) self.pbNext.pressed.connect(self.searchNext) self.pbPrevious.pressed.connect(self.searchPrevious) self.pbSearchStop.pressed.connect(self.searchInDirectoryStopPressed) self.pbReplaceCheckedStop.pressed.connect(self.replaceCheckedStopPressed) core.mainWindow().hideAllWindows.connect(self.hide) core.workspace().escPressed.connect(self.hide) core.workspace().currentDocumentChanged.connect( lambda old, new: self.setVisible(self.isVisible() and new is not None)) def show(self): """Reimplemented function. Sends signal """ super(SearchWidget, self).show() self.visibilityChanged.emit(self.isVisible()) def hide(self): """Reimplemented function. Sends signal, returns focus to workspace """ super(SearchWidget, self).hide() core.workspace().focusCurrentDocument() self.visibilityChanged.emit(self.isVisible()) def setVisible(self, visible): """Reimplemented function. Sends signal """ super(SearchWidget, self).setVisible(visible) self.visibilityChanged.emit(self.isVisible()) def _regExEscape(self, text): """Improved version of re.escape() Doesn't escape space, comma, underscore. Escapes \n and \t """ text = re.escape(text) # re.escape escapes space, comma, underscore, but, it is not necessary and makes text not readable for symbol in (' ,_=\'"/:@#%&'): text = text.replace('\\' + symbol, symbol) text = text.replace('\\\n', '\\n') text = text.replace('\\\t', '\\t') return text def _makeEscapeSeqsVisible(self, text): """Replace invisible \n and \t with escape sequences """ text = text.replace('\\', '\\\\') text = text.replace('\t', '\\t') text = text.replace('\n', '\\n') return text def setMode(self, mode): """Change search mode. i.e. from "Search file" to "Replace directory" """ if self._mode == mode and self.isVisible(): if core.workspace().currentDocument() is not None and \ not core.workspace().currentDocument().hasFocus(): self.cbSearch.lineEdit().selectAll() self.cbSearch.setFocus() self._mode = mode # Set Search and Replace text document = core.workspace().currentDocument() if document is not None and \ document.hasFocus() and \ document.qutepart.selectedText: searchText = document.qutepart.selectedText self.cbReplace.setEditText(self._makeEscapeSeqsVisible(searchText)) if self.cbRegularExpression.checkState() == Qt.Checked: searchText = self._regExEscape(searchText) self.cbSearch.setEditText(searchText) if not self.cbReplace.lineEdit().text() and \ self.cbSearch.lineEdit().text() and \ not self.cbRegularExpression.checkState() == Qt.Checked: replaceText = self.cbSearch.lineEdit().text().replace('\\', '\\\\') self.cbReplace.setEditText(replaceText) # Move focus to Search edit self.cbSearch.setFocus() self.cbSearch.lineEdit().selectAll() # Set search path if mode & MODE_FLAG_DIRECTORY and \ not (self.isVisible() and self.cbPath.isVisible()): try: searchPath = os.path.abspath(str(os.path.curdir)) except OSError: # current directory might have been deleted pass else: self.cbPath.setEditText(searchPath) # Set widgets visibility flag according to state widgets = (self.wSearch, self.pbPrevious, self.pbNext, self.pbSearch, self.wReplace, self.wPath, self.pbReplace, self.pbReplaceAll, self.pbReplaceChecked, self.wOptions, self.wMask) # wSear pbPrev pbNext pbSear wRepl wPath pbRep pbRAll pbRCHK wOpti wMask visible = \ {MODE_SEARCH: (1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,), MODE_REPLACE: (1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0,), MODE_SEARCH_DIRECTORY: (1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1,), MODE_REPLACE_DIRECTORY: (1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1,), MODE_SEARCH_OPENED_FILES: (1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1,), MODE_REPLACE_OPENED_FILES: (1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1,)} for i, widget in enumerate(widgets): widget.setVisible(visible[mode][i]) # Search next button text if mode == MODE_REPLACE: self.pbNext.setText('Next') else: self.pbNext.setText('Next↵') # Finaly show all with valid size self.show() # show before updating widgets and labels self._updateLabels() self._updateWidgets() def eventFilter(self, object_, event): """ Event filter for mode switch tool button Draws icons in the search and path lineEdits """ if event.type() == QEvent.Paint and object_ is self.tbCdUp: # draw CdUp button in search path QLineEdit toolButton = object_ lineEdit = self.cbPath.lineEdit() lineEdit.setContentsMargins(lineEdit.height(), 0, 0, 0) height = lineEdit.height() availableRect = QRect(0, 0, height, height) if toolButton.rect() != availableRect: toolButton.setGeometry(availableRect) painter = QPainter(toolButton) toolButton.icon().paint(painter, availableRect) return True elif event.type() == QEvent.KeyPress: # Tab and Shift+Tab in QLineEdits if event.key() == Qt.Key_Tab: self._moveFocus(1) return True elif event.key() == Qt.Key_Backtab: self._moveFocus(-1) return True return QFrame.eventFilter(self, object_, event) def _onReturnPressed(self): """Return or Enter pressed on widget. Search next or Replace next """ if self.pbReplace.isVisible(): self.pbReplace.click() elif self.pbNext.isVisible(): self.pbNext.click() elif self.pbSearch.isVisible(): self.pbSearch.click() elif self.pbSearchStop.isVisible(): self.pbSearchStop.click() def _onPathBackspace(self): """Ctrl+Backspace pressed on path. Remove 1 path level. Default behavior would be to remove one word on Linux or all on Windows """ path = self.cbPath.currentText() if path.endswith('/') or \ path.endswith('\\'): path = path[:-1] head, tail = os.path.split(path) if head and \ head != path: if not head.endswith(os.sep): head += os.sep self.cbPath.lineEdit().setText(head) def _moveFocus(self, step): """Move focus forward or backward according to step. Standard Qt Keyboard focus algorithm doesn't allow circular navigation """ allFocusableWidgets = (self.cbSearch, self.cbReplace, self.cbPath, self.cbMask) visibleWidgets = [widget for widget in allFocusableWidgets if widget.isVisible()] try: focusedIndex = visibleWidgets.index(QApplication.focusWidget()) except ValueError: print('Invalid focused widget in Search Widget', file=sys.stderr) return nextFocusedIndex = (focusedIndex + step) % len(visibleWidgets) visibleWidgets[nextFocusedIndex].setFocus() visibleWidgets[nextFocusedIndex].lineEdit().selectAll() def _updateLabels(self): """Update 'Search' 'Replace' 'Path' labels geometry """ width = 0 if self.lSearch.isVisible(): width = max(width, self.lSearch.minimumSizeHint().width()) if self.lReplace.isVisible(): width = max(width, self.lReplace.minimumSizeHint().width()) if self.lPath.isVisible(): width = max(width, self.lPath.minimumSizeHint().width()) self.lSearch.setMinimumWidth(width) self.lReplace.setMinimumWidth(width) self.lPath.setMinimumWidth(width) def _updateWidgets(self): """Update geometry of widgets with buttons """ width = 0 if self.wSearchRight.isVisible(): width = max(width, self.wSearchRight.minimumSizeHint().width()) if self.wReplaceRight.isVisible(): width = max(width, self.wReplaceRight.minimumSizeHint().width()) if self.wPathRight.isVisible(): width = max(width, self.wPathRight.minimumSizeHint().width()) self.wSearchRight.setMinimumWidth(width) self.wReplaceRight.setMinimumWidth(width) self.wPathRight.setMinimumWidth(width) def updateComboBoxes(self): """Update comboboxes with last used texts """ searchText = self.cbSearch.currentText() replaceText = self.cbReplace.currentText() maskText = self.cbMask.currentText() # search if searchText: index = self.cbSearch.findText(searchText) if index == -1: self.cbSearch.addItem(searchText) # replace if replaceText: index = self.cbReplace.findText(replaceText) if index == -1: self.cbReplace.addItem(replaceText) # mask if maskText: index = self.cbMask.findText(maskText) if index == -1: self.cbMask.addItem(maskText) def _searchPatternTextAndFlags(self): """Get search pattern and flags """ pattern = self.cbSearch.currentText() pattern = pattern.replace('\u2029', '\n') # replace unicode paragraph separator with habitual \n if not self.cbRegularExpression.checkState() == Qt.Checked: pattern = re.escape(pattern) if self.cbWholeWord.checkState() == Qt.Checked: pattern = r'\b' + pattern + r'\b' flags = 0 if not self.cbCaseSensitive.checkState() == Qt.Checked: flags = re.IGNORECASE return pattern, flags def getRegExp(self): """Read search parameters from controls and present it as a regular expression """ pattern, flags = self._searchPatternTextAndFlags() return re.compile(pattern, flags) def isSearchRegExpValid(self): """Try to compile search pattern to check if it is valid Returns bool result and text error """ pattern, flags = self._searchPatternTextAndFlags() try: re.compile(pattern, flags) except re.error as ex: return False, str(ex) return True, None def _getSearchMask(self): """Get search mask as list of patterns """ mask = [s.strip() for s in self.cbMask.currentText().split(' ')] # remove empty mask = [_f for _f in mask if _f] return mask def setState(self, state): """Change line edit color according to search result """ widget = self.cbSearch.lineEdit() color = {SearchWidget.Normal: QApplication.instance().palette().color(QPalette.Base), SearchWidget.Good: QColor(Qt.green), SearchWidget.Bad: QColor(Qt.red), SearchWidget.Incorrect: QColor(Qt.darkYellow)} stateColor = color[state] if state != SearchWidget.Normal: stateColor.setAlpha(100) pal = widget.palette() pal.setColor(widget.backgroundRole(), stateColor) widget.setPalette(pal) def setSearchInProgress(self, inProgress): """Search thread started or stopped """ self.pbSearchStop.setVisible(inProgress) self.pbSearch.setVisible(not inProgress) self._updateWidgets() self._progress.setVisible(inProgress) def onSearchProgressChanged(self, value, total): """Signal from the thread, progress changed """ self._progress.setValue(value) self._progress.setMaximum(total) def setReplaceInProgress(self, inProgress): """Replace thread started or stopped """ self.pbReplaceCheckedStop.setVisible(inProgress) self.pbReplaceChecked.setVisible(not inProgress) self._updateWidgets() def setSearchInFileActionsEnabled(self, enabled): """Set enabled state for Next, Prev, Replace, ReplaceAll """ for button in (self.pbNext, self.pbPrevious, self.pbReplace, self.pbReplaceAll): button.setEnabled(enabled) def _onSearchRegExpChanged(self): """User edited search text or checked/unchecked checkboxes """ valid, error = self.isSearchRegExpValid() if valid: self.setState(self.Normal) core.mainWindow().statusBar().clearMessage() self.pbSearch.setEnabled(len(self.getRegExp().pattern) > 0) else: core.mainWindow().statusBar().showMessage(error, 3000) self.setState(self.Incorrect) self.pbSearch.setEnabled(False) self.searchRegExpChanged.emit(re.compile('')) return self.searchRegExpChanged.emit(self.getRegExp()) def _onCdUpPressed(self): """User pressed "Up" button, need to remove one level from search path """ text = self.cbPath.currentText() if not os.path.exists(text): return editText = os.path.normpath(os.path.join(text, os.path.pardir)) self.cbPath.setEditText(editText) def on_pbSearch_pressed(self): """Handler of click on "Search" button (for search in directory) """ self.setState(SearchWidget.Normal) self.searchInDirectoryStartPressed.emit(self.getRegExp(), self._getSearchMask(), self.cbPath.currentText()) def on_pbReplace_pressed(self): """Handler of click on "Replace" (in file) button """ self.replaceFileOne.emit(self.cbReplace.currentText()) def on_pbReplaceAll_pressed(self): """Handler of click on "Replace all" (in file) button """ self.replaceFileAll.emit(self.cbReplace.currentText()) def on_pbReplaceChecked_pressed(self): """Handler of click on "Replace checked" (in directory) button """ self.replaceCheckedStartPressed.emit(self.cbReplace.currentText()) def on_pbBrowse_pressed(self): """Handler of click on "Browse" button. Explores FS for search directory path """ path = QFileDialog.getExistingDirectory(self, self.tr("Search path"), self.cbPath.currentText()) if path: self.cbPath.setEditText(path)
class SearchWidget(QFrame): """Widget, appeared, when Ctrl+F pressed. Has different forms for different search modes """ Normal = 'normal' Good = 'good' Bad = 'bad' Incorrect = 'incorrect' visibilityChanged = pyqtSignal(bool) """ visibilityChanged(visible) **Signal** emitted, when widget has been shown or hidden """ # pylint: disable=W0105 searchInDirectoryStartPressed = pyqtSignal(type(re.compile('')), list, str) """ searchInDirectoryStartPressed(regEx, mask, path) **Signal** emitted, when 'search in directory' button had been pressed """ # pylint: disable=W0105 searchInDirectoryStopPressed = pyqtSignal() """ searchInDirectoryStopPressed() **Signal** emitted, when 'stop search in directory' button had been pressed """ # pylint: disable=W0105 replaceCheckedStartPressed = pyqtSignal(str) """ replaceCheckedStartPressed(replText) **Signal** emitted, when 'replace checked' button had been pressed """ # pylint: disable=W0105 replaceCheckedStopPressed = pyqtSignal() """ replaceCheckedStartPressed() **Signal** emitted, when 'stop replacing checked' button had been pressed """ # pylint: disable=W0105 searchRegExpChanged = pyqtSignal(type(re.compile(''))) """ searchRegExpValidStateChanged(regEx) **Signal** emitted, when search regexp has been changed. If reg exp is invalid - regEx object contains empty pattern """ # pylint: disable=W0105 searchNext = pyqtSignal() """ searchNext() **Signal** emitted, when 'Search Next' had been pressed """ # pylint: disable=W0105 searchPrevious = pyqtSignal() """ searchPrevious() **Signal** emitted, when 'Search Previous' had been pressed """ # pylint: disable=W0105 replaceFileOne = pyqtSignal(str) """ replaceFileOne(replText) **Signal** emitted, when 'Replace' had been pressed """ # pylint: disable=W0105 replaceFileAll = pyqtSignal(str) """ replaceFileAll(replText) **Signal** emitted, when 'Replace All' had been pressed """ # pylint: disable=W0105 def __init__(self, plugin): QFrame.__init__(self, core.workspace()) self._mode = None self.plugin = plugin uic.loadUi(os.path.join(os.path.dirname(__file__), 'SearchWidget.ui'), self) self.cbSearch.setCompleter(None) self.cbReplace.setCompleter(None) self.cbMask.setCompleter(None) self.fsModel = QDirModel(self.cbPath.lineEdit()) self.fsModel.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot) self.cbPath.lineEdit().setCompleter( QCompleter(self.fsModel, self.cbPath.lineEdit())) self._pathBackspaceShortcut = QShortcut(QKeySequence("Ctrl+Backspace"), self.cbPath, self._onPathBackspace) self._pathBackspaceShortcut.setContext(Qt.WidgetWithChildrenShortcut) # TODO QDirModel is deprecated but QCompleter does not yet handle # QFileSystemodel - please update when possible.""" self.cbSearch.setCompleter(None) self.pbSearchStop.setVisible(False) self.pbReplaceCheckedStop.setVisible(False) self._progress = QProgressBar(self) self._progress.setAlignment(Qt.AlignCenter) self._progress.setToolTip(self.tr("Search in progress...")) self._progress.setMaximumSize(QSize(80, 16)) core.mainWindow().statusBar().insertPermanentWidget(1, self._progress) self._progress.setVisible(False) # cd up action self.tbCdUp = QToolButton(self.cbPath.lineEdit()) self.tbCdUp.setIcon(QIcon(":/enkiicons/go-up.png")) self.tbCdUp.setCursor(Qt.ArrowCursor) self.tbCdUp.installEventFilter(self) # for drawing button self.cbSearch.installEventFilter( self) # for catching Tab and Shift+Tab self.cbReplace.installEventFilter( self) # for catching Tab and Shift+Tab self.cbPath.installEventFilter(self) # for catching Tab and Shift+Tab self.cbMask.installEventFilter(self) # for catching Tab and Shift+Tab self._closeShortcut = QShortcut(QKeySequence("Esc"), self) self._closeShortcut.setContext(Qt.WidgetWithChildrenShortcut) self._closeShortcut.activated.connect(self.hide) # connections self.cbSearch.lineEdit().textChanged.connect( self._onSearchRegExpChanged) self.cbSearch.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbReplace.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbPath.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbMask.lineEdit().returnPressed.connect(self._onReturnPressed) self.cbRegularExpression.stateChanged.connect( self._onSearchRegExpChanged) self.cbCaseSensitive.stateChanged.connect(self._onSearchRegExpChanged) self.cbWholeWord.stateChanged.connect(self._onSearchRegExpChanged) self.tbCdUp.clicked.connect(self._onCdUpPressed) self.pbNext.pressed.connect(self.searchNext) self.pbPrevious.pressed.connect(self.searchPrevious) self.pbSearchStop.pressed.connect(self.searchInDirectoryStopPressed) self.pbReplaceCheckedStop.pressed.connect( self.replaceCheckedStopPressed) core.mainWindow().hideAllWindows.connect(self.hide) core.workspace().escPressed.connect(self.hide) core.workspace().currentDocumentChanged.connect( lambda old, new: self.setVisible(self.isVisible() and new is not None)) def show(self): """Reimplemented function. Sends signal """ super(SearchWidget, self).show() self.visibilityChanged.emit(self.isVisible()) def hide(self): """Reimplemented function. Sends signal, returns focus to workspace """ super(SearchWidget, self).hide() core.workspace().focusCurrentDocument() self.visibilityChanged.emit(self.isVisible()) def setVisible(self, visible): """Reimplemented function. Sends signal """ super(SearchWidget, self).setVisible(visible) self.visibilityChanged.emit(self.isVisible()) def _regExEscape(self, text): """Improved version of re.escape() Doesn't escape space, comma, underscore. Escapes \n and \t """ text = re.escape(text) # re.escape escapes space, comma, underscore, but, it is not necessary and makes text not readable for symbol in (' ,_=\'"/:@#%&'): text = text.replace('\\' + symbol, symbol) text = text.replace('\\\n', '\\n') text = text.replace('\\\t', '\\t') return text def _makeEscapeSeqsVisible(self, text): """Replace invisible \n and \t with escape sequences """ text = text.replace('\\', '\\\\') text = text.replace('\t', '\\t') text = text.replace('\n', '\\n') return text def setMode(self, mode): """Change search mode. i.e. from "Search file" to "Replace directory" """ if self._mode == mode and self.isVisible(): if core.workspace().currentDocument() is not None and \ not core.workspace().currentDocument().hasFocus(): self.cbSearch.lineEdit().selectAll() self.cbSearch.setFocus() self._mode = mode # Set Search and Replace text document = core.workspace().currentDocument() if document is not None and \ document.hasFocus() and \ document.qutepart.selectedText: searchText = document.qutepart.selectedText self.cbReplace.setEditText(self._makeEscapeSeqsVisible(searchText)) if self.cbRegularExpression.checkState() == Qt.Checked: searchText = self._regExEscape(searchText) self.cbSearch.setEditText(searchText) if not self.cbReplace.lineEdit().text() and \ self.cbSearch.lineEdit().text() and \ not self.cbRegularExpression.checkState() == Qt.Checked: replaceText = self.cbSearch.lineEdit().text().replace('\\', '\\\\') self.cbReplace.setEditText(replaceText) # Move focus to Search edit self.cbSearch.setFocus() self.cbSearch.lineEdit().selectAll() # Set search path if mode & MODE_FLAG_DIRECTORY and \ not (self.isVisible() and self.cbPath.isVisible()): try: searchPath = os.path.abspath(str(os.path.curdir)) except OSError: # current directory might have been deleted pass else: self.cbPath.setEditText(searchPath) # Set widgets visibility flag according to state widgets = (self.wSearch, self.pbPrevious, self.pbNext, self.pbSearch, self.wReplace, self.wPath, self.pbReplace, self.pbReplaceAll, self.pbReplaceChecked, self.wOptions, self.wMask) # wSear pbPrev pbNext pbSear wRepl wPath pbRep pbRAll pbRCHK wOpti wMask visible = \ {MODE_SEARCH: (1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,), MODE_REPLACE: (1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0,), MODE_SEARCH_DIRECTORY: (1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1,), MODE_REPLACE_DIRECTORY: (1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1,), MODE_SEARCH_OPENED_FILES: (1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1,), MODE_REPLACE_OPENED_FILES: (1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1,)} for i, widget in enumerate(widgets): widget.setVisible(visible[mode][i]) # Search next button text if mode == MODE_REPLACE: self.pbNext.setText('Next') else: self.pbNext.setText('Next↵') # Finaly show all with valid size self.show() # show before updating widgets and labels self._updateLabels() self._updateWidgets() def eventFilter(self, object_, event): """ Event filter for mode switch tool button Draws icons in the search and path lineEdits """ if event.type( ) == QEvent.Paint and object_ is self.tbCdUp: # draw CdUp button in search path QLineEdit toolButton = object_ lineEdit = self.cbPath.lineEdit() lineEdit.setContentsMargins(lineEdit.height(), 0, 0, 0) height = lineEdit.height() availableRect = QRect(0, 0, height, height) if toolButton.rect() != availableRect: toolButton.setGeometry(availableRect) painter = QPainter(toolButton) toolButton.icon().paint(painter, availableRect) return True elif event.type( ) == QEvent.KeyPress: # Tab and Shift+Tab in QLineEdits if event.key() == Qt.Key_Tab: self._moveFocus(1) return True elif event.key() == Qt.Key_Backtab: self._moveFocus(-1) return True return QFrame.eventFilter(self, object_, event) def _onReturnPressed(self): """Return or Enter pressed on widget. Search next or Replace next """ if self.pbReplace.isVisible(): self.pbReplace.click() elif self.pbNext.isVisible(): self.pbNext.click() elif self.pbSearch.isVisible(): self.pbSearch.click() elif self.pbSearchStop.isVisible(): self.pbSearchStop.click() def _onPathBackspace(self): """Ctrl+Backspace pressed on path. Remove 1 path level. Default behavior would be to remove one word on Linux or all on Windows """ path = self.cbPath.currentText() if path.endswith('/') or \ path.endswith('\\'): path = path[:-1] head, tail = os.path.split(path) if head and \ head != path: if not head.endswith(os.sep): head += os.sep self.cbPath.lineEdit().setText(head) def _moveFocus(self, step): """Move focus forward or backward according to step. Standard Qt Keyboard focus algorithm doesn't allow circular navigation """ allFocusableWidgets = (self.cbSearch, self.cbReplace, self.cbPath, self.cbMask) visibleWidgets = [ widget for widget in allFocusableWidgets if widget.isVisible() ] try: focusedIndex = visibleWidgets.index(QApplication.focusWidget()) except ValueError: print('Invalid focused widget in Search Widget', file=sys.stderr) return nextFocusedIndex = (focusedIndex + step) % len(visibleWidgets) visibleWidgets[nextFocusedIndex].setFocus() visibleWidgets[nextFocusedIndex].lineEdit().selectAll() def _updateLabels(self): """Update 'Search' 'Replace' 'Path' labels geometry """ width = 0 if self.lSearch.isVisible(): width = max(width, self.lSearch.minimumSizeHint().width()) if self.lReplace.isVisible(): width = max(width, self.lReplace.minimumSizeHint().width()) if self.lPath.isVisible(): width = max(width, self.lPath.minimumSizeHint().width()) self.lSearch.setMinimumWidth(width) self.lReplace.setMinimumWidth(width) self.lPath.setMinimumWidth(width) def _updateWidgets(self): """Update geometry of widgets with buttons """ width = 0 if self.wSearchRight.isVisible(): width = max(width, self.wSearchRight.minimumSizeHint().width()) if self.wReplaceRight.isVisible(): width = max(width, self.wReplaceRight.minimumSizeHint().width()) if self.wPathRight.isVisible(): width = max(width, self.wPathRight.minimumSizeHint().width()) self.wSearchRight.setMinimumWidth(width) self.wReplaceRight.setMinimumWidth(width) self.wPathRight.setMinimumWidth(width) def updateComboBoxes(self): """Update comboboxes with last used texts """ searchText = self.cbSearch.currentText() replaceText = self.cbReplace.currentText() maskText = self.cbMask.currentText() # search if searchText: index = self.cbSearch.findText(searchText) if index == -1: self.cbSearch.addItem(searchText) # replace if replaceText: index = self.cbReplace.findText(replaceText) if index == -1: self.cbReplace.addItem(replaceText) # mask if maskText: index = self.cbMask.findText(maskText) if index == -1: self.cbMask.addItem(maskText) def _searchPatternTextAndFlags(self): """Get search pattern and flags """ pattern = self.cbSearch.currentText() pattern = pattern.replace( '\u2029', '\n') # replace unicode paragraph separator with habitual \n if not self.cbRegularExpression.checkState() == Qt.Checked: pattern = re.escape(pattern) if self.cbWholeWord.checkState() == Qt.Checked: pattern = r'\b' + pattern + r'\b' flags = 0 if not self.cbCaseSensitive.checkState() == Qt.Checked: flags = re.IGNORECASE return pattern, flags def getRegExp(self): """Read search parameters from controls and present it as a regular expression """ pattern, flags = self._searchPatternTextAndFlags() return re.compile(pattern, flags) def isSearchRegExpValid(self): """Try to compile search pattern to check if it is valid Returns bool result and text error """ pattern, flags = self._searchPatternTextAndFlags() try: re.compile(pattern, flags) except re.error as ex: return False, str(ex) return True, None def _getSearchMask(self): """Get search mask as list of patterns """ mask = [s.strip() for s in self.cbMask.currentText().split(' ')] # remove empty mask = [_f for _f in mask if _f] return mask def setState(self, state): """Change line edit color according to search result """ widget = self.cbSearch.lineEdit() color = { SearchWidget.Normal: QApplication.instance().palette().color(QPalette.Base), SearchWidget.Good: QColor(Qt.green), SearchWidget.Bad: QColor(Qt.red), SearchWidget.Incorrect: QColor(Qt.darkYellow) } stateColor = color[state] if state != SearchWidget.Normal: stateColor.setAlpha(100) pal = widget.palette() pal.setColor(widget.backgroundRole(), stateColor) widget.setPalette(pal) def setSearchInProgress(self, inProgress): """Search thread started or stopped """ self.pbSearchStop.setVisible(inProgress) self.pbSearch.setVisible(not inProgress) self._updateWidgets() self._progress.setVisible(inProgress) def onSearchProgressChanged(self, value, total): """Signal from the thread, progress changed """ self._progress.setValue(value) self._progress.setMaximum(total) def setReplaceInProgress(self, inProgress): """Replace thread started or stopped """ self.pbReplaceCheckedStop.setVisible(inProgress) self.pbReplaceChecked.setVisible(not inProgress) self._updateWidgets() def setSearchInFileActionsEnabled(self, enabled): """Set enabled state for Next, Prev, Replace, ReplaceAll """ for button in (self.pbNext, self.pbPrevious, self.pbReplace, self.pbReplaceAll): button.setEnabled(enabled) def _onSearchRegExpChanged(self): """User edited search text or checked/unchecked checkboxes """ valid, error = self.isSearchRegExpValid() if valid: self.setState(self.Normal) core.mainWindow().statusBar().clearMessage() self.pbSearch.setEnabled(len(self.getRegExp().pattern) > 0) else: core.mainWindow().statusBar().showMessage(error, 3000) self.setState(self.Incorrect) self.pbSearch.setEnabled(False) self.searchRegExpChanged.emit(re.compile('')) return self.searchRegExpChanged.emit(self.getRegExp()) def _onCdUpPressed(self): """User pressed "Up" button, need to remove one level from search path """ text = self.cbPath.currentText() if not os.path.exists(text): return editText = os.path.normpath(os.path.join(text, os.path.pardir)) self.cbPath.setEditText(editText) def on_pbSearch_pressed(self): """Handler of click on "Search" button (for search in directory) """ self.setState(SearchWidget.Normal) self.searchInDirectoryStartPressed.emit(self.getRegExp(), self._getSearchMask(), self.cbPath.currentText()) def on_pbReplace_pressed(self): """Handler of click on "Replace" (in file) button """ self.replaceFileOne.emit(self.cbReplace.currentText()) def on_pbReplaceAll_pressed(self): """Handler of click on "Replace all" (in file) button """ self.replaceFileAll.emit(self.cbReplace.currentText()) def on_pbReplaceChecked_pressed(self): """Handler of click on "Replace checked" (in directory) button """ self.replaceCheckedStartPressed.emit(self.cbReplace.currentText()) def on_pbBrowse_pressed(self): """Handler of click on "Browse" button. Explores FS for search directory path """ path = QFileDialog.getExistingDirectory(self, self.tr("Search path"), self.cbPath.currentText()) if path: self.cbPath.setEditText(path)