Update copyright notices.
[toast/confclerk.git] / src / gui / mainwindow.cpp
1 /*
2  * Copyright (C) 2010 Ixonos Plc.
3  * Copyright (C) 2011-2014 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
28 #include "sqlengine.h"
29
30 #include "track.h"
31 #include "eventmodel.h"
32 #include "delegate.h"
33 #include "room.h"
34
35 #include "conference.h"
36
37 #include <QDialog>
38 #include <QMessageBox>
39
40 #include "ui_about.h"
41 #include "eventdialog.h"
42 #include "daynavigatorwidget.h"
43 #include "settingsdialog.h"
44 #include "conferenceeditor.h"
45 #include "schedulexmlparser.h"
46 #include "errormessage.h"
47
48 #include "tabcontainer.h"
49 #include "appsettings.h"
50
51 const QString PROXY_USERNAME;
52 const QString PROXY_PASSWD;
53
54 MainWindow::MainWindow(QWidget* parent): QMainWindow(parent) {
55     setupUi(this);
56
57     // Open database
58     sqlEngine = new SqlEngine(this);
59     searchTabContainer->setSqlEngine(sqlEngine);
60     connect(sqlEngine, SIGNAL(dbError(QString)), this, SLOT(showError(QString)));
61     sqlEngine->open();
62     sqlEngine->createOrUpdateDbSchema();
63
64     conferenceModel = new ConferenceModel(this);
65     mXmlParser = new ScheduleXmlParser(sqlEngine, this);
66     mNetworkAccessManager = new QNetworkAccessManager(this);
67     systemTrayIcon = new QSystemTrayIcon(qApp->windowIcon(), this);
68     alarmTimer = new QTimer(this);
69
70     alarmTimer->setInterval(60000);
71     alarmTimer->start();
72     saved_title = windowTitle();
73
74 #ifdef N810
75     tabWidget->setTabText(1,"Favs");
76     //tabWidget->setTabText(2,"Day");
77 #endif
78
79     // first time run aplication: -> let's have it direct connection in this case
80     if(!AppSettings::contains("proxyIsDirectConnection"))
81         AppSettings::setDirectConnection(true);
82
83     QNetworkProxy proxy(
84             AppSettings::isDirectConnection() ? QNetworkProxy::NoProxy : QNetworkProxy::HttpProxy,
85             AppSettings::proxyAddress(),
86             AppSettings::proxyPort(),
87             PROXY_USERNAME,
88             PROXY_PASSWD);
89     QNetworkProxy::setApplicationProxy(proxy);
90
91     // event details have changed
92     connect(dayTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
93     connect(favsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
94     connect(tracksTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
95     connect(roomsTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
96     connect(searchTabContainer, SIGNAL(eventChanged(int,bool)), SLOT(onEventChanged(int,bool)));
97
98     // date has changed
99     connect(dayNavigator, SIGNAL(dateChanged(QDate)), dayTabContainer, SLOT(redisplayDate(QDate)));
100     connect(dayNavigator, SIGNAL(dateChanged(QDate)), favsTabContainer, SLOT(redisplayDate(QDate)));
101     connect(dayNavigator, SIGNAL(dateChanged(QDate)), tracksTabContainer, SLOT(redisplayDate(QDate)));
102     connect(dayNavigator, SIGNAL(dateChanged(QDate)), roomsTabContainer, SLOT(redisplayDate(QDate)));
103     connect(dayNavigator, SIGNAL(dateChanged(QDate)), searchTabContainer, SLOT(redisplayDate(QDate)));
104
105     // search result has changed
106     connect(searchTabContainer, SIGNAL(searchResultChanged()), SLOT(onSearchResultChanged()));
107
108     // systm tray icon
109     connect(systemTrayIcon, SIGNAL(messageClicked()), SLOT(onSystemTrayMessageClicked()));
110     connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(onSystemTrayMessageClicked()));
111
112     // timer
113     connect(alarmTimer, SIGNAL(timeout()), SLOT(onAlarmTimerTimeout()));
114
115     // add the actions from the main menu to the window, otherwise the shortcuts don't work on MAEMO
116     addAction(conferencesAction);
117     addAction(settingsAction);
118     addAction(quitAction);
119
120     // make it impossible to hide the toolbar by disallowing its context menu
121     toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
122
123     // open conference
124     useConference(Conference::activeConference());
125     // optimization, see useConference() code
126     try {
127         initTabs();
128     } catch (const OrmException& e) {
129         qDebug() << "OrmException:" << e.text();
130         clearTabs();
131     }
132
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     QList<Event> events = Event::getImminentAlarmEvents(AppSettings::preEventAlarmSec(), Conference::activeConference());
243     if (events.empty()) return;
244
245     // build a message string
246     Event event;
247     QString title;
248     QString message;
249     if (events.size() == 1) {
250         event = events.first();
251         title = tr("Next event at %1").arg(event.start().toString("HH:mm"));
252         message = tr("\"%1\"\n(%2)").arg(event.title()).arg(event.room()->name());
253     } else {
254         title = tr("%1 upcoming events").arg(events.size());
255         QStringList messages;
256         foreach (event, events) {
257             messages += tr("%1: \"%2\" (%3)").arg(event.start().toString("HH:mm")).arg(event.title()).arg(event.room()->name());
258         }
259         message = messages.join("\n");
260     }
261
262     // and delete the corresponding alarm
263     foreach (event, events) {
264         event.setHasAlarm(false);
265         event.update("alarm");
266         onEventChanged(event.id(), false);
267     }
268
269     // show message
270     systemTrayIcon->show();
271     // The next two lines are to prevent a very strange position of the message box the first time at X11/aweseome (not Win32/XP)
272     systemTrayIcon->showMessage("ConfClerk", "Your upcoming events", QSystemTrayIcon::Information);
273     qApp->processEvents();
274     systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, 60*60*24*1000);
275     QApplication::alert(this);
276     QApplication::beep();
277 }
278
279
280 void MainWindow::useConference(int conferenceId)
281 {
282     if (conferenceId == -1)  // in case no conference is active
283     {
284         unsetConference();
285         return;
286     }
287     try {
288         int oldActiveConferenceId = Conference::activeConference();
289         bool switchActiveConference = conferenceId != oldActiveConferenceId;
290         if (switchActiveConference) Conference::getById(oldActiveConferenceId).update("active", 0);
291         Conference activeConference = Conference::getById(conferenceId);
292         if (switchActiveConference) activeConference.update("active",1);
293
294         // looks like it does not work at n900
295         setWindowTitle(activeConference.title());
296
297         // optimization.
298         // dont run initTabs() here
299         // it takes much CPU, making travelling between conferences in ConferenceEditor longer
300         // and is not seen in maemo WM anyway
301         // instead run it explicitly
302         // 1. at startup
303         // 2. when ConferenceEditor finished
304         // dont forget to protect the calls by try-catch!
305
306         // just in case, clear conference selection instead
307         clearTabs();
308
309         // end of optimization
310         // initTabs();
311     } catch (OrmException& e) {
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::HttpProxy,
364                 AppSettings::proxyAddress(),
365                 AppSettings::proxyPort(),
366                 PROXY_USERNAME,
367                 PROXY_PASSWD);
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 (OrmException) {
407         clearTabs();
408     }
409 }
410
411 void MainWindow::networkQueryFinished(QNetworkReply *aReply) {
412     if (aReply->error() != QNetworkReply::NoError) {
413         error_message(QString("Error occured during download: ") + aReply->errorString());
414     } else {
415         QUrl redirectUrl = aReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
416         if (!redirectUrl.isEmpty()) {
417             if (redirectUrl != aReply->request().url()) {
418                 importFromNetwork(redirectUrl.toString(), aReply->request().attribute(QNetworkRequest::User).toInt());
419                 return; // don't enable controls
420             } else {
421                 error_message(QString("Error: Cyclic redirection from %1 to itself.").arg(redirectUrl.toString()));
422             }
423         } else {
424             importData(aReply->readAll(), aReply->url().toEncoded(), aReply->request().attribute(QNetworkRequest::User).toInt());
425         }
426     }
427     setEnabled(true);
428 }
429
430 void MainWindow::importData(const QByteArray &aData, const QString& url, int conferenceId)
431 {
432     mXmlParser->parseData(aData, url, conferenceId);
433 }
434
435 void MainWindow::importFromNetwork(const QString& url, int conferenceId)
436 {
437     QNetworkRequest request;
438     request.setUrl(QUrl(url));
439     request.setAttribute(QNetworkRequest::User, conferenceId);
440
441     mNetworkAccessManager->setProxy(QNetworkProxy::applicationProxy());
442     mNetworkAccessManager->get(request);
443 }
444
445 void MainWindow::importFromFile(const QString& filename, int conferenceId)
446 {
447     QFile file(filename);
448     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {    
449         static const QString format("Cannot read \"%1\": error %2");
450         error_message(format.arg(filename, QString::number(file.error())));
451     }
452
453     importData(file.readAll(), "", conferenceId);
454 }
455
456
457 void MainWindow::removeConference(int id) {
458     sqlEngine->deleteConference(id);
459     conferenceModel->conferenceRemoved();
460     emit conferenceRemoved();
461 }
462
463
464 void MainWindow::changeConferenceUrl(int id, const QString& url) {
465     Conference::getById(id).setUrl(url);
466 }
467