File indexing completed on 2024-11-24 04:52:59

0001 /* Copyright (C) 2012 Thomas Lübking <thomas.luebking@gmail.com>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "ComposerTextEdit.h"
0024 #include <QAction>
0025 #include <QApplication>
0026 #include <QClipboard>
0027 #include <QFontDatabase>
0028 #include <QMenu>
0029 #include <QMimeData>
0030 #include <QPainter>
0031 #include <QPaintEvent>
0032 #include <QTimer>
0033 #include <QUrl>
0034 
0035 #include "Gui/Util.h"
0036 #include "UiUtils/Color.h"
0037 
0038 ComposerTextEdit::ComposerTextEdit(QWidget *parent) : QTextEdit(parent)
0039 , m_couldBeSendRequest(false)
0040 , m_wrapIndicatorOffset(-1)
0041 {
0042     setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0043     setAcceptRichText(false);
0044     setLineWrapMode(QTextEdit::WidgetWidth);
0045     setWordWrapMode(QTextOption::WordWrap);
0046     m_wrapIndicatorOffset = fontMetrics().averageCharWidth() * 78;
0047     QColor c = palette().color(QPalette::Active, foregroundRole());
0048     c.setAlpha(26);
0049     m_wrapIndicatorColor = UiUtils::tintColor(palette().color(QPalette::Active, backgroundRole()), c);
0050 
0051     m_notificationTimer = new QTimer(this);
0052     m_notificationTimer->setSingleShot(true);
0053     connect(m_notificationTimer, &QTimer::timeout, this, &ComposerTextEdit::resetNotification);
0054 
0055     m_pasteQuoted = new QAction(tr("Paste as Quoted Text"), this);
0056     connect(m_pasteQuoted, &QAction::triggered, this, &ComposerTextEdit::slotPasteAsQuotation);
0057 }
0058 
0059 void ComposerTextEdit::notify(const QString &n, uint timeout)
0060 {
0061     m_notification = n;
0062     if (m_notification.isEmpty() || !timeout) {
0063         m_notificationTimer->stop();
0064     } else {
0065         m_notificationTimer->start(timeout);
0066     }
0067     viewport()->update();
0068 }
0069 
0070 int ComposerTextEdit::idealWidth() const
0071 {
0072     const auto margins = contentsMargins();
0073     return margins.left() + margins.right() + 80*QFontMetrics(font()).averageCharWidth();
0074 }
0075 
0076 void ComposerTextEdit::resetNotification()
0077 {
0078     notify(QString());
0079 }
0080 
0081 bool ComposerTextEdit::canInsertFromMimeData( const QMimeData * source ) const
0082 {
0083     QList<QUrl> urls = source->urls();
0084     foreach (const QUrl &url, urls) {
0085         if (url.isLocalFile())
0086             return true;
0087     }
0088     return QTextEdit::canInsertFromMimeData(source);
0089 }
0090 
0091 void ComposerTextEdit::insertFromMimeData(const QMimeData *source)
0092 {
0093     QList<QUrl> urls = source->urls();
0094     foreach (const QUrl &url, urls) {
0095         if (url.isLocalFile()) {
0096             emit urlsAdded(urls);
0097             return;
0098         }
0099     }
0100     QTextEdit::insertFromMimeData(source);
0101 }
0102 
0103 
0104 static inline bool isSendCombo(QKeyEvent *ke) {
0105     return (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) && ke->modifiers() == Qt::ControlModifier;
0106 }
0107 
0108 void ComposerTextEdit::keyPressEvent(QKeyEvent *ke) {
0109     m_couldBeSendRequest = false;
0110     if (isSendCombo(ke)) {
0111         m_couldBeSendRequest = true;
0112     }
0113     QTextEdit::keyPressEvent(ke);
0114 }
0115 
0116 void ComposerTextEdit::keyReleaseEvent(QKeyEvent *ke)
0117 {
0118     if (m_couldBeSendRequest && isSendCombo(ke)) {
0119         m_couldBeSendRequest = false;
0120         emit sendRequest();
0121         return;
0122     }
0123     m_couldBeSendRequest = false;
0124     QTextEdit::keyReleaseEvent(ke);
0125 }
0126 
0127 void ComposerTextEdit::paintEvent(QPaintEvent *pe)
0128 {
0129     // wrap indicator
0130     QPainter p(viewport());
0131     p.setPen(m_wrapIndicatorColor);
0132     const QRect geo = viewport()->geometry();
0133     const int x = geo.x() + m_wrapIndicatorOffset;
0134     p.drawLine(x, geo.y(), x, geo.x() + viewport()->height());
0135     p.end();
0136 
0137     QTextEdit::paintEvent(pe);
0138 
0139     if ( !m_notification.isEmpty() )
0140     {
0141         const int s = width()/5;
0142         QRect r(s, 0, width()-2*s, height());
0143         QPainter p(viewport());
0144         p.setRenderHint(QPainter::Antialiasing);
0145         p.setClipRegion(pe->region());
0146         QFont fnt = font();
0147         fnt.setBold(true);
0148         fnt.setPointSize( fnt.pointSize()*2*r.width()/(3*QFontMetrics(fnt).horizontalAdvance(m_notification)) );
0149         r.setHeight( QFontMetrics(fnt).height() + 16 );
0150         r.moveCenter( rect().center() );
0151 
0152         QColor c = palette().color(viewport()->foregroundRole());
0153         c.setAlpha( 2 * c.alpha() / 3 );
0154         p.setBrush( c );
0155         p.setPen( Qt::NoPen );
0156         p.drawRoundedRect( r, 8,8 );
0157 
0158         p.setPen( palette().color(viewport()->backgroundRole()) );
0159         p.setFont( fnt );
0160         p.drawText(r, Qt::AlignCenter|Qt::TextDontClip, m_notification );
0161         p.end();
0162     }
0163 }
0164 
0165 void ComposerTextEdit::contextMenuEvent(QContextMenuEvent *e)
0166 {
0167     QScopedPointer<QMenu> menu(createStandardContextMenu(e->pos()));
0168 
0169     // We would like to place the action next to the existing "Paste" item. How to find it? These actions are created
0170     // in Qt4's src/gui/text/qtextcontrol.cpp, QTextControl::createStandardContextMenu.
0171     //
0172     // The first possibility is to take a look at where are these actions connected; we're looking for a connection
0173     // between triggered() and SLOT(paste()). Unfortunately, it seems that these are only available via QObjectPrivate.
0174     //
0175     // Another possibility is to take a look at the shortcuts. Unfortunately, these "shortcuts" are not really "shortcuts"
0176     // in the sense of "being available via QAction::shortcuts or QAction::shortcuts; they are instead (at least in Qt4)
0177     // handled via event handlers.
0178     //
0179     // Thomas suggested a nice hack, trying the QObject::disconnect. Unfortunately, the QAction is not connected to
0180     // QTextEdit::paste but to a QTextControl/QWidgetTextControl::paste, and these are private classes, which makes it
0181     // a tad complicated to find them via QObject::findChildren().
0182     //
0183     // The API of QWebView with its standard actions looks like heaven compared to this stuff.
0184     //
0185     // This is why we take a look at the action's text and look for a particular string. Yes, it's ugly; patches welcome.
0186     QAction *pasteAction = 0;
0187     QString candidateStringForPaste = QKeySequence(QKeySequence::Paste).toString();
0188     // Finally, the API for adding functions leaves something to be desired; QMenu::insertAction takes a pointer to the
0189     // "before" thing which is just... annoying here (even though it makes certain amount of sense with addAction which
0190     // appends).
0191     QAction *followingActionAfterPaste = 0;
0192     QList<QAction*> actions = menu->actions();
0193     for (QList<QAction*>::const_iterator it = actions.constBegin(); it != actions.constEnd() && !pasteAction; ++it) {
0194         if (!candidateStringForPaste.isEmpty() && (*it)->text().contains(candidateStringForPaste)) {
0195             pasteAction = *it;
0196             if (it + 1 != actions.constEnd()) {
0197                 followingActionAfterPaste = *(it + 1);
0198             }
0199             break;
0200         }
0201     }
0202 
0203     menu->insertAction(followingActionAfterPaste, m_pasteQuoted);
0204     if (pasteAction) {
0205         m_pasteQuoted->setEnabled(pasteAction->isEnabled());
0206     } else {
0207         m_pasteQuoted->setEnabled(true);
0208     }
0209     menu->exec(e->globalPos());
0210 }
0211 
0212 void ComposerTextEdit::slotPasteAsQuotation()
0213 {
0214     QString text = qApp->clipboard()->text();
0215     if (text.isEmpty())
0216         return;
0217 
0218     QStringList lines = text.split(QLatin1Char('\n'));
0219     for (QStringList::iterator it = lines.begin(); it != lines.end(); ++it) {
0220         it->remove(QLatin1Char('\r'));
0221         if (it->startsWith(QLatin1Char('>'))) {
0222             *it = QLatin1Char('>') + *it;
0223         } else {
0224             *it = QLatin1String("> ") + *it;
0225         }
0226     }
0227     text = lines.join(QStringLiteral("\n"));
0228     if (!text.endsWith(QLatin1Char('\n'))) {
0229         text += QLatin1Char('\n');
0230     }
0231 
0232     QTextCursor cursor = textCursor();
0233     cursor.beginEditBlock();
0234     cursor.insertBlock();
0235     cursor.insertText(text);
0236     cursor.endEditBlock();
0237     setTextCursor(cursor);
0238 }