Browse Source

split code into multiple files

Thomas Buck 1 year ago
parent
commit
9259bcb7e9
15 changed files with 640 additions and 590 deletions
  1. 1
    0
      .gitignore
  2. 12
    21
      README.md
  3. 3
    1
      build_arch.sh
  4. 1
    1
      build_linux.sh
  5. 0
    5
      build_unix.sh
  6. 1
    1
      build_win.sh
  7. 1
    1
      data/de.xythobuz.octotray.desktop
  8. 1
    1
      dist/PKGBUILD
  9. 2
    2
      dist/setup_mac.py
  10. 41
    0
      src/AspectRatioPixmapLabel.py
  11. 282
    0
      src/CamWindow.py
  12. 29
    0
      src/MainWindow.py
  13. 10
    557
      src/OctoTray.py
  14. 224
    0
      src/SettingsWindow.py
  15. 32
    0
      src/main.py

+ 1
- 0
.gitignore View File

@@ -1 +1,2 @@
1 1
 build
2
+src/__pycache__

+ 12
- 21
README.md View File

@@ -19,7 +19,7 @@ You have different options of building and running OctoTray:
19 19
 
20 20
 OctoTray can simply be run from the checked out repository, if all dependencies are installed.
21 21
 
22
-    ./src/octotray.py
22
+    ./src/main.py
23 23
 
24 24
 For this you need Python 3 as well as PyQt5.
25 25
 
@@ -53,31 +53,22 @@ To create your own bundle from source, simply run:
53 53
 
54 54
 The generated bundle will then be in 'build/mac/dist/OctoTray.app' as well as 'build/dist/OctoTray_Mac.zip'.
55 55
 
56
-### Arch Linux Package
57
-
58
-Create and install an Arch Linux package like this:
59
-
60
-    ./build_arch.sh
61
-    sudo pacman -U build/dist/octotray-0.3-1-any.pkg.tar.xz
62
-
63
-Then run it from your desktop environment menu or even add it to the autostart there.
64
-
65
-### Manual Installation on Linux
66
-
67
-You can also install the required files manually, which should work for most other Linux distribution and Unices:
68
-
69
-    sudo ./build_unix.sh
70
-
71
-After logging out and back in, you should find OctoTray in the menu of your graphical desktop environment.
72
-Take a look at the script to see exactly what is installed where.
73
-
74 56
 ### Pre-Built Linux Binary
75 57
 
76
-For completeness, a single pre-build Linux binary is also provided on GitHub, made with PyInstaller like the Windows build.
77
-It is however not recommended for productive use.
58
+A single pre-build Linux binary is also provided on GitHub, made with PyInstaller like the Windows build.
78 59
 
79 60
 To create it yourself, simply run:
80 61
 
81 62
     ./build_linux.sh
82 63
 
83 64
 The resulting binary will be in 'build/linux/dist' as well as 'build/dist/OctoTray_Linux.zip'.
65
+
66
+### Arch Linux Package
67
+
68
+Create and install an Arch Linux package like this:
69
+
70
+    ./build_arch.sh
71
+    sudo pacman -U build/dist/octotray-0.5-1-any.pkg.tar.xz
72
+
73
+Then run it from your desktop environment menu or even add it to the autostart there.
74
+This uses the same pre-built Linux binary as described above.

+ 3
- 1
build_arch.sh View File

@@ -3,7 +3,9 @@
3 3
 rm -rf build/archlinux
4 4
 mkdir -p build/archlinux
5 5
 
