1 //
2 // Copyright (C) 2017 James Turner  zakalawe@mac.com
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License as
6 // published by the Free Software Foundation; either version 2 of the
7 // License, or (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but
10 // WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 // General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program; if not, write to the Free Software
16 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 
18 #include "applicationcontroller.h"
19 
20 #include <QNetworkDiskCache>
21 #include <QStandardPaths>
22 #include <QNetworkRequest>
23 #include <QNetworkAccessManager>
24 #include <QNetworkReply>
25 #include <QJsonDocument>
26 #include <QJsonArray>
27 #include <QJsonObject>
28 #include <QJsonValue>
29 #include <QDebug>
30 #include <QFile>
31 #include <QDir>
32 #include <QFileInfo>
33 #include <QRegularExpression>
34 #include <QDataStream>
35 #include <QWindow>
36 #include <QTimer>
37 #include <QGuiApplication>
38 #include <QSettings>
39 #include <QQuickView>
40 #include <QQmlContext>
41 
42 #include "jsonutils.h"
43 #include "canvasconnection.h"
44 #include "WindowData.h"
45 
ApplicationController(QObject * parent)46 ApplicationController::ApplicationController(QObject *parent)
47     : QObject(parent)
48     , m_status(Idle)
49 {
50     m_netAccess = new QNetworkAccessManager;
51 
52     QSettings settings;
53     m_host = settings.value("last-host", "localhost").toString();
54     m_port = settings.value("last-port", 8080).toUInt();
55 
56     QNetworkDiskCache* cache = new QNetworkDiskCache;
57     cache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
58     m_netAccess->setCache(cache); // takes ownership
59 
60     setStatus(Idle);
61     rebuildConfigData();
62     rebuildSnapshotData();
63 
64     m_uiIdleTimer = new QTimer(this);
65     m_uiIdleTimer->setInterval(10 * 1000);
66     connect(m_uiIdleTimer, &QTimer::timeout, this,
67             &ApplicationController::onUIIdleTimeout);
68     m_uiIdleTimer->start();
69 
70     qApp->installEventFilter(this);
71 }
72 
~ApplicationController()73 ApplicationController::~ApplicationController()
74 {
75     delete m_netAccess;
76 }
77 
loadFromFile(QString path)78 void ApplicationController::loadFromFile(QString path)
79 {
80     if (!QFile::exists(path)) {
81         qWarning() << Q_FUNC_INFO << "no such file:" << path;
82     }
83 
84     QFile f(path);
85     if (!f.open(QIODevice::ReadOnly)) {
86         qWarning() << Q_FUNC_INFO << "failed to open" << path;
87         return;
88     }
89 
90     restoreState(f.readAll());
91 }
92 
setDaemonMode()93 void ApplicationController::setDaemonMode()
94 {
95     m_daemonMode = true;
96 }
97 
createWindows()98 void ApplicationController::createWindows()
99 {
100     if (m_windowList.empty()) {
101         defineDefaultWindow();
102     }
103 
104     for (int index = 0; index < m_windowList.size(); ++index) {
105         auto wd = m_windowList.at(index);
106         QQuickView* qqv = new QQuickView;
107         qqv->rootContext()->setContextProperty("_application", this);
108         qqv->rootContext()->setContextProperty("_windowNumber", index);
109         qqv->setResizeMode(QQuickView::SizeRootObjectToView);
110         qqv->setSource(QUrl{"qrc:///qml/Window.qml"});
111         qqv->setTitle(wd->title());
112 
113         if (m_daemonMode) {
114             qqv->setScreen(wd->screen());
115             qqv->setGeometry(wd->windowRect());
116             qqv->setWindowState(wd->windowState());
117         } else {
118             // interactive mode, restore window size etc
119 
120         }
121 
122         qqv->show();
123     }
124 }
125 
defineDefaultWindow()126 void ApplicationController::defineDefaultWindow()
127 {
128     auto w = new WindowData(this);
129     w->setWindowRect(QRect{0, 0, 1024, 768});
130     m_windowList.append(w);
131     emit windowListChanged();
132 }
133 
save(QString configName)134 void ApplicationController::save(QString configName)
135 {
136     QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
137     if (!d.exists()) {
138         d.mkpath(".");
139     }
140 
141     // convert spaces to underscores
142     QString filesystemCleanName = configName.replace(QRegularExpression("[\\s-\\\"/]"), "_");
143 
144     QFile f(d.filePath(filesystemCleanName + ".json"));
145     if (f.exists()) {
146         qWarning() << "not over-writing" << f.fileName();
147         return;
148     }
149 
150     f.open(QIODevice::WriteOnly | QIODevice::Truncate);
151     f.write(saveState(configName));
152 
153     QVariantMap m;
154     m["path"] = f.fileName();
155     m["name"] = configName;
156     m_configs.append(m);
157     emit configListChanged(m_configs);
158 }
159 
rebuildConfigData()160 void ApplicationController::rebuildConfigData()
161 {
162     m_configs.clear();
163     QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
164     if (!d.exists()) {
165         emit configListChanged(m_configs);
166         return;
167     }
168 
169     // this requires parsing each config in its entirety just to extract
170     // the name, which is horrible.
171     Q_FOREACH (auto entry, d.entryList(QStringList() << "*.json")) {
172         QString path = d.filePath(entry);
173         QFile f(path);
174         f.open(QIODevice::ReadOnly);
175         QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
176 
177         QVariantMap m;
178         m["path"] = path;
179         m["name"] = doc.object().value("configName").toString();
180         m_configs.append(m);
181     }
182 
183     emit configListChanged(m_configs);
184 }
185 
saveSnapshot(QString snapshotName)186 void ApplicationController::saveSnapshot(QString snapshotName)
187 {
188     QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
189     d.cd("Snapshots");
190     if (!d.exists()) {
191         d.mkpath(".");
192     }
193 
194     // convert spaces to underscores
195     QString filesystemCleanName = snapshotName.replace(QRegularExpression("[\\s-\\\"/]"), "_");
196     QFile f(d.filePath(filesystemCleanName + ".fgcanvassnapshot"));
197     if (f.exists()) {
198         qWarning() << "not over-writing" << f.fileName();
199         return;
200     }
201 
202     f.open(QIODevice::WriteOnly | QIODevice::Truncate);
203     f.write(createSnapshot(snapshotName));
204 
205     QVariantMap m;
206     m["path"] = f.fileName();
207     m["name"] = snapshotName;
208     m_snapshots.append(m);
209     emit snapshotListChanged();
210 }
211 
restoreSnapshot(int index)212 void ApplicationController::restoreSnapshot(int index)
213 {
214     QString path = m_snapshots.at(index).toMap().value("path").toString();
215     QFile f(path);
216     if (!f.open(QIODevice::ReadOnly)) {
217         qWarning() << Q_FUNC_INFO << "failed to open the file";
218         return;
219     }
220 
221     clearConnections();
222 
223     {
224         QDataStream ds(&f);
225         int version, canvasCount;
226         QString name;
227         ds >> version >> name >> canvasCount;
228 
229         for (int i=0; i < canvasCount; ++i) {
230             CanvasConnection* cc = new CanvasConnection(this);
231             cc->restoreSnapshot(ds);
232             m_activeCanvases.append(cc);
233         }
234     }
235 
236     emit activeCanvasesChanged();
237 }
238 
rebuildSnapshotData()239 void ApplicationController::rebuildSnapshotData()
240 {
241     m_snapshots.clear();
242     QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
243     d.cd("Snapshots");
244     if (!d.exists()) {
245         emit snapshotListChanged();
246         return;
247     }
248 
249     Q_FOREACH (auto entry, d.entryList(QStringList() << "*.fgcanvassnapshot")) {
250         QFile f(d.filePath(entry));
251         f.open(QIODevice::ReadOnly);
252         {
253             QDataStream ds(&f);
254             int version;
255             QString name;
256             ds >> version;
257 
258             QVariantMap m;
259             m["path"] = f.fileName();
260             ds >>name;
261             m["name"] = name;
262             m_snapshots.append(m);
263         }
264     }
265 
266     emit snapshotListChanged();
267 }
268 
query()269 void ApplicationController::query()
270 {
271     if (m_query) {
272         cancelQuery();
273     }
274 
275     if (m_host.isEmpty() || (m_port == 0))
276         return;
277 
278     QSettings settings;
279     settings.setValue("last-host", m_host);
280     settings.setValue("last-port", m_port);
281 
282     QUrl queryUrl;
283     queryUrl.setScheme("http");
284     queryUrl.setHost(m_host);
285     queryUrl.setPort(static_cast<int>(m_port));
286     queryUrl.setPath("/json/canvas/by-index");
287     queryUrl.setQuery("d=2");
288 
289     m_query = m_netAccess->get(QNetworkRequest(queryUrl));
290     connect(m_query, &QNetworkReply::finished,
291             this, &ApplicationController::onFinishedGetCanvasList);
292 
293     setStatus(Querying);
294 }
295 
cancelQuery()296 void ApplicationController::cancelQuery()
297 {
298     setStatus(Idle);
299     if (m_query) {
300         m_query->abort();
301         m_query->deleteLater();
302     }
303 
304     m_query = nullptr;
305     m_canvases.clear();
306     emit canvasListChanged();
307 }
308 
clearQuery()309 void ApplicationController::clearQuery()
310 {
311     cancelQuery();
312 }
313 
restoreConfig(int index)314 void ApplicationController::restoreConfig(int index)
315 {
316     QString path = m_configs.at(index).toMap().value("path").toString();
317     QFile f(path);
318     if (!f.open(QIODevice::ReadOnly)) {
319         qWarning() << Q_FUNC_INFO << "failed to open the file";
320         return;
321     }
322 
323     restoreState(f.readAll());
324 }
325 
deleteConfig(int index)326 void ApplicationController::deleteConfig(int index)
327 {
328     QString path = m_configs.at(index).toMap().value("path").toString();
329     QFile f(path);
330     if (!f.remove()) {
331         qWarning() << "failed to remove file";
332         return;
333     }
334 
335     m_configs.removeAt(index);
336     emit configListChanged(m_configs);
337 }
338 
saveConfigChanges(int index)339 void ApplicationController::saveConfigChanges(int index)
340 {
341     QString path = m_configs.at(index).toMap().value("path").toString();
342     QString name = m_configs.at(index).toMap().value("name").toString();
343     doSaveToFile(path, name);
344 }
345 
doSaveToFile(QString path,QString configName)346 void ApplicationController::doSaveToFile(QString path, QString configName)
347 {
348     QFile f(path);
349     f.open(QIODevice::WriteOnly | QIODevice::Truncate);
350     f.write(saveState(configName));
351 }
352 
openCanvas(QString path)353 void ApplicationController::openCanvas(QString path)
354 {
355     CanvasConnection* cc = new CanvasConnection(this);
356 
357     cc->setNetworkAccess(m_netAccess);
358     m_activeCanvases.append(cc);
359 
360     cc->setRootPropertyPath(path.toUtf8());
361     cc->connectWebSocket(m_host.toUtf8(), m_port);
362 
363     emit activeCanvasesChanged();
364 }
365 
closeCanvas(CanvasConnection * canvas)366 void ApplicationController::closeCanvas(CanvasConnection *canvas)
367 {
368     Q_ASSERT(m_activeCanvases.indexOf(canvas) >= 0);
369     m_activeCanvases.removeOne(canvas);
370     canvas->deleteLater();
371     emit activeCanvasesChanged();
372 }
373 
host() const374 QString ApplicationController::host() const
375 {
376     return m_host;
377 }
378 
port() const379 unsigned int ApplicationController::port() const
380 {
381     return m_port;
382 }
383 
canvases() const384 QVariantList ApplicationController::canvases() const
385 {
386     return m_canvases;
387 }
388 
activeCanvases()389 QQmlListProperty<CanvasConnection> ApplicationController::activeCanvases()
390 {
391     return QQmlListProperty<CanvasConnection>(this, m_activeCanvases);
392 }
393 
windowList()394 QQmlListProperty<WindowData> ApplicationController::windowList()
395 {
396     return QQmlListProperty<WindowData>(this, m_windowList);
397 }
398 
netAccess() const399 QNetworkAccessManager *ApplicationController::netAccess() const
400 {
401     return m_netAccess;
402 }
403 
showUI() const404 bool ApplicationController::showUI() const
405 {
406     if (m_daemonMode)
407         return false;
408 
409     if (m_blockUIIdle)
410         return true;
411 
412     return m_showUI;
413 }
414 
gettingStartedText() const415 QString ApplicationController::gettingStartedText() const
416 {
417     QFile f(":/doc/gettingStarted.html");
418     f.open(QIODevice::ReadOnly);
419     return QString::fromUtf8(f.readAll());
420 }
421 
showGettingStarted() const422 bool ApplicationController::showGettingStarted() const
423 {
424     if (m_daemonMode) return false;
425     QSettings settings;
426     return settings.value("show-getting-started", true).toBool();
427 }
428 
setHost(QString host)429 void ApplicationController::setHost(QString host)
430 {
431     if (m_host == host)
432         return;
433 
434     m_host = host;
435     emit hostChanged(m_host);
436     setStatus(Idle);
437 }
438 
setPort(unsigned int port)439 void ApplicationController::setPort(unsigned int port)
440 {
441     if (m_port == port)
442         return;
443 
444     m_port = port;
445     emit portChanged(m_port);
446     setStatus(Idle);
447 }
448 
setShowGettingStarted(bool show)449 void ApplicationController::setShowGettingStarted(bool show)
450 {
451     QSettings settings;
452     if (settings.value("show-getting-started", true).toBool() == show)
453         return;
454 
455     settings.setValue("show-getting-started", show);
456     emit showGettingStartedChanged(show);
457 }
458 
jsonPropNodeFindChild(QJsonObject obj,QByteArray name)459 QJsonObject jsonPropNodeFindChild(QJsonObject obj, QByteArray name)
460 {
461     Q_FOREACH (QJsonValue v, obj.value("children").toArray()) {
462         QJsonObject vo = v.toObject();
463         if (vo.value("name").toString() == name) {
464             return vo;
465         }
466     }
467 
468     return QJsonObject();
469 }
470 
onFinishedGetCanvasList()471 void ApplicationController::onFinishedGetCanvasList()
472 {
473     m_canvases.clear();
474     QNetworkReply* reply = m_query;
475     m_query = nullptr;
476     reply->deleteLater();
477 
478     if (reply->error() != QNetworkReply::NoError) {
479         setStatus(QueryFailed);
480         emit canvasListChanged();
481         return;
482     }
483 
484     QJsonDocument json = QJsonDocument::fromJson(reply->readAll());
485 
486     QJsonArray canvasArray = json.object().value("children").toArray();
487     Q_FOREACH (QJsonValue canvasValue, canvasArray) {
488         QJsonObject canvas = canvasValue.toObject();
489         QString canvasName = jsonPropNodeFindChild(canvas, "name").value("value").toString();
490         QString propPath = canvas.value("path").toString();
491 
492         QVariantMap info;
493         info["name"] = canvasName;
494         info["path"] = propPath;
495         m_canvases.append(info);
496     }
497 
498     emit canvasListChanged();
499     setStatus(SuccessfulQuery);
500 }
501 
onUIIdleTimeout()502 void ApplicationController::onUIIdleTimeout()
503 {
504     m_showUI = false;
505     emit showUIChanged();
506 }
507 
setStatus(ApplicationController::Status newStatus)508 void ApplicationController::setStatus(ApplicationController::Status newStatus)
509 {
510     if (newStatus == m_status)
511         return;
512 
513     m_status = newStatus;
514     emit statusChanged(m_status);
515 }
516 
saveState(QString name) const517 QByteArray ApplicationController::saveState(QString name) const
518 {
519     QJsonObject json;
520     json["configName"] = name;
521 
522     QJsonArray canvases;
523     Q_FOREACH (auto canvas, m_activeCanvases) {
524         canvases.append(canvas->saveState());
525     }
526 
527     json["canvases"] = canvases;
528 
529     QJsonArray windows;
530     Q_FOREACH (auto w, m_windowList) {
531         windows.append(w->saveState());
532     }
533     json["windows"] = windows;
534 
535     // background color?
536 
537     QJsonDocument doc;
538     doc.setObject(json);
539     return doc.toJson();
540 }
541 
restoreState(QByteArray bytes)542 void ApplicationController::restoreState(QByteArray bytes)
543 {
544     clearConnections();
545 
546     QJsonDocument jsonDoc = QJsonDocument::fromJson(bytes);
547     QJsonObject json = jsonDoc.object();
548 
549     // clear windows
550     Q_FOREACH(auto w, m_windowList) {
551         w->deleteLater();
552     }
553     m_windowList.clear();
554 
555     for (auto w : json.value("windows").toArray()) {
556         auto wd = new WindowData(this);
557         m_windowList.append(wd);
558         wd->restoreState(w.toObject());
559     }
560 
561     if (m_windowList.isEmpty()) {
562         // check for previous single-window data
563         auto w = new WindowData(this);
564         if (json.contains("window-rect")) {
565             w->setWindowRect(jsonArrayToRect(json.value("window-rect").toArray()));
566         }
567         if (json.contains("window-state")) {
568             w->setWindowState(static_cast<Qt::WindowState>(json.value("window-state").toInt()));
569         }
570         m_windowList.append(w);
571     }
572 
573     for (auto c : json.value("canvases").toArray()) {
574         auto cc = new CanvasConnection(this);
575         if (m_daemonMode)
576             cc->setAutoReconnect();
577 
578         cc->setNetworkAccess(m_netAccess);
579         m_activeCanvases.append(cc);
580         cc->restoreState(c.toObject());
581         cc->reconnect();
582     }
583 
584     emit windowListChanged();
585     emit activeCanvasesChanged();
586 }
587 
clearConnections()588 void ApplicationController::clearConnections()
589 {
590     Q_FOREACH(auto c, m_activeCanvases) {
591         c->deleteLater();
592     }
593     m_activeCanvases.clear();
594     emit activeCanvasesChanged();
595 }
596 
createSnapshot(QString name) const597 QByteArray ApplicationController::createSnapshot(QString name) const
598 {
599     QByteArray bytes;
600     const int version = 1;
601     {
602          QDataStream ds(&bytes, QIODevice::WriteOnly);
603          ds << version << name;
604 
605          ds << m_activeCanvases.size();
606          Q_FOREACH(auto c, m_activeCanvases) {
607              c->saveSnapshot(ds);
608          }
609     }
610 
611     return bytes;
612 }
613 
eventFilter(QObject * obj,QEvent * event)614 bool ApplicationController::eventFilter(QObject* obj, QEvent* event)
615 {
616     Q_UNUSED(obj);
617     switch (event->type()) {
618     case QEvent::MouseButtonPress:
619     case QEvent::TouchUpdate:
620     case QEvent::MouseMove:
621     case QEvent::TouchBegin:
622     case QEvent::KeyPress:
623     case QEvent::KeyRelease:
624         if (!m_showUI) {
625             m_showUI = true;
626             emit showUIChanged();
627         } else {
628             m_uiIdleTimer->start();
629         }
630 
631         break;
632     default:
633         break;
634     }
635 
636     return false; //process as normal
637 }
638