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