6
-cp -r src/* build/archlinux/
6
+./build_linux.sh
7
+cp -r build/linux/dist/OctoTray build/archlinux/octotray
8
+
7 9
 cp -r data/* build/archlinux/
8 10
 cp dist/PKGBUILD build/archlinux/
9 11
 

+ 1
- 1
build_linux.sh View File

@@ -7,7 +7,7 @@ cp src/* build/linux/
7 7
 cp data/* build/linux/
8 8
 
9 9
 cd build/linux
10
-pyinstaller --noconfirm --onefile --name=OctoTray --add-data="octotray_icon.png:." octotray.py
10
+pyinstaller --noconfirm --onefile --name=OctoTray --add-data="octotray_icon.png:." main.py
11 11
 
12 12
 cd dist
13 13
 zip -r OctoTray_Linux.zip *

+ 0
- 5
build_unix.sh View File

@@ -1,5 +0,0 @@
1
-#!/bin/sh
2
-
3
-cp src/octotray.py /usr/bin/octotray
4
-cp data/octotray_icon.png /usr/share/pixmaps/octotray_icon.png
5
-cp data/de.xythobuz.octotray.desktop /usr/share/applications/de.xythobuz.octotray.desktop

+ 1
- 1
build_win.sh View File

@@ -8,7 +8,7 @@ cp data/* build/win/
8 8
 cp dist/setup_win.py build/win/
9 9
 
10 10
 cd build/win
11
-pyinstaller --noconfirm --onefile --name=OctoTray --windowed --add-data="octotray_icon.png;." --icon="octotray_icon.ico" octotray.py
11
+pyinstaller --noconfirm --onefile --name=OctoTray --windowed --add-data="octotray_icon.png;." --icon="octotray_icon.ico" main.py
12 12
 
13 13
 cd ../..
14 14
 mkdir -p build/dist/win

+ 1
- 1
data/de.xythobuz.octotray.desktop View File

@@ -1,6 +1,6 @@
1 1
 [Desktop Entry]
2 2
 Type=Application
3
-Version=0.3
3
+Version=0.4
4 4
 Name=OctoTray
5 5
 Comment=Control OctoPrint instances from system tray
6 6
 Path=/usr/bin

+ 1
- 1
dist/PKGBUILD View File

@@ -15,7 +15,7 @@ md5sums=(SKIP
15 15
 
16 16
 package() {
17 17
 	mkdir -p "$pkgdir/usr/bin"
18
-	cp octotray.py "$pkgdir/usr/bin/octotray"
18
+	cp octotray "$pkgdir/usr/bin/octotray"
19 19
 	mkdir -p "$pkgdir/usr/share/pixmaps"
20 20
 	cp octotray_icon.png "$pkgdir/usr/share/pixmaps/octotray_icon.png"
21 21
 	mkdir -p "$pkgdir/usr/share/applications"

+ 2
- 2
dist/setup_mac.py View File

@@ -4,7 +4,7 @@
4 4
 from setuptools import setup
5 5
 
6 6
 APP_NAME = "OctoTray"
7
-APP = [ 'octotray.py' ]
7
+APP = [ 'main.py' ]
8 8
 DATA_FILES = [ 'octotray_icon.png' ]
9 9
 VERSION="0.4.0"
10 10
 
@@ -18,7 +18,7 @@ OPTIONS = {
18 18
         'CFBundleIdentifier': "de.xythobuz.octotray",
19 19
         'CFBundleVersion': VERSION,
20 20
         'CFBundleShortVersionString': VERSION,
21
-        'NSHumanReadableCopyright': u"Copyright © 2021, Thomas Buck, All Rights Reserved"
21
+        'NSHumanReadableCopyright': u"Copyright © 2021 - 2022, Thomas Buck, All Rights Reserved"
22 22
     }
23 23
 }
24 24
 

+ 41
- 0
src/AspectRatioPixmapLabel.py View File

@@ -0,0 +1,41 @@
1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# AspectRatioPixmapLabel.py
6
+#
7
+# see also:
8
+# https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
9
+# https://stackoverflow.com/a/22618496
10
+
11
+from PyQt5.QtWidgets import QLabel
12
+from PyQt5.QtGui import QPixmap
13
+from PyQt5.QtCore import QSize, Qt
14
+
15
+class AspectRatioPixmapLabel(QLabel):
16
+    def __init__(self, *args, **kwargs):
17
+        super(AspectRatioPixmapLabel, self).__init__(*args, **kwargs)
18
+        self.setMinimumSize(1, 1)
19
+        self.setScaledContents(False)
20
+        self.pix = QPixmap(0, 0)
21
+
22
+    def setPixmap(self, p):
23
+        self.pix = p
24
+        super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
25
+
26
+    def heightForWidth(self, width):
27
+        if self.pix.isNull():
28
+            return self.height()
29
+        else:
30
+            return (self.pix.height() * width) / self.pix.width()
31
+
32
+    def sizeHint(self):
33
+        w = self.width()
34
+        return QSize(int(w), int(self.heightForWidth(w)))
35
+
36
+    def scaledPixmap(self):
37
+        return self.pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
38
+
39
+    def resizeEvent(self, e):
40
+        if not self.pix.isNull():
41
+            super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())

+ 282
- 0
src/CamWindow.py View File

@@ -0,0 +1,282 @@
1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# CamWindow.py
6
+#
7
+# see also:
8
+# https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
9
+# https://stackoverflow.com/a/22618496
10
+
11
+import time
12
+from PyQt5 import QtNetwork
13
+from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QSlider, QPushButton
14
+from PyQt5.QtGui import QPixmap, QImageReader
15
+from PyQt5.QtCore import QUrl, QTimer, Qt
16
+from AspectRatioPixmapLabel import AspectRatioPixmapLabel
17
+
18
+class CamWindow(QWidget):
19
+    reloadDelayDefault = 1000 # in ms
20
+    statusDelayFactor = 2
21
+    reloadOn = True
22
+    sliderFactor = 100
23
+
24
+    def __init__(self, parent, printer, *args, **kwargs):
25
+        super(CamWindow, self).__init__(*args, **kwargs)
26
+        self.app = parent.app
27
+        self.manager = parent.manager
28
+        self.manager.finished.connect(self.handleResponse)
29
+        self.parent = parent
30
+        self.printer = printer
31
+        self.host = self.printer[0]
32
+        self.url = "http://" + self.host + ":8080/?action=snapshot"
33
+
34
+        self.setWindowTitle(parent.name + " Webcam Stream")
35
+        self.setWindowIcon(parent.icon)
36
+
37
+        box = QVBoxLayout()
38
+        self.setLayout(box)
39
+
40
+        label = QLabel(self.url)
41
+        box.addWidget(label, 0)
42
+        box.setAlignment(label, Qt.AlignHCenter)
43
+
44
+        slide = QHBoxLayout()
45
+        box.addLayout(slide, 0)
46
+
47
+        self.slideStaticLabel = QLabel("Refresh")
48
+        slide.addWidget(self.slideStaticLabel, 0)
49
+
50
+        self.slider = QSlider(Qt.Horizontal)
51
+        self.slider.setMinimum(int(100 / self.sliderFactor))
52
+        self.slider.setMaximum(int(2000 / self.sliderFactor))
53
+        self.slider.setTickInterval(int(100 / self.sliderFactor))
54
+        self.slider.setPageStep(int(100 / self.sliderFactor))
55
+        self.slider.setSingleStep(int(100 / self.sliderFactor))
56
+        self.slider.setTickPosition(QSlider.TicksBelow)
57
+        self.slider.setValue(int(self.reloadDelayDefault / self.sliderFactor))
58
+        self.slider.valueChanged.connect(self.sliderChanged)
59
+        slide.addWidget(self.slider, 1)
60
+
61
+        self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
62
+        slide.addWidget(self.slideLabel, 0)
63
+
64
+        self.img = AspectRatioPixmapLabel()
65
+        self.img.setPixmap(QPixmap(640, 480))
66
+        box.addWidget(self.img, 1)
67
+
68
+        self.statusLabel = QLabel("Status: unavailable")
69
+        box.addWidget(self.statusLabel, 0)
70
+        box.setAlignment(self.statusLabel, Qt.AlignHCenter)
71
+
72
+        self.method = self.parent.getMethod(self.printer[0], self.printer[1])
73
+        if self.method != "unknown":
74
+            controls_power = QHBoxLayout()
75
+            box.addLayout(controls_power, 0)
76
+
77
+            self.turnOnButton = QPushButton("Turn O&n")
78
+            self.turnOnButton.clicked.connect(self.turnOn)
79
+            controls_power.addWidget(self.turnOnButton)
80
+
81
+            self.turnOffButton = QPushButton("Turn O&ff")
82
+            self.turnOffButton.clicked.connect(self.turnOff)
83
+            controls_power.addWidget(self.turnOffButton)
84
+
85
+        controls_temp = QHBoxLayout()
86
+        box.addLayout(controls_temp, 0)
87
+
88
+        self.cooldownButton = QPushButton("&Cooldown")
89
+        self.cooldownButton.clicked.connect(self.cooldown)
90
+        controls_temp.addWidget(self.cooldownButton)
91
+
92
+        self.preheatToolButton = QPushButton("Preheat &Tool")
93
+        self.preheatToolButton.clicked.connect(self.preheatTool)
94
+        controls_temp.addWidget(self.preheatToolButton)
95
+
96
+        self.preheatBedButton = QPushButton("Preheat &Bed")
97
+        self.preheatBedButton.clicked.connect(self.preheatBed)
98
+        controls_temp.addWidget(self.preheatBedButton)
99
+
100
+        controls_home = QHBoxLayout()
101
+        box.addLayout(controls_home, 0)
102
+
103
+        self.homeAllButton = QPushButton("Home &All")
104
+        self.homeAllButton.clicked.connect(self.homeAll)
105
+        controls_home.addWidget(self.homeAllButton, 1)
106
+
107
+        self.homeXButton = QPushButton("Home &X")
108
+        self.homeXButton.clicked.connect(self.homeX)
109
+        controls_home.addWidget(self.homeXButton, 0)
110
+
111
+        self.homeYButton = QPushButton("Home &Y")
112
+        self.homeYButton.clicked.connect(self.homeY)
113
+        controls_home.addWidget(self.homeYButton, 0)
114
+
115
+        self.homeZButton = QPushButton("Home &Z")
116
+        self.homeZButton.clicked.connect(self.homeZ)
117
+        controls_home.addWidget(self.homeZButton, 0)
118
+
119
+        controls_move = QHBoxLayout()
120
+        box.addLayout(controls_move, 0)
121
+
122
+        self.XPButton = QPushButton("X+")
123
+        self.XPButton.clicked.connect(self.moveXP)
124
+        controls_move.addWidget(self.XPButton)
125
+
126
+        self.XMButton = QPushButton("X-")
127
+        self.XMButton.clicked.connect(self.moveXM)
128
+        controls_move.addWidget(self.XMButton)
129
+
130
+        self.YPButton = QPushButton("Y+")
131
+        self.YPButton.clicked.connect(self.moveYP)
132
+        controls_move.addWidget(self.YPButton)
133
+
134
+        self.YMButton = QPushButton("Y-")
135
+        self.YMButton.clicked.connect(self.moveYM)
136
+        controls_move.addWidget(self.YMButton)
137
+
138
+        self.ZPButton = QPushButton("Z+")
139
+        self.ZPButton.clicked.connect(self.moveZP)
140
+        controls_move.addWidget(self.ZPButton)
141
+
142
+        self.ZMButton = QPushButton("Z-")
143
+        self.ZMButton.clicked.connect(self.moveZM)
144
+        controls_move.addWidget(self.ZMButton)
145
+
146
+        controls_job = QHBoxLayout()
147
+        box.addLayout(controls_job, 0)
148
+
149
+        self.PauseButton = QPushButton("Pause/Resume")
150
+        self.PauseButton.clicked.connect(self.pauseResume)
151
+        controls_job.addWidget(self.PauseButton)
152
+
153
+        self.CancelButton = QPushButton("Cancel Job")
154
+        self.CancelButton.clicked.connect(self.cancelJob)
155
+        controls_job.addWidget(self.CancelButton)
156
+
157
+        self.loadImage()
158
+        self.loadStatus()
159
+
160
+    def pauseResume(self):
161
+        self.parent.printerPauseResume(self.printer)
162
+
163
+    def cancelJob(self):
164
+        self.parent.printerJobCancel(self.printer)
165
+
166
+    def moveXP(self):
167
+        self.parent.printerMoveAction(self.printer, "x", int(self.parent.jogMoveLength), True)
168
+
169
+    def moveXM(self):
170
+        self.parent.printerMoveAction(self.printer, "x", -1 * int(self.parent.jogMoveLength), True)
171
+
172
+    def moveYP(self):
173
+        self.parent.printerMoveAction(self.printer, "y", int(self.parent.jogMoveLength), True)
174
+
175
+    def moveYM(self):
176
+        self.parent.printerMoveAction(self.printer, "y", -1 * int(self.parent.jogMoveLength), True)
177
+
178
+    def moveZP(self):
179
+        self.parent.printerMoveAction(self.printer, "z", int(self.parent.jogMoveLength), True)
180
+
181
+    def moveZM(self):
182
+        self.parent.printerMoveAction(self.printer, "z", -1 * int(self.parent.jogMoveLength), True)
183
+
184
+    def homeX(self):
185
+        self.parent.printerHomingAction(self.printer, "x")
186
+
187
+    def homeY(self):
188
+        self.parent.printerHomingAction(self.printer, "y")
189
+
190
+    def homeZ(self):
191
+        self.parent.printerHomingAction(self.printer, "z")
192
+
193
+    def homeAll(self):
194
+        self.parent.printerHomingAction(self.printer, "xyz")
195
+
196
+    def turnOn(self):
197
+        if self.method == "psucontrol":
198
+            self.parent.printerOnAction(self.printer)
199
+        elif self.method == "system":
200
+            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
201
+            for cmd in cmds:
202
+                if "on" in cmd:
203
+                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
204
+                    break
205
+
206
+    def turnOff(self):
207
+        if self.method == "psucontrol":
208
+            self.parent.printerOffAction(self.printer)
209
+        elif self.method == "system":
210
+            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
211
+            for cmd in cmds:
212
+                if "off" in cmd:
213
+                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
214
+                    break
215
+
216
+    def cooldown(self):
217
+        self.parent.printerCooldown(self.printer)
218
+
219
+    def preheatTool(self):
220
+        self.parent.printerHeatTool(self.printer)
221
+
222
+    def preheatBed(self):
223
+        self.parent.printerHeatBed(self.printer)
224
+
225
+    def getHost(self):
226
+        return self.host
227
+
228
+    def sliderChanged(self):
229
+        self.slideLabel.setText(str(self.slider.value() * self.sliderFactor) + "ms")
230
+
231
+    def closeEvent(self, event):
232
+        self.reloadOn = False
233
+        self.url = ""
234
+        self.parent.removeWebcamWindow(self)
235
+
236
+    def scheduleLoadImage(self):
237
+        if self.reloadOn:
238
+            QTimer.singleShot(self.slider.value() * self.sliderFactor, self.loadImage)
239
+
240
+    def scheduleLoadStatus(self):
241
+        if self.reloadOn:
242
+            QTimer.singleShot(self.slider.value() * self.sliderFactor * self.statusDelayFactor, self.loadStatus)
243
+
244
+    def loadImage(self):
245
+        url = QUrl(self.url)
246
+        request = QtNetwork.QNetworkRequest(url)
247
+        self.manager.get(request)
248
+
249
+    def loadStatus(self):
250
+        s = "Status: "
251
+        t = self.parent.getTemperatureString(self.host, self.printer[1])
252
+        if len(t) > 0:
253
+            s += t
254
+        else:
255
+            s += "Unknown"
256
+
257
+        progress = self.parent.getProgress(self.host, self.printer[1])
258
+        if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
259
+            s += " - %.1f%%" % progress["completion"]
260
+            s += " - runtime "
261
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
262
+            s += " - "
263
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
264
+
265
+        self.statusLabel.setText(s)
266
+        self.scheduleLoadStatus()
267
+
268
+    def handleResponse(self, reply):
269
+        if reply.url().url() == self.url:
270
+            if reply.error() == QtNetwork.QNetworkReply.NoError:
271
+                reader = QImageReader(reply)
272
+                reader.setAutoTransform(True)
273
+                image = reader.read()
274
+                if image != None:
275
+                    if image.colorSpace().isValid():
276
+                        image.convertToColorSpace(QColorSpace.SRgb)
277
+                    self.img.setPixmap(QPixmap.fromImage(image))
278
+                    self.scheduleLoadImage()
279
+                else:
280
+                    print("Error decoding image: " + reader.errorString())
281
+            else:
282
+                print("Error loading image: " + reply.errorString())

+ 29
- 0
src/MainWindow.py View File

@@ -0,0 +1,29 @@
1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# MainWindow.py
6
+#
7
+# Used when calling application with arguments
8
+# '--windowed' or '-w' on command line,
9
+# or when no system tray is available.
10
+
11
+from PyQt5.QtWidgets import QWidget, QVBoxLayout
12
+
13
+class MainWindow(QWidget):
14
+    def __init__(self, parent, *args, **kwargs):
15
+        super(MainWindow, self).__init__(*args, **kwargs)
16
+        self.parent = parent
17
+
18
+        self.mainLayout = QVBoxLayout()
19
+        self.setLayout(self.mainLayout)
20
+        self.mainLayout.addWidget(self.parent.menu)
21
+
22
+        self.parent.menu.aboutToHide.connect(self.aboutToHide)
23
+
24
+    def aboutToHide(self):
25
+        self.parent.menu.show()
26
+
27
+    def closeEvent(self, event):
28
+        self.parent.exit()
29
+        event.accept()

src/octotray.py → src/OctoTray.py View File

@@ -2,552 +2,25 @@
2 2
 
3 3
 # OctoTray Linux Qt System Tray OctoPrint client
4 4
 #
5
-# depends on:
6
-# - python-pyqt5
5
+# OctoTray.py
7 6
 #
8
-# see also:
9
-# https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
10
-# https://stackoverflow.com/a/22618496
7
+# Main application logic.
11 8
 
12 9
 import json
13 10
 import sys
14
-import os
15 11
 import time
16
-import string
17 12
 import urllib.parse
18 13
 import urllib.request
19
-import signal
20 14
 import operator
21 15
 import socket
22 16
 from os import path
23
-from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
24
-from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout, QTableWidget, QTableWidgetItem, QPushButton, QApplication, QLineEdit, QGridLayout
25
-from PyQt5.QtGui import QIcon, QPixmap, QImageReader, QDesktopServices, QFontDatabase, QCursor, QIntValidator
26
-from PyQt5.QtCore import QCoreApplication, QSettings, QUrl, QTimer, QSize, Qt, QSettings
27
-
28
-class SettingsWindow(QWidget):
29
-    columns = [ "Hostname", "API Key", "Tool Preheat", "Bed Preheat" ]
30
-    presets = [ "octopi.local", "000000000_API_KEY_HERE_000000000", "0", "0" ]
31
-
32
-    def __init__(self, parent, *args, **kwargs):
33
-        super(SettingsWindow, self).__init__(*args, **kwargs)
34
-        self.parent = parent
35
-
36
-        self.setWindowTitle(parent.name + " Settings")
37
-        self.setWindowIcon(parent.icon)
38
-
39
-
40
-        box = QVBoxLayout()
41
-        self.setLayout(box)
42
-
43
-        staticSettings = QGridLayout()
44
-        box.addLayout(staticSettings, 0)
45
-
46
-        self.jogSpeedText = QLabel("Jog Speed")
47
-        staticSettings.addWidget(self.jogSpeedText, 0, 0)
48
-
49
-        self.jogSpeed = QLineEdit(str(self.parent.jogMoveSpeed))
50
-        self.jogSpeed.setValidator(QIntValidator(1, 6000))
51
-        staticSettings.addWidget(self.jogSpeed, 0, 1)
52
-
53
-        self.jogSpeedUnitText = QLabel("mm/min")
54
-        staticSettings.addWidget(self.jogSpeedUnitText, 0, 2)
55
-
56
-        self.jogLengthText = QLabel("Jog Length")
57
-        staticSettings.addWidget(self.jogLengthText, 1, 0)
58
-
59
-        self.jogLength = QLineEdit(str(self.parent.jogMoveLength))
60
-        self.jogLength.setValidator(QIntValidator(1, 100))
61
-        staticSettings.addWidget(self.jogLength, 1, 1)
62
-
63
-        self.jogLengthUnitText = QLabel("mm")
64
-        staticSettings.addWidget(self.jogLengthUnitText, 1, 2)
65
-
66
-        helpText = "Usage:\n"
67
-        helpText += "1st Column: Printer Hostname or IP address\n"
68
-        helpText += "2nd Column: OctoPrint API Key (32 char hexadecimal)\n"
69
-        helpText += "3rd Column: Tool Preheat Temperature (0 to disable)\n"
70
-        helpText += "4th Column: Bed Preheat Temperature (0 to disable)"
71
-        self.helpText = QLabel(helpText)
72
-        box.addWidget(self.helpText, 0)
73
-        box.setAlignment(self.helpText, Qt.AlignHCenter)
74
-
75
-        buttons = QHBoxLayout()
76
-        box.addLayout(buttons, 0)
77
-
78
-        self.add = QPushButton("&Add Printer")
79
-        self.add.clicked.connect(self.addPrinter)
80
-        buttons.addWidget(self.add)
81
-
82
-        self.remove = QPushButton("&Remove Printer")
83
-        self.remove.clicked.connect(self.removePrinter)
84
-        buttons.addWidget(self.remove)
85
-
86
-        printers = self.parent.readSettings()
87
-        self.rows = len(printers)
88
-        self.table = QTableWidget(self.rows, len(self.columns))
89
-        box.addWidget(self.table, 1)
90
-
91
-        for i in range(0, self.rows):
92
-            p = printers[i]
93
-            for j in range(0, len(self.columns)):
94
-                text = p[j]
95
-                if (j >= 2) and (j <= 3) and (text == None):
96
-                    text = "0"
97
-                item = QTableWidgetItem(text)
98
-                self.table.setItem(i, j, item)
99
-                if j == 1:
100
-                    font = item.font()
101
-                    font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
102
-                    item.setFont(font)
103
-
104
-        buttons2 = QHBoxLayout()
105
-        box.addLayout(buttons2, 0)
106
-
107
-        self.up = QPushButton("Move &Up")
108
-        self.up.clicked.connect(self.moveUp)
109
-        buttons2.addWidget(self.up)
110
-
111
-        self.down = QPushButton("Move &Down")
112
-        self.down.clicked.connect(self.moveDown)
113
-        buttons2.addWidget(self.down)
114
-
115
-        self.openWeb = QPushButton("&Open Web UI of selected")
116
-        self.openWeb.clicked.connect(self.openWebUI)
117
-        box.addWidget(self.openWeb, 0)
118
-
119
-        self.table.setHorizontalHeaderLabels(self.columns)
120
-        self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
121
-        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
122
-        self.table.resizeColumnsToContents()
123
-
124
-        if self.rows <= 0:
125
-            self.addPrinter()
126
-
127
-    def tableToList(self):
128
-        printers = []
129
-        for i in range(0, self.rows):
130
-            p = []
131
-            for j in range(0, len(self.columns)):
132
-                text = self.table.item(i, j).text()
133
-                if (j >= 2) and (j <= 3) and (text == "0"):
134
-                    text = None
135
-                p.append(text)
136
-            printers.append(p)
137
-        return printers
138
-
139
-    def settingsValid(self, printers):
140
-        for p in printers:
141
-            # p[0] needs to be valid hostname or IP
142
-            # TODO
143
-
144
-            # p[1] needs to be valid API key (hexadecimal, 32 chars)
145
-            if (len(p[1]) != 32) or not all(c in string.hexdigits for c in p[1]):
146
-                return (False, "API Key not 32-digit hexadecimal")
147
-
148
-            # p[2] and p[3] need to be integer temperatures (0...999)
149
-            for s in [ p[2], p[3] ]:
150
-                if s == None:
151
-                    s = "0"
152
-                if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
153
-                    return (False, "Temperature not a number from 0...999")
154
-
155
-        js = int(self.jogSpeed.text())
156
-        if (js < 1) or (js > 6000):
157
-            return (False, "Jog Speed not a number from 1...6000")
158
-
159
-        jl = int(self.jogLength.text())
160
-        if (jl < 1) or (jl > 100):
161
-            return (False, "Jog Length not a number from 1...100")
162
-
163
-        return (True, "")
164
-
165
-    def closeEvent(self, event):
166
-        oldPrinters = [item[0:len(self.columns)] for item in self.parent.printers]
167
-        newPrinters = self.tableToList()
168
-
169
-        valid, errorText = self.settingsValid(newPrinters)
170
-        if valid == False:
171
-            r = self.parent.showDialog(self.parent.name + " Settings Invalid", errorText + "!", "Do you want to edit it again?", True, True, False)
172
-            if r == True:
173
-                event.ignore()
174
-                return
175
-            else:
176
-                self.parent.removeSettingsWindow()
177
-                return
178
-
179
-        js = int(self.jogSpeed.text())
180
-        jl = int(self.jogLength.text())
181
-
182
-        if (oldPrinters != newPrinters) or (js != self.parent.jogMoveSpeed) or (jl != self.parent.jogMoveLength):
183
-            r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new configuration?", "This will restart the application!", True, False, False)
184
-            if r == True:
185
-                self.parent.jogMoveSpeed = js
186
-                self.parent.jogMoveLength = jl
187
-                self.parent.writeSettings(newPrinters)
188
-                self.parent.restartApp()
189
-
190
-        self.parent.removeSettingsWindow()
191
-
192
-    def addPrinter(self):
193
-        self.rows += 1
194
-        self.table.setRowCount(self.rows)
195
-        for i in range(0, len(self.columns)):
196
-            item = QTableWidgetItem(self.presets[i])
197
-            self.table.setItem(self.rows - 1, i, item)
198
-            if i == 1:
199
-                font = item.font()
200
-                font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
201
-                item.setFont(font)
202
-        self.table.resizeColumnsToContents()
203
-        self.table.setCurrentItem(self.table.item(self.rows - 1, 0))
204
-
205
-    def removePrinter(self):
206
-        r = self.table.currentRow()
207
-        if (r >= 0) and (r < self.rows):
208
-            self.rows -= 1
209
-            self.table.removeRow(r)
210
-            self.table.setCurrentItem(self.table.item(min(r, self.rows - 1), 0))
211
-
212
-    def moveUp(self):
213
-        i = self.table.currentRow()
214
-        if i <= 0:
215
-            return
216
-        host = self.table.item(i, 0).text()
217
-        key = self.table.item(i, 1).text()
218
-        self.table.item(i, 0).setText(self.table.item(i - 1, 0).text())
219
-        self.table.item(i, 1).setText(self.table.item(i - 1, 1).text())
220
-        self.table.item(i - 1, 0).setText(host)
221
-        self.table.item(i - 1, 1).setText(key)
222
-        self.table.setCurrentItem(self.table.item(i - 1, 0))
223
-
224
-    def moveDown(self):
225
-        i = self.table.currentRow()
226
-        if i >= (self.rows - 1):
227
-            return
228
-        host = self.table.item(i, 0).text()
229
-        key = self.table.item(i, 1).text()
230
-        self.table.item(i, 0).setText(self.table.item(i + 1, 0).text())
231
-        self.table.item(i, 1).setText(self.table.item(i + 1, 1).text())
232
-        self.table.item(i + 1, 0).setText(host)
233
-        self.table.item(i + 1, 1).setText(key)
234
-        self.table.setCurrentItem(self.table.item(i + 1, 0))
235
-
236
-    def openWebUI(self):
237
-        host = self.table.item(self.table.currentRow(), 0).text()
238
-        self.parent.openBrowser(host)
239
-
240
-class AspectRatioPixmapLabel(QLabel):
241
-    def __init__(self, *args, **kwargs):
242
-        super(AspectRatioPixmapLabel, self).__init__(*args, **kwargs)
243
-        self.setMinimumSize(1, 1)
244
-        self.setScaledContents(False)
245
-        self.pix = QPixmap(0, 0)
246
-
247
-    def setPixmap(self, p):
248
-        self.pix = p
249
-        super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
250
-
251
-    def heightForWidth(self, width):
252
-        if self.pix.isNull():
253
-            return self.height()
254
-        else:
255
-            return (self.pix.height() * width) / self.pix.width()
256
-
257
-    def sizeHint(self):
258
-        w = self.width()
259
-        return QSize(int(w), int(self.heightForWidth(w)))
260
-
261
-    def scaledPixmap(self):
262
-        return self.pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
263
-
264
-    def resizeEvent(self, e):
265
-        if not self.pix.isNull():
266
-            super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
267
-
268
-class CamWindow(QWidget):
269
-    reloadDelayDefault = 1000 # in ms
270
-    statusDelayFactor = 2
271
-    reloadOn = True
272
-    sliderFactor = 100
273
-
274
-    def __init__(self, parent, printer, *args, **kwargs):
275
-        super(CamWindow, self).__init__(*args, **kwargs)
276
-        self.app = parent.app
277
-        self.manager = parent.manager
278
-        self.manager.finished.connect(self.handleResponse)
279
-        self.parent = parent
280
-        self.printer = printer
281
-        self.host = self.printer[0]
282
-        self.url = "http://" + self.host + ":8080/?action=snapshot"
283
-
284
-        self.setWindowTitle(parent.name + " Webcam Stream")
285
-        self.setWindowIcon(parent.icon)
286
-
287
-        box = QVBoxLayout()
288
-        self.setLayout(box)
289
-
290
-        label = QLabel(self.url)
291
-        box.addWidget(label, 0)
292
-        box.setAlignment(label, Qt.AlignHCenter)
293
-
294
-        slide = QHBoxLayout()
295
-        box.addLayout(slide, 0)
296
-
297
-        self.slideStaticLabel = QLabel("Refresh")
298
-        slide.addWidget(self.slideStaticLabel, 0)
299
-
300
-        self.slider = QSlider(Qt.Horizontal)
301
-        self.slider.setMinimum(int(100 / self.sliderFactor))
302
-        self.slider.setMaximum(int(2000 / self.sliderFactor))
303
-        self.slider.setTickInterval(int(100 / self.sliderFactor))
304
-        self.slider.setPageStep(int(100 / self.sliderFactor))
305
-        self.slider.setSingleStep(int(100 / self.sliderFactor))
306
-        self.slider.setTickPosition(QSlider.TicksBelow)
307
-        self.slider.setValue(int(self.reloadDelayDefault / self.sliderFactor))
308
-        self.slider.valueChanged.connect(self.sliderChanged)
309
-        slide.addWidget(self.slider, 1)
310
-
311
-        self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
312
-        slide.addWidget(self.slideLabel, 0)
313
-
314
-        self.img = AspectRatioPixmapLabel()
315
-        self.img.setPixmap(QPixmap(640, 480))
316
-        box.addWidget(self.img, 1)
317
-
318
-        self.statusLabel = QLabel("Status: unavailable")
319
-        box.addWidget(self.statusLabel, 0)
320
-        box.setAlignment(self.statusLabel, Qt.AlignHCenter)
321
-
322
-        self.method = self.parent.getMethod(self.printer[0], self.printer[1])
323
-        if self.method != "unknown":
324
-            controls_power = QHBoxLayout()
325
-            box.addLayout(controls_power, 0)
326
-
327
-            self.turnOnButton = QPushButton("Turn O&n")
328
-            self.turnOnButton.clicked.connect(self.turnOn)
329
-            controls_power.addWidget(self.turnOnButton)
330
-
331
-            self.turnOffButton = QPushButton("Turn O&ff")
332
-            self.turnOffButton.clicked.connect(self.turnOff)
333
-            controls_power.addWidget(self.turnOffButton)
334
-
335
-        controls_temp = QHBoxLayout()
336
-        box.addLayout(controls_temp, 0)
337
-
338
-        self.cooldownButton = QPushButton("&Cooldown")
339
-        self.cooldownButton.clicked.connect(self.cooldown)
340
-        controls_temp.addWidget(self.cooldownButton)
341
-
342
-        self.preheatToolButton = QPushButton("Preheat &Tool")
343
-        self.preheatToolButton.clicked.connect(self.preheatTool)
344
-        controls_temp.addWidget(self.preheatToolButton)
345
-
346
-        self.preheatBedButton = QPushButton("Preheat &Bed")
347
-        self.preheatBedButton.clicked.connect(self.preheatBed)
348
-        controls_temp.addWidget(self.preheatBedButton)
349
-
350
-        controls_home = QHBoxLayout()
351
-        box.addLayout(controls_home, 0)
352
-
353
-        self.homeAllButton = QPushButton("Home &All")
354
-        self.homeAllButton.clicked.connect(self.homeAll)
355
-        controls_home.addWidget(self.homeAllButton, 1)
356
-
357
-        self.homeXButton = QPushButton("Home &X")
358
-        self.homeXButton.clicked.connect(self.homeX)
359
-        controls_home.addWidget(self.homeXButton, 0)
360
-
361
-        self.homeYButton = QPushButton("Home &Y")
362
-        self.homeYButton.clicked.connect(self.homeY)
363
-        controls_home.addWidget(self.homeYButton, 0)
364
-
365
-        self.homeZButton = QPushButton("Home &Z")
366
-        self.homeZButton.clicked.connect(self.homeZ)
367
-        controls_home.addWidget(self.homeZButton, 0)
368
-
369
-        controls_move = QHBoxLayout()
370
-        box.addLayout(controls_move, 0)
371
-
372
-        self.XPButton = QPushButton("X+")
373
-        self.XPButton.clicked.connect(self.moveXP)
374
-        controls_move.addWidget(self.XPButton)
375
-
376
-        self.XMButton = QPushButton("X-")
377
-        self.XMButton.clicked.connect(self.moveXM)
378
-        controls_move.addWidget(self.XMButton)
379
-
380
-        self.YPButton = QPushButton("Y+")
381
-        self.YPButton.clicked.connect(self.moveYP)
382
-        controls_move.addWidget(self.YPButton)
383
-
384
-        self.YMButton = QPushButton("Y-")
385
-        self.YMButton.clicked.connect(self.moveYM)
386
-        controls_move.addWidget(self.YMButton)
387
-
388
-        self.ZPButton = QPushButton("Z+")
389
-        self.ZPButton.clicked.connect(self.moveZP)
390
-        controls_move.addWidget(self.ZPButton)
391
-
392
-        self.ZMButton = QPushButton("Z-")
393
-        self.ZMButton.clicked.connect(self.moveZM)
394
-        controls_move.addWidget(self.ZMButton)
395
-
396
-        controls_job = QHBoxLayout()
397
-        box.addLayout(controls_job, 0)
398
-
399
-        self.PauseButton = QPushButton("Pause/Resume")
400
-        self.PauseButton.clicked.connect(self.pauseResume)
401
-        controls_job.addWidget(self.PauseButton)
402
-
403
-        self.CancelButton = QPushButton("Cancel Job")
404
-        self.CancelButton.clicked.connect(self.cancelJob)
405
-        controls_job.addWidget(self.CancelButton)
406
-
407
-        self.loadImage()
408
-        self.loadStatus()
409
-
410
-    def pauseResume(self):
411
-        self.parent.printerPauseResume(self.printer)
412
-
413
-    def cancelJob(self):
414
-        self.parent.printerJobCancel(self.printer)
415
-
416
-    def moveXP(self):
417
-        self.parent.printerMoveAction(self.printer, "x", int(self.parent.jogMoveLength), True)
418
-
419
-    def moveXM(self):
420
-        self.parent.printerMoveAction(self.printer, "x", -1 * int(self.parent.jogMoveLength), True)
421
-
422
-    def moveYP(self):
423
-        self.parent.printerMoveAction(self.printer, "y", int(self.parent.jogMoveLength), True)
424
-
425
-    def moveYM(self):
426
-        self.parent.printerMoveAction(self.printer, "y", -1 * int(self.parent.jogMoveLength), True)
427
-
428
-    def moveZP(self):
429
-        self.parent.printerMoveAction(self.printer, "z", int(self.parent.jogMoveLength), True)
430
-
431
-    def moveZM(self):
432
-        self.parent.printerMoveAction(self.printer, "z", -1 * int(self.parent.jogMoveLength), True)
433
-
434
-    def homeX(self):
435
-        self.parent.printerHomingAction(self.printer, "x")
436
-
437
-    def homeY(self):
438
-        self.parent.printerHomingAction(self.printer, "y")
439
-
440
-    def homeZ(self):
441
-        self.parent.printerHomingAction(self.printer, "z")
442
-
443
-    def homeAll(self):
444
-        self.parent.printerHomingAction(self.printer, "xyz")
445
-
446
-    def turnOn(self):
447
-        if self.method == "psucontrol":
448
-            self.parent.printerOnAction(self.printer)
449
-        elif self.method == "system":
450
-            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
451
-            for cmd in cmds:
452
-                if "on" in cmd:
453
-                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
454
-                    break
455
-
456
-    def turnOff(self):
457
-        if self.method == "psucontrol":
458
-            self.parent.printerOffAction(self.printer)
459
-        elif self.method == "system":
460
-            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
461
-            for cmd in cmds:
462
-                if "off" in cmd:
463
-                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
464
-                    break
465
-
466
-    def cooldown(self):
467
-        self.parent.printerCooldown(self.printer)
468
-
469
-    def preheatTool(self):
470
-        self.parent.printerHeatTool(self.printer)
471
-
472
-    def preheatBed(self):
473
-        self.parent.printerHeatBed(self.printer)
474
-
475
-    def getHost(self):
476
-        return self.host
477
-
478
-    def sliderChanged(self):
479
-        self.slideLabel.setText(str(self.slider.value() * self.sliderFactor) + "ms")
480
-
481
-    def closeEvent(self, event):
482
-        self.reloadOn = False
483
-        self.url = ""
484
-        self.parent.removeWebcamWindow(self)
485
-
486
-    def scheduleLoadImage(self):
487
-        if self.reloadOn:
488
-            QTimer.singleShot(self.slider.value() * self.sliderFactor, self.loadImage)
489
-
490
-    def scheduleLoadStatus(self):
491
-        if self.reloadOn:
492
-            QTimer.singleShot(self.slider.value() * self.sliderFactor * self.statusDelayFactor, self.loadStatus)
493
-
494
-    def loadImage(self):
495
-        url = QUrl(self.url)
496
-        request = QtNetwork.QNetworkRequest(url)
497
-        self.manager.get(request)
498
-
499
-    def loadStatus(self):
500
-        s = "Status: "
501
-        t = self.parent.getTemperatureString(self.host, self.printer[1])
502
-        if len(t) > 0:
503
-            s += t
504
-        else:
505
-            s += "Unknown"
506
-
507
-        progress = self.parent.getProgress(self.host, self.printer[1])
508
-        if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
509
-            s += " - %.1f%%" % progress["completion"]
510
-            s += " - runtime "
511
-            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
512
-            s += " - "
513
-            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
514
-
515
-        self.statusLabel.setText(s)
516
-        self.scheduleLoadStatus()
517
-
518
-    def handleResponse(self, reply):
519
-        if reply.url().url() == self.url:
520
-            if reply.error() == QtNetwork.QNetworkReply.NoError:
521
-                reader = QImageReader(reply)
522
-                reader.setAutoTransform(True)
523
-                image = reader.read()
524
-                if image != None:
525
-                    if image.colorSpace().isValid():
526
-                        image.convertToColorSpace(QColorSpace.SRgb)
527
-                    self.img.setPixmap(QPixmap.fromImage(image))
528
-                    self.scheduleLoadImage()
529
-                else:
530
-                    print("Error decoding image: " + reader.errorString())
531
-            else:
532
-                print("Error loading image: " + reply.errorString())
533
-
534
-class MainWindow(QWidget):
535
-    def __init__(self, parent, *args, **kwargs):
536
-        super(MainWindow, self).__init__(*args, **kwargs)
537
-        self.parent = parent
538
-
539
-        self.mainLayout = QVBoxLayout()
540
-        self.setLayout(self.mainLayout)
541
-        self.mainLayout.addWidget(self.parent.menu)
542
-
543
-        self.parent.menu.aboutToHide.connect(self.aboutToHide)
544
-
545
-    def aboutToHide(self):
546
-        self.parent.menu.show()
547
-
548
-    def closeEvent(self, event):
549
-        self.parent.exit()
550
-        event.accept()
17
+from PyQt5 import QtNetwork
18
+from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QDesktopWidget
19
+from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices, QCursor
20
+from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
21
+from CamWindow import CamWindow
22
+from SettingsWindow import SettingsWindow
23
+from MainWindow import MainWindow
551 24
 
552 25
 class OctoTray():
553 26
     name = "OctoTray"
@@ -696,7 +169,7 @@ class OctoTray():
696 169
 
697 170
         self.iconPathName = None
698 171
         for p in self.iconPaths:
699
-            if os.path.isfile(path.join(p, self.iconName)):
172
+            if path.isfile(path.join(p, self.iconName)):
700 173
                 self.iconPathName = path.join(p, self.iconName)
701 174
                 break
702 175
         if self.iconPathName == None:
@@ -1181,23 +654,3 @@ class OctoTray():
1181 654
             self.trayIcon.setVisible(False)
1182 655
         else:
1183 656
             self.mainWindow.setVisible(False)
1184
-
1185
-if __name__ == "__main__":
1186
-    app = QApplication(sys.argv)
1187
-    app.setQuitOnLastWindowClosed(False)
1188
-
1189
-    signal.signal(signal.SIGINT, signal.SIG_DFL)
1190
-
1191
-    inSysTray = QSystemTrayIcon.isSystemTrayAvailable()
1192
-    if ("windowed" in sys.argv) or ("--windowed" in sys.argv) or ("-w" in sys.argv):
1193
-        inSysTray = False
1194
-
1195
-    tray = OctoTray(app, inSysTray)
1196
-    rc = app.exec_()
1197
-
1198
-    while rc == 42:
1199
-        tray.closeAll()
1200
-        tray = OctoTray(app, inSysTray)
1201
-        rc = app.exec_()
1202
-
1203
-    sys.exit(rc)

+ 224
- 0
src/SettingsWindow.py View File

@@ -0,0 +1,224 @@
1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# SettingsWindow.py
6
+#
7
+# UI for changes to application configuration.
8
+
9
+import string
10
+from PyQt5 import QtWidgets
11
+from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLineEdit, QGridLayout
12
+from PyQt5.QtGui import QFontDatabase, QIntValidator
13
+from PyQt5.QtCore import Qt
14
+
15
+class SettingsWindow(QWidget):
16
+    columns = [ "Hostname", "API Key", "Tool Preheat", "Bed Preheat" ]
17
+    presets = [ "octopi.local", "000000000_API_KEY_HERE_000000000", "0", "0" ]
18
+
19
+    def __init__(self, parent, *args, **kwargs):
20
+        super(SettingsWindow, self).__init__(*args, **kwargs)
21
+        self.parent = parent
22
+
23
+        self.setWindowTitle(parent.name + " Settings")
24
+        self.setWindowIcon(parent.icon)
25
+
26
+        box = QVBoxLayout()
27
+        self.setLayout(box)
28
+
29
+        staticSettings = QGridLayout()
30
+        box.addLayout(staticSettings, 0)
31
+
32
+        self.jogSpeedText = QLabel("Jog Speed")
33
+        staticSettings.addWidget(self.jogSpeedText, 0, 0)
34
+
35
+        self.jogSpeed = QLineEdit(str(self.parent.jogMoveSpeed))
36
+        self.jogSpeed.setValidator(QIntValidator(1, 6000))
37
+        staticSettings.addWidget(self.jogSpeed, 0, 1)
38
+
39
+        self.jogSpeedUnitText = QLabel("mm/min")
40
+        staticSettings.addWidget(self.jogSpeedUnitText, 0, 2)
41
+
42
+        self.jogLengthText = QLabel("Jog Length")
43
+        staticSettings.addWidget(self.jogLengthText, 1, 0)
44
+
45
+        self.jogLength = QLineEdit(str(self.parent.jogMoveLength))
46
+        self.jogLength.setValidator(QIntValidator(1, 100))
47
+        staticSettings.addWidget(self.jogLength, 1, 1)
48
+
49
+        self.jogLengthUnitText = QLabel("mm")
50
+        staticSettings.addWidget(self.jogLengthUnitText, 1, 2)
51
+
52
+        helpText = "Usage:\n"
53
+        helpText += "1st Column: Printer Hostname or IP address\n"
54
+        helpText += "2nd Column: OctoPrint API Key (32 char hexadecimal)\n"
55
+        helpText += "3rd Column: Tool Preheat Temperature (0 to disable)\n"
56
+        helpText += "4th Column: Bed Preheat Temperature (0 to disable)"
57
+        self.helpText = QLabel(helpText)
58
+        box.addWidget(self.helpText, 0)
59
+        box.setAlignment(self.helpText, Qt.AlignHCenter)
60
+
61
+        buttons = QHBoxLayout()
62
+        box.addLayout(buttons, 0)
63
+
64
+        self.add = QPushButton("&Add Printer")
65
+        self.add.clicked.connect(self.addPrinter)
66
+        buttons.addWidget(self.add)
67
+
68
+        self.remove = QPushButton("&Remove Printer")
69
+        self.remove.clicked.connect(self.removePrinter)
70
+        buttons.addWidget(self.remove)
71
+
72
+        printers = self.parent.readSettings()
73
+        self.rows = len(printers)
74
+        self.table = QTableWidget(self.rows, len(self.columns))
75
+        box.addWidget(self.table, 1)
76
+
77
+        for i in range(0, self.rows):
78
+            p = printers[i]
79
+            for j in range(0, len(self.columns)):
80
+                text = p[j]
81
+                if (j >= 2) and (j <= 3) and (text == None):
82
+                    text = "0"
83
+                item = QTableWidgetItem(text)
84
+                self.table.setItem(i, j, item)
85
+                if j == 1:
86
+                    font = item.font()
87
+                    font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
88
+                    item.setFont(font)
89
+
90
+        buttons2 = QHBoxLayout()
91
+        box.addLayout(buttons2, 0)
92
+
93
+        self.up = QPushButton("Move &Up")
94
+        self.up.clicked.connect(self.moveUp)
95
+        buttons2.addWidget(self.up)
96
+
97
+        self.down = QPushButton("Move &Down")
98
+        self.down.clicked.connect(self.moveDown)
99
+        buttons2.addWidget(self.down)
100
+
101
+        self.openWeb = QPushButton("&Open Web UI of selected")
102
+        self.openWeb.clicked.connect(self.openWebUI)
103
+        box.addWidget(self.openWeb, 0)
104
+
105
+        self.table.setHorizontalHeaderLabels(self.columns)
106
+        self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
107
+        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
108
+        self.table.resizeColumnsToContents()
109
+
110
+        if self.rows <= 0:
111
+            self.addPrinter()
112
+
113
+    def tableToList(self):
114
+        printers = []
115
+        for i in range(0, self.rows):
116
+            p = []
117
+            for j in range(0, len(self.columns)):
118
+                text = self.table.item(i, j).text()
119
+                if (j >= 2) and (j <= 3) and (text == "0"):
120
+                    text = None
121
+                p.append(text)
122
+            printers.append(p)
123
+        return printers
124
+
125
+    def settingsValid(self, printers):
126
+        for p in printers:
127
+            # p[0] needs to be valid hostname or IP
128
+            # TODO
129
+
130
+            # p[1] needs to be valid API key (hexadecimal, 32 chars)
131
+            if (len(p[1]) != 32) or not all(c in string.hexdigits for c in p[1]):
132
+                return (False, "API Key not 32-digit hexadecimal")
133
+
134
+            # p[2] and p[3] need to be integer temperatures (0...999)
135
+            for s in [ p[2], p[3] ]:
136
+                if s == None:
137
+                    s = "0"
138
+                if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
139
+                    return (False, "Temperature not a number from 0...999")
140
+
141
+        js = int(self.jogSpeed.text())
142
+        if (js < 1) or (js > 6000):
143
+            return (False, "Jog Speed not a number from 1...6000")
144
+
145
+        jl = int(self.jogLength.text())
146
+        if (jl < 1) or (jl > 100):
147
+            return (False, "Jog Length not a number from 1...100")
148
+
149
+        return (True, "")
150
+
151
+    def closeEvent(self, event):
152
+        oldPrinters = [item[0:len(self.columns)] for item in self.parent.printers]
153
+        newPrinters = self.tableToList()
154
+
155
+        valid, errorText = self.settingsValid(newPrinters)
156
+        if valid == False:
157
+            r = self.parent.showDialog(self.parent.name + " Settings Invalid", errorText + "!", "Do you want to edit it again?", True, True, False)
158
+            if r == True:
159
+                event.ignore()
160
+                return
161
+            else:
162
+                self.parent.removeSettingsWindow()
163
+                return
164
+
165
+        js = int(self.jogSpeed.text())
166
+        jl = int(self.jogLength.text())
167
+
168
+        if (oldPrinters != newPrinters) or (js != self.parent.jogMoveSpeed) or (jl != self.parent.jogMoveLength):
169
+            r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new configuration?", "This will restart the application!", True, False, False)
170
+            if r == True:
171
+                self.parent.jogMoveSpeed = js
172
+                self.parent.jogMoveLength = jl
173
+                self.parent.writeSettings(newPrinters)
174
+                self.parent.restartApp()
175
+
176
+        self.parent.removeSettingsWindow()
177
+
178
+    def addPrinter(self):
179
+        self.rows += 1
180
+        self.table.setRowCount(self.rows)
181
+        for i in range(0, len(self.columns)):
182
+            item = QTableWidgetItem(self.presets[i])
183
+            self.table.setItem(self.rows - 1, i, item)
184
+            if i == 1:
185
+                font = item.font()
186
+                font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
187
+                item.setFont(font)
188
+        self.table.resizeColumnsToContents()
189
+        self.table.setCurrentItem(self.table.item(self.rows - 1, 0))
190
+
191
+    def removePrinter(self):
192
+        r = self.table.currentRow()
193
+        if (r >= 0) and (r < self.rows):
194
+            self.rows -= 1
195
+            self.table.removeRow(r)
196
+            self.table.setCurrentItem(self.table.item(min(r, self.rows - 1), 0))
197
+
198
+    def moveUp(self):
199
+        i = self.table.currentRow()
200
+        if i <= 0:
201
+            return
202
+        host = self.table.item(i, 0).text()
203
+        key = self.table.item(i, 1).text()
204
+        self.table.item(i, 0).setText(self.table.item(i - 1, 0).text())
205
+        self.table.item(i, 1).setText(self.table.item(i - 1, 1).text())
206
+        self.table.item(i - 1, 0).setText(host)
207
+        self.table.item(i - 1, 1).setText(key)
208
+        self.table.setCurrentItem(self.table.item(i - 1, 0))
209
+
210
+    def moveDown(self):
211
+        i = self.table.currentRow()
212
+        if i >= (self.rows - 1):
213
+            return
214
+        host = self.table.item(i, 0).text()
215
+        key = self.table.item(i, 1).text()
216
+        self.table.item(i, 0).setText(self.table.item(i + 1, 0).text())
217
+        self.table.item(i, 1).setText(self.table.item(i + 1, 1).text())
218
+        self.table.item(i + 1, 0).setText(host)
219
+        self.table.item(i + 1, 1).setText(key)
220
+        self.table.setCurrentItem(self.table.item(i + 1, 0))
221
+
222
+    def openWebUI(self):
223
+        host = self.table.item(self.table.currentRow(), 0).text()
224
+        self.parent.openBrowser(host)

+ 32
- 0
src/main.py View File

@@ -0,0 +1,32 @@
1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# main.py
6
+#
7
+# Entry point for OctoTray application.
8
+# Depends on 'python-pyqt5'.
9
+
10
+import sys
11
+import signal
12
+from PyQt5.QtWidgets import QSystemTrayIcon, QApplication
13
+from OctoTray import OctoTray
14
+
15
+app = QApplication(sys.argv)
16
+app.setQuitOnLastWindowClosed(False)
17
+
18
+signal.signal(signal.SIGINT, signal.SIG_DFL)
19
+
20
+inSysTray = QSystemTrayIcon.isSystemTrayAvailable()
21
+if ("windowed" in sys.argv) or ("--windowed" in sys.argv) or ("-w" in sys.argv):
22
+    inSysTray = False
23
+
24
+tray = OctoTray(app, inSysTray)
25
+rc = app.exec_()
26
+
27
+while rc == 42:
28
+    tray.closeAll()
29
+    tray = OctoTray(app, inSysTray)
30
+    rc = app.exec_()
31
+
32
+sys.exit(rc)

Loading…
Cancel
Save