コード例 #1
0
ファイル: hgemail.py プロジェクト: gilshwartz/tortoisehg-caja
    def __init__(self, repo, revs, parent=None, outgoing=False,
                 outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repo = repo
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.body_check.toggled.connect(self._body_mode_clicked)
        self._qui.attach_check.toggled.connect(self._attach_mode_clicked)
        self._qui.inline_check.toggled.connect(self._inline_mode_clicked)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._readsettings()
        QShortcut(QKeySequence('CTRL+Return'), self, self.accept)
        QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)
コード例 #2
0
ファイル: hgemail.py プロジェクト: allenk/tortoisehg-caja
    def __init__(self,
                 repo,
                 revs,
                 parent=None,
                 outgoing=False,
                 outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repo = repo
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.body_check.toggled.connect(self._body_mode_clicked)
        self._qui.attach_check.toggled.connect(self._attach_mode_clicked)
        self._qui.inline_check.toggled.connect(self._inline_mode_clicked)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._readsettings()
        QShortcut(QKeySequence('CTRL+Return'), self, self.accept)
        QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)
コード例 #3
0
ファイル: hgemail.py プロジェクト: gilshwartz/tortoisehg-caja
class EmailDialog(QDialog):
    """Dialog for sending patches via email"""

    def __init__(self, repo, revs, parent=None, outgoing=False,
                 outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repo = repo
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.body_check.toggled.connect(self._body_mode_clicked)
        self._qui.attach_check.toggled.connect(self._attach_mode_clicked)
        self._qui.inline_check.toggled.connect(self._inline_mode_clicked)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._readsettings()
        QShortcut(QKeySequence('CTRL+Return'), self, self.accept)
        QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)

    def closeEvent(self, event):
        self._writesettings()
        super(EmailDialog, self).closeEvent(event)

    def _readsettings(self):
        s = QSettings()
        self.restoreGeometry(s.value('email/geom').toByteArray())
        self._qui.intro_changesets_splitter.restoreState(
            s.value('email/intro_changesets_splitter').toByteArray())

    def _writesettings(self):
        s = QSettings()
        s.setValue('email/geom', self.saveGeometry())
        s.setValue('email/intro_changesets_splitter',
                   self._qui.intro_changesets_splitter.saveState())

    def _readhistory(self):
        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag'):
            w = getattr(self._qui, '%s_edit' % k)
            w.addItems(s.value('email/%s_history' % k).toStringList())
            w.setCurrentIndex(-1)  # unselect

    def _writehistory(self):
        def itercombo(w):
            if w.currentText():
                yield w.currentText()
            for i in xrange(w.count()):
                if w.itemText(i) != w.currentText():
                    yield w.itemText(i)

        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag'):
            w = getattr(self._qui, '%s_edit' % k)
            s.setValue('email/%s_history' % k, list(itercombo(w))[:10])

    def _initchangesets(self, revs):
        def purerevs(revs):
            return scmutil.revrange(self._repo, iter(str(e) for e in revs))

        self._changesets = _ChangesetsModel(self._repo,
                                            # TODO: [':'] is inefficient
                                            revs=purerevs(revs or [':']),
                                            selectedrevs=purerevs(revs),
                                            parent=self)
        self._changesets.dataChanged.connect(self._updateforms)
        self._qui.changesets_view.setModel(self._changesets)

    @property
    def _ui(self):
        return self._repo.ui

    @property
    def _revs(self):
        """Returns list of revisions to be sent"""
        return self._changesets.selectedrevs

    def _filldefaults(self):
        """Fill form by default values"""
        def getfromaddr(ui):
            """Get sender address in the same manner as patchbomb"""
            addr = ui.config('email', 'from') or ui.config('patchbomb', 'from')
            if addr:
                return addr
            try:
                return ui.username()
            except error.Abort:
                return ''

        self._qui.to_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'to', '')))
        self._qui.cc_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'cc', '')))
        self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui)))

        self.setdiffformat(self._ui.configbool('diff', 'git') and 'git' or 'hg')

    def setdiffformat(self, format):
        """Set diff format, 'hg', 'git' or 'plain'"""
        try:
            radio = getattr(self._qui, '%spatch_radio' % format)
        except AttributeError:
            raise ValueError('unknown diff format: %r' % format)

        radio.setChecked(True)

    def getdiffformat(self):
        """Selected diff format"""
        for e in self._qui.patch_frame.children():
            m = re.match(r'(\w+)patch_radio', str(e.objectName()))
            if m and e.isChecked():
                return m.group(1)

        return 'hg'

    def getextraopts(self):
        """Dict of extra options"""
        opts = {}
        for e in self._qui.extra_frame.children():
            m = re.match(r'(\w+)_check', str(e.objectName()))
            if m:
                opts[m.group(1)] = e.isChecked()

        return opts

    def _patchbombopts(self, **opts):
        """Generate opts for patchbomb by form values"""
        def headertext(s):
            # QLineEdit may contain newline character
            return re.sub(r'\s', ' ', hglib.fromunicode(s))

        opts['to'] = [headertext(self._qui.to_edit.currentText())]
        opts['cc'] = [headertext(self._qui.cc_edit.currentText())]
        opts['from'] = headertext(self._qui.from_edit.currentText())
        opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text())
        opts['flag'] = [headertext(self._qui.flag_edit.currentText())]

        if self._qui.bundle_radio.isChecked():
            assert self._outgoing  # only outgoing bundle is supported
            opts['rev'] = map(str, self._outgoingrevs)
            opts['bundle'] = True
        else:
            opts['rev'] = map(str, self._revs)

        def diffformat():
            n = self.getdiffformat()
            if n == 'hg':
                return {}
            else:
                return {n: True}
        opts.update(diffformat())

        opts.update(self.getextraopts())

        def writetempfile(s):
            fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_')
            try:
                os.write(fd, s)
                return fname
            finally:
                os.close(fd)

        opts['intro'] = self._qui.writeintro_check.isChecked()
        if opts['intro']:
            opts['subject'] = headertext(self._qui.subject_edit.text())
            opts['desc'] = writetempfile(hglib.fromunicode(self._qui.body_edit.toPlainText()))
            # TODO: change patchbomb not to use temporary file

        # Include the repo in the command so it can be found when thg is not
        # run from within a hg path
        opts['repository'] = self._repo.root

        return opts

    def _isvalid(self):
        """Filled all required values?"""
        for e in ('to_edit', 'from_edit'):
            if not getattr(self._qui, e).currentText():
                return False

        if self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.text():
            return False

        if not self._revs:
            return False

        return True

    @pyqtSlot()
    def _updateforms(self):
        """Update availability of form widgets"""
        valid = self._isvalid()
        self._qui.send_button.setEnabled(valid)
        self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid)
        self._qui.writeintro_check.setEnabled(not self._introrequired())

        self._qui.bundle_radio.setEnabled(
            self._outgoing and self._changesets.isselectedall())
        self._changesets.setReadOnly(self._qui.bundle_radio.isChecked())
        if self._qui.bundle_radio.isChecked():
            # workaround to disable preview for outgoing bundle because it
            # may freeze main thread
            self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False)

        if self._introrequired():
            self._qui.writeintro_check.setChecked(True)

    def _body_mode_clicked(self):
        # Only allow a single attachment type to be active at a time
        sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked()
        if not sendattachment:
            # If no attachment, ensure that the body mode is enabled
            self._qui.body_check.setChecked(True)

    def _attach_mode_clicked(self):
        sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked()
        self._qui.body_check.setDisabled(not sendattachment)
        if not sendattachment:
            self._qui.body_check.setChecked(True)

        # Only allow a single attachment type to be active at a time
        if self._qui.attach_check.isChecked():
            self._qui.inline_check.setChecked(False)

    def _inline_mode_clicked(self):
        sendattachment = self._qui.attach_check.isChecked() or self._qui.inline_check.isChecked()
        self._qui.body_check.setDisabled(not sendattachment)
        if not sendattachment:
            self._qui.body_check.setChecked(True)

        # Only allow a single attachment type to be active at a time
        if self._qui.inline_check.isChecked():
            self._qui.attach_check.setChecked(False)

    def _initenvelopebox(self):
        for e in ('to_edit', 'from_edit'):
            getattr(self._qui, e).editTextChanged.connect(self._updateforms)

    def accept(self):
        # TODO: want to pass patchbombopts directly
        def cmdargs(opts):
            args = []
            for k, v in opts.iteritems():
                if isinstance(v, bool):
                    if v:
                        args.append('--%s' % k.replace('_', '-'))
                else:
                    for e in isinstance(v, basestring) and [v] or v:
                        args += ['--%s' % k.replace('_', '-'), e]

            return args

        hglib.loadextension(self._ui, 'patchbomb')

        opts = self._patchbombopts()
        try:
            cmd = cmdui.Dialog(['email'] + cmdargs(opts), parent=self)
            cmd.setWindowTitle(_('Sending Email'))
            cmd.setShowOutput(False)
            cmd.finished.connect(cmd.deleteLater)
            if cmd.exec_():
                self._writehistory()
        finally:
            if 'desc' in opts:
                os.unlink(opts['desc'])  # TODO: don't use tempfile

    def _initintrobox(self):
        self._qui.intro_box.hide()  # hidden by default
        self._qui.subject_edit.textChanged.connect(self._updateforms)
        self._qui.writeintro_check.toggled.connect(self._updateforms)

    def _introrequired(self):
        """Is intro message required?"""
        return len(self._revs) > 1 or self._qui.bundle_radio.isChecked()

    def _initpreviewtab(self):
        def initqsci(w):
            w.setUtf8(True)
            w.setReadOnly(True)
            w.setMarginWidth(1, 0)  # hide area for line numbers
            self.lexer = lex = lexers.get_diff_lexer(self)
            fh = qtlib.getfont('fontdiff')
            fh.changed.connect(self.forwardFont)
            lex.setFont(fh.font())
            w.setLexer(lex)
            # TODO: better way to setup diff lexer
          
        initqsci(self._qui.preview_edit)

        self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab)
        self._refreshpreviewtab(self._qui.main_tabs.currentIndex())

    def forwardFont(self, font):
        if self.lexer:
            self.lexer.setFont(font)

    @pyqtSlot(int)
    def _refreshpreviewtab(self, index):
        """Generate preview text if current tab is preview"""
        if self._previewtabindex() != index:
            return

        self._qui.preview_edit.setText(self._preview())

    def _preview(self):
        """Generate preview text by running patchbomb"""
        def loadpatchbomb():
            hglib.loadextension(self._ui, 'patchbomb')
            return extensions.find('patchbomb')

        def wrapui(ui):
            buf = StringIO()
            # TODO: common way to prepare pure ui
            newui = ui.copy()
            newui.setconfig('ui', 'interactive', False)
            newui.setconfig('diff', 'git', False)
            newui.write = lambda *args, **opts: buf.write(''.join(args))
            newui.status = lambda *args, **opts: None
            return newui, buf

        def stripheadmsg(s):
            # TODO: skip until first Content-type: line ??
            return '\n'.join(s.splitlines()[3:])

        ui, buf = wrapui(self._ui)
        opts = self._patchbombopts(test=True)
        try:
            # TODO: fix hgext.patchbomb's implementation instead
            if 'PAGER' in os.environ:
                del os.environ['PAGER']

            loadpatchbomb().patchbomb(ui, self._repo, **opts)
            return stripheadmsg(hglib.tounicode(buf.getvalue()))
        finally:
            if 'desc' in opts:
                os.unlink(opts['desc'])  # TODO: don't use tempfile

    def _previewtabindex(self):
        """Index of preview tab"""
        return self._qui.main_tabs.indexOf(self._qui.preview_tab)

    @pyqtSlot()
    def on_settings_button_clicked(self):
        from tortoisehg.hgqt import settings
        if settings.SettingsDialog(parent=self, focus='email.from').exec_():
            # not use repo.configChanged because it can clobber user input
            # accidentally.
            self._repo.invalidateui()  # force reloading config immediately
            self._filldefaults()

    @pyqtSlot()
    def on_selectall_button_clicked(self):
        self._changesets.selectAll()

    @pyqtSlot()
    def on_selectnone_button_clicked(self):
        self._changesets.selectNone()
