コード例 #1
0
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)
コード例 #2
0
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()