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 }