コード例 #4
0
ファイル: hgemail.py プロジェクト: seewindcn/tortoisehg
class EmailDialog(QDialog):
    """Dialog for sending patches via email"""

    def __init__(self, repoagent, revs, parent=None, outgoing=False,
                 outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repoagent = repoagent
        self._cmdsession = cmdcore.nullCmdSession()
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.attach_check.toggled.connect(self._updateattachmodes)
        self._qui.inline_check.toggled.connect(self._updateattachmodes)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._updateattachmodes()
        self._readsettings()
        QShortcut(QKeySequence('CTRL+Return'), self, self.accept)
        QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)

    def closeEvent(self, event):
        self._writesettings()
        super(EmailDialog, self).closeEvent(event)

    def _readsettings(self):
        s = QSettings()
        self.restoreGeometry(s.value('email/geom').toByteArray())
        self._qui.intro_changesets_splitter.restoreState(
            s.value('email/intro_changesets_splitter').toByteArray())

    def _writesettings(self):
        s = QSettings()
        s.setValue('email/geom', self.saveGeometry())
        s.setValue('email/intro_changesets_splitter',
                   self._qui.intro_changesets_splitter.saveState())

    def _readhistory(self):
        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag', 'subject'):
            w = getattr(self._qui, '%s_edit' % k)
            w.addItems(s.value('email/%s_history' % k).toStringList())
            w.setCurrentIndex(-1)  # unselect
        for k in ('body', 'attach', 'inline', 'diffstat'):
            w = getattr(self._qui, '%s_check' % k)
            w.setChecked(s.value('email/%s' % k).toBool())

    def _writehistory(self):
        def itercombo(w):
            if w.currentText():
                yield w.currentText()
            for i in xrange(w.count()):
                if w.itemText(i) != w.currentText():
                    yield w.itemText(i)

        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag', 'subject'):
            w = getattr(self._qui, '%s_edit' % k)
            s.setValue('email/%s_history' % k, list(itercombo(w))[:10])
        for k in ('body', 'attach', 'inline', 'diffstat'):
            w = getattr(self._qui, '%s_check' % k)
            s.setValue('email/%s' % k, w.isChecked())

    def _initchangesets(self, revs):
        self._changesets = _ChangesetsModel(self._repo,
                                            revs=revs or list(self._repo),
                                            selectedrevs=revs,
                                            parent=self)
        self._changesets.dataChanged.connect(self._updateforms)
        self._qui.changesets_view.setModel(self._changesets)

    @property
    def _repo(self):
        return self._repoagent.rawRepo()

    @property
    def _ui(self):
        return self._repo.ui

    @property
    def _revs(self):
        """Returns list of revisions to be sent"""
        return self._changesets.selectedrevs

    def _filldefaults(self):
        """Fill form by default values"""
        def getfromaddr(ui):
            """Get sender address in the same manner as patchbomb"""
            addr = ui.config('email', 'from') or ui.config('patchbomb', 'from')
            if addr:
                return addr
            try:
                return ui.username()
            except error.Abort:
                return ''

        self._qui.to_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'to', '')))
        self._qui.cc_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'cc', '')))
        self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui)))

        self.setdiffformat(self._ui.configbool('diff', 'git') and 'git' or 'hg')

    def setdiffformat(self, format):
        """Set diff format, 'hg', 'git' or 'plain'"""
        try:
            radio = getattr(self._qui, '%spatch_radio' % format)
        except AttributeError:
            raise ValueError('unknown diff format: %r' % format)

        radio.setChecked(True)

    def getdiffformat(self):
        """Selected diff format"""
        for e in self._qui.patch_frame.children():
            m = re.match(r'(\w+)patch_radio', str(e.objectName()))
            if m and e.isChecked():
                return m.group(1)

        return 'hg'

    def getextraopts(self):
        """Dict of extra options"""
        opts = {}
        for e in self._qui.extra_frame.children():
            m = re.match(r'(\w+)_check', str(e.objectName()))
            if m:
                opts[m.group(1)] = e.isChecked()

        return opts

    def _patchbombopts(self, **opts):
        """Generate opts for patchbomb by form values"""
        def headertext(s):
            # QLineEdit may contain newline character
            return re.sub(r'\s', ' ', unicode(s))

        opts['to'] = headertext(self._qui.to_edit.currentText())
        opts['cc'] = headertext(self._qui.cc_edit.currentText())
        opts['from'] = headertext(self._qui.from_edit.currentText())
        opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text())
        opts['flag'] = headertext(self._qui.flag_edit.currentText())

        if self._qui.bundle_radio.isChecked():
            assert self._outgoing  # only outgoing bundle is supported
            opts['rev'] = hglib.compactrevs(self._outgoingrevs)
            opts['bundle'] = True
        else:
            opts['rev'] = hglib.compactrevs(self._revs)

        fmt = self.getdiffformat()
        if fmt != 'hg':
            opts[fmt] = True

        opts.update(self.getextraopts())

        def writetempfile(s):
            fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_',
                                         dir=qtlib.gettempdir())
            try:
                os.write(fd, s)
                return hglib.tounicode(fname)
            finally:
                os.close(fd)

        opts['intro'] = self._qui.writeintro_check.isChecked()
        if opts['intro']:
            opts['subject'] = headertext(self._qui.subject_edit.currentText())
            opts['desc'] = writetempfile(
                hglib.fromunicode(self._qui.body_edit.toPlainText()))

        # The email dialog is available no matter if patchbomb extension isn't
        # enabled.  The extension name makes it unlikely first-time users
        # would discover that Mercurial ships with a functioning patch MTA.
        # Since patchbomb doesn't monkey patch any Mercurial code, it's safe
        # to enable it on demand.
        opts['config'] = 'extensions.patchbomb='

        return opts

    def _isvalid(self):
        """Filled all required values?"""
        for e in ('to_edit', 'from_edit'):
            if not getattr(self._qui, e).currentText():
                return False

        if (self._qui.writeintro_check.isChecked()
            and not self._qui.subject_edit.currentText()):
            return False

        if not self._revs:
            return False

        return True

    @pyqtSlot()
    def _updateforms(self):
        """Update availability of form widgets"""
        valid = self._isvalid()
        self._qui.send_button.setEnabled(valid)
        self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid)
        self._qui.writeintro_check.setEnabled(not self._introrequired())

        self._qui.bundle_radio.setEnabled(
            self._outgoing and self._changesets.isselectedall())
        self._changesets.setReadOnly(self._qui.bundle_radio.isChecked())
        if self._qui.bundle_radio.isChecked():
            # workaround to disable preview for outgoing bundle because it
            # may freeze main thread
            self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False)

        if self._introrequired():
            self._qui.writeintro_check.setChecked(True)

    @qtlib.senderSafeSlot()
    def _updateattachmodes(self):
        """Update checkboxes to select the embedding style of the patch"""
        attachmodes = [self._qui.attach_check, self._qui.inline_check]
        body = self._qui.body_check

        # --attach and --inline are exclusive
        if self.sender() in attachmodes and self.sender().isChecked():
            for w in attachmodes:
                if w is not self.sender():
                    w.setChecked(False)

        # --body is mandatory if no attach modes are specified
        body.setEnabled(any(w.isChecked() for w in attachmodes))
        if not body.isEnabled():
            body.setChecked(True)

    def _initenvelopebox(self):
        for e in ('to_edit', 'from_edit'):
            getattr(self._qui, e).editTextChanged.connect(self._updateforms)

    def accept(self):
        opts = self._patchbombopts()
        cmdline = hglib.buildcmdargs('email', **opts)
        cmd = cmdui.CmdSessionDialog(self)
        cmd.setWindowTitle(_('Sending Email'))
        cmd.setLogVisible(False)
        uih = cmdui.PasswordUiHandler(cmd)  # skip "intro" and "diffstat" prompt
        cmd.setSession(self._repoagent.runCommand(cmdline, uih))
        if cmd.exec_() == 0:
            self._writehistory()

    def _initintrobox(self):
        self._qui.intro_box.hide()  # hidden by default
        self._qui.subject_edit.editTextChanged.connect(self._updateforms)
        self._qui.writeintro_check.toggled.connect(self._updateforms)

    def _introrequired(self):
        """Is intro message required?"""
        return self._qui.bundle_radio.isChecked()

    def _initpreviewtab(self):
        def initqsci(w):
            w.setUtf8(True)
            w.setReadOnly(True)
            w.setMarginWidth(1, 0)  # hide area for line numbers
            self.lexer = lex = lexers.difflexer(self)
            fh = qtlib.getfont('fontdiff')
            fh.changed.connect(self.forwardFont)
            lex.setFont(fh.font())
            w.setLexer(lex)
            # TODO: better way to setup diff lexer

        initqsci(self._qui.preview_edit)

        self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab)
        self._refreshpreviewtab(self._qui.main_tabs.currentIndex())

    def forwardFont(self, font):
        if self.lexer:
            self.lexer.setFont(font)

    @pyqtSlot(int)
    def _refreshpreviewtab(self, index):
        """Generate preview text if current tab is preview"""
        if self._previewtabindex() != index:
            return

        self._qui.preview_edit.clear()
        opts = self._patchbombopts(test=True)
        cmdline = hglib.buildcmdargs('email', **opts)
        self._cmdsession = sess = self._repoagent.runCommand(cmdline)
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._updatepreview)

    @pyqtSlot()
    def _updatepreview(self):
        msg = hglib.tounicode(str(self._cmdsession.readAll()))
        self._qui.preview_edit.append(msg)

    def _previewtabindex(self):
        """Index of preview tab"""
        return self._qui.main_tabs.indexOf(self._qui.preview_tab)

    @pyqtSlot()
    def on_settings_button_clicked(self):
        from tortoisehg.hgqt import settings
        if settings.SettingsDialog(parent=self, focus='email.from').exec_():
            # not use repo.configChanged because it can clobber user input
            # accidentally.
            self._repo.invalidateui()  # force reloading config immediately
            self._filldefaults()

    @pyqtSlot()
    def on_selectall_button_clicked(self):
        self._changesets.selectAll()

    @pyqtSlot()
    def on_selectnone_button_clicked(self):
        self._changesets.selectNone()
