class DiaryApp(QtWidgets.QMainWindow): # pylint: disable=too-many-public-methods,too-many-instance-attributes """Diary application class inheriting from QMainWindow.""" def __init__(self, parent=None): """Initialize member variables and GUI.""" self.maxRecentItems = 10 self.markdownAction = None self.newNoteAction = None self.saveNoteAction = None self.deleteNoteAction = None self.exportToHTMLAction = None self.exportToPDFAction = None self.newDiaryAction = None self.openDiaryAction = None self.searchLineAction = None self.recentDiariesActions = None self.clearRecentDiariesAction = None self.quitAction = None self.searchLine = None self.toolbar = None self.fileMenu = None self.noteMenu = None self.noteDate = None self.noteId = None self.recentDiaries = None self.recentNotes = None self.diary = None QtWidgets.QMainWindow.__init__(self, parent) renderer = markdown_math.HighlightRenderer() self.toMarkdown = markdown_math.MarkdownWithMath(renderer=renderer) self.tempFiles = [] self.initUI() self.settings = QtCore.QSettings("markdown-diary", application="settings") self.loadSettings() if self.recentDiaries and os.path.isfile(self.recentDiaries[0]): self.loadDiary(self.recentDiaries[0]) else: self.text.setDisabled(True) self.saveNoteAction.setDisabled(True) self.newNoteAction.setDisabled(True) self.deleteNoteAction.setDisabled(True) self.exportToHTMLAction.setDisabled(True) self.exportToPDFAction.setDisabled(True) self.markdownAction.setDisabled(True) self.searchLineAction.setDisabled(True) def closeEvent(self, event): """Check if there are unsaved changes and display dialog if there are. This redefines the basic close event to give the user a chance to save his work. It also saves the current settings. Args: event (QEvent): """ if self.text.document().isModified(): reply = self.promptToSaveOrDiscard() if reply == QtWidgets.QMessageBox.Cancel: event.ignore() return elif reply == QtWidgets.QMessageBox.Save: self.saveNote() self.writeSettings() def initUI(self): """Initialize the UI - create widgets, set their pars, etc.""" self.window = QtWidgets.QWidget(self) self.splitter = QtWidgets.QSplitter() self.initToolbar() self.initMenu() self.shortcutFindNext = QtWidgets.QShortcut(QtGui.QKeySequence('F3'), self) self.shortcutFindNext.activated.connect(self.searchNext) self.text = MyQTextEdit(self) self.text.setAcceptRichText(False) self.text.setFont( QtGui.QFont( QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont))) self.text.textChanged.connect(self.setTitle) self.web = QWebEngineView(self) self.web.settings().setAttribute( QWebEngineSettings.FocusOnNavigationEnabled, False) self.page = MyWebEnginePage() self.web.setPage(self.page) self.web.loadFinished.connect(self.webLoadFinished) self.highlighter = MarkdownHighlighter(self.text) self.setCentralWidget(self.window) self.setWindowTitle("Markdown Diary") self.stack = QtWidgets.QStackedWidget() self.stack.addWidget(self.text) self.stack.addWidget(self.web) self.tree = QtWidgets.QTreeWidget() self.tree.setUniformRowHeights(True) self.tree.setColumnCount(3) self.tree.setHeaderLabels(["Id", "Date", "Title"]) self.tree.setColumnHidden(0, True) self.tree.setSortingEnabled(True) self.tree.sortByColumn(1, QtCore.Qt.DescendingOrder) self.tree.itemSelectionChanged.connect(self.itemSelectionChanged) self.tree.itemChanged.connect(self.itemChanged) self.tree.itemDoubleClicked.connect(self.itemDoubleClicked) # Disable editing for the 'title' column self.tree.setItemDelegateForColumn(2, DummyItemDelegate()) self.splitter.addWidget(self.stack) self.splitter.addWidget(self.tree) layout = QtWidgets.QHBoxLayout() layout.addWidget(self.splitter) self.window.setLayout(layout) def initToolbar(self): """Initialize toolbar - create QActions and bind to functions, etc.""" self.markdownAction = QtWidgets.QAction(QtGui.QIcon.fromTheme("down"), "Toggle Markdown", self) self.markdownAction.setShortcut("Ctrl+M") self.markdownAction.setStatusTip("Toggle markdown rendering") self.markdownAction.triggered.connect(self.markdownToggle) self.newNoteAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("document-new"), "New note", self) self.newNoteAction.setShortcut("Ctrl+N") self.newNoteAction.setStatusTip("Create a new note") self.newNoteAction.triggered.connect(self.newNote) self.saveNoteAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("document-save"), "Save note", self) self.saveNoteAction.setShortcut("Ctrl+S") self.saveNoteAction.setStatusTip("Save note") self.saveNoteAction.triggered.connect(self.saveNote) self.newDiaryAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("folder-new"), "New diary", self) self.newDiaryAction.setStatusTip("New diary") self.newDiaryAction.triggered.connect(self.newDiary) self.openDiaryAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("document-open"), "Open diary", self) self.openDiaryAction.setShortcut("Ctrl+O") self.openDiaryAction.setStatusTip("Open diary") self.openDiaryAction.triggered.connect(self.openDiary) self.clearRecentDiariesAction = QtWidgets.QAction("Clear list", self) self.clearRecentDiariesAction.setStatusTip("Clear list") self.clearRecentDiariesAction.triggered.connect( lambda: self.clearRecentDiaries()) # pylint: disable=unnecessary-lambda self.quitAction = QtWidgets.QAction("Quit", self) self.quitAction.setStatusTip("Quit the application") self.quitAction.setMenuRole(QtWidgets.QAction.QuitRole) self.quitAction.setShortcut(QtGui.QKeySequence.Quit) self.quitAction.triggered.connect(lambda: self.close()) # pylint: disable=unnecessary-lambda self.deleteNoteAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("remove"), "Delete Note", self) self.deleteNoteAction.setShortcut("Del") self.deleteNoteAction.setStatusTip("Delete note") self.deleteNoteAction.triggered.connect(lambda: self.deleteNote()) # pylint: disable=unnecessary-lambda self.exportToHTMLAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("document-export"), "Export to HTML", self) self.exportToHTMLAction.setStatusTip("Export to HTML") self.exportToHTMLAction.triggered.connect(self.exportToHTML) self.exportToPDFAction = QtWidgets.QAction( QtGui.QIcon.fromTheme("document-export"), "Export to PDF", self) self.exportToPDFAction.setStatusTip("Export to PDF") self.exportToPDFAction.triggered.connect(self.exportToPDF) self.searchLine = QtWidgets.QLineEdit(self) self.searchLine.setFixedWidth(200) self.searchLine.setPlaceholderText("Search...") self.searchLine.setClearButtonEnabled(True) self.searchLineAction = QtWidgets.QWidgetAction(self) self.searchLineAction.setDefaultWidget(self.searchLine) self.searchLineAction.setShortcut(QtGui.QKeySequence.Find) self.searchLineAction.triggered.connect(self.selectSearch) self.searchLine.textChanged.connect(self.search) self.searchLine.returnPressed.connect(self.searchNext) self.toolbar = self.addToolBar("Main toolbar") self.toolbar.setFloatable(False) self.toolbar.setAllowedAreas(QtCore.Qt.TopToolBarArea | QtCore.Qt.BottomToolBarArea) self.toolbar.addAction(self.markdownAction) self.toolbar.addSeparator() self.toolbar.addAction(self.newNoteAction) self.toolbar.addAction(self.saveNoteAction) self.toolbar.addAction(self.deleteNoteAction) self.toolbar.addSeparator() self.toolbar.addAction(self.openDiaryAction) self.toolbar.addSeparator() self.toolbar.addAction(self.searchLineAction) def initMenu(self): """Create the main application menu - File, etc.""" self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newDiaryAction) self.fileMenu.addAction(self.openDiaryAction) self.fileMenu.addSeparator() self.recentDiariesActions = [] for _ in range(self.maxRecentItems): action = QtWidgets.QAction(self) action.setVisible(False) self.recentDiariesActions.append(action) self.fileMenu.addAction(action) self.fileMenu.addSeparator() self.fileMenu.addAction(self.clearRecentDiariesAction) self.fileMenu.addSeparator() self.fileMenu.addAction(self.quitAction) self.noteMenu = self.menuBar().addMenu("&Note") self.noteMenu.addAction(self.newNoteAction) self.noteMenu.addAction(self.saveNoteAction) self.noteMenu.addAction(self.deleteNoteAction) self.noteMenu.addSeparator() self.noteMenu.addAction(self.exportToHTMLAction) self.noteMenu.addAction(self.exportToPDFAction) def loadTree(self, metadata): """Load notes tree from diary metadata. Load notes tree from diary metadata and populate the QTreeWidget with it. """ entries = [] for note in metadata: entries.append( QtWidgets.QTreeWidgetItem( [note["note_id"], note["date"], note["title"]])) for entry in entries: entry.setFlags(entry.flags() | QtCore.Qt.ItemIsEditable) self.tree.clear() self.tree.addTopLevelItems(entries) def loadSettings(self): """Load settings via self.settings QSettings object.""" self.recentDiaries = self.settings.value("diary/recent_diaries", []) self.updateRecentDiaries() self.recentNotes = self.settings.value("diary/recent_notes", []) self.resize(self.settings.value("window/size", QtCore.QSize(600, 400))) self.move( self.settings.value("window/position", QtCore.QPoint(200, 200))) self.splitter.setSizes([ int(val) for val in self.settings.value("window/splitter", [70, 30]) ]) toolBarArea = int( self.settings.value("window/toolbar_area", QtCore.Qt.TopToolBarArea)) # addToolBar() actually just moves the specified toolbar if it # was already added, which is what we want self.addToolBar(QtCore.Qt.ToolBarArea(toolBarArea), self.toolbar) self.mathjax = self.settings.value( "mathjax/location", "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js") def writeSettings(self): """Save settings via self.settings QSettings object.""" self.settings.setValue("window/size", self.size()) self.settings.setValue("window/position", self.pos()) self.settings.setValue("window/splitter", self.splitter.sizes()) self.settings.setValue("window/toolbar_area", self.toolBarArea(self.toolbar)) if self.recentDiaries: self.settings.setValue("diary/recent_diaries", self.recentDiaries) if self.recentNotes: self.settings.setValue("diary/recent_notes", self.recentNotes) def markdownToggle(self): """Switch between displaying Markdown source and rendered HTML.""" if self.stack.currentIndex() == 1: self.stack.setCurrentIndex(0) else: self.stack.setCurrentIndex(1) if (self.text.document().isModified()): self.displayHTMLRenderedMarkdown(self.text.toPlainText()) def createHTML(self, markdownText): """Create full, valid HTML from Markdown source. Args: markdownText (str): Markdown source to convert to HTML. Returns: Full HTML page text. """ html = style.header # We load MathJax only when there is a good chance there is # math in the note. We first perform inline math search as # as that should be faster then the re.DOTALL multiline # block math search, which gets executed only if we don't # find inline math. mathInline = re.compile(r"\$(.+?)\$") mathBlock = re.compile(r"^\$\$(.+?)^\$\$", re.DOTALL | re.MULTILINE) if mathInline.search(markdownText) or mathBlock.search(markdownText): html += style.mathjax mathjaxScript = ('<script type="text/javascript" src="{}?config=' 'TeX-AMS-MML_HTMLorMML"></script>\n').format( self.mathjax) html += mathjaxScript html += self.toMarkdown(markdownText) # pylint: disable=not-callable html += style.footer return html def displayHTMLRenderedMarkdown(self, markdownText): """Display HTML rendered Markdown.""" html = self.createHTML(markdownText) # QWebEngineView resolves relative links (like images and stylesheets) # with respect to the baseUrl mainPath = self.diary.fname self.web.setHtml(html, baseUrl=QtCore.QUrl.fromLocalFile(mainPath)) def newNote(self): """Create an empty note and add it to the QTreeWidget. The note is not added to the diary until it is saved. """ self.noteDate = datetime.date.today().isoformat() self.noteId = str(uuid.uuid1()) self.text.clear() self.stack.setCurrentIndex(0) self.text.setFocus() self.text.setText("# <Untitled note>") self.saveNote() self.loadTree(self.diary.data) self.selectItemWithoutReload(self.noteId) # Select the '<Untitled note>' part of the new note for convenient # renaming cursor = self.text.textCursor() cursor.setPosition(2) cursor.setPosition(17, QtGui.QTextCursor.KeepAnchor) self.text.setTextCursor(cursor) def saveNote(self): """Save the displayed note. Either updates an existing note or adds a new one to a diary. """ if self.text.toPlainText().lstrip() == "": QtWidgets.QMessageBox.information(self, 'Message', "You can't save an empty note!") return # Notes should begin with a title, so strip any whitespace, # including newlines from the beggining self.diary.saveNote(self.text.toPlainText().lstrip(), self.noteId, self.noteDate) self.text.document().setModified(False) self.setTitle() # Rerender the HTML since we removed the modified flag from # self.text.document() self.displayHTMLRenderedMarkdown(self.text.toPlainText()) # Change the title in the tree, without reloading the tree (that would # cause the filtered results when searching to be lost) self.tree.blockSignals(True) # When there are no items in the tree (new diary) must add the item # first and select it if self.tree.topLevelItemCount() == 0: newItem = QtWidgets.QTreeWidgetItem( [self.noteId, self.noteDate, ""]) self.tree.addTopLevelItem(newItem) self.tree.setCurrentItem(newItem) self.tree.currentItem().setText( 2, self.diary.getNoteMetadata(self.noteId)["title"]) self.tree.blockSignals(False) def deleteNote(self, noteId=None): """Delete a specified note. If there are unsaved changes, prompt the user. Refresh note tree after deletion. Args: noteId (str, optional): UUID of the note to delete """ if noteId is None: noteId = self.noteId noteTitle = self.diary.getNoteMetadata(self.noteId)["title"] deleteMsg = "Do you really want to delete the note '" + noteTitle + "'?" reply = QtWidgets.QMessageBox.question(self, 'Message', deleteMsg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.No: return nextNoteId = self.tree.itemBelow(self.tree.currentItem()).text(0) self.diary.deleteNote(noteId) self.text.document().setModified(False) self.loadTree(self.diary.data) self.tree.setCurrentItem( self.tree.findItems(nextNoteId, QtCore.Qt.MatchExactly)[0]) def newDiary(self): """Display a file save dialog and create diary at specified path. Enable relevant toolbar items (new note, save note, etc.), in case no diary was open before and they were disabled. """ if self.text.document().isModified(): reply = self.promptToSaveOrDiscard() if reply == QtWidgets.QMessageBox.Cancel: return elif reply == QtWidgets.QMessageBox.Discard: self.text.document().setModified(False) elif reply == QtWidgets.QMessageBox.Save: self.saveNote() fname = QtWidgets.QFileDialog.getSaveFileName( caption="Create a New Diary", filter="Markdown Files (*.md);;All Files (*)")[0] if fname: with open(fname, 'w'): os.utime(fname) self.loadDiary(fname) self.newNote() def openDiary(self): """Display a file open dialog and load the selected diary. Enable relevant toolbar items (new note, save note, etc.), in case no diary was open before and they were disabled. """ if self.text.document().isModified(): reply = self.promptToSaveOrDiscard() if reply == QtWidgets.QMessageBox.Cancel: return elif reply == QtWidgets.QMessageBox.Discard: self.text.document().setModified(False) elif reply == QtWidgets.QMessageBox.Save: self.saveNote() fname = QtWidgets.QFileDialog.getOpenFileName( caption="Open Diary", filter="Markdown Files (*.md);;All Files (*)")[0] if fname: if self.isValidDiary(fname): self.loadDiary(fname) self.text.setDisabled(False) self.saveNoteAction.setDisabled(False) self.newNoteAction.setDisabled(False) self.deleteNoteAction.setDisabled(False) self.exportToHTMLAction.setDisabled(False) self.exportToPDFAction.setDisabled(False) self.markdownAction.setDisabled(False) self.searchLineAction.setDisabled(False) else: print("ERROR:" + fname + "is not a valid diary file!") @staticmethod def isValidDiary(fname): """Check if a file path leads to a valid diary. Args: fname (str): Path to a diary file to be validated. Returns: bool: True for valid, False for invalid diary. """ # TODO Implement checks return True def loadDiary(self, fname): """Load diary from file. Display last note from the diary if it exists. Args: fname (str): Path to a file containing a diary. """ if self.text.document().isModified(): reply = self.promptToSaveOrDiscard() if reply == QtWidgets.QMessageBox.Cancel: return elif reply == QtWidgets.QMessageBox.Discard: self.text.document().setModified(False) elif reply == QtWidgets.QMessageBox.Save: self.saveNote() self.updateRecentDiaries(fname) self.diary = diary.Diary(fname) # Save the diary path to QWebEnginePage, so we can fix external links, # which (for some reason) look like file://DIARY_PATH/EXTERNAL_LINK self.page.diaryPath = fname self.loadTree(self.diary.data) # Display empty editor if the diary has no notes (e.g., new diary) if not self.diary.data: self.text.clear() self.stack.setCurrentIndex(0) return # Check if we saved a recent noteId for this diary and open it if we # did, otherwise open the newest note lastNoteId = "" for recentNote in self.recentNotes: if recentNote in (metaDict["note_id"] for metaDict in self.diary.data): lastNoteId = recentNote break if lastNoteId == "": lastNoteId = self.diary.data[-1]["note_id"] self.tree.setCurrentItem( self.tree.findItems(lastNoteId, QtCore.Qt.MatchExactly)[0]) self.stack.setCurrentIndex(1) def updateRecentDiaries(self, fname=""): """Update list of recently opened diaries. When fname is specified, adds/moves the specified diary to the beggining of a list. Otherwise just populates the list in the file menu. Args: fname (str, optional): The most recent diary to be added/moved to the top of the list. """ if fname != "": if fname in self.recentDiaries: self.recentDiaries.remove(fname) self.recentDiaries.insert(0, fname) if len(self.recentDiaries) > self.maxRecentItems: del self.recentDiaries[self.maxRecentItems:] for recent in self.recentDiariesActions: recent.setVisible(False) for i, recent in enumerate(self.recentDiaries): self.recentDiariesActions[i].setText(os.path.basename(recent)) self.recentDiariesActions[i].setData(recent) self.recentDiariesActions[i].setVisible(True) # Multiple signals can be connected, so to avoid old signals we # disconnect them self.recentDiariesActions[i].triggered.disconnect() self.recentDiariesActions[i].triggered.connect( lambda dummy=False, recent=recent: self.loadDiary(recent)) def clearRecentDiaries(self): """Clear the list of recent diaries.""" self.recentDiaries = [] self.updateRecentDiaries() def updateRecentNotes(self, noteId): """Update list of recently viewed notes. Adds/moves the specified noteId to the beggining of the list. Args: noteId (str): The most recent note to be added/moved to the top of the list. """ if noteId in self.recentNotes: self.recentNotes.remove(noteId) self.recentNotes.insert(0, noteId) if len(self.recentNotes) > self.maxRecentItems: del self.recentNotes[self.maxRecentItems:] @staticmethod def promptToSaveOrDiscard(): """Display a message box asking whether to save or discard changes. Returns: One of the three options: QtWidgets.QMessageBox.Save QtWidgets.QMessageBox.Discard QtWidgets.QMessageBox.Cancel """ msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle("Save or Discard") msgBox.setIcon(QtWidgets.QMessageBox.Question) msgBox.setText("Save changes before closing note?") msgBox.setStandardButtons(QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel) msgBox.setDefaultButton(QtWidgets.QMessageBox.Save) return msgBox.exec() def itemSelectionChanged(self): """Display a new selected note. Prompts the user if there is unsaved work. If there is an active search, reruns it on the new note. """ if len(self.tree.selectedItems()) == 0: # pylint: disable=len-as-condition return newNoteId = self.tree.selectedItems()[0].text(0) if self.text.document().isModified(): # Keep the cursor on the note in question while the dialog is # displayed self.tree.blockSignals(True) self.tree.setCurrentItem( self.tree.findItems(self.noteId, QtCore.Qt.MatchExactly)[0]) self.tree.blockSignals(False) reply = self.promptToSaveOrDiscard() # We just save note/flag it as unmodified and recursively call # this method again if reply == QtWidgets.QMessageBox.Save: self.saveNote() self.tree.setCurrentItem( self.tree.findItems(newNoteId, QtCore.Qt.MatchExactly)[0]) elif reply == QtWidgets.QMessageBox.Discard: self.text.document().setModified(False) self.tree.setCurrentItem( self.tree.findItems(newNoteId, QtCore.Qt.MatchExactly)[0]) return self.displayNote(newNoteId) if self.searchLine.text() != "": # Search in the editor (searching in WebView happens # asynchronously, once the page is loaded) self.text.highlightSearch(self.searchLine.text()) def displayNote(self, noteId): """Display a specified note.""" self.text.setText(self.diary.getNote(noteId)) self.setTitle() self.noteId = noteId self.updateRecentNotes(noteId) self.noteDate = self.diary.getNoteMetadata(noteId)["date"] self.displayHTMLRenderedMarkdown(self.text.toPlainText()) def selectSearch(self): """Focus the search widget and select its contents.""" self.searchLine.setFocus() self.searchLine.selectAll() def search(self): """Search and highlight text in all notes. Highlights text occurrences in the editor and web view. Searches all notes for the text and removes non-matching from the note tree. The text to search for is taken from the searchLine widget. """ if self.searchLine.text() == "": self.loadTree(self.diary.data) self.selectItemWithoutReload(self.noteId) self.text.highlightSearch("") self.web.findText("") return # Search in the editor self.text.highlightSearch(self.searchLine.text()) # Search in the WebView self.web.findText(self.searchLine.text()) # Search for matching notes entries = self.diary.searchNotes(self.searchLine.text()) self.loadTree(entries) if entries: # Select the matching item in the tree. Either the current one, if it # is among the matching items, or the last matching one. if self.noteId in (entry["note_id"] for entry in entries): self.selectItemWithoutReload(self.noteId) else: self.tree.setCurrentItem( self.tree.findItems(entries[-1]["note_id"], QtCore.Qt.MatchExactly)[0]) self.searchLine.setFocus() def searchNext(self): """Move main highlight (and scroll) to the next search match.""" self.web.findText(self.searchLine.text()) if self.text.extraSelections(): if not self.text.find(self.searchLine.text()): self.text.moveCursor(QtGui.QTextCursor.Start) self.text.find(self.searchLine.text()) def setTitle(self): """Set the application title; add '*' if editor in dirty state.""" if self.text.document().isModified(): self.setWindowTitle("*Markdown Diary") else: self.setWindowTitle("Markdown Diary") if hasattr(self, 'diary') and self.diary is not None: self.setWindowTitle(self.windowTitle() + " - " + os.path.basename(self.diary.fname)) def itemDoubleClicked(self, _item, column): """Decide action based on which column the user clicked. If the user clicked the title, toggle Markdown. """ if column == 2: self.markdownToggle() def itemChanged(self, item, _column): """Update note when some of its metadata are changed in the TreeWidget. Currently only the date can be changed. The date is first validated, otherwise no action is taken. """ noteId = item.text(0) noteDate = item.text(1) if self.diary.isValidDate(noteDate): self.diary.changeNoteDate(noteId, noteDate) self.noteDate = noteDate self.loadTree(self.diary.data) self.selectItemWithoutReload(noteId) else: print("Invalid date") self.loadTree(self.diary.data) self.selectItemWithoutReload(noteId) def selectItemWithoutReload(self, noteId): """Select an item in the QtTreeWidget without reloading the note.""" self.tree.blockSignals(True) self.tree.setCurrentItem( self.tree.findItems(noteId, QtCore.Qt.MatchExactly)[0]) self.tree.blockSignals(False) def exportToHTML(self): """Export the displayed note to HTML.""" markdownText = self.diary.getNote(self.noteId) html = self.createHTML(markdownText) # To be able to load the CSS during normal operation correctly, we have # to use an absolute path. This is not desirable when exporting to # HTML, so we change it to a relative path. newhtml = "" stillInHead = True for line in html.splitlines(): if stillInHead: if "github-markdown.css" in line: newhtml += '<link rel="stylesheet" href="css/github-markdown.css">\n' elif "github-pygments.css" in line: newhtml += '<link rel="stylesheet" href="css/github-pygments.css">\n' else: newhtml += line + '\n' if "</head>" in line: stillInHead = False else: newhtml += line + '\n' fname = QtWidgets.QFileDialog.getSaveFileName( caption="Export Note to HTML", filter="HTML Files (*.html);;All Files (*)")[0] if fname: with open(fname, 'w') as f: os.utime(fname) f.write(newhtml) def exportToPDF(self): """Export the displayed note to PDF.""" fname = QtWidgets.QFileDialog.getSaveFileName( caption="Export Note to PDF", filter="PDF Files (*.pdf);;All Files (*)")[0] if fname: # Make sure we export the current version of the text if self.stack.currentIndex() == 0: self.displayHTMLRenderedMarkdown(self.text.toPlainText()) pageLayout = QtGui.QPageLayout(QtGui.QPageSize(QtGui.QPageSize.A4), QtGui.QPageLayout.Landscape, QtCore.QMarginsF(0, 0, 0, 0)) self.web.page().printToPdf(fname, pageLayout) def webLoadFinished(self): if self.searchLine.text() != "": # Search in the WebView self.web.findText(self.searchLine.text()) def __del__(self): """Clean up temporary files on exit.""" # Delete temporary files # We put it into __del__ deliberately, so if one wants, one can avoid # the temporary files being deleted by killing the process. This might # be useful, e.g., in case of accidental over-write. for f in self.tempFiles: os.unlink(f.name)
class MainWindow(BaseWindow): def __init__(self): super().__init__() self.qObject = QObject() self.initConsole() self.initUI() self.show() def initConsole(self): self.consoleWindow = ButtomWindow() def initUI(self): self.resize(int(Utils.getWindowWidth() * 0.8), int(Utils.getWindowHeight() * 0.8)) self.initMenuBar() self.initLayout() super().initWindow() def initMenuBar(self): menuBar = self.menuBar() fileMenu = menuBar.addMenu('File') disConnectAction = QAction(IconTool.buildQIcon('disconnect.png'), '&Disconnect', self) disConnectAction.setShortcut('Ctrl+D') disConnectAction.setShortcutContext(Qt.ApplicationShortcut) disConnectAction.triggered.connect(self.restartProgram) settingAction = QAction(IconTool.buildQIcon('setting.png'), '&Setting...', self) settingAction.setShortcut('Ctrl+Shift+S') settingAction.setShortcutContext(Qt.ApplicationShortcut) settingAction.triggered.connect(lambda: STCLogger().d('setting')) clearCacheAction = QAction(IconTool.buildQIcon('clearCache.png'), '&ClearCache', self) clearCacheAction.setShortcut('Ctrl+Alt+C') clearCacheAction.setShortcutContext(Qt.ApplicationShortcut) clearCacheAction.triggered.connect(self.clearCache) clearSearchHistoryAction = QAction( IconTool.buildQIcon('clearCache.png'), '&ClearSearchHistory', self) clearSearchHistoryAction.triggered.connect(self.clearSearchHistory) settingLogPathAction = QAction(IconTool.buildQIcon('path.png'), '&LogPath', self) settingLogPathAction.triggered.connect(self.setLogPath) fileMenu.addAction(disConnectAction) fileMenu.addAction(clearCacheAction) fileMenu.addAction(settingLogPathAction) fileMenu.addAction(clearSearchHistoryAction) # fileMenu.addAction(settingAction) # fileMenu.addAction(showLogAction) # settingAction.triggered.connect(self.openSettingWindow) editMenu = menuBar.addMenu('Edit') findAction = QAction(IconTool.buildQIcon('find.png'), '&Find', self) findAction.setShortcut('Ctrl+F') findAction.triggered.connect(self.findActionClick) editMenu.addAction(findAction) focusItemAction = QAction(IconTool.buildQIcon('focus.png'), '&Focus Item', self) focusItemAction.triggered.connect(self.focusChooseItem) editMenu.addAction(focusItemAction) self.chooseItemType = '' self.chooseItemId = '' settingMenu = menuBar.addMenu('Setting') autoLoginAction = QAction(IconTool.buildQIcon('setting.png'), '&Auto Login', self) autoLoginAction.setShortcutContext(Qt.ApplicationShortcut) autoLoginAction.triggered.connect(self.setAutoState) settingMenu.addAction(autoLoginAction) helpMenu = menuBar.addMenu('Help') aboutAction = QAction(IconTool.buildQIcon('about.png'), '&About', self) aboutAction.triggered.connect(self.openAboutWindow) helpMenu.addAction(aboutAction) def closeEvent(self, e): self.settings.setValue('searchHistory', self.searchHistory) e.accept() def openAboutWindow(self): print('open about') self.aboutWindow = AboutWindow() self.aboutWindow.show() def setAutoState(self): reply = QMessageBox.question(self, "提示", "是否取消自动登录功能", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: Utils.setAutoLoginState(False) def restartProgram(self): from XulDebugTool.ui.ConnectWindow import ConnectWindow # 不应该在这里导入,但是放在前面会有问题 try: hasAbout = object.__getattribute__(self, "aboutWindow") except: hasAbout = None if hasAbout is not None: self.aboutWindow.close() print("新建连接页面") self.con = ConnectWindow() self.close() # def openSettingWindow(self): # self.tableInfoModel = SettingWindow() # self.tableInfoModel.show() def initLayout(self): # ----------------------------left layout---------------------------- # self.treeModel = QStandardItemModel() self.pageItem = QStandardItem(ROOT_ITEM_PAGE) self.pageItem.type = ITEM_TYPE_PAGE_ROOT self.buildPageItem() self.userobjectItem = QStandardItem(ROOT_ITEM_USER_OBJECT) self.userobjectItem.type = ITEM_TYPE_USER_OBJECT_ROOT self.buildUserObjectItem() self.providerRequestItem = QStandardItem(ROOT_ITEM_PROVIDER_REQUESTS) self.providerRequestItem.type = ITEM_TYPE_PROVIDER_REQUESTS_ROOT self.pluginItem = QStandardItem(ROOT_ITEM_PLUGIN) self.pluginItem.type = ITEM_TYPE_PLUGIN_ROOT self.treeModel.appendColumn([ self.pageItem, self.userobjectItem, self.providerRequestItem, self.pluginItem ]) self.treeModel.setHeaderData(0, Qt.Horizontal, 'Model') self.treeView = QTreeView() self.treeView.setModel(self.treeModel) self.treeView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.openContextMenu) self.treeView.doubleClicked.connect(self.onTreeItemDoubleClicked) self.treeView.clicked.connect(self.getDebugData) leftContainer = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 6, 0) # left, top, right, bottom layout.addWidget(self.treeView) leftContainer.setLayout(layout) # ----------------------------middle layout---------------------------- # middleContainer = QWidget() # search shall start not before the user completed typing # filter_delay = DelayedExecutionTimer(self) # new_column.search_bar.textEdited[str].connect(filter_delay.trigger) # filter_delay.triggered[str].connect(self.search) self.tabBar = QTabBar() self.tabBar.setUsesScrollButtons(False) self.tabBar.setDrawBase(False) # self.tabBar.addTab('tab1') # self.tabBar.addTab('tab2') self.pathBar = QWidget() layout = QHBoxLayout() layout.setAlignment(Qt.AlignLeft) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self.pathBar.setLayout(layout) self.searchHolder = QWidget() layout = QHBoxLayout() layout.addWidget(self.tabBar) layout.addWidget(self.pathBar) layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding)) self.searchHolder.setLayout(layout) self.searchHolder.layout().setContentsMargins(6, 6, 6, 0) self.tabContentWidget = QWidget() self.browser = QWebEngineView() self.browser.setZoomFactor(1.3) self.channel = QWebChannel() self.webObject = WebShareObject() self.channel.registerObject('bridge', self.webObject) self.browser.page().setWebChannel(self.channel) self.webObject.jsCallback.connect(lambda value: self.addUpdate(value)) qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js') if not qwebchannel_js.open(QIODevice.ReadOnly): raise SystemExit('Failed to load qwebchannel.js with error: %s' % qwebchannel_js.errorString()) qwebchannel_js = bytes(qwebchannel_js.readAll()).decode('utf-8') script = QWebEngineScript() script.setSourceCode(qwebchannel_js) script.setInjectionPoint(QWebEngineScript.DocumentCreation) script.setName('qtwebchannel.js') script.setWorldId(QWebEngineScript.MainWorld) script.setRunsOnSubFrames(True) self.browser.page().scripts().insert(script) Utils.scriptCreator(os.path.join('..', 'resources', 'js', 'event.js'), 'event.js', self.browser.page()) self.browser.page().setWebChannel(self.channel) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.initQCheckBoxUI()) layout.addWidget(self.initSearchView()) layout.addWidget(self.browser) self.tabContentWidget.setLayout(layout) self.searchWidget.hide() middleContainer.stackedWidget = QStackedWidget() self.url = XulDebugServerHelper.HOST + 'list-pages' self.showXulDebugData(self.url) middleContainer.stackedWidget.addWidget(self.tabContentWidget) middleContainer.stackedWidget.addWidget(QLabel('tab2 content')) self.tabBar.currentChanged.connect( lambda: middleContainer.stackedWidget.setCurrentIndex( self.tabBar.currentIndex())) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.searchHolder) layout.addWidget(middleContainer.stackedWidget) middleContainer.setLayout(layout) # ----------------------------right layout---------------------------- # self.rightSiderClickInfo = 'Property' self.rightSiderTabWidget = QTabWidget() self.rightSiderTabBar = QTabBar() self.rightSiderTabWidget.setTabBar(self.rightSiderTabBar) self.rightSiderTabWidget.setTabPosition(QTabWidget.East) self.favoriteTreeView = FavoriteTreeView(self) # self.propertyEditor = PropertyEditor(['Key', 'Value']) self.inputWidget = UpdateProperty() self.rightSiderTabWidget.addTab(self.inputWidget, IconTool.buildQIcon('property.png'), 'Property') self.rightSiderTabWidget.setStyleSheet( ('QTab::tab{height:60px;width:32px;color:black;padding:0px}' 'QTabBar::tab:selected{background:lightgray}')) # self.rightSiderTabWidget.addTab(self.propertyEditor,IconTool.buildQIcon('property.png'),'property') self.rightSiderTabWidget.addTab(self.favoriteTreeView, IconTool.buildQIcon('favorites.png'), 'Favorites') self.rightSiderTabBar.tabBarClicked.connect(self.rightSiderClick) # ----------------------------entire layout---------------------------- # self.contentSplitter = QSplitter(Qt.Horizontal) self.contentSplitter.setHandleWidth(0) # thing to grab the splitter self.contentSplitter.addWidget(leftContainer) self.contentSplitter.addWidget(middleContainer) self.contentSplitter.addWidget(self.rightSiderTabWidget) self.contentSplitter.setStretchFactor(0, 0) self.contentSplitter.setStretchFactor(1, 6) self.contentSplitter.setStretchFactor(2, 6) self.mainSplitter = QSplitter(Qt.Vertical) self.mainSplitter.setHandleWidth(0) self.mainSplitter.addWidget(self.contentSplitter) self.mainSplitter.addWidget(self.consoleWindow) self.mainSplitter.setStretchFactor(1, 0) self.mainSplitter.setStretchFactor(2, 1) self.setCentralWidget(self.mainSplitter) # 默认隐藏掉复选框 self.groupBox.setHidden(True) def addUpdate(self, value=None): self.inputWidget.initData(self.pageId, value) self.inputWidget.updateItemUI() dict = json.loads(value) if dict['action'] == "click": self.chooseItemId = dict['Id'] self.chooseItemType = Utils.findNodeById(dict['Id'], dict['xml']).tag elif dict['action'] == "load": self.browser.load(QUrl(dict['url'])) else: pass def focusChooseItem(self): if self.chooseItemType in ('area', 'item'): XulDebugServerHelper.focusChooseItemUrl(self.chooseItemId) def initQCheckBoxUI(self): self.groupBox = QGroupBox() self.skipPropCheckBox = QCheckBox(SKIP_PROP, self) self.skipPropCheckBox.setChecked(False) self.skipPropCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.skipPropCheckBox, SKIP_PROP)) self.withChildrenCheckBox = QCheckBox(WITH_CHILDREN, self) self.withChildrenCheckBox.setChecked(False) self.withChildrenCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withChildrenCheckBox, WITH_CHILDREN )) self.withBindingDataCheckBox = QCheckBox(WITH_BINDING_DATA, self) self.withBindingDataCheckBox.setChecked(False) self.withBindingDataCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withBindingDataCheckBox, WITH_BINDING_DATA)) self.withPositionCheckBox = QCheckBox(WITH_POSITION, self) self.withPositionCheckBox.setChecked(False) self.withPositionCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withPositionCheckBox, WITH_POSITION )) self.withSelectorCheckBox = QCheckBox(WITH_SELECTOR, self) self.withSelectorCheckBox.setChecked(False) self.withSelectorCheckBox.stateChanged.connect( lambda: self.clickCheckBox(self.withSelectorCheckBox, WITH_SELECTOR )) checkGrouplayout = QHBoxLayout() checkGrouplayout.addWidget(self.skipPropCheckBox) checkGrouplayout.addWidget(self.withChildrenCheckBox) checkGrouplayout.addWidget(self.withBindingDataCheckBox) checkGrouplayout.addWidget(self.withPositionCheckBox) checkGrouplayout.addWidget(self.withSelectorCheckBox) self.groupBox.setLayout(checkGrouplayout) return self.groupBox def initSearchView(self): self.settings = QSettings('XulDebugTool') self.searchHistory = self.settings.value('searchHistory', []) self.searchWidget = QWidget() self.searchWidget.setStyleSheet( ".QWidget{border:1px solid rgb(220, 220, 220)}") searchPageLayout = QHBoxLayout() self.searchLineEdit = QLineEdit() self.searchIcon = QAction(self) self.searchIcon.setIcon(IconTool.buildQIcon('find_h.png')) self.searchMenu = QMenu(self) for text in self.searchHistory: self.action = QAction( text, self, triggered=lambda: self.searchLineEdit.setText(text)) self.searchMenu.addAction(self.action) self.searchIcon.setMenu(self.searchMenu) self.searchDelIcon = QAction(self) self.searchDelIcon.setIcon(IconTool.buildQIcon('del.png')) self.searchLineEdit.addAction(self.searchIcon, QLineEdit.LeadingPosition) self.searchLineEdit.addAction(self.searchDelIcon, QLineEdit.TrailingPosition) self.searchDelIcon.setVisible(False) self.searchLineEdit.setStyleSheet( "border:2px groove gray;border-radius:10px;padding:2px 4px") searchPageLayout.addWidget(self.searchLineEdit) self.searchLineEdit.textChanged.connect(self.searchPage) self.searchLineEdit.editingFinished.connect(self.saveSearchHistory) self.searchDelIcon.triggered.connect(self.searchDelClick) self.previousBtn = QPushButton() self.previousBtn.setStyleSheet("background:transparent;") self.previousBtn.setIcon(IconTool.buildQIcon('up.png')) self.previousBtn.setFixedSize(15, 20) searchPageLayout.addWidget(self.previousBtn) self.previousBtn.clicked.connect( lambda: self.previousBtnClick(self.searchLineEdit.text())) self.nextBtn = QPushButton() self.nextBtn.setIcon(IconTool.buildQIcon('down.png')) self.nextBtn.setStyleSheet("background:transparent;") self.nextBtn.setFixedSize(15, 20) self.nextBtn.clicked.connect( lambda: self.nextBtnClick(self.searchLineEdit.text())) searchPageLayout.addWidget(self.nextBtn) self.matchCase = QCheckBox("Match Case") self.matchCase.setChecked(False) self.matchCase.stateChanged.connect(self.matchCaseChange) searchPageLayout.addWidget(self.matchCase) self.matchTips = QLabel() self.matchTips.setFixedWidth(100) self.searchClose = QPushButton("×") self.searchClose.setFixedWidth(10) self.searchClose.setStyleSheet("background:transparent;") self.searchClose.clicked.connect(self.searchCloseClick) searchPageLayout.addWidget(self.matchTips) searchPageLayout.addWidget(self.searchClose) self.searchWidget.setLayout(searchPageLayout) return self.searchWidget def clickCheckBox(self, checkBox, name): if checkBox.isChecked(): STCLogger().i('select ' + name) self.selectCheckBoxInfo(name) else: STCLogger().i('cancel ' + name) self.cancelCheckBoxInfo(name) def selectCheckBoxInfo(self, str): if None != self.url: checkedStr = str + '=' + 'true' unCheckedStr = str + '=' + 'false' if self.url.find('?') == -1: self.url += '?' self.url += checkedStr else: if self.url.find(str) == -1: self.url += '&' self.url += checkedStr elif self.url.find(unCheckedStr) != -1: self.url = self.url.replace(unCheckedStr, checkedStr) self.showXulDebugData(self.url) def cancelCheckBoxInfo(self, str): if None != self.url: checkedStr = str + '=' + 'true' if self.url.find(checkedStr) >= -1: split = self.url.split(checkedStr) self.url = ''.join(split) self.url = self.url.replace('&&', '&') self.url = self.url.replace('?&', '?') if self.url.endswith('?'): self.url = self.url[:-1] if self.url.endswith('&'): self.url = self.url[:-1] self.showXulDebugData(self.url) def rightSiderClick(self, index): # 两次单击同一个tabBar时显示隐藏内容区域 if self.rightSiderTabBar.tabText(index) == self.rightSiderClickInfo: if self.rightSiderTabWidget.width() == Utils.getItemHeight(): self.rightSiderTabWidget.setMaximumWidth( Utils.getWindowWidth()) self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight()) else: self.rightSiderTabWidget.setFixedWidth(Utils.getItemHeight()) else: if self.rightSiderTabWidget.width() == Utils.getItemHeight(): self.rightSiderTabWidget.setMaximumWidth( Utils.getWindowWidth()) self.rightSiderTabWidget.setMinimumWidth(Utils.getItemHeight()) self.rightSiderClickInfo = self.rightSiderTabBar.tabText(index) def clearCache(self): r = XulDebugServerHelper.clearAllCaches() if r.status == 200: self.statusBar().showMessage('cache cleanup success') else: self.statusBar().showMessage('cache cleanup failed') def setLogPath(self): file_path = QFileDialog.getSaveFileName(self, 'save file', ConfigHelper.LOGCATPATH, "Txt files(*.txt)") if len(file_path[0]) > 0: ConfigurationDB.saveConfiguration(ConfigHelper.KEY_LOGCATPATH, file_path[0]) @pyqtSlot(QPoint) def openContextMenu(self, point): index = self.treeView.indexAt(point) if not index.isValid(): return item = self.treeModel.itemFromIndex(index) menu = QMenu() if item.type == ITEM_TYPE_PROVIDER: queryAction = QAction( IconTool.buildQIcon('data.png'), '&Query Data...', self, triggered=lambda: self.showQueryDialog(item.data)) queryAction.setShortcut('Alt+Q') menu.addAction(queryAction) copyAction = QAction( IconTool.buildQIcon('copy.png'), '&Copy', self, triggered=lambda: pyperclip.copy('%s' % index.data())) copyAction.setShortcut(QKeySequence.Copy) menu.addAction(copyAction) menu.exec_(self.treeView.viewport().mapToGlobal(point)) @pyqtSlot(QModelIndex) def getDebugData(self, index): item = self.treeModel.itemFromIndex(index) if item.type == ITEM_TYPE_PAGE_ROOT: # 树第一层,page节点 self.buildPageItem() self.url = XulDebugServerHelper.HOST + 'list-pages' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_USER_OBJECT_ROOT: # 树第一层,userObject节点 self.buildUserObjectItem() self.url = XulDebugServerHelper.HOST + 'list-user-objects' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PROVIDER_REQUESTS_ROOT: # 树第一层,Provider Request节点 self.url = XulDebugServerHelper.HOST + 'list-provider-requests' self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PLUGIN_ROOT: # 树第一层,plugin节点 pass elif item.type == ITEM_TYPE_PAGE: # 树第二层,page下的子节点 pageId = item.id self.url = XulDebugServerHelper.HOST + 'get-layout/' + pageId self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_USER_OBJECT: # 树第二层,userObject下的子节点 objectId = item.id self.url = XulDebugServerHelper.HOST + 'get-user-object/' + objectId self.showXulDebugData(self.url) elif item.type == ITEM_TYPE_PROVIDER: # 树第三层,userObject下的DataService下的子节点 pass self.groupBox.setHidden(item.type != ITEM_TYPE_PAGE) # self.fillPropertyEditor(item.data) @pyqtSlot(QModelIndex) def onTreeItemDoubleClicked(self, index): item = self.treeModel.itemFromIndex(index) if item.type == ITEM_TYPE_PROVIDER: self.showQueryDialog(item.data) def buildPageItem(self): self.pageItem.removeRows(0, self.pageItem.rowCount()) r = XulDebugServerHelper.listPages() if r: pagesNodes = Utils.xml2json(r.data, 'pages') if pagesNodes == '': return # 如果只有一个page,转化出来的json不是数据.分开处理 if isinstance(pagesNodes['page'], list): for i, page in enumerate(pagesNodes['page']): # 把page解析了以后放page节点下 row = QStandardItem(page['@pageId']) row.id = self.pageId = page['@id'] row.data = page row.type = ITEM_TYPE_PAGE self.pageItem.appendRow(row) else: page = pagesNodes['page'] row = QStandardItem(page['@pageId']) row.id = self.pageId = page['@id'] row.data = page row.type = ITEM_TYPE_PAGE self.pageItem.appendRow(row) if self.pageItem.rowCount() > 0: self.pageItem.setText( '%s(%s)' % (ROOT_ITEM_PAGE, self.pageItem.rowCount())) def buildUserObjectItem(self): self.userobjectItem.removeRows(0, self.userobjectItem.rowCount()) r = XulDebugServerHelper.listUserObject() if r: userObjectNodes = Utils.xml2json(r.data, 'objects') # 如果只有一个userObject,转化出来的json不是数据.分开处理 if userObjectNodes and isinstance(userObjectNodes['object'], list): for i, o in enumerate(userObjectNodes['object']): # 把userObject加到User-Object节点下 row = QStandardItem(o['@name']) row.id = o['@id'] row.data = o row.type = ITEM_TYPE_USER_OBJECT self.userobjectItem.appendRow(row) # 如果是DataServcie, 填充所有的Provider到该节点下 if o['@name'] == CHILD_ITEM_DATA_SERVICE: r = XulDebugServerHelper.getUserObject(o['@id']) if r: dataServiceNodes = Utils.xml2json(r.data, 'object') if isinstance( dataServiceNodes['object']['provider'], list): for j, provider in enumerate( dataServiceNodes['object'] ['provider']): dsRow = QStandardItem( provider['ds']['@providerClass']) dsRow.id = provider['@name'] dsRow.data = provider dsRow.type = ITEM_TYPE_PROVIDER row.appendRow(dsRow) else: provider = dataServiceNodes['object'][ 'provider'] dsRow = QStandardItem( provider['ds']['@providerClass']) dsRow.id = provider['@name'] dsRow.data = provider dsRow.type = ITEM_TYPE_PROVIDER row.appendRow(dsRow) # 对Provider按升序排序 row.sortChildren(0) if row.rowCount() > 0: row.setText('%s(%s)' % (row.text(), row.rowCount())) else: # 没有只有一个userObject的情况, 暂不处理 pass if self.userobjectItem.rowCount() > 0: self.userobjectItem.setText( '%s(%s)' % (ROOT_ITEM_USER_OBJECT, self.userobjectItem.rowCount())) def showXulDebugData(self, url): STCLogger().i('request url:' + url) self.browser.load(QUrl(url)) self.statusBar().showMessage(url) def convertProperty(self, k, v): """递归的将多层属性字典转成单层的.""" if isinstance(v, dict): for subk, subv in v.items(): self.convertProperty(subk, subv) else: setattr(self.qObject, k, v) def showQueryDialog(self, data): STCLogger().i('show query dialog: ', data) self.dialog = DataQueryDialog(data) self.dialog.finishSignal.connect(self.onGetQueryUrl) self.dialog.show() def onGetQueryUrl(self, url): STCLogger().i('request url:' + url) self.favoriteTreeView.updateTree() self.browser.load(QUrl(url)) self.statusBar().showMessage(url) def findActionClick(self): self.searchWidget.show() self.searchLineEdit.setFocus() self.searchLineEdit.setText(self.browser.selectedText()) def searchPage(self, text): if not text.strip(): self.searchDelIcon.setVisible(False) else: self.searchDelIcon.setVisible(True) check = self.matchCase.isChecked() if check: self.browser.findText(text, QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(0), lambda result: self.changeMatchTip(result)) def saveSearchHistory(self): text = self.searchLineEdit.text() if text != '' and text not in self.searchHistory: self.action = QAction( text, self, triggered=lambda: self.searchLineEdit.setText(text)) self.searchMenu.addAction(self.action) self.searchHistory.append(text) def clearSearchHistory(self): self.searchHistory = [] def previousBtnClick(self, text): check = self.matchCase.isChecked() if check: self.browser.findText( text, QWebEnginePage.FindFlags(1) | QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(1), lambda result: self.changeMatchTip(result)) def nextBtnClick(self, text): check = self.matchCase.isChecked() if check: self.browser.findText(text, QWebEnginePage.FindFlags(2), lambda result: self.changeMatchTip(result)) else: self.browser.findText(text, QWebEnginePage.FindFlags(0), lambda result: self.changeMatchTip(result)) def matchCaseChange(self): self.browser.findText("") self.searchPage(self.searchLineEdit.text()) def changeMatchTip(self, result): if result: self.matchTips.setText("Find matches") else: self.matchTips.setText("No matches") def searchDelClick(self): self.searchLineEdit.setText("") self.browser.findText("") self.matchTips.setText("") def searchCloseClick(self): self.searchLineEdit.setText("") self.browser.findText("") self.matchTips.setText("") self.searchWidget.hide()