Used tr() for some GUI strings (there are plenty more that should be treated this...
[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 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", "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 (OrmException& e) {
314         // cannon set an active conference
315         unsetConference();   // TODO: as no active conference is now correctly managed this should be handled as a fatal error
316         return;
317     }
318
319 }
320
321 void MainWindow::initTabs()
322 {
323     int confId = Conference::activeConference();
324     if (confId != -1)   // only init tabs if a conference is active
325     {
326         Conference active = Conference::getById(confId);
327         QDate startDate = active.start();
328         QDate endDate = active.end();
329
330         // 'dayNavigator' emits signal 'dateChanged' after setting valid START:END dates
331         dayNavigator->setDates(startDate, endDate);
332         nowAction->trigger();
333     }
334 }
335
336 void MainWindow::clearTabs()
337 {
338     dayTabContainer->clearModel();
339     tracksTabContainer->clearModel();
340     roomsTabContainer->clearModel();
341     favsTabContainer->clearModel();
342     searchTabContainer->clearModel();
343 }
344
345 void MainWindow::unsetConference()
346 {
347     clearTabs();
348     dayNavigator->unsetDates();
349     setWindowTitle(saved_title);
350 }
351
352
353 void MainWindow::showError(const QString& message) {
354     error_message(message);
355 }
356
357
358 void MainWindow::on_settingsAction_triggered()
359 {
360     SettingsDialog dialog;
361     dialog.loadDialogData();
362     if (dialog.exec() == QDialog::Accepted) {
363         dialog.saveDialogData();
364         QNetworkProxy proxy(
365                 AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : QNetworkProxy::HttpProxy,
366                 AppSettings::proxyAddress(),
367                 AppSettings::proxyPort(),
368                 PROXY_USERNAME,
369                 PROXY_PASSWD);
370         QNetworkProxy::setApplicationProxy(proxy);
371     }
372 }
373
374 /** Create and run ConferenceEditor dialog, making required connections for it.
375
376 This method manages, which classes actually perform changes in conference list.
377
378 There are several classes that modify the conferences:
379 this:
380  deletion and URL update.
381 this, mXmlParser and mNetworkAccessManager:
382  addition and refresh.
383 */
384 void MainWindow::on_conferencesAction_triggered()
385 {
386     ConferenceEditor dialog(conferenceModel, this);
387
388     connect(&dialog, SIGNAL(haveConferenceUrl(const QString&, int)), SLOT(importFromNetwork(const QString&, int)));
389     connect(&dialog, SIGNAL(haveConferenceFile(const QString&, int)), SLOT(importFromFile(const QString&, int)));
390     connect(&dialog, SIGNAL(removeConferenceRequested(int)), SLOT(removeConference(int)));
391     connect(&dialog, SIGNAL(changeUrlRequested(int, const QString&)),
392                     SLOT(changeConferenceUrl(int, const QString&)));
393
394     connect(&dialog, SIGNAL(haveConferenceSelected(int)), SLOT(useConference(int)));
395     connect(&dialog, SIGNAL(noneConferenceSelected()), SLOT(unsetConference()));
396
397     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), &dialog, SLOT(importStarted()));
398     connect(mXmlParser, SIGNAL(progressStatus(int)), &dialog, SLOT(showParsingProgress(int)));
399     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), &dialog, SLOT(importFinished(int)));
400
401     connect(this, SIGNAL(conferenceRemoved()), &dialog, SLOT(conferenceRemoved()));
402
403     dialog.exec();
404
405     // optimization, see useConference() code
406     try {
407         initTabs();
408     } catch (OrmException) {
409         clearTabs();
410     }
411 }
412
413 void MainWindow::sslErrors(QNetworkReply *aReply, const QList<QSslError> &errors) {
414     QString errorString;
415     foreach (const QSslError &error, errors) {
416         if (!errorString.isEmpty()) {
417             errorString += ", ";
418         }
419         errorString += error.errorString();
420     }
421
422     if (QMessageBox::warning(
423                 this,
424                 tr("SSL errors"),
425                 tr("One or more SSL errors have occurred: %1", 0, errors.size()).arg(errorString),
426                 QMessageBox::Ignore | QMessageBox::Cancel) == QMessageBox::Ignore) {
427         aReply->ignoreSslErrors();
428     } else {
429         aReply->abort();
430     }
431 }
432
433 void MainWindow::networkQueryFinished(QNetworkReply *aReply) {
434     if (aReply->error() != QNetworkReply::NoError) {
435         error_message(QString("Error occurred during download: ") + aReply->errorString());
436     } else {
437         QUrl redirectUrl = aReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
438         if (!redirectUrl.isEmpty()) {
439             if (redirectUrl != aReply->request().url()) {
440                 importFromNetwork(redirectUrl.toString(), aReply->request().attribute(QNetworkRequest::User).toInt());
441                 return; // don't enable controls
442             } else {
443                 error_message(QString("Error: Cyclic redirection from %1 to itself.").arg(redirectUrl.toString()));
444             }
445         } else {
446             importData(aReply->readAll(), aReply->url().toEncoded(), aReply->request().attribute(QNetworkRequest::User).toInt());
447         }
448     }
449     setEnabled(true);
450 }
451
452 void MainWindow::importData(const QByteArray &aData, const QString& url, int conferenceId)
453 {
454     mXmlParser->parseData(aData, url, conferenceId);
455 }
456
457 void MainWindow::importFromNetwork(const QString& url, int conferenceId)
458 {
459     QNetworkRequest request;
460     QSslConfiguration qSslConfiguration = request.sslConfiguration();
461     qSslConfiguration.setProtocol(QSsl::AnyProtocol);
462     qSslConfiguration.setPeerVerifyMode(QSslSocket::QueryPeer);
463     request.setUrl(QUrl(url));
464     request.setSslConfiguration(qSslConfiguration);
465     request.setAttribute(QNetworkRequest::User, conferenceId);
466
467     mNetworkAccessManager->setProxy(QNetworkProxy::applicationProxy());
468     mNetworkAccessManager->get(request);
469 }
470
471 void MainWindow::importFromFile(const QString& filename, int conferenceId)
472 {
473     QFile file(filename);
474     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {    
475         static const QString format("Cannot read \"%1\": error %2");
476         error_message(format.arg(filename, QString::number(file.error())));
477     }
478
479     importData(file.readAll(), "", conferenceId);
480 }
481
482
483 void MainWindow::removeConference(int id) {
484     sqlEngine->deleteConference(id);
485     conferenceModel->conferenceRemoved();
486     emit conferenceRemoved();
487 }
488
489
490 void MainWindow::changeConferenceUrl(int id, const QString& url) {
491     Conference::getById(id).setUrl(url);
492 }
493