File indexing completed on 2024-11-24 04:53:24

0001 /* Copyright (C) 2012 Thomas Lübking <thomas.luebking@gmail.com>
0002    Copyright (C) 2013 Caspar Schutijser <caspar@schutijser.com>
0003    Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0004 
0005    This file is part of the Trojita Qt IMAP e-mail client,
0006    http://trojita.flaska.net/
0007 
0008    This program is free software; you can redistribute it and/or
0009    modify it under the terms of the GNU General Public License as
0010    published by the Free Software Foundation; either version 2 of
0011    the License or (at your option) version 3 or any later version
0012    accepted by the membership of KDE e.V. (or its successor approved
0013    by the membership of KDE e.V.), which shall act as a proxy
0014    defined in Section 14 of version 3 of the license.
0015 
0016    This program is distributed in the hope that it will be useful,
0017    but WITHOUT ANY WARRANTY; without even the implied warranty of
0018    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0019    GNU General Public License for more details.
0020 
0021    You should have received a copy of the GNU General Public License
0022    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0023 */
0024 
0025 #include <QDateTime>
0026 #include <QDir>
0027 #include <QDropEvent>
0028 #include <QFileInfo>
0029 #include <QFileSystemWatcher>
0030 #include <QImageReader>
0031 #include <QKeyEvent>
0032 #include <QMessageBox>
0033 #include <QMetaProperty>
0034 #include <QMimeData>
0035 #include <QPainter>
0036 #include <QPainterPath>
0037 #include <QSettings>
0038 #include <QSortFilterProxyModel>
0039 #include <QStandardItemModel>
0040 #include <QTextDocument>
0041 #include <QTimer>
0042 #include <QUrl>
0043 
0044 #include <QtDebug>
0045 
0046 #include "be-contacts.h"
0047 #include "ui_be-contacts.h"
0048 #include "ui_onecontact.h"
0049 #include "AbookAddressbook.h"
0050 
0051 namespace BE {
0052 
0053 class Field {
0054 public:
0055     typedef AbookAddressbook::Type Type;
0056     Field(Type type, QLabel *label, QString key)
0057     {
0058         this->type = type;
0059         this->label = label;
0060         this->key = key;
0061     }
0062     Field(const Field &other)
0063     {
0064         this->type = other.type;
0065         this->label = other.label;
0066         this->key = other.key;
0067     }
0068     QLabel *label;
0069     Type type;
0070     QString key;
0071 };
0072 }
0073 
0074 BE::Contacts::Contacts(AbookAddressbook *abook): m_abook(abook), m_dirty(false)
0075 {
0076     m_currentContact = 0;
0077     QImage img(QDir::homePath() + QLatin1String("/.abook/incognito.png"));
0078     if (!img.isNull())
0079         m_incognitoPic = QPixmap::fromImage(img.scaled(160,160,Qt::KeepAspectRatio,Qt::SmoothTransformation));
0080     m_ui = new Ui::Contacts;
0081     m_ui->setupUi(this);
0082     m_ui->filter->setPlaceholderText(tr("Filter"));
0083     m_ui2 = new Ui::OneContact;
0084     m_ui2->setupUi(m_ui->oneContact);
0085 
0086     fields <<   Field(AbookAddressbook::Name, m_ui2->name, QStringLiteral("name")) << Field(AbookAddressbook::Mail, m_ui2->mail, QStringLiteral("email")) <<
0087                 Field(AbookAddressbook::Address, m_ui2->address, QStringLiteral("address")) << Field(AbookAddressbook::City, m_ui2->city, QStringLiteral("city")) <<
0088                 Field(AbookAddressbook::State, m_ui2->state, QStringLiteral("state")) << Field(AbookAddressbook::ZIP, m_ui2->zip, QStringLiteral("zip")) <<
0089                 Field(AbookAddressbook::Country, m_ui2->country, QStringLiteral("country")) << Field(AbookAddressbook::Phone, m_ui2->phone, QStringLiteral("phone")) <<
0090                 Field(AbookAddressbook::Workphone, m_ui2->workphone, QStringLiteral("workphone")) << Field(AbookAddressbook::Fax, m_ui2->fax, QStringLiteral("fax")) <<
0091                 Field(AbookAddressbook::Mobile, m_ui2->mobile, QStringLiteral("mobile")) << Field(AbookAddressbook::Nick, m_ui2->nick, QStringLiteral("nick")) <<
0092                 Field(AbookAddressbook::URL, m_ui2->url, QStringLiteral("url")) << Field(AbookAddressbook::Notes, m_ui2->notes, QStringLiteral("notes")) <<
0093                 Field(AbookAddressbook::Anniversary, m_ui2->anniversary, QStringLiteral("anniversary")) << Field(AbookAddressbook::Photo, m_ui2->photo, QStringLiteral("photo"));
0094 
0095     connect(m_abook->model(), &QStandardItemModel::itemChanged,
0096             this, [&](QStandardItem *i) {
0097                 if (i == m_currentContact) {
0098                     m_ui2->name->setText(i->text());
0099                     m_dirty = true;
0100                 }
0101             });
0102 
0103     m_sortFilterProxy = new QSortFilterProxyModel(this);
0104     m_sortFilterProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
0105     m_sortFilterProxy->setFilterKeyColumn(-1);
0106     m_sortFilterProxy->setSourceModel(m_abook->model());
0107     connect(m_ui->filter, &QLineEdit::textChanged, m_sortFilterProxy, &QSortFilterProxyModel::setFilterWildcard);
0108     m_ui->filter->installEventFilter(this);
0109 
0110     QFont fnt = m_ui2->name->font();
0111     fnt.setPointSize(fnt.pointSize()*2);
0112     m_ui2->name->setFont(fnt);
0113     for (QList<Field>::const_iterator   it = fields.constBegin(),
0114                                         end = fields.constEnd(); it != end; ++it) {
0115         it->label->installEventFilter(this);
0116     }
0117 
0118     m_ui->contacts->setModel(m_sortFilterProxy);
0119     m_ui->contacts->setSelectionMode(QAbstractItemView::SingleSelection);
0120     connect(m_ui->contacts->selectionModel(), &QItemSelectionModel::currentChanged, this, &Contacts::setContact);
0121     QModelIndex idx = m_sortFilterProxy->index(0,0);
0122     if (idx.isValid())
0123         m_ui->contacts->setCurrentIndex(idx);
0124     m_ui->contacts->installEventFilter(this);
0125 
0126     connect(m_ui->add, &QAbstractButton::clicked, this, &Contacts::addContact);
0127     connect(m_ui->remove, &QAbstractButton::clicked, this, &Contacts::removeCurrentContact);
0128     connect(qApp, &QApplication::focusChanged, this, &Contacts::updateFocusPolicy);
0129 
0130     // cheat to correct the focuspolicies ;-)
0131     updateFocusPolicy(m_ui2->name, m_ui->filter);
0132 
0133     Q_FOREACH(const Field &field, fields) {
0134         if (QTextDocument *doc = field.label->findChild<QTextDocument*>()) {
0135             connect(doc, &QTextDocument::contentsChanged, this, &Contacts::updateLabel);
0136         }
0137     }
0138 }
0139 
0140 void BE::Contacts::updateLabel()
0141 {
0142     QTextDocument *doc = qobject_cast<QTextDocument*>(sender());
0143     Q_ASSERT(doc);
0144     QObject *o = doc;
0145     while ((o = o->parent())) {
0146         if (QLabel *l = qobject_cast<QLabel*>(o)) {
0147             l->setMinimumWidth(qMax(l->fontMetrics().horizontalAdvance(l->text()),
0148                                     l->fontMetrics().horizontalAdvance(doc->toPlainText())));
0149             break;
0150         }
0151     }
0152 }
0153 
0154 BE::Contacts::~Contacts()
0155 {
0156 }
0157 
0158 void BE::Contacts::closeEvent(QCloseEvent *)
0159 {
0160     setContact(QModelIndex()); // store current contact changes
0161     if (m_dirty) {
0162         if (QMessageBox::question(this, tr("Contacts"), tr("Save changes?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
0163             saveContacts();
0164         } else {
0165             m_abook->model()->removeRows(0, m_abook->model()->rowCount());
0166             m_abook->readAbook(false);
0167         }
0168     }
0169 }
0170 
0171 void BE::Contacts::saveContacts()
0172 {
0173     m_abook->saveContacts();
0174 }
0175 
0176 static QString lifeText(const QLabel *l)
0177 {
0178     if (QTextDocument *t = l->findChild<QTextDocument*>())
0179         return t->toPlainText();
0180     return l->text();
0181 }
0182 
0183 static void setText(QLabel *l, const QString &text)
0184 {
0185     if (QTextDocument *t = l->findChild<QTextDocument*>())
0186         t->setPlainText(text);
0187     l->setText(QString());
0188     l->setText(text);
0189 }
0190 
0191 bool BE::Contacts::eventFilter(QObject *o, QEvent *e)
0192 {
0193     if (o == m_ui->filter) {
0194         if (e->type() == QEvent::KeyPress)
0195         if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Down)
0196             m_ui->contacts->setFocus();
0197         return false;
0198     }
0199 
0200     if (o == m_ui->contacts) {
0201         if (e->type() == QEvent::KeyPress) {
0202             QKeyEvent *ke = static_cast<QKeyEvent*>(e);
0203             if (ke->key() == Qt::Key_Up && !m_ui->contacts->currentIndex().row()) {
0204             m_ui->filter->setFocus();
0205             return true;
0206             } else if (ke->key() == Qt::Key_Delete) {
0207                 removeCurrentContact();
0208                 return true;
0209             }
0210         }
0211         return false;
0212     }
0213 
0214     if (!m_currentContact)
0215         return false;
0216     switch (e->type()) {
0217     case QEvent::DragEnter:
0218     case QEvent::DragMove:
0219     case QEvent::Drop: {
0220         if (o != m_ui2->photo)
0221             return false;
0222         QDropEvent *de = static_cast<QDropEvent*>(e);
0223         if (!de->mimeData())
0224             return false;
0225         QList<QUrl> urls = de->mimeData()->urls();
0226         if (urls.isEmpty())
0227             return false;
0228         QUrl url = urls.first();
0229         if (url.isLocalFile() && QImageReader(url.path()).canRead()) {
0230             if (e->type() == QEvent::Drop)
0231                 importPhoto(url.path());
0232             else
0233                 de->acceptProposedAction();
0234         }
0235         return false;
0236     }
0237     case QEvent::KeyPress: {
0238         const int key = static_cast<QKeyEvent*>(e)->key();
0239         if (key == Qt::Key_Delete && o == m_ui2->photo) { // reset photo
0240             if (m_currentContact)
0241                 m_currentContact->setData(QString(), AbookAddressbook::Photo);
0242             m_ui2->photo->setPixmap(m_incognitoPic);
0243         }
0244         else if (key == Qt::Key_Escape && o != m_ui2->photo)
0245         if (QLabel *l = qobject_cast<QLabel*>(o)) {
0246             setText(l, l->text());
0247             return true; // prevent closing the dialog!
0248         }
0249     }
0250     default:
0251         return false;
0252     }
0253     return false;
0254 }
0255 
0256 void BE::Contacts::importPhoto(const QString &path)
0257 {
0258     if (!m_currentContact) {
0259         qWarning("CANNOT IMPORT PHOTO FOR INVALID CONTACT!");
0260         return;
0261     }
0262     const QString photo = QString::number(
0263                 QDateTime::currentMSecsSinceEpoch()
0264                 ) + QLatin1Char('.') + QFileInfo(path).suffix();
0265 
0266     const QString file = QDir::homePath() + QLatin1String("/.abook/") + photo;
0267     if (QFile::copy(path, file)) {
0268         m_currentContact->setData(photo, AbookAddressbook::Photo);
0269         setPhoto(file);
0270     }
0271 }
0272 
0273 void BE::Contacts::addContact()
0274 {
0275     m_dirty = true;
0276     QStandardItem *item = new QStandardItem(tr("[New Contact]"));
0277     for (QList<Field>::const_iterator   it = fields.constBegin(),
0278                                         end = fields.constEnd(); it != end; ++it) {
0279         if (it->type == AbookAddressbook::Name || it->type == AbookAddressbook::Photo)
0280             continue;
0281         item->setData( QString(QLatin1Char('[') + it->key + QLatin1Char(']')), it->type );
0282     }
0283     if (m_currentContact)
0284         m_abook->model()->insertRow(m_currentContact->index().row() + 1, item);
0285     else
0286         m_abook->model()->appendRow(item);
0287     m_ui->contacts->setCurrentIndex(m_sortFilterProxy->mapFromSource(item->index()));
0288 }
0289 
0290 void BE::Contacts::removeCurrentContact()
0291 {
0292     m_dirty = true;
0293     if (m_currentContact) {
0294         QModelIndex idx = m_sortFilterProxy->mapFromSource(m_currentContact->index());
0295         delete m_currentContact;
0296         m_currentContact = 0;
0297         m_sortFilterProxy->removeRow(idx.row());
0298     }
0299 }
0300 
0301 void BE::Contacts::setContact(const QModelIndex &index)
0302 {
0303     QModelIndex translated = index;
0304     if (index.model() == m_sortFilterProxy)
0305         translated = m_sortFilterProxy->mapToSource(index);
0306 
0307     if (m_currentContact) {
0308         if (m_abook->model()->itemFromIndex(translated) == m_currentContact)
0309             return; // same one
0310         for (QList<Field>::const_iterator   it = fields.constBegin(),
0311                                             end = fields.constEnd(); it != end; ++it) {
0312             if (it->type != AbookAddressbook::Photo) {
0313                 QString s = lifeText(it->label);
0314                 if (s.startsWith(QLatin1String("[")))
0315                     s.clear();
0316                 if (m_currentContact->data(it->type).toString() != s) {
0317                     m_currentContact->setData( true, AbookAddressbook::Dirty );
0318                     if (s.isEmpty())
0319                         m_currentContact->setData( QVariant(), it->type ); // invalidate
0320                     else
0321                         m_currentContact->setData( s, it->type );
0322                     m_dirty = true;
0323                 }
0324             }
0325         }
0326     }
0327 
0328     m_currentContact = m_abook->model()->itemFromIndex(translated);
0329     if (!m_currentContact)
0330         return;
0331 
0332     for (QList<Field>::const_iterator   it = fields.constBegin(),
0333         end = fields.constEnd(); it != end; ++it) {
0334         if (it->type != AbookAddressbook::Photo) {
0335             QString s;
0336             QVariant v = m_currentContact->data(it->type);
0337             if (v.isValid())
0338                 s = v.toString();
0339             else
0340                 s = QLatin1Char('[') + it->key + QLatin1Char(']');
0341             setText(it->label, s);
0342         }
0343     }
0344     QPixmap userPic = m_incognitoPic;
0345     QString photo = m_currentContact->data(AbookAddressbook::Photo).toString();
0346     if (!photo.isEmpty()) {
0347         if (QDir::isRelativePath(photo))
0348             photo = QDir::homePath() + QLatin1String("/.abook/") + photo;
0349         if (QFile::exists(photo) && setPhoto(photo))
0350             return;
0351     }
0352     m_ui2->photo->setPixmap(userPic);
0353 
0354     m_ui->contacts->setCurrentIndex(m_sortFilterProxy->mapFromSource(translated));
0355 }
0356 
0357 bool BE::Contacts::setPhoto(const QString &path)
0358 {
0359     QRect r(0,0,160,160);
0360     QImage  img = QImage( path );
0361     if (img.isNull())
0362         return false;
0363     const float f = qMin( float(img.width())/float(r.width()),
0364                             float(img.height())/float(r.height()) );
0365     r.setSize( r.size()*f );
0366     r.moveTopRight( img.rect().topRight() );
0367     img = img.copy(r).scaled( QSize(160,160), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
0368 
0369     img = img.convertToFormat(QImage::Format_ARGB32);
0370 
0371 //     if ( !MPC::setting("rgbCover").toBool() )
0372     {
0373         int r,g,b;
0374         palette().color(foregroundRole()).getRgb(&r,&g,&b);
0375 
0376         int n = img.width() * img.height();
0377         const uchar *bits = img.bits();
0378         QRgb *pixel = (QRgb*)(const_cast<uchar*>(bits));
0379 
0380         // this creates a (slightly) translucent monochromactic version of the
0381         // image using the foreground color
0382         // the gray value is turned into the opacity
0383         #define ALPHA qAlpha(pixel[i])
0384         #define GRAY qGray(pixel[i])
0385         #define OPACITY 224
0386         if ( qMax( qMax(r,g), b ) > 128 ) // value > 50%, bright foreground
0387             for (int i = 0; i < n; ++i)
0388                 pixel[i] = qRgba( r,g,b, ( ALPHA * ( (OPACITY*GRAY) / 255 ) ) / 255 );
0389             else // inverse
0390             for (int i = 0; i < n; ++i)
0391                 pixel[i] = qRgba( r,g,b, ( ALPHA * ( (OPACITY*(255-GRAY)) / 255 ) ) / 255 );
0392     }
0393 #if 1
0394     QPainterPath glasPath;
0395     glasPath.moveTo( img.rect().topLeft() );
0396     glasPath.lineTo( img.rect().topRight() );
0397     glasPath.quadTo( img.rect().center()/2, img.rect().bottomLeft() );
0398 
0399     QPainter p( &img );
0400     p.setRenderHint( QPainter::Antialiasing );
0401     p.setPen( Qt::NoPen );
0402     p.setBrush( QColor(255,255,255,64) );
0403     p.drawPath(glasPath);
0404     p.end();
0405 #endif
0406     m_ui2->photo->setPixmap( QPixmap::fromImage( img ) );
0407     return true;
0408 }
0409 
0410 void BE::Contacts::updateFocusPolicy(QWidget *oldFocus, QWidget *newFocus)
0411 {
0412     bool wasEdit(false), isEdit(false);
0413     for (QList<Field>::const_iterator it = fields.constBegin(), end = fields.constEnd(); it != end; ++it) {
0414         if (oldFocus == it->label)
0415             wasEdit = true;
0416         else if (newFocus == it->label)
0417             isEdit = true;
0418         if (isEdit && wasEdit)
0419             break;
0420     }
0421 
0422     if (isEdit == wasEdit)
0423         return;
0424 
0425     Qt::FocusPolicy policy = isEdit ? Qt::StrongFocus : Qt::ClickFocus;
0426 
0427     for (QList<Field>::const_iterator it = fields.constBegin(), end = fields.constEnd(); it != end; ++it)
0428         it->label->setFocusPolicy(policy);
0429 
0430     m_ui2->photo->setFocusPolicy(Qt::ClickFocus);
0431 
0432     policy = isEdit ? Qt::ClickFocus : Qt::StrongFocus;
0433     m_ui->filter->setFocusPolicy(policy);
0434     m_ui->contacts->setFocusPolicy(policy);
0435     m_ui->add->setFocusPolicy(policy);
0436     m_ui->remove->setFocusPolicy(policy);
0437 }
0438 
0439 void BE::Contacts::manageContact(const QString &mail, const QString &prettyName)
0440 {
0441     QStandardItemModel *model = m_abook->model();
0442     for (int i = 0; i < model->rowCount(); ++i) {
0443         QStandardItem *item = model->item(i);
0444         if (QString::compare(item->data(AbookAddressbook::Mail).toString(), mail, Qt::CaseInsensitive) == 0) {
0445             setContact(model->index(i, 0));
0446             return;
0447         }
0448     }
0449 
0450     // no match -> create one
0451     addContact();
0452     m_ui2->mail->setText(mail);
0453     if (!prettyName.isEmpty()) {
0454         m_ui2->name->setText(prettyName);
0455         m_currentContact->setText(prettyName);
0456     } else {
0457         m_ui2->name->setText(tr("[name]"));
0458     }
0459 }