83b1a646a34971b933c23be1a6668840454a76d8
[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
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 #include <application.h>
41
42 #include "ui_about.h"
43 #include "eventdialog.h"
44 #include "daynavigatorwidget.h"
45 #include "settingsdialog.h"
46 #include "conferenceeditor.h"
47 #include "schedulexmlparser.h"
48 #include "errormessage.h"
49
50 #include "tabcontainer.h"
51 #include "appsettings.h"
52
53 MainWindow::MainWindow(QWidget* parent): QMainWindow(parent) {
54     setupUi(this);
55
56     // Open database
57     sqlEngine = new SqlEngine(this);
58     searchTabContainer->setSqlEngine(sqlEngine);
59     connect(sqlEngine, SIGNAL(dbError(QString)), this, SLOT(showError(QString)));
60     sqlEngine->open();
61     sqlEngine->createOrUpdateDbSchema();
62
63     conferenceModel = new ConferenceModel(this);
64     mXmlParser = new ScheduleXmlParser(sqlEngine, this);
65     mNetworkAccessManager = new QNetworkAccessManager(this);
66     systemTrayIcon = new QSystemTrayIcon(qApp->windowIcon(), this);
67     alarmTimer = new QTimer(this);
68
69     alarmTimer->setInterval(60000);
70     alarmTimer->start();
71     saved_title = windowTitle();
72
73 #ifdef N810
74     tabWidget->setTabText(1,"Favs");
75     //tabWidget->setTabText(2,"Day");
76 #endif
77
78     // first time run aplication: -> let's have it direct connection in this case
79     if(!AppSettings::contains("proxyIsDirectConnection"))
80         AppSettings::setDirectConnection(true);
81
82     QNetworkProxy proxy(
83             AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : (QNetworkProxy::ProxyType)AppSettings::proxyType(),
84             AppSettings::proxyAddress(),
85             AppSettings::proxyPort(),
86             AppSettings::proxyUsername(),
87             AppSettings::proxyPassword());
88     QNetworkProxy::setApplicationProxy(proxy);
89
90     // event details have changed
91     connect(dayTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
92     connect(favsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
93     connect(tracksTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
94     connect(roomsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
95     connect(searchTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
96
97     // date has changed
98     connect(dayNavigator, SIGNAL(dateChanged(QDate)), dayTabContainer, SLOT(redisplayDate(QDate)));
99     connect(dayNavigator, SIGNAL(dateChanged(QDate)), favsTabContainer, SLOT(redisplayDate(QDate)));
100     connect(dayNavigator, SIGNAL(dateChanged(QDate)), tracksTabContainer, SLOT(redisplayDate(QDate)));
101     connect(dayNavigator, SIGNAL(dateChanged(QDate)), roomsTabContainer, SLOT(redisplayDate(QDate)));
102     connect(dayNavigator, SIGNAL(dateChanged(QDate)), searchTabContainer, SLOT(redisplayDate(QDate)));
103
104     // search result has changed
105     connect(searchTabContainer, SIGNAL(searchResultChanged()), SLOT(onSearchResultChanged()));
106
107     // systm tray icon
108     connect(systemTrayIcon, SIGNAL(messageClicked()), SLOT(onSystemTrayMessageClicked()));
109     connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(onSystemTrayMessageClicked()));
110
111     // timer
112     connect(alarmTimer, SIGNAL(timeout()), SLOT(onAlarmTimerTimeout()));
113
114     // add the actions from the main menu to the window, otherwise the shortcuts don't work on MAEMO
115     addAction(conferencesAction);
116     addAction(settingsAction);
117     addAction(quitAction);
118
119     // make it impossible to hide the toolbar by disallowing its context menu
120     toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
121
122     // open conference
123     useConference(Conference::activeConference());
124     // optimization, see useConference() code
125     try {
126         initTabs();
127     } catch (const OrmException& e) {
128         qDebug() << "OrmException:" << e.text();
129         clearTabs();
130     }
131
132     connect(mNetworkAccessManager, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)), SLOT(sslErrors(QNetworkReply*, QList<QSslError>)));
133     connect(mNetworkAccessManager, SIGNAL(finished(QNetworkReply*)), SLOT(networkQueryFinished(QNetworkReply*)));
134     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), conferenceModel, SLOT(newConferenceBegin()));
135     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), conferenceModel, SLOT(newConferenceEnd(int)));
136 }
137
138
139 MainWindow::~MainWindow() {
140     sqlEngine->close();
141 }
142
143
144 void MainWindow::on_aboutAction_triggered()
145 {
146     QDialog dialog(this);
147     Ui::AboutDialog ui;
148     ui.setupUi(&dialog);
149     ui.labDescription->setText(ui.labDescription->text().arg(qApp->applicationVersion()));
150 #ifdef N810
151     dialog.setFixedWidth(width());
152 #endif
153     dialog.exec();
154 }
155
156
157 void MainWindow::on_reloadAction_triggered() {
158     int confId = Conference::activeConference();
159     if (confId== -1) return;
160     Conference active = Conference::getById(confId);
161     if (active.url().isEmpty()) return;
162     importFromNetwork(active.url(), confId);
163     setEnabled(false);
164 }
165
166
167 void MainWindow::on_nowAction_triggered() {
168     int confId = Conference::activeConference();
169     if (confId== -1) return;
170     dayNavigator->setCurDate(QDate::currentDate());
171     dayTabContainer->expandTimeGroup(QTime::currentTime(), confId);
172 }
173
174
175 void MainWindow::on_searchAction_triggered() {
176     if (tabWidget->currentWidget() == searchTab)
177         searchTabContainer->showSearchDialog(!searchTabContainer->searchDialogIsVisible());
178     else {
179         tabWidget->setCurrentWidget(searchTab);
180         searchTabContainer->showSearchDialog();
181     }
182 }
183
184
185 void MainWindow::on_expandAllAction_triggered() {
186     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->expandAll();
187     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->expandAll();
188     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->expandAll();
189     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->expandAll();
190     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->expandAll();
191 }
192
193
194 void MainWindow::on_collapseAllAction_triggered() {
195     if (tabWidget->currentWidget() == favouritesTab) favsTabContainer->treeView->collapseAll();
196     if (tabWidget->currentWidget() == dayViewTab) dayTabContainer->treeView->collapseAll();
197     if (tabWidget->currentWidget() == tracksTab) tracksTabContainer->treeView->collapseAll();
198     if (tabWidget->currentWidget() == roomsTab) roomsTabContainer->treeView->collapseAll();
199     if (tabWidget->currentWidget() == searchTab) searchTabContainer->treeView->collapseAll();
200 }
201
202
203 void MainWindow::onEventChanged(int aEventId, bool favouriteChanged) {
204     dayTabContainer->redisplayEvent(aEventId);
205     if (favouriteChanged) favsTabContainer->redisplayDate(dayNavigator->curDate());
206     else favsTabContainer->redisplayEvent(aEventId);
207     tracksTabContainer->redisplayEvent(aEventId);
208     roomsTabContainer->redisplayEvent(aEventId);
209     searchTabContainer->redisplayEvent(aEventId);
210 }
211
212
213 void MainWindow::onSearchResultChanged() {
214     // Are results found on the current date?
215     QDate date = dayNavigator->curDate();
216     int count = searchTabContainer->searchResultCount(date);
217     if (count > 0) {searchTabContainer->redisplayDate(date); return;}
218
219     // Are results found in the future?
220     for (date = date.addDays(1); date <= dayNavigator->endDate(); date = date.addDays(1)) {
221         int count = searchTabContainer->searchResultCount(date);
222         if (count > 0) {dayNavigator->setCurDate(date); return;}
223     }
224
225     // Are results found in the past?
226     for (date = dayNavigator->startDate(); date < dayNavigator->curDate(); date = date.addDays(1)) {
227         int count = searchTabContainer->searchResultCount(date);
228         if (count > 0) {dayNavigator->setCurDate(date); return;}
229     }
230     // No results were found
231     searchTabContainer->redisplayDate(dayNavigator->curDate());
232 }
233
234
235 void MainWindow::onSystemTrayMessageClicked() {
236     systemTrayIcon->hide();
237 }
238
239
240 void MainWindow::onAlarmTimerTimeout() {
241     // determine if an alarm is set on an event that's starting soon
242     int conferenceId = Conference::activeConference();
243     QList<Event> events = Event::getImminentAlarmEvents(AppSettings::preEventAlarmSec(), conferenceId);
244     if (events.empty()) return;
245
246     // build a message string
247     const Conference conference = Conference::getById(conferenceId);
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(conference.shiftTime(event.start().time()).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(conference.shiftTime(event.start().time()).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         ((Application*) qApp)->setActiveConference(active);
329         QDate startDate = active.start();
330         QDate endDate = active.end();
331
332         // 'dayNavigator' emits signal 'dateChanged' after setting valid START:END dates
333         dayNavigator->setDates(startDate, endDate);
334         nowAction->trigger();
335     }
336 }
337
338 void MainWindow::clearTabs()
339 {
340     dayTabContainer->clearModel();
341     tracksTabContainer->clearModel();
342     roomsTabContainer->clearModel();
343     favsTabContainer->clearModel();
344     searchTabContainer->clearModel();
345 }
346
347 void MainWindow::unsetConference()
348 {
349     clearTabs();
350     dayNavigator->unsetDates();
351     setWindowTitle(saved_title);
352     ((Application*) qApp)->unsetActiveConference();
353 }
354
355
356 void MainWindow::showError(const QString& message) {
357     error_message(message);
358 }
359
360
361 void MainWindow::on_settingsAction_triggered()
362 {
363     SettingsDialog dialog;
364     dialog.loadDialogData();
365     if (dialog.exec() == QDialog::Accepted) {
366         dialog.saveDialogData();
367         QNetworkProxy proxy(
368                 AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : (QNetworkProxy::ProxyType)AppSettings::proxyType(),
369                 AppSettings::proxyAddress(),
370                 AppSettings::proxyPort(),
371                 AppSettings::proxyUsername(),
372                 AppSettings::proxyPassword());
373         QNetworkProxy::setApplicationProxy(proxy);
374     }
375 }
376
377 /** Create and run ConferenceEditor dialog, making required connections for it.
378
379 This method manages, which classes actually perform changes in conference list.
380
381 There are several classes that modify the conferences:
382 this:
383  deletion and URL update.
384 this, mXmlParser and mNetworkAccessManager:
385  addition and refresh.
386 */
387 void MainWindow::on_conferencesAction_triggered()
388 {
389     ConferenceEditor dialog(conferenceModel, this);
390
391     connect(&dialog, SIGNAL(haveConferenceUrl(const QString&, int)), SLOT(importFromNetwork(const QString&, int)));
392     connect(&dialog, SIGNAL(haveConferenceFile(const QString&, int)), SLOT(importFromFile(const QString&, int)));
393     connect(&dialog, SIGNAL(removeConferenceRequested(int)), SLOT(removeConference(int)));
394     connect(&dialog, SIGNAL(changeUrlRequested(int, const QString&)),
395                     SLOT(changeConferenceUrl(int, const QString&)));
396
397     connect(&dialog, SIGNAL(haveConferenceSelected(int)), SLOT(useConference(int)));
398     connect(&dialog, SIGNAL(noneConferenceSelected()), SLOT(unsetConference()));
399
400     connect(mXmlParser, SIGNAL(parsingScheduleBegin()), &dialog, SLOT(importStarted()));
401     connect(mXmlParser, SIGNAL(progressStatus(int)), &dialog, SLOT(showParsingProgress(int)));
402     connect(mXmlParser, SIGNAL(parsingScheduleEnd(int)), &dialog, SLOT(importFinished(int)));
403
404     connect(this, SIGNAL(conferenceRemoved()), &dialog, SLOT(conferenceRemoved()));
405
406     dialog.exec();
407
408     // optimization, see useConference() code
409     try {
410         initTabs();
411     } catch (const OrmException& e) {
412         qDebug() << "OrmException:" << e.text();
413         clearTabs();
414     }
415 }
416
417 void MainWindow::sslErrors(QNetworkReply *aReply, const QList<QSslError> &errors) {
418     QString errorString;
419     foreach (const QSslError &error, errors) {
420         if (!errorString.isEmpty()) {
421             errorString += ", ";
422         }
423         errorString += error.errorString();
424     }
425
426     if (QMessageBox::warning(
427                 this,
428                 tr("SSL errors"),
429                 tr("One or more SSL errors have occurred: %1", 0, errors.size()).arg(errorString),
430                 QMessageBox::Ignore | QMessageBox::Cancel) == QMessageBox::Ignore) {
431         aReply->ignoreSslErrors();
432     } else {
433         aReply->abort();
434     }
435 }
436
437 void MainWindow::networkQueryFinished(QNetworkReply *aReply) {
438     if (aReply->error() != QNetworkReply::NoError) {
439         error_message(tr("Error occurred during download: %1").arg(aReply->errorString()));
440     } else {
441         QUrl redirectUrl = aReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
442         if (!redirectUrl.isEmpty()) {
443             if (redirectUrl != aReply->request().url()) {
444                 importFromNetwork(redirectUrl.toString(), aReply->request().attribute(QNetworkRequest::User).toInt());
445                 return; // don't enable controls
446             } else {
447                 error_message(tr("Error: Cyclic redirection from %1 to itself.").arg(redirectUrl.toString()));
448             }
449         } else {
450             importData(aReply->readAll(), aReply->url().toEncoded(), aReply->request().attribute(QNetworkRequest::User).toInt());
451         }
452     }
453     setEnabled(true);
454 }
455
456 void MainWindow::importData(const QByteArray &aData, const QString& url, int conferenceId)
457 {
458     mXmlParser->parseData(aData, url, conferenceId);
459 }
460
461 void MainWindow::importFromNetwork(const QString& url, int conferenceId)
462 {
463     QNetworkRequest request;
464     QSslConfiguration qSslConfiguration = request.sslConfiguration();
465     qSslConfiguration.setProtocol(QSsl::AnyProtocol);
466     qSslConfiguration.setPeerVerifyMode(QSslSocket::QueryPeer);
467     request.setUrl(QUrl(url));
468     request.setSslConfiguration(qSslConfiguration);
469     request.setAttribute(QNetworkRequest::User, conferenceId);
470
471     mNetworkAccessManager->setProxy(QNetworkProxy::applicationProxy());
472     mNetworkAccessManager->get(request);
473 }
474
475 void MainWindow::importFromFile(const QString& filename, int conferenceId)
476 {
477     QFile file(filename);
478     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {    
479         static const QString format("Cannot read \"%1\": error %2");
480         error_message(format.arg(filename, QString::number(file.error())));
481     }
482
483     importData(file.readAll(), "", conferenceId);
484 }
485
486
487 void MainWindow::removeConference(int id) {
488     sqlEngine->deleteConference(id);
489     conferenceModel->conferenceRemoved();
490     emit conferenceRemoved();
491 }
492
493
494 void MainWindow::changeConferenceUrl(int id, const QString& url) {
495     Conference::getById(id).setUrl(url);
496 }
497