3b2582a213eab1f72b0a87d5ac4e5c4283005957
[debian/confclerk.git] / src / gui / mainwindow.cpp
1 /*
2  * Copyright (C) 2010 Ixonos Plc.
3  * Copyright (C) 2011-2017 Philipp Spitzer, gregor herrmann, Stefan Stahl
4  *
5  * This file is part of ConfClerk.
6  *
7  * ConfClerk is free software: you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation, either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * ConfClerk is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * ConfClerk.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 #include "mainwindow.h"
21
22 #include <QTreeView>
23 #include <QFile>
24 #include <QNetworkProxy>
25 #include <QNetworkAccessManager>
26 #include <QNetworkReply>
27 #include <QSslConfiguration>
28
29 #include "sqlengine.h"
30
31 #include "track.h"
32 #include "eventmodel.h"
33 #include "delegate.h"
34 #include "room.h"
35
36 #include "conference.h"
37
38 #include <QDialog>
39 #include <QMessageBox>
40
41 #include "ui_about.h"
42 #include "eventdialog.h"
43 #include "daynavigatorwidget.h"
44 #include "settingsdialog.h"
45 #include "conferenceeditor.h"
46 #include "schedulexmlparser.h"
47 #include "errormessage.h"
48
49 #include "tabcontainer.h"
50 #include "appsettings.h"
51
52 const QString PROXY_USERNAME;
53 const QString PROXY_PASSWD;
54
55 MainWindow::MainWindow(QWidget* parent): QMainWindow(parent) {
56     setupUi(this);
57
58     // Open database
59     sqlEngine = new SqlEngine(this);
60     searchTabContainer->setSqlEngine(sqlEngine);
61     connect(sqlEngine, SIGNAL(dbError(QString)), this, SLOT(showError(QString)));
62     sqlEngine->open();
63     sqlEngine->createOrUpdateDbSchema();
64
65     conferenceModel = new ConferenceModel(this);
66     mXmlParser = new ScheduleXmlParser(sqlEngine, this);
67     mNetworkAccessManager = new QNetworkAccessManager(this);
68     systemTrayIcon = new QSystemTrayIcon(qApp->windowIcon(), this);
69     alarmTimer = new QTimer(this);
70
71     alarmTimer->setInterval(60000);
72     alarmTimer->start();
73     saved_title = windowTitle();
74
75 #ifdef N810
76     tabWidget->setTabText(1,"Favs");
77     //tabWidget->setTabText(2,"Day");
78 #endif
79
80     // first time run aplication: -> let's have it direct connection in this case
81     if(!AppSettings::contains("proxyIsDirectConnection"))
82         AppSettings::setDirectConnection(true);
83
84     QNetworkProxy proxy(
85             AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : QNetworkProxy::HttpProxy,
86             AppSettings::proxyAddress(),
87             AppSettings::proxyPort(),
88             PROXY_USERNAME,
89             PROXY_PASSWD);
90     QNetworkProxy::setApplicationProxy(proxy);
91
92     // event details have changed
93     connect(dayTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
94     connect(favsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
95     connect(tracksTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
96     connect(roomsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
97     connect(searchTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
98
99     // date has changed
100     connect(dayNavigator, SIGNAL(dateChanged(QDate)), dayTabContainer, SLOT(redisplayDate(QDate)));
101     connect(dayNavigator, SIGNAL(dateChanged(QDate)), favsTabContainer, SLOT(redisplayDate(QDate)));
102     connect(dayNavigator, SIGNAL(dateChanged(QDate)), tracksTabContainer, SLOT(redisplayDate(QDate)));
103     connect(dayNavigator, SIGNAL(dateChanged(QDate)), roomsTabContainer, SLOT(redisplayDate(QDate)));
104     connect(dayNavigator, SIGNAL(dateChanged(QDate)), searchTabContainer, SLOT(redisplayDate(QDate)));
105
106     // search result has changed
107     connect(searchTabContainer, SIGNAL(searchResultChanged()), SLOT(onSearchResultChanged()));
108
109     // systm tray icon
110     connect(systemTrayIcon, SIGNAL(messageClicked()), SLOT(onSystemTrayMessageClicked()));
111     connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(onSystemTrayMessageClicked()));
112
113     // timer
114     connect(alarmTimer, SIGNAL(timeout()), SLOT(onAlarmTimerTimeout()));
115
116     // add the actions from the main menu to the window, otherwise the shortcuts don't work on MAEMO
117     addAction(conferencesAction);
118     addAction(settingsAction);
119     addAction(quitAction);
120
121     // make it impossible to hide the toolbar by disallowing its context menu
122     toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
123
124     // open conference
125     useConference(Conference::activeConference());
126     // optimization, see useConference() code
127     try {
128         initTabs();
129     } catch (const OrmException& e) {
130         qDebug() << "OrmException:" << e.text();
131         clearTabs();
132     }
133
134     connect(mNetworkAccessManager, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)), SLOT(sslErrors(QNetworkReply*, QList<QSslError>)));
135     connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), SLOT(networkQueryFinished(QNetworkReply*)));
136     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), conferenceModel, SLOT(newConferenceBegin()));
137     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), conferenceModel, SLOT(newConferenceEnd(int)));
138 }
139
140
141 MainWindow::~MainWindow() {
142     sqlEngine->close();
143 }
144
145
146 void MainWindow::on_aboutAction_triggered()
147 {
148     QDialog dialog(this);
149     Ui::AboutDialog ui;
150     ui.setupUi(&dialog);
151     ui.labDescription->setText(ui.labDescription->text().arg(qApp->applicationVersion()));
152 #ifdef N810
153     dialog.setFixedWidth(width());
154 #endif
155     dialog.exec();
156 }
157
158
159 void MainWindow::on_reloadAction_triggered() {
160     int confId = Conference::activeConference();
161     if (confId== -1) return;
162     Conference active = Conference::getById(confId);
163     if (active.url().isEmpty()) return;
164     importFromNetwork(active.url(), confId);
165     setEnabled(false);
166 }
167
168
169 void MainWindow::on_nowAction_triggered() {
170     int confId = Conference::activeConference();
171     if (confId== -1) return;
172     dayNavigator->setCurDate(QDate::currentDate());
173     dayTabContainer->expandTimeGroup(QTime::currentTime(), confId);
174 }
175
176
177 void MainWindow::on_searchAction_triggered() {
178     if (tabWidget->currentWidget() == searchTab)
179         searchTabContainer->showSearchDialog(!searchTabContainer->searchDialogIsVisible());
180     else {
181         tabWidget->setCurrentWidget(searchTab);
182         searchTabContainer->showSearchDialog();
183     }
184 }
185
186
187 void MainWindow::on_expandAllAction_triggered() {
188     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->expandAll();
189     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->expandAll();
190     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->expandAll();
191     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->expandAll();
192     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->expandAll();
193 }
194
195
196 void MainWindow::on_collapseAllAction_triggered() {
197     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->collapseAll();
198     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->collapseAll();
199     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->collapseAll();
200     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->collapseAll();
201     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->collapseAll();
202 }
203
204
205 void MainWindow::onEventChanged(int aEventId, bool favouriteChanged) {
206     dayTabContainer->redisplayEvent(aEventId);
207     if (favouriteChanged) favsTabContainer->redisplayDate(dayNavigator->curDate());
208     else favsTabContainer->redisplayEvent(aEventId);
209     tracksTabContainer->redisplayEvent(aEventId);
210     roomsTabContainer->redisplayEvent(aEventId);
211     searchTabContainer->redisplayEvent(aEventId);
212 }
213
214
215 void MainWindow::onSearchResultChanged() {
216     // Are results found on the current date?
217     QDate date = dayNavigator->curDate();
218     int count = searchTabContainer->searchResultCount(date);
219     if (count > 0) {searchTabContainer->redisplayDate(date); return;}
220
221     // Are results found in the future?
222     for (date = date.addDays(1); date <= dayNavigator->endDate(); date = date.addDays(1)) {
223         int count = searchTabContainer->searchResultCount(date);
224         if (count > 0) {dayNavigator->setCurDate(date); return;}
225     }
226
227     // Are results found in the past?
228     for (date = dayNavigator->startDate(); date < dayNavigator->curDate(); date = date.addDays(1)) {
229         int count = searchTabContainer->searchResultCount(date);
230         if (count > 0) {dayNavigator->setCurDate(date); return;}
231     }
232     // No results were found
233     searchTabContainer->redisplayDate(dayNavigator->curDate());
234 }
235
236
237 void MainWindow::onSystemTrayMessageClicked() {
238     systemTrayIcon->hide();
239 }
240
241
242 void MainWindow::onAlarmTimerTimeout() {
243     // determine if an alarm is set on an event that's starting soon
244     QList<Event> events = Event::getImminentAlarmEvents(AppSettings::preEventAlarmSec(), Conference::activeConference());
245     if (events.empty()) return;
246
247     // build a message string
248     Event event;
249     QString title;
250     QString message;
251     if (events.size() == 1) {
252         event = events.first();
253         title = tr("Next event at %1").arg(event.start().toString("HH:mm"));
254         message = tr("\"%1\"\n(%2)").arg(event.title()).arg(event.room()->name());
255     } else {
256         title = tr("%1 upcoming events").arg(events.size());
257         QStringList messages;
258         foreach (event, events) {
259             messages += tr("%1: \"%2\" (%3)").arg(event.start().toString("HH:mm")).arg(event.title()).arg(event.room()->name());
260         }
261         message = messages.join("\n");
262     }
263
264     // and delete the corresponding alarm
265     foreach (event, events) {
266         event.setHasAlarm(false);
267         event.update("alarm");
268         onEventChanged(event.id(), false);
269     }
270
271     // show message
272     systemTrayIcon->show();
273     // The next two lines are to prevent a very strange position of the message box the first time at X11/aweseome (not Win32/XP)
274     systemTrayIcon->showMessage("ConfClerk", tr("Your upcoming events"), QSystemTrayIcon::Information);
275     qApp->processEvents();
276     systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, 60*60*24*1000);
277     QApplication::alert(this);
278     QApplication::beep();
279 }
280
281
282 void MainWindow::useConference(int conferenceId)
283 {
284     if (conferenceId == -1)  // in case no conference is active
285     {
286         unsetConference();
287         return;
288     }
289     try {
290         int oldActiveConferenceId = Conference::activeConference();
291         bool switchActiveConference = conferenceId != oldActiveConferenceId;
292         if (switchActiveConference) Conference::getById(oldActiveConferenceId).update("active", 0);
293         Conference activeConference = Conference::getById(conferenceId);
294         if (switchActiveConference) activeConference.update("active",1);
295
296         // looks like it does not work at n900
297         setWindowTitle(activeConference.title());
298
299         // optimization.
300         // dont run initTabs() here
301         // it takes much CPU, making travelling between conferences in ConferenceEditor longer
302         // and is not seen in maemo WM anyway
303         // instead run it explicitly
304         // 1. at startup
305         // 2. when ConferenceEditor finished
306         // dont forget to protect the calls by try-catch!
307
308         // just in case, clear conference selection instead
309         clearTabs();
310
311         // end of optimization
312         // initTabs();
313     } catch (const OrmException& e) {
314         qDebug() << "OrmException:" << e.text();
315         // cannon set an active conference
316         unsetConference();   // TODO: as no active conference is now correctly managed this should be handled as a fatal error
317         return;
318     }
319
320 }
321
322 void MainWindow::initTabs()
323 {
324     int confId = Conference::activeConference();
325     if (confId != -1)   // only init tabs if a conference is active
326     {
327         Conference active = Conference::getById(confId);
328         QDate startDate = active.start();
329         QDate endDate = active.end();
330
331         // 'dayNavigator' emits signal 'dateChanged' after setting valid START:END dates
332         dayNavigator->setDates(startDate, endDate);
333         nowAction->trigger();
334     }
335 }
336
337 void MainWindow::clearTabs()
338 {
339     dayTabContainer->clearModel();
340     tracksTabContainer->clearModel();
341     roomsTabContainer->clearModel();
342     favsTabContainer->clearModel();
343     searchTabContainer->clearModel();
344 }
345
346 void MainWindow::unsetConference()
347 {
348     clearTabs();
349     dayNavigator->unsetDates();
350     setWindowTitle(saved_title);
351 }
352
353
354 void MainWindow::showError(const QString& message) {
355     error_message(message);
356 }
357
358
359 void MainWindow::on_settingsAction_triggered()
360 {
361     SettingsDialog dialog;
362     dialog.loadDialogData();
363     if (dialog.exec() == QDialog::Accepted) {
364         dialog.saveDialogData();
365         QNetworkProxy proxy(
366                 AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : QNetworkProxy::HttpProxy,
367                 AppSettings::proxyAddress(),
368                 AppSettings::proxyPort(),
369                 PROXY_USERNAME,
370                 PROXY_PASSWD);
371         QNetworkProxy::setApplicationProxy(proxy);
372     }
373 }
374
375 /** Create and run ConferenceEditor dialog, making required connections for it.
376
377 This method manages, which classes actually perform changes in conference list.
378
379 There are several classes that modify the conferences:
380 this:
381  deletion and URL update.
382 this, mXmlParser and mNetworkAccessManager:
383  addition and refresh.
384 */
385 void MainWindow::on_conferencesAction_triggered()
386 {
387     ConferenceEditor dialog(conferenceModel, this);
388
389     connect(&dialog, SIGNAL(haveConferenceUrl(const QString&, int)), SLOT(importFromNetwork(const QString&, int)));
390     connect(&dialog, SIGNAL(haveConferenceFile(const QString&, int)), SLOT(importFromFile(const QString&, int)));
391     connect(&dialog, SIGNAL(removeConferenceRequested(int)), SLOT(removeConference(int)));
392     connect(&dialog, SIGNAL(changeUrlRequested(int, const QString&)),
393                     SLOT(changeConferenceUrl(int, const QString&)));
394
395     connect(&dialog, SIGNAL(haveConferenceSelected(int)), SLOT(useConference(int)));
396     connect(&dialog, SIGNAL(noneConferenceSelected()), SLOT(unsetConference()));
397
398     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), &dialog, SLOT(importStarted()));
399     connect(mXmlParser, SIGNAL(progressStatus(int)), &dialog, SLOT(showParsingProgress(int)));
400     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), &dialog, SLOT(importFinished(int)));
401
402     connect(this, SIGNAL(conferenceRemoved()), &dialog, SLOT(conferenceRemoved()));
403
404     dialog.exec();
405
406     // optimization, see useConference() code
407     try {
408         initTabs();
409     } catch (const OrmException& e) {
410         qDebug() << "OrmException:" << e.text();
411         clearTabs();
412     }
413 }
414
415 void MainWindow::sslErrors(QNetworkReply *aReply, const QList<QSslError> &errors) {
416     QString errorString;
417     foreach (const QSslError &error, errors) {
418         if (!errorString.isEmpty()) {
419             errorString += ", ";
420         }
421         errorString += error.errorString();
422     }
423
424     if (QMessageBox::warning(
425                 this,
426                 tr("SSL errors"),
427                 tr("One or more SSL errors have occurred: %1", 0, errors.size()).arg(errorString),
428                 QMessageBox::Ignore | QMessageBox::Cancel) == QMessageBox::Ignore) {
429         aReply->ignoreSslErrors();
430     } else {
431         aReply->abort();
432     }
433 }
434
435 void MainWindow::networkQueryFinished(QNetworkReply *aReply) {
436     if (aReply->error() != QNetworkReply::NoError) {
437         error_message(tr("Error occurred during download: %1").arg(aReply->errorString()));
438     } else {
439         QUrl redirectUrl = aReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
440         if (!redirectUrl.isEmpty()) {
441             if (redirectUrl != aReply->request().url()) {
442                 importFromNetwork(redirectUrl.toString(), aReply->request().attribute(QNetworkRequest::User).toInt());
443                 return; // don't enable controls
444             } else {
445                 error_message(tr("Error: Cyclic redirection from %1 to itself.").arg(redirectUrl.toString()));
446             }
447         } else {
448             importData(aReply->readAll(), aReply->url().toEncoded(), aReply->request().attribute(QNetworkRequest::User).toInt());
449         }
450     }
451     setEnabled(true);
452 }
453
454 void MainWindow::importData(const QByteArray &aData, const QString& url, int conferenceId)
455 {
456     mXmlParser->parseData(aData, url, conferenceId);
457 }
458
459 void MainWindow::importFromNetwork(const QString& url, int conferenceId)
460 {
461     QNetworkRequest request;
462     QSslConfiguration qSslConfiguration = request.sslConfiguration();
463     qSslConfiguration.setProtocol(QSsl::AnyProtocol);
464     qSslConfiguration.setPeerVerifyMode(QSslSocket::QueryPeer);
465     request.setUrl(QUrl(url));
466     request.setSslConfiguration(qSslConfiguration);
467     request.setAttribute(QNetworkRequest::User, conferenceId);
468
469     mNetworkAccessManager->setProxy(QNetworkProxy::applicationProxy());
470     mNetworkAccessManager->get(request);
471 }
472
473 void MainWindow::importFromFile(const QString& filename, int conferenceId)
474 {
475     QFile file(filename);
476     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {    
477         static const QString format("Cannot read \"%1\": error %2");
478         error_message(format.arg(filename, QString::number(file.error())));
479     }
480
481     importData(file.readAll(), "", conferenceId);
482 }
483
484
485 void MainWindow::removeConference(int id) {
486     sqlEngine->deleteConference(id);
487     conferenceModel->conferenceRemoved();
488     emit conferenceRemoved();
489 }
490
491
492 void MainWindow::changeConferenceUrl(int id, const QString& url) {
493     Conference::getById(id).setUrl(url);
494 }
495