コード例 #5
0
ファイル: hgemail.py プロジェクト: allenk/tortoisehg-caja
class EmailDialog(QDialog):
    """Dialog for sending patches via email"""
    def __init__(self,
                 repo,
                 revs,
                 parent=None,
                 outgoing=False,
                 outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repo = repo
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.body_check.toggled.connect(self._body_mode_clicked)
        self._qui.attach_check.toggled.connect(self._attach_mode_clicked)
        self._qui.inline_check.toggled.connect(self._inline_mode_clicked)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._readsettings()
        QShortcut(QKeySequence('CTRL+Return'), self, self.accept)
        QShortcut(QKeySequence('Ctrl+Enter'), self, self.accept)

    def closeEvent(self, event):
        self._writesettings()
        super(EmailDialog, self).closeEvent(event)

    def _readsettings(self):
        s = QSettings()
        self.restoreGeometry(s.value('email/geom').toByteArray())
        self._qui.intro_changesets_splitter.restoreState(
            s.value('email/intro_changesets_splitter').toByteArray())

    def _writesettings(self):
        s = QSettings()
        s.setValue('email/geom', self.saveGeometry())
        s.setValue('email/intro_changesets_splitter',
                   self._qui.intro_changesets_splitter.saveState())

    def _readhistory(self):
        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag'):
            w = getattr(self._qui, '%s_edit' % k)
            w.addItems(s.value('email/%s_history' % k).toStringList())
            w.setCurrentIndex(-1)  # unselect

    def _writehistory(self):
        def itercombo(w):
            if w.currentText():
                yield w.currentText()
            for i in xrange(w.count()):
                if w.itemText(i) != w.currentText():
                    yield w.itemText(i)

        s = QSettings()
        for k in ('to', 'cc', 'from', 'flag'):
            w = getattr(self._qui, '%s_edit' % k)
            s.setValue('email/%s_history' % k, list(itercombo(w))[:10])

    def _initchangesets(self, revs):
        def purerevs(revs):
            return scmutil.revrange(self._repo, iter(str(e) for e in revs))

        self._changesets = _ChangesetsModel(
            self._repo,
            # TODO: [':'] is inefficient
            revs=purerevs(revs or [':']),
            selectedrevs=purerevs(revs),
            parent=self)
        self._changesets.dataChanged.connect(self._updateforms)
        self._qui.changesets_view.setModel(self._changesets)

    @property
    def _ui(self):
        return self._repo.ui

    @property
    def _revs(self):
        """Returns list of revisions to be sent"""
        return self._changesets.selectedrevs

    def _filldefaults(self):
        """Fill form by default values"""
        def getfromaddr(ui):
            """Get sender address in the same manner as patchbomb"""
            addr = ui.config('email', 'from') or ui.config('patchbomb', 'from')
            if addr:
                return addr
            try:
                return ui.username()
            except error.Abort:
                return ''

        self._qui.to_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'to', '')))
        self._qui.cc_edit.setEditText(
            hglib.tounicode(self._ui.config('email', 'cc', '')))
        self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui)))

        self.setdiffformat(
            self._ui.configbool('diff', 'git') and 'git' or 'hg')

    def setdiffformat(self, format):
        """Set diff format, 'hg', 'git' or 'plain'"""
        try:
            radio = getattr(self._qui, '%spatch_radio' % format)
        except AttributeError:
            raise ValueError('unknown diff format: %r' % format)

        radio.setChecked(True)

    def getdiffformat(self):
        """Selected diff format"""
        for e in self._qui.patch_frame.children():
            m = re.match(r'(\w+)patch_radio', str(e.objectName()))
            if m and e.isChecked():
                return m.group(1)

        return 'hg'

    def getextraopts(self):
        """Dict of extra options"""
        opts = {}
        for e in self._qui.extra_frame.children():
            m = re.match(r'(\w+)_check', str(e.objectName()))
            if m:
                opts[m.group(1)] = e.isChecked()

        return opts

    def _patchbombopts(self, **opts):
        """Generate opts for patchbomb by form values"""
        def headertext(s):
            # QLineEdit may contain newline character
            return re.sub(r'\s', ' ', hglib.fromunicode(s))

        opts['to'] = [headertext(self._qui.to_edit.currentText())]
        opts['cc'] = [headertext(self._qui.cc_edit.currentText())]
        opts['from'] = headertext(self._qui.from_edit.currentText())
        opts['in_reply_to'] = headertext(self._qui.inreplyto_edit.text())
        opts['flag'] = [headertext(self._qui.flag_edit.currentText())]

        if self._qui.bundle_radio.isChecked():
            assert self._outgoing  # only outgoing bundle is supported
            opts['rev'] = map(str, self._outgoingrevs)
            opts['bundle'] = True
        else:
            opts['rev'] = map(str, self._revs)

        def diffformat():
            n = self.getdiffformat()
            if n == 'hg':
                return {}
            else:
                return {n: True}

        opts.update(diffformat())

        opts.update(self.getextraopts())

        def writetempfile(s):
            fd, fname = tempfile.mkstemp(prefix='thg_emaildesc_')
            try:
                os.write(fd, s)
                return fname
            finally:
                os.close(fd)

        opts['intro'] = self._qui.writeintro_check.isChecked()
        if opts['intro']:
            opts['subject'] = headertext(self._qui.subject_edit.text())
            opts['desc'] = writetempfile(
                hglib.fromunicode(self._qui.body_edit.toPlainText()))
            # TODO: change patchbomb not to use temporary file

        # Include the repo in the command so it can be found when thg is not
        # run from within a hg path
        opts['repository'] = self._repo.root

        return opts

    def _isvalid(self):
        """Filled all required values?"""
        for e in ('to_edit', 'from_edit'):
            if not getattr(self._qui, e).currentText():
                return False

        if self._qui.writeintro_check.isChecked(
        ) and not self._qui.subject_edit.text():
            return False

        if not self._revs:
            return False

        return True

    @pyqtSlot()
    def _updateforms(self):
        """Update availability of form widgets"""
        valid = self._isvalid()
        self._qui.send_button.setEnabled(valid)
        self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid)
        self._qui.writeintro_check.setEnabled(not self._introrequired())

        self._qui.bundle_radio.setEnabled(self._outgoing
                                          and self._changesets.isselectedall())
        self._changesets.setReadOnly(self._qui.bundle_radio.isChecked())
        if self._qui.bundle_radio.isChecked():
            # workaround to disable preview for outgoing bundle because it
            # may freeze main thread
            self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False)

        if self._introrequired():
            self._qui.writeintro_check.setChecked(True)

    def _body_mode_clicked(self):
        # Only allow a single attachment type to be active at a time
        sendattachment = self._qui.attach_check.isChecked(
        ) or self._qui.inline_check.isChecked()
        if not sendattachment:
            # If no attachment, ensure that the body mode is enabled
            self._qui.body_check.setChecked(True)

    def _attach_mode_clicked(self):
        sendattachment = self._qui.attach_check.isChecked(
        ) or self._qui.inline_check.isChecked()
        self._qui.body_check.setDisabled(not sendattachment)
        if not sendattachment:
            self._qui.body_check.setChecked(True)

        # Only allow a single attachment type to be active at a time
        if self._qui.attach_check.isChecked():
            self._qui.inline_check.setChecked(False)

    def _inline_mode_clicked(self):
        sendattachment = self._qui.attach_check.isChecked(
        ) or self._qui.inline_check.isChecked()
        self._qui.body_check.setDisabled(not sendattachment)
        if not sendattachment:
            self._qui.body_check.setChecked(True)

        # Only allow a single attachment type to be active at a time
        if self._qui.inline_check.isChecked():
            self._qui.attach_check.setChecked(False)

    def _initenvelopebox(self):
        for e in ('to_edit', 'from_edit'):
            getattr(self._qui, e).editTextChanged.connect(self._updateforms)

    def accept(self):
        # TODO: want to pass patchbombopts directly
        def cmdargs(opts):
            args = []
            for k, v in opts.iteritems():
                if isinstance(v, bool):
                    if v:
                        args.append('--%s' % k.replace('_', '-'))
                else:
                    for e in isinstance(v, basestring) and [v] or v:
                        args += ['--%s' % k.replace('_', '-'), e]

            return args

        hglib.loadextension(self._ui, 'patchbomb')

        opts = self._patchbombopts()
        try:
            cmd = cmdui.Dialog(['email'] + cmdargs(opts), parent=self)
            cmd.setWindowTitle(_('Sending Email'))
            cmd.setShowOutput(False)
            cmd.finished.connect(cmd.deleteLater)
            if cmd.exec_():
                self._writehistory()
        finally:
            if 'desc' in opts:
                os.unlink(opts['desc'])  # TODO: don't use tempfile

    def _initintrobox(self):
        self._qui.intro_box.hide()  # hidden by default
        self._qui.subject_edit.textChanged.connect(self._updateforms)
        self._qui.writeintro_check.toggled.connect(self._updateforms)

    def _introrequired(self):
        """Is intro message required?"""
        return len(self._revs) > 1 or self._qui.bundle_radio.isChecked()

    def _initpreviewtab(self):
        def initqsci(w):
            w.setUtf8(True)
            w.setReadOnly(True)
            w.setMarginWidth(1, 0)  # hide area for line numbers
            self.lexer = lex = lexers.get_diff_lexer(self)
            fh = qtlib.getfont('fontdiff')
            fh.changed.connect(self.forwardFont)
            lex.setFont(fh.font())
            w.setLexer(lex)
            # TODO: better way to setup diff lexer

        initqsci(self._qui.preview_edit)

        self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab)
        self._refreshpreviewtab(self._qui.main_tabs.currentIndex())

    def forwardFont(self, font):
        if self.lexer:
            self.lexer.setFont(font)

    @pyqtSlot(int)
    def _refreshpreviewtab(self, index):
        """Generate preview text if current tab is preview"""
        if self._previewtabindex() != index:
            return

        self._qui.preview_edit.setText(self._preview())

    def _preview(self):
        """Generate preview text by running patchbomb"""
        def loadpatchbomb():
            hglib.loadextension(self._ui, 'patchbomb')
            return extensions.find('patchbomb')

        def wrapui(ui):
            buf = StringIO()
            # TODO: common way to prepare pure ui
            newui = ui.copy()
            newui.setconfig('ui', 'interactive', False)
            newui.setconfig('diff', 'git', False)
            newui.write = lambda *args, **opts: buf.write(''.join(args))
            newui.status = lambda *args, **opts: None
            return newui, buf

        def stripheadmsg(s):
            # TODO: skip until first Content-type: line ??
            return '\n'.join(s.splitlines()[3:])

        ui, buf = wrapui(self._ui)
        opts = self._patchbombopts(test=True)
        try:
            # TODO: fix hgext.patchbomb's implementation instead
            if 'PAGER' in os.environ:
                del os.environ['PAGER']

            loadpatchbomb().patchbomb(ui, self._repo, **opts)
            return stripheadmsg(hglib.tounicode(buf.getvalue()))
        finally:
            if 'desc' in opts:
                os.unlink(opts['desc'])  # TODO: don't use tempfile

    def _previewtabindex(self):
        """Index of preview tab"""
        return self._qui.main_tabs.indexOf(self._qui.preview_tab)

    @pyqtSlot()
    def on_settings_button_clicked(self):
        from tortoisehg.hgqt import settings
        if settings.SettingsDialog(parent=self, focus='email.from').exec_():
            # not use repo.configChanged because it can clobber user input
            # accidentally.
            self._repo.invalidateui()  # force reloading config immediately
            self._filldefaults()

    @pyqtSlot()
    def on_selectall_button_clicked(self):
        self._changesets.selectAll()

    @pyqtSlot()
    def on_selectnone_button_clicked(self):
        self._changesets.selectNone()
