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