|
@@ -6,6 +6,10 @@
|
6
|
6
|
# - python-pyqt5
|
7
|
7
|
# - curl
|
8
|
8
|
# - xdg-open
|
|
9
|
+#
|
|
10
|
+# see also:
|
|
11
|
+# https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
|
|
12
|
+# https://stackoverflow.com/a/22618496
|
9
|
13
|
|
10
|
14
|
import json
|
11
|
15
|
import subprocess
|
|
@@ -13,15 +17,127 @@ import sys
|
13
|
17
|
import os
|
14
|
18
|
import threading
|
15
|
19
|
import time
|
16
|
|
-from PyQt5 import QtWidgets, QtGui, QtCore
|
17
|
|
-from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox
|
18
|
|
-from PyQt5.QtGui import QIcon, QPixmap
|
19
|
|
-from PyQt5.QtCore import QCoreApplication, QSettings
|
|
20
|
+from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
|
|
21
|
+from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout
|
|
22
|
+from PyQt5.QtGui import QIcon, QPixmap, QImageReader
|
|
23
|
+from PyQt5.QtCore import QCoreApplication, QSettings, QUrl, QTimer, QSize, Qt
|
|
24
|
+
|
|
25
|
+class AspectRatioPixmapLabel(QLabel):
|
|
26
|
+ def __init__(self, *args, **kwargs):
|
|
27
|
+ super(AspectRatioPixmapLabel, self).__init__(*args, **kwargs)
|
|
28
|
+ self.setMinimumSize(1, 1)
|
|
29
|
+ self.setScaledContents(False)
|
|
30
|
+ self.pix = QPixmap(0, 0)
|
|
31
|
+
|
|
32
|
+ def setPixmap(self, p):
|
|
33
|
+ self.pix = p
|
|
34
|
+ super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
|
|
35
|
+
|
|
36
|
+ def heightForWidth(self, width):
|
|
37
|
+ if self.pix.isNull():
|
|
38
|
+ return self.height()
|
|
39
|
+ else:
|
|
40
|
+ return (self.pix.height() * width) / self.pix.width()
|
|
41
|
+
|
|
42
|
+ def sizeHint(self):
|
|
43
|
+ w = self.width()
|
|
44
|
+ return QSize(int(w), int(self.heightForWidth(w)))
|
|
45
|
+
|
|
46
|
+ def scaledPixmap(self):
|
|
47
|
+ return self.pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
48
|
+
|
|
49
|
+ def resizeEvent(self, e):
|
|
50
|
+ if not self.pix.isNull():
|
|
51
|
+ super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
|
|
52
|
+
|
|
53
|
+class CamWindow(QWidget):
|
|
54
|
+ reloadDelayDefault = 1000 # in ms
|
|
55
|
+ addSize = 100
|
|
56
|
+ reloadOn = True
|
|
57
|
+
|
|
58
|
+ def __init__(self, parent, name, icon, app, manager, host, *args, **kwargs):
|
|
59
|
+ super(CamWindow, self).__init__(*args, **kwargs)
|
|
60
|
+ self.url = "http://" + host + ":8080/?action=snapshot"
|
|
61
|
+
|
|
62
|
+ self.app = app
|
|
63
|
+ self.parent = parent
|
|
64
|
+ self.manager = manager
|
|
65
|
+ self.manager.finished.connect(self.handleResponse)
|
|
66
|
+
|
|
67
|
+ self.setWindowTitle(name + " Webcam Stream")
|
|
68
|
+ self.setWindowIcon(icon)
|
|
69
|
+
|
|
70
|
+ box = QVBoxLayout()
|
|
71
|
+ self.setLayout(box)
|
|
72
|
+
|
|
73
|
+ label = QLabel(self.url)
|
|
74
|
+ box.addWidget(label, 0)
|
|
75
|
+ box.setAlignment(label, Qt.AlignHCenter)
|
|
76
|
+
|
|
77
|
+ self.img = AspectRatioPixmapLabel()
|
|
78
|
+ self.img.setPixmap(QPixmap(640, 480))
|
|
79
|
+ box.addWidget(self.img, 1)
|
|
80
|
+
|
|
81
|
+ slide = QHBoxLayout()
|
|
82
|
+ box.addLayout(slide, 0)
|
|
83
|
+
|
|
84
|
+ self.slider = QSlider(Qt.Horizontal)
|
|
85
|
+ self.slider.setMinimum(0)
|
|
86
|
+ self.slider.setMaximum(2000)
|
|
87
|
+ self.slider.setTickInterval(100)
|
|
88
|
+ self.slider.setPageStep(100)
|
|
89
|
+ self.slider.setSingleStep(100)
|
|
90
|
+ self.slider.setTickPosition(QSlider.TicksBelow)
|
|
91
|
+ self.slider.setValue(self.reloadDelayDefault)
|
|
92
|
+ self.slider.valueChanged.connect(self.sliderChanged)
|
|
93
|
+ slide.addWidget(self.slider, 1)
|
|
94
|
+
|
|
95
|
+ self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
|
|
96
|
+ slide.addWidget(self.slideLabel, 0)
|
|
97
|
+
|
|
98
|
+ size = self.size()
|
|
99
|
+ size.setHeight(size.height() + self.addSize)
|
|
100
|
+ self.resize(size)
|
|
101
|
+
|
|
102
|
+ self.loadImage()
|
|
103
|
+
|
|
104
|
+ def sliderChanged(self):
|
|
105
|
+ self.slideLabel.setText(str(self.slider.value()) + "ms")
|
|
106
|
+
|
|
107
|
+ def closeEvent(self, event):
|
|
108
|
+ self.reloadOn = False
|
|
109
|
+ self.url = ""
|
|
110
|
+ self.parent.removeWebcamWindow(self)
|
|
111
|
+
|
|
112
|
+ def scheduleLoad(self):
|
|
113
|
+ if self.reloadOn:
|
|
114
|
+ QTimer.singleShot(self.slider.value(), self.loadImage)
|
|
115
|
+
|
|
116
|
+ def loadImage(self):
|
|
117
|
+ url = QUrl(self.url)
|
|
118
|
+ request = QtNetwork.QNetworkRequest(url)
|
|
119
|
+ self.manager.get(request)
|
|
120
|
+
|
|
121
|
+ def handleResponse(self, reply):
|
|
122
|
+ if reply.url().url() == self.url:
|
|
123
|
+ if reply.error() == QtNetwork.QNetworkReply.NoError:
|
|
124
|
+ reader = QImageReader(reply)
|
|
125
|
+ reader.setAutoTransform(True)
|
|
126
|
+ image = reader.read()
|
|
127
|
+ if image != None:
|
|
128
|
+ if image.colorSpace().isValid():
|
|
129
|
+ image.convertToColorSpace(QColorSpace.SRgb)
|
|
130
|
+ self.img.setPixmap(QPixmap.fromImage(image))
|
|
131
|
+ self.scheduleLoad()
|
|
132
|
+ else:
|
|
133
|
+ print("Error decoding image: " + reader.errorString())
|
|
134
|
+ else:
|
|
135
|
+ print("Error loading image: " + reply.errorString())
|
20
|
136
|
|
21
|
137
|
class OctoTray():
|
22
|
138
|
name = "OctoTray"
|
23
|
139
|
vendor = "xythobuz"
|
24
|
|
- version = "0.1"
|
|
140
|
+ version = "0.2"
|
25
|
141
|
|
26
|
142
|
iconPath = "/usr/share/pixmaps/"
|
27
|
143
|
iconName = "octotray_icon.png"
|
|
@@ -36,14 +152,18 @@ class OctoTray():
|
36
|
152
|
"Printing", "Pausing", "Paused"
|
37
|
153
|
]
|
38
|
154
|
|
|
155
|
+ camWindows = []
|
|
156
|
+
|
39
|
157
|
def __init__(self):
|
40
|
|
- app = QtWidgets.QApplication(sys.argv)
|
|
158
|
+ self.app = QtWidgets.QApplication(sys.argv)
|
41
|
159
|
QCoreApplication.setApplicationName(self.name)
|
42
|
160
|
|
43
|
161
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
44
|
162
|
print("System Tray is not available on this platform!")
|
45
|
163
|
sys.exit(0)
|
46
|
164
|
|
|
165
|
+ self.manager = QtNetwork.QNetworkAccessManager()
|
|
166
|
+
|
47
|
167
|
self.menu = QMenu()
|
48
|
168
|
|
49
|
169
|
for p in self.printers:
|
|
@@ -72,6 +192,11 @@ class OctoTray():
|
72
|
192
|
p.append(action)
|
73
|
193
|
menu.addAction(action)
|
74
|
194
|
|
|
195
|
+ action = QAction("Show Webcam")
|
|
196
|
+ action.triggered.connect(lambda chk, x=p: self.printerWebcamAction(x))
|
|
197
|
+ p.append(action)
|
|
198
|
+ menu.addAction(action)
|
|
199
|
+
|
75
|
200
|
action = QAction("Open Web UI")
|
76
|
201
|
action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
|
77
|
202
|
p.append(action)
|
|
@@ -89,18 +214,18 @@ class OctoTray():
|
89
|
214
|
else:
|
90
|
215
|
print("no icon found")
|
91
|
216
|
|
92
|
|
- icon = QIcon()
|
|
217
|
+ self.icon = QIcon()
|
93
|
218
|
if iconPathName != "":
|
94
|
219
|
pic = QPixmap(32, 32)
|
95
|
220
|
pic.load(iconPathName)
|
96
|
|
- icon = QIcon(pic)
|
|
221
|
+ self.icon = QIcon(pic)
|
97
|
222
|
|
98
|
|
- trayIcon = QSystemTrayIcon(icon)
|
|
223
|
+ trayIcon = QSystemTrayIcon(self.icon)
|
99
|
224
|
trayIcon.setToolTip(self.name + " " + self.version)
|
100
|
225
|
trayIcon.setContextMenu(self.menu)
|
101
|
226
|
trayIcon.setVisible(True)
|
102
|
227
|
|
103
|
|
- sys.exit(app.exec_())
|
|
228
|
+ sys.exit(self.app.exec_())
|
104
|
229
|
|
105
|
230
|
def openBrowser(self, url):
|
106
|
231
|
os.system("xdg-open http://" + url)
|
|
@@ -256,5 +381,22 @@ class OctoTray():
|
256
|
381
|
warning = True
|
257
|
382
|
self.showDialog("OctoTray Status", s, None, False, warning)
|
258
|
383
|
|
|
384
|
+ def printerWebcamAction(self, item):
|
|
385
|
+ window = CamWindow(self, self.name, self.icon, self.app, self.manager, item[0])
|
|
386
|
+ self.camWindows.append(window)
|
|
387
|
+
|
|
388
|
+ screenGeometry = QDesktopWidget().screenGeometry()
|
|
389
|
+ width = screenGeometry.width()
|
|
390
|
+ height = screenGeometry.height()
|
|
391
|
+ x = (width - window.width()) / 2
|
|
392
|
+ y = (height - window.height()) / 2
|
|
393
|
+ window.setGeometry(int(x), int(y), int(window.width()), int(window.height()))
|
|
394
|
+
|
|
395
|
+ window.show()
|
|
396
|
+ window.activateWindow()
|
|
397
|
+
|
|
398
|
+ def removeWebcamWindow(self, window):
|
|
399
|
+ self.camWindows.remove(window)
|
|
400
|
+
|
259
|
401
|
if __name__ == "__main__":
|
260
|
402
|
tray = OctoTray()
|