1 /*
2  * Copyright (C) 2013 Dan Vrátil <dvratil@redhat.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17  */
18 
19 #include "contact-info-dialog.h"
20 #include "contact.h"
21 
22 #include <QGridLayout>
23 #include <QPicture>
24 #include <QLabel>
25 #include <QVBoxLayout>
26 #include <QFormLayout>
27 
28 #include <TelepathyQt/Contact>
29 #include <TelepathyQt/PendingContactInfo>
30 #include <TelepathyQt/AvatarData>
31 #include <TelepathyQt/Presence>
32 #include <TelepathyQt/SharedPtr>
33 #include <TelepathyQt/ContactManager>
34 #include <TelepathyQt/Connection>
35 #include <TelepathyQt/Account>
36 #include <TelepathyQt/PendingContacts>
37 #include <TelepathyQt/PendingReady>
38 
39 #include <QDebug>
40 #include <QPushButton>
41 #include <QLineEdit>
42 #include <QFileDialog>
43 #include <QMimeType>
44 #include <QMimeDatabase>
45 #include <QDialogButtonBox>
46 
47 #include <KMessageBox>
48 #include <KTitleWidget>
49 #include <KLocalizedString>
50 #include <KDateComboBox>
51 #include <KImageFilePreview>
52 #include <KIconLoader>
53 
54 namespace KTp {
55 
56 enum InfoRowIndex {
57     FullName = 0,
58     Nickname,
59     Email,
60     Phone,
61     Homepage,
62     Birthday,
63     Organization,
64     _InfoRowCount
65 };
66 
67 static struct InfoRow {
68     const InfoRowIndex index;
69     const QString fieldName;
70     const char* title;
71 } InfoRows[] = {                                        // Don't use i18n in global static vars
72     { FullName,         QLatin1String("fn"),            I18N_NOOP("Full name:") },
73     { Nickname,         QLatin1String("nickname"),      I18N_NOOP("Nickname:") },
74     { Email,            QLatin1String("email"),         I18N_NOOP("Email:") },
75     { Phone,            QLatin1String("tel"),           I18N_NOOP("Phone:") },
76     { Homepage,         QLatin1String("url"),           I18N_NOOP("Homepage:") },
77     { Birthday,         QLatin1String("bday"),          I18N_NOOP("Birthday:") },
78     { Organization,     QLatin1String("org"),           I18N_NOOP("Organization:") }
79 };
80 
81 class ContactInfoDialog::Private
82 {
83   public:
Private(ContactInfoDialog * parent)84     Private(ContactInfoDialog *parent):
85         editable(false),
86         infoDataChanged(false),
87         avatarChanged(false),
88         columnsLayout(nullptr),
89         infoLayout(nullptr),
90         stateLayout(nullptr),
91         changeAvatarButton(nullptr),
92         clearAvatarButton(nullptr),
93         avatarLabel(nullptr),
94         q(parent)
95     {}
96 
97     void onContactUpgraded(Tp::PendingOperation *op);
98     void onContactInfoReceived(Tp::PendingOperation *op);
99     void onChangeAvatarButtonClicked();
100     void onClearAvatarButtonClicked();
101     void onInfoDataChanged();
102     void onFeatureRosterReady(Tp::PendingOperation *op);
103 
104     void addInfoRow(InfoRowIndex index, const QString &value);
105     void addStateRow(const QString &description, Tp::Contact::PresenceState state);
106     void loadStateRows();
107 
108     Tp::AccountPtr account;
109     KTp::ContactPtr contact;
110     bool editable;
111 
112     bool infoDataChanged;
113     bool avatarChanged;
114     QString newAvatarFile;
115 
116     QMap<InfoRowIndex,QWidget*> infoValueWidgets;
117 
118     QHBoxLayout *columnsLayout;
119     QFormLayout *infoLayout;
120     QFormLayout *stateLayout;
121     QPushButton *changeAvatarButton;
122     QPushButton *clearAvatarButton;
123     QLabel *avatarLabel;
124     QDialogButtonBox *buttonBox;
125 
126   private:
127     ContactInfoDialog *q;
128 };
129 
onContactUpgraded(Tp::PendingOperation * op)130 void ContactInfoDialog::Private::onContactUpgraded(Tp::PendingOperation* op)
131 {
132     Tp::PendingContacts *contacts = qobject_cast<Tp::PendingContacts*>(op);
133     if (op->isError()) {
134         return;
135     }
136 
137     Q_ASSERT(contacts->contacts().count() == 1);
138 
139     contact = KTp::ContactPtr::qObjectCast(contacts->contacts().first());
140 
141     /* Show avatar immediatelly */
142     if (contacts->features().contains(Tp::Contact::FeatureAvatarData)) {
143         QVBoxLayout *avatarLayout = new QVBoxLayout();
144         avatarLayout->setSpacing(5);
145         avatarLayout->setAlignment(Qt::AlignHCenter);
146         columnsLayout->addLayout(avatarLayout);
147 
148         avatarLabel = new QLabel(q);
149         avatarLabel->setMaximumSize(150, 150);
150         avatarLayout->addWidget(avatarLabel, 0, Qt::AlignTop);
151 
152         if (editable) {
153             changeAvatarButton = new QPushButton(i18n("Change Avatar"), q);
154             connect(changeAvatarButton, SIGNAL(clicked(bool)),
155                     q, SLOT(onChangeAvatarButtonClicked()));
156             avatarLayout->addWidget(changeAvatarButton);
157 
158             clearAvatarButton = new QPushButton(i18n("Clear Avatar"), q);
159             connect(clearAvatarButton, SIGNAL(clicked(bool)),
160                     q, SLOT(onClearAvatarButtonClicked()));
161             avatarLayout->addWidget(clearAvatarButton);
162 
163             avatarLayout->addStretch(1);
164         }
165 
166         QPixmap avatar(contact->avatarPixmap());
167         avatarLabel->setPixmap(avatar.scaled(avatarLabel->maximumSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
168     }
169 
170     /* Request detailed contact info */
171     if (contacts->features().contains(Tp::Contact::FeatureInfo)) {
172         infoLayout = new QFormLayout();
173         infoLayout->setSpacing(10);
174         columnsLayout->addLayout(infoLayout);
175 
176         Tp::PendingContactInfo *op = contact->requestInfo();
177         connect(op, SIGNAL(finished(Tp::PendingOperation*)),
178                 q, SLOT(onContactInfoReceived(Tp::PendingOperation*)));
179     }
180 }
181 
onFeatureRosterReady(Tp::PendingOperation * op)182 void ContactInfoDialog::Private::onFeatureRosterReady(Tp::PendingOperation *op)
183 {
184     loadStateRows();
185 }
186 
onContactInfoReceived(Tp::PendingOperation * op)187 void ContactInfoDialog::Private::onContactInfoReceived(Tp::PendingOperation* op)
188 {
189     Tp::PendingContactInfo *ci = qobject_cast<Tp::PendingContactInfo*>(op);
190     const Tp::ContactInfoFieldList fieldList = ci->infoFields().allFields();
191 
192     for (InfoRowIndex index = (InfoRowIndex) 0; index < _InfoRowCount; index = (InfoRowIndex)(index + 1)) {
193         QString value;
194 
195         Q_FOREACH(const Tp::ContactInfoField &field, fieldList) {
196             if (field.fieldValue.count() == 0) {
197                 continue;
198             }
199 
200             if (field.fieldName == InfoRows[index].fieldName) {
201                 value = field.fieldValue.first();
202                 break;
203             }
204         }
205 
206         /* Show edits for all values when in editable mode */
207         if (!editable && value.isEmpty()) {
208             continue;
209         }
210 
211         addInfoRow(index, value);
212     }
213 }
214 
onChangeAvatarButtonClicked()215 void ContactInfoDialog::Private::onChangeAvatarButtonClicked()
216 {
217     QPointer<QFileDialog> fileDialog = new QFileDialog(q);
218 //     fileDialog->setPreviewWidget(new KImageFilePreview(fileDialog)); //TODO KF5 - is there a replacement?
219     fileDialog->setMimeTypeFilters(QStringList() << QStringLiteral("image/*"));
220     fileDialog->setFileMode(QFileDialog::ExistingFile);
221 
222     int c = fileDialog->exec();
223     if (fileDialog && c && !fileDialog->selectedFiles().isEmpty()) {
224         newAvatarFile = fileDialog->selectedFiles().first();
225 
226         QPixmap avatar(newAvatarFile);
227         if (avatar.isNull()) {
228             KMessageBox::error(q, i18n("Failed to load the new avatar image"));
229             newAvatarFile.clear();
230             delete fileDialog;
231             return;
232         }
233         avatarLabel->setPixmap(avatar.scaled(avatarLabel->maximumSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
234         avatarChanged = true;
235         clearAvatarButton->setEnabled(true);
236     }
237 
238     delete fileDialog;
239 }
240 
onClearAvatarButtonClicked()241 void ContactInfoDialog::Private::onClearAvatarButtonClicked()
242 {
243     QPixmap avatar;
244     avatar = KIconLoader::global()->loadIcon(QLatin1String("im-user"), KIconLoader::Desktop, 128);
245 
246     newAvatarFile.clear();
247     avatarChanged = true;
248 }
249 
onInfoDataChanged()250 void ContactInfoDialog::Private::onInfoDataChanged()
251 {
252     infoDataChanged = true;
253 }
254 
addInfoRow(InfoRowIndex index,const QString & value)255 void ContactInfoDialog::Private::addInfoRow(InfoRowIndex index, const QString &value)
256 {
257     InfoRow *row = &InfoRows[index];
258 
259     // I18N_NOOP only marks the string for translation, the actual lookup of
260     // translated row->title happens here
261     QLabel *descriptionLabel = new QLabel(i18n(row->title), q);
262     QFont font = descriptionLabel->font();
263     font.setBold(true);
264     descriptionLabel->setFont(font);
265 
266     if (editable) {
267         if (index == Birthday) {
268             KDateComboBox *combo = new KDateComboBox(q);
269             combo->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker);
270             combo->setMinimumWidth(200);
271             combo->setDate(QDate::fromString(value));
272             connect(combo, SIGNAL(dateChanged(QDate)), q, SLOT(onInfoDataChanged()));
273 
274             infoValueWidgets.insert(index, combo);
275         } else {
276             QLineEdit *edit = new QLineEdit(q);
277             edit->setMinimumWidth(200);
278             edit->setText(value);
279             connect(edit, SIGNAL(textChanged(QString)), q, SLOT(onInfoDataChanged()));
280 
281             infoValueWidgets.insert(index, edit);
282         }
283     } else {
284         QLabel *label = new QLabel(q);
285         label->setOpenExternalLinks(true);
286         label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
287         if (index == Email) {
288             label->setText(QString::fromLatin1("<a href=\"mailto:%1\">%1</a>").arg(value));
289         } else if (index == Homepage) {
290             QString format;
291             if (!value.startsWith(QLatin1String("http"), Qt::CaseInsensitive)) {
292                 format = QLatin1String("<a href=\"http://%1\">%1</a>");
293             } else {
294                 format = QLatin1String("<a href=\"%1\">%1</a>");
295             }
296             label->setText(format.arg(value));
297         } else {
298             label->setText(value);
299         }
300 
301         infoValueWidgets.insert(index, label);
302     }
303 
304     infoLayout->addRow(descriptionLabel, infoValueWidgets.value(index));
305 }
306 
addStateRow(const QString & description,Tp::Contact::PresenceState state)307 void ContactInfoDialog::Private::addStateRow(const QString& description, Tp::Contact::PresenceState state)
308 {
309     QLabel *descriptionLabel = new QLabel(description, q);
310 
311     QIcon icon;
312     switch (state) {
313         case Tp::Contact::PresenceStateYes:
314             icon = QIcon::fromTheme(QStringLiteral("task-complete"));
315             break;
316         case Tp::Contact::PresenceStateNo:
317             icon = QIcon::fromTheme(QStringLiteral("task-reject"));
318             break;
319         case Tp::Contact::PresenceStateAsk:
320         default:
321             icon = QIcon::fromTheme(QStringLiteral("task-attempt"));
322             break;
323     }
324 
325     QLabel *stateLabel = new QLabel(q);
326     stateLabel->setPixmap(icon.pixmap(16));
327 
328     stateLayout->addRow(descriptionLabel, stateLabel);
329 }
330 
loadStateRows()331 void ContactInfoDialog::Private::loadStateRows()
332 {
333     if(stateLayout) {
334         addStateRow(i18n("Contact can see when you are online:"), contact->publishState());
335         addStateRow(i18n("You can see when the contact is online:"), contact->subscriptionState());
336         addStateRow(i18n("Contact is blocked:"), contact->isBlocked() ? Tp::Contact::PresenceStateYes : Tp::Contact::PresenceStateNo);
337     }
338 }
339 
ContactInfoDialog(const Tp::AccountPtr & account,const Tp::ContactPtr & contact,QWidget * parent)340 ContactInfoDialog::ContactInfoDialog(const Tp::AccountPtr &account, const Tp::ContactPtr &contact, QWidget *parent)
341     : QDialog(parent)
342     , d(new Private(this))
343 {
344 #if 0   // Editing contacts is not yet supported in TpQt
345     /* Whether contact is the user himself */
346     d->editable = (contact == account->connection()->selfContact());
347 #endif
348     d->editable = false;
349     d->account = account;
350     d->contact = KTp::ContactPtr::qObjectCast(contact);
351 
352     d->buttonBox = new QDialogButtonBox(this);
353 
354 
355     if (d->editable) {
356         d->buttonBox->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Close);
357     } else {
358         d->buttonBox->setStandardButtons(QDialogButtonBox::Close);
359     }
360 
361     connect(d->buttonBox, &QDialogButtonBox::clicked, this, &ContactInfoDialog::slotButtonClicked);
362 
363     setMaximumSize(sizeHint());
364 
365     QVBoxLayout *layout = new QVBoxLayout(this);
366     layout->setSpacing(30);
367 
368     /* Title - presence icon, alias, id */
369     KTitleWidget *titleWidget = new KTitleWidget(this);
370     KTp::Presence presence(contact->presence());
371     titleWidget->setPixmap(presence.icon().pixmap(32, 32), KTitleWidget::ImageLeft);
372     titleWidget->setText(contact->alias());
373     titleWidget->setComment(contact->id());
374     layout->addWidget(titleWidget);
375 
376     /* 1st column: avatar; 2nd column: details */
377     d->columnsLayout = new QHBoxLayout();
378     d->columnsLayout->setSpacing(30);
379     layout->addLayout(d->columnsLayout);
380 
381     /* Make sure the contact has all neccessary features ready */
382     Tp::PendingContacts *op = contact->manager()->upgradeContacts(
383             QList<Tp::ContactPtr>() << contact,
384             Tp::Features() << Tp::Contact::FeatureAvatarData
385                            << Tp::Contact::FeatureInfo);
386     connect(op, SIGNAL(finished(Tp::PendingOperation*)), SLOT(onContactUpgraded(Tp::PendingOperation*)));
387 
388     /* State Info - there is no point showing this information when it's about ourselves */
389     if (!d->editable) {
390         d->stateLayout = new QFormLayout();
391         d->stateLayout->setSpacing(10);
392         layout->addLayout(d->stateLayout);
393 
394         // Fetch roster feature, if it is supported, but not loaded
395         Tp::ConnectionPtr conn = contact->manager()->connection();
396         if(!conn->actualFeatures().contains(Tp::Connection::FeatureRoster) && !conn->missingFeatures().contains(Tp::Connection::FeatureRoster)) {
397             Tp::PendingReady *pr = conn->becomeReady(Tp::Features() << Tp::Connection::FeatureRoster);
398 
399             connect(pr, SIGNAL(finished(Tp::PendingOperation*)),
400                     SLOT(onFeatureRosterReady(Tp::PendingOperation*)));
401         } else {
402             d->loadStateRows();
403         }
404     }
405 
406     layout->addWidget(d->buttonBox);
407 }
408 
~ContactInfoDialog()409 ContactInfoDialog::~ContactInfoDialog()
410 {
411     delete d;
412 }
413 
slotButtonClicked(QAbstractButton * button)414 void ContactInfoDialog::slotButtonClicked(QAbstractButton *button)
415 {
416     if (button == d->buttonBox->button(QDialogButtonBox::Save)) {
417         if (d->avatarChanged) {
418             Tp::Avatar avatar;
419             if (!d->newAvatarFile.isEmpty()) {
420                 QFile file(d->newAvatarFile);
421                 file.open(QIODevice::ReadOnly);
422 
423                 QFileInfo fi(file);
424 
425                 avatar.avatarData = file.readAll();
426                 file.seek(0); // reset before passing to KMimeType
427 
428                 QMimeDatabase db;
429                 avatar.MIMEType = db.mimeTypeForFileNameAndData(d->newAvatarFile, &file).name();
430             }
431 
432             d->account->setAvatar(avatar);
433         }
434 
435         if (d->infoDataChanged) {
436             Tp::ContactInfoFieldList fieldList;
437 
438             for (InfoRowIndex index = (InfoRowIndex) 0; index < _InfoRowCount; index = (InfoRowIndex) (index + 1)) {
439                 InfoRow *row = &InfoRows[index];
440 
441                 Tp::ContactInfoField field;
442                 field.fieldName = row->fieldName;
443 
444                 if (index == Birthday) {
445                     KDateComboBox *combo = qobject_cast<KDateComboBox*>(d->infoValueWidgets.value(index));
446                     field.fieldValue << combo->date().toString();
447                 } else {
448                     QLineEdit *lineEdit = qobject_cast<QLineEdit*>(d->infoValueWidgets.value(index));
449                     field.fieldValue << lineEdit->text();
450                 }
451 
452                 fieldList << field;
453             }
454 
455 #if 0   // This method does not exist in TpQt (yet)
456             d->account->connection()->setContactInfo(fieldList);
457 #endif
458         }
459 
460         accept();
461         return;
462     }
463 }
464 
465 
466 } /* namespace KTp */
467 
468 #include "moc_contact-info-dialog.cpp"
469