Clean up #includes.
[debian/confclerk.git] / src / gui / mainwindow.cpp
1 /*
2  * Copyright (C) 2010 Ixonos Plc.
3  * Copyright (C) 2011-2021 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 #include <QDialog>
29 #include <QMessageBox>
30
31 #include "sqlengine.h"
32 #include "track.h"
33 #include "eventmodel.h"
34 #include "delegate.h"
35 #include "room.h"
36 #include "conference.h"
37 #include "ui_about.h"
38 #include "eventdialog.h"
39 #include "daynavigatorwidget.h"
40 #include "settingsdialog.h"
41 #include "conferenceeditor.h"
42 #include "schedulexmlparser.h"
43 #include "errormessage.h"
44 #include "tabcontainer.h"
45 #include "appsettings.h"
46 #include "application.h"
47
48 MainWindow::MainWindow(QWidget* parent): QMainWindow(parent) {
49     setupUi(this);
50
51     // Open database
52     sqlEngine = new SqlEngine(this);
53     searchTabContainer->setSqlEngine(sqlEngine);
54     connect(sqlEngine, SIGNAL(dbError(QString)), this, SLOT(showError(QString)));
55     sqlEngine->open();
56     sqlEngine->createOrUpdateDbSchema();
57
58     conferenceModel = new ConferenceModel(this);
59     mXmlParser = new ScheduleXmlParser(sqlEngine, this);
60     mNetworkAccessManager = new QNetworkAccessManager(this);
61     systemTrayIcon = new QSystemTrayIcon(qApp->windowIcon(), this);
62     alarmTimer = new QTimer(this);
63
64     alarmTimer->setInterval(60000);
65     alarmTimer->start();
66     saved_title = windowTitle();
67
68 #ifdef N810
69     tabWidget->setTabText(1,"Favs");
70     //tabWidget->setTabText(2,"Day");
71 #endif
72
73     // first time run aplication: -> let's have it direct connection in this case
74     if(!AppSettings::contains("proxyIsDirectConnection"))
75         AppSettings::setDirectConnection(true);
76
77     QNetworkProxy proxy(
78             AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : (QNetworkProxy::ProxyType)AppSettings::proxyType(),
79             AppSettings::proxyAddress(),
80             AppSettings::proxyPort(),
81             AppSettings::proxyUsername(),
82             AppSettings::proxyPassword());
83     QNetworkProxy::setApplicationProxy(proxy);
84
85     // event details have changed
86     connect(dayTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
87     connect(favsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
88     connect(tracksTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
89     connect(roomsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
90     connect(searchTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
91
92     // date has changed
93     connect(dayNavigator, SIGNAL(dateChanged(QDate)), dayTabContainer, SLOT(redisplayDate(QDate)));
94     connect(dayNavigator, SIGNAL(dateChanged(QDate)), favsTabContainer, SLOT(redisplayDate(QDate)));
95     connect(dayNavigator, SIGNAL(dateChanged(QDate)), tracksTabContainer, SLOT(redisplayDate(QDate)));
96     connect(dayNavigator, SIGNAL(dateChanged(QDate)), roomsTabContainer, SLOT(redisplayDate(QDate)));
97     connect(dayNavigator, SIGNAL(dateChanged(QDate)), searchTabContainer, SLOT(redisplayDate(QDate)));
98
99     // search result has changed
100     connect(searchTabContainer, SIGNAL(searchResultChanged()), SLOT(onSearchResultChanged()));
101
102     // systm tray icon
103     connect(systemTrayIcon, SIGNAL(messageClicked()), SLOT(onSystemTrayMessageClicked()));
104     connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(onSystemTrayMessageClicked()));
105
106     // timer
107     connect(alarmTimer, SIGNAL(timeout()), SLOT(onAlarmTimerTimeout()));
108
109     // add the actions from the main menu to the window, otherwise the shortcuts don't work on MAEMO
110     addAction(conferencesAction);
111     addAction(settingsAction);
112     addAction(quitAction);
113
114     // make it impossible to hide the toolbar by disallowing its context menu
115     toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
116
117     // open conference
118     useConference(Conference::activeConference());
119     // optimization, see useConference() code
120     try {
121         initTabs();
122     } catch (const OrmException& e) {
123         qDebug() << "OrmException:" << e.text();
124         clearTabs();
125     }
126
127     connect(mNetworkAccessManager, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)), SLOT(sslErrors(QNetworkReply*, QList<QSslError>)));
128     connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), SLOT(networkQueryFinished(QNetworkReply*)));
129     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), conferenceModel, SLOT(newConferenceBegin()));
130     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), conferenceModel, SLOT(newConferenceEnd(int)));
131 }
132
133
134 MainWindow::~MainWindow() {
135     sqlEngine->close();
136 }
137
138
139 void MainWindow::on_aboutAction_triggered()
140 {
141     QDialog dialog(this);
142     Ui::AboutDialog ui;
143     ui.setupUi(&dialog);
144     ui.labDescription->setText(ui.labDescription->text().arg(qApp->applicationVersion()));
145 #ifdef N810
146     dialog.setFixedWidth(width());
147 #endif
148     dialog.exec();
149 }
150
151
152 void MainWindow::on_reloadAction_triggered() {
153     int confId = Conference::activeConference();
154     if (confId== -1) return;
155     Conference active = Conference::getById(confId);
156     if (active.url().isEmpty()) return;
157     importFromNetwork(active.url(), confId);
158     setEnabled(false);
159 }
160
161
162 void MainWindow::on_nowAction_triggered() {
163     int confId = Conference::activeConference();
164     if (confId== -1) return;
165     dayNavigator->setCurDate(QDate::currentDate());
166     dayTabContainer->expandTimeGroup(QTime::currentTime(), confId);
167 }
168
169
170 void MainWindow::on_searchAction_triggered() {
171     if (tabWidget->currentWidget() == searchTab)
172         searchTabContainer->showSearchDialog(!searchTabContainer->searchDialogIsVisible());
173     else {
174         tabWidget->setCurrentWidget(searchTab);
175         searchTabContainer->showSearchDialog();
176     }
177 }
178
179
180 void MainWindow::on_expandAllAction_triggered() {
181     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->expandAll();
182     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->expandAll();
183     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->expandAll();
184     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->expandAll();
185     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->expandAll();
186 }
187
188
189 void MainWindow::on_collapseAllAction_triggered() {
190     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->collapseAll();
191     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->collapseAll();
192     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->collapseAll();
193     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->collapseAll();
194     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->collapseAll();
195 }
196
197
198 void MainWindow::onEventChanged(int aEventId, bool favouriteChanged) {
199     dayTabContainer->redisplayEvent(aEventId);
200     if (favouriteChanged) favsTabContainer->redisplayDate(dayNavigator->curDate());
201     else favsTabContainer->redisplayEvent(aEventId);
202     tracksTabContainer->redisplayEvent(aEventId);
203     roomsTabContainer->redisplayEvent(aEventId);
204     searchTabContainer->redisplayEvent(aEventId);
205 }
206
207
208 void MainWindow::onSearchResultChanged() {
209     // Are results found on the current date?
210     QDate date = dayNavigator->curDate();
211     int count = searchTabContainer->searchResultCount(date);
212     if (count > 0) {searchTabContainer->redisplayDate(date); return;}
213
214     // Are results found in the future?
215     for (date = date.addDays(1); date <= dayNavigator->endDate(); date = date.addDays(1)) {
216         int count = searchTabContainer->searchResultCount(date);
217         if (count > 0) {dayNavigator->setCurDate(date); return;}
218     }
219
220     // Are results found in the past?
221     for (date = dayNavigator->startDate(); date < dayNavigator->curDate(); date = date.addDays(1)) {
222         int count = searchTabContainer->searchResultCount(date);
223         if (count > 0) {dayNavigator->setCurDate(date); return;}
224     }
225     // No results were found
226     searchTabContainer->redisplayDate(dayNavigator->curDate());
227 }
228
229
230 void MainWindow::onSystemTrayMessageClicked() {
231     systemTrayIcon->hide();
232 }
233
234
235 void MainWindow::onAlarmTimerTimeout() {
236     // determine if an alarm is set on an event that's starting soon
237     int conferenceId = Conference::activeConference();
238     QList<Event> events = Event::getImminentAlarmEvents(AppSettings::preEventAlarmSec(), conferenceId);
239     if (events.empty()) return;
240
241     // build a message string
242     const Conference conference = Conference::getById(conferenceId);
243     Event event;
244     QString title;
245     QString message;
246     if (events.size() == 1) {
247         event = events.first();
248         title = tr("Next event at %1").arg(conference.shiftTime(event.start().time()).toString("HH:mm"));
249         message = tr("\"%1\"\n(%2)").arg(event.title()).arg(event.room()->name());
250     } else {
251         title = tr("%1 upcoming events").arg(events.size());
252         QStringList messages;
253         foreach (event, events) {
254             messages += tr("%1: \"%2\" (%3)").arg(conference.shiftTime(event.start().time()).toString("HH:mm")).arg(event.title()).arg(event.room()->name());
255         }
256         message = messages.join("\n");
257     }
258
259     // and delete the corresponding alarm
260     foreach (event, events) {
261         event.setHasAlarm(false);
262         event.update("alarm");
263         onEventChanged(event.id(), false);
264     }
265
266     // show message
267     systemTrayIcon->show();
268     // The next two lines are to prevent a very strange position of the message box the first time at X11/aweseome (not Win32/XP)
269     systemTrayIcon->showMessage("ConfClerk", tr("Your upcoming events"), QSystemTrayIcon::Information);
270     qApp->processEvents();
271     systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, 60*60*24*1000);
272     QApplication::alert(this);
273     QApplication::beep();
274 }
275
276
277 void MainWindow::useConference(int conferenceId)
278 {
279     if (conferenceId == -1)  // in case no conference is active
280     {
281         unsetConference();
282         return;
283     }
284     try {
285         int oldActiveConferenceId = Conference::activeConference();
286         bool switchActiveConference = conferenceId != oldActiveConferenceId;
287         if (switchActiveConference) Conference::getById(oldActiveConferenceId).update("active", 0);
288         Conference activeConference = Conference::getById(conferenceId);
289         if (switchActiveConference) activeConference.update("active",1);
290
291         // looks like it does not work at n900
292         setWindowTitle(activeConference.title());
293
294         // optimization.
295         // dont run initTabs() here
296         // it takes much CPU, making travelling between conferences in ConferenceEditor longer
297         // and is not seen in maemo WM anyway
298         // instead run it explicitly
299         // 1. at startup
300         // 2. when ConferenceEditor finished
301         // dont forget to protect the calls by try-catch!
302
303         // just in case, clear conference selection instead
304         clearTabs();
305
306         // end of optimization
307         // initTabs();
308     } catch (const OrmException& e) {
309         qDebug() << "OrmException:" << e.text();
310         // cannon set an active conference
311         unsetConference();   // TODO: as no active conference is now correctly managed this should be handled as a fatal error
312         return;
313     }
314
315 }
316
317 void MainWindow::initTabs()
318 {
319     int confId = Conference::activeConference();
320     if (confId != -1)   // only init tabs if a conference is active
321     {
322         Conference active = Conference::getById(confId);
323         ((Application*) qApp)->setActiveConference(active);
324         QDate startDate = active.start();
325         QDate endDate = active.end();
326
327         // 'dayNavigator' emits signal 'dateChanged' after setting valid START:END dates
328         dayNavigator->setDates(startDate, endDate);
329         nowAction->trigger();
330     }
331 }
332
333 void MainWindow::clearTabs()
334 {
335     dayTabContainer->clearModel();
336     tracksTabContainer->clearModel();
337     roomsTabContainer->clearModel();
338     favsTabContainer->clearModel();
339     searchTabContainer->clearModel();
340 }
341
342 void MainWindow::unsetConference()
343 {
344     clearTabs();
345     dayNavigator->unsetDates();
346     setWindowTitle(saved_title);
347     ((Application*) qApp)->unsetActiveConference();
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