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