コード例 #6
0
ファイル: hgemail.py プロジェクト: velorientc/git_test7
class EmailDialog(QDialog):
    """Dialog for sending patches via email"""

    def __init__(self, repoagent, revs, parent=None, outgoing=False, outgoingrevs=None):
        """Create EmailDialog for the given repo and revs

        :revs: List of revisions to be sent.
        :outgoing: Enable outgoing bundle support. You also need to set
                   outgoing revisions to `revs`.
        :outgoingrevs: Target revision of outgoing bundle.
                       (Passed as `hg email --bundle --rev {rev}`)
        """
        super(EmailDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)
        self._repoagent = repoagent
        self._outgoing = outgoing
        self._outgoingrevs = outgoingrevs or []

        self._qui = Ui_EmailDialog()
        self._qui.setupUi(self)

        self._initchangesets(revs)
        self._initpreviewtab()
        self._initenvelopebox()
        self._qui.bundle_radio.toggled.connect(self._updateforms)
        self._qui.attach_check.toggled.connect(self._updateattachmodes)
        self._qui.inline_check.toggled.connect(self._updateattachmodes)
        self._initintrobox()
        self._readhistory()
        self._filldefaults()
        self._updateforms()
        self._updateattachmodes()
        self._readsettings()
        QShortcut(QKeySequence("CTRL+Return"), self, self.accept)
        QShortcut(QKeySequence("Ctrl+Enter"), self, self.accept)

    def closeEvent(self, event):
        self._writesettings()
        super(EmailDialog, self).closeEvent(event)

    def _readsettings(self):
        s = QSettings()
        self.restoreGeometry(s.value("email/geom").toByteArray())
        self._qui.intro_changesets_splitter.restoreState(s.value("email/intro_changesets_splitter").toByteArray())

    def _writesettings(self):
        s = QSettings()
        s.setValue("email/geom", self.saveGeometry())
        s.setValue("email/intro_changesets_splitter", self._qui.intro_changesets_splitter.saveState())

    def _readhistory(self):
        s = QSettings()
        for k in ("to", "cc", "from", "flag", "subject"):
            w = getattr(self._qui, "%s_edit" % k)
            w.addItems(s.value("email/%s_history" % k).toStringList())
            w.setCurrentIndex(-1)  # unselect
        for k in ("body", "attach", "inline", "diffstat"):
            w = getattr(self._qui, "%s_check" % k)
            w.setChecked(s.value("email/%s" % k).toBool())

    def _writehistory(self):
        def itercombo(w):
            if w.currentText():
                yield w.currentText()
            for i in xrange(w.count()):
                if w.itemText(i) != w.currentText():
                    yield w.itemText(i)

        s = QSettings()
        for k in ("to", "cc", "from", "flag", "subject"):
            w = getattr(self._qui, "%s_edit" % k)
            s.setValue("email/%s_history" % k, list(itercombo(w))[:10])
        for k in ("body", "attach", "inline", "diffstat"):
            w = getattr(self._qui, "%s_check" % k)
            s.setValue("email/%s" % k, w.isChecked())

    def _initchangesets(self, revs):
        def purerevs(revs):
            return scmutil.revrange(self._repo, iter(str(e) for e in revs))

        self._changesets = _ChangesetsModel(
            self._repo,
            # TODO: [':'] is inefficient
            revs=purerevs(revs or [":"]),
            selectedrevs=purerevs(revs),
            parent=self,
        )
        self._changesets.dataChanged.connect(self._updateforms)
        self._qui.changesets_view.setModel(self._changesets)

    @property
    def _repo(self):
        return self._repoagent.rawRepo()

    @property
    def _ui(self):
        return self._repo.ui

    @property
    def _revs(self):
        """Returns list of revisions to be sent"""
        return self._changesets.selectedrevs

    def _filldefaults(self):
        """Fill form by default values"""

        def getfromaddr(ui):
            """Get sender address in the same manner as patchbomb"""
            addr = ui.config("email", "from") or ui.config("patchbomb", "from")
            if addr:
                return addr
            try:
                return ui.username()
            except error.Abort:
                return ""

        self._qui.to_edit.setEditText(hglib.tounicode(self._ui.config("email", "to", "")))
        self._qui.cc_edit.setEditText(hglib.tounicode(self._ui.config("email", "cc", "")))
        self._qui.from_edit.setEditText(hglib.tounicode(getfromaddr(self._ui)))

        self.setdiffformat(self._ui.configbool("diff", "git") and "git" or "hg")

    def setdiffformat(self, format):
        """Set diff format, 'hg', 'git' or 'plain'"""
        try:
            radio = getattr(self._qui, "%spatch_radio" % format)
        except AttributeError:
            raise ValueError("unknown diff format: %r" % format)

        radio.setChecked(True)

    def getdiffformat(self):
        """Selected diff format"""
        for e in self._qui.patch_frame.children():
            m = re.match(r"(\w+)patch_radio", str(e.objectName()))
            if m and e.isChecked():
                return m.group(1)

        return "hg"

    def getextraopts(self):
        """Dict of extra options"""
        opts = {}
        for e in self._qui.extra_frame.children():
            m = re.match(r"(\w+)_check", str(e.objectName()))
            if m:
                opts[m.group(1)] = e.isChecked()

        return opts

    def _patchbombopts(self, **opts):
        """Generate opts for patchbomb by form values"""

        def headertext(s):
            # QLineEdit may contain newline character
            return re.sub(r"\s", " ", hglib.fromunicode(s))

        opts["to"] = [headertext(self._qui.to_edit.currentText())]
        opts["cc"] = [headertext(self._qui.cc_edit.currentText())]
        opts["from"] = headertext(self._qui.from_edit.currentText())
        opts["in_reply_to"] = headertext(self._qui.inreplyto_edit.text())
        opts["flag"] = [headertext(self._qui.flag_edit.currentText())]

        if self._qui.bundle_radio.isChecked():
            assert self._outgoing  # only outgoing bundle is supported
            opts["rev"] = map(str, self._outgoingrevs)
            opts["bundle"] = True
        else:
            opts["rev"] = map(str, self._revs)

        def diffformat():
            n = self.getdiffformat()
            if n == "hg":
                return {}
            else:
                return {n: True}

        opts.update(diffformat())

        opts.update(self.getextraopts())

        def writetempfile(s):
            fd, fname = tempfile.mkstemp(prefix="thg_emaildesc_")
            try:
                os.write(fd, s)
                return fname
            finally:
                os.close(fd)

        opts["intro"] = self._qui.writeintro_check.isChecked()
        if opts["intro"]:
            opts["subject"] = headertext(self._qui.subject_edit.currentText())
            opts["desc"] = writetempfile(hglib.fromunicode(self._qui.body_edit.toPlainText()))
            # TODO: change patchbomb not to use temporary file

        # Include the repo in the command so it can be found when thg is not
        # run from within a hg path
        opts["repository"] = self._repo.root

        return opts

    def _isvalid(self):
        """Filled all required values?"""
        for e in ("to_edit", "from_edit"):
            if not getattr(self._qui, e).currentText():
                return False

        if self._qui.writeintro_check.isChecked() and not self._qui.subject_edit.currentText():
            return False

        if not self._revs:
            return False

        return True

    @pyqtSlot()
    def _updateforms(self):
        """Update availability of form widgets"""
        valid = self._isvalid()
        self._qui.send_button.setEnabled(valid)
        self._qui.main_tabs.setTabEnabled(self._previewtabindex(), valid)
        self._qui.writeintro_check.setEnabled(not self._introrequired())

        self._qui.bundle_radio.setEnabled(self._outgoing and self._changesets.isselectedall())
        self._changesets.setReadOnly(self._qui.bundle_radio.isChecked())
        if self._qui.bundle_radio.isChecked():
            # workaround to disable preview for outgoing bundle because it
            # may freeze main thread
            self._qui.main_tabs.setTabEnabled(self._previewtabindex(), False)

        if self._introrequired():
            self._qui.writeintro_check.setChecked(True)

    # @pyqtSlot()
    def _updateattachmodes(self):
        """Update checkboxes to select the embedding style of the patch"""
        attachmodes = [self._qui.attach_check, self._qui.inline_check]
        body = self._qui.body_check

        # --attach and --inline are exclusive
        if self.sender() in attachmodes and self.sender().isChecked():
            for w in attachmodes:
                if w is not self.sender():
                    w.setChecked(False)

        # --body is mandatory if no attach modes are specified
        body.setEnabled(util.any(w.isChecked() for w in attachmodes))
        if not body.isEnabled():
            body.setChecked(True)

    def _initenvelopebox(self):
        for e in ("to_edit", "from_edit"):
            getattr(self._qui, e).editTextChanged.connect(self._updateforms)

    def accept(self):
        hglib.loadextension(self._ui, "patchbomb")

        opts = self._patchbombopts()
        try:
            cmd = cmdui.Dialog(hglib.buildcmdargs("email", **opts), parent=self)
            cmd.setWindowTitle(_("Sending Email"))
            cmd.setShowOutput(False)
            cmd.finished.connect(cmd.deleteLater)
            if cmd.exec_():
                self._writehistory()
        finally:
            if "desc" in opts:
                os.unlink(opts["desc"])  # TODO: don't use tempfile

    def _initintrobox(self):
        self._qui.intro_box.hide()  # hidden by default
        self._qui.subject_edit.editTextChanged.connect(self._updateforms)
        self._qui.writeintro_check.toggled.connect(self._updateforms)

    def _introrequired(self):
        """Is intro message required?"""
        return len(self._revs) > 1 or self._qui.bundle_radio.isChecked()

    def _initpreviewtab(self):
        def initqsci(w):
            w.setUtf8(True)
            w.setReadOnly(True)
            w.setMarginWidth(1, 0)  # hide area for line numbers
            self.lexer = lex = lexers.difflexer(self)
            fh = qtlib.getfont("fontdiff")
            fh.changed.connect(self.forwardFont)
            lex.setFont(fh.font())
            w.setLexer(lex)
            # TODO: better way to setup diff lexer

        initqsci(self._qui.preview_edit)

        self._qui.main_tabs.currentChanged.connect(self._refreshpreviewtab)
        self._refreshpreviewtab(self._qui.main_tabs.currentIndex())

    def forwardFont(self, font):
        if self.lexer:
            self.lexer.setFont(font)

    @pyqtSlot(int)
    def _refreshpreviewtab(self, index):
        """Generate preview text if current tab is preview"""
        if self._previewtabindex() != index:
            return

        self._qui.preview_edit.setText(self._preview())

    def _preview(self):
        """Generate preview text by running patchbomb"""

        def loadpatchbomb():
            hglib.loadextension(self._ui, "patchbomb")
            return extensions.find("patchbomb")

        def wrapui(ui):
            buf = StringIO()
            # TODO: common way to prepare pure ui
            newui = ui.copy()
            newui.setconfig("ui", "interactive", False)
            newui.setconfig("diff", "git", False)
            newui.write = lambda *args, **opts: buf.write("".join(args))
            newui.status = lambda *args, **opts: None
            return newui, buf

        def stripheadmsg(s):
            # TODO: skip until first Content-type: line ??
            return "\n".join(s.splitlines()[3:])

        ui, buf = wrapui(self._ui)
        opts = self._patchbombopts(test=True)
        try:
            # TODO: fix hgext.patchbomb's implementation instead
            if "PAGER" in os.environ:
                del os.environ["PAGER"]

            loadpatchbomb().patchbomb(ui, self._repo, **opts)
            return stripheadmsg(hglib.tounicode(buf.getvalue()))
        finally:
            if "desc" in opts:
                os.unlink(opts["desc"])  # TODO: don't use tempfile

    def _previewtabindex(self):
        """Index of preview tab"""
        return self._qui.main_tabs.indexOf(self._qui.preview_tab)

    @pyqtSlot()
    def on_settings_button_clicked(self):
        from tortoisehg.hgqt import settings

        if settings.SettingsDialog(parent=self, focus="email.from").exec_():
            # not use repo.configChanged because it can clobber user input
            # accidentally.
            self._repo.invalidateui()  # force reloading config immediately
            self._filldefaults()

    @pyqtSlot()
    def on_selectall_button_clicked(self):
        self._changesets.selectAll()

    @pyqtSlot()
    def on_selectnone_button_clicked(self):
        self._changesets.selectNone()