Browse Source

split code into multiple files

Thomas Buck 2 years 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
 build
1
 build
2
+src/__pycache__

+ 12
- 21
README.md View File

19
 
19
 
20
 OctoTray can simply be run from the checked out repository, if all dependencies are installed.
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
 For this you need Python 3 as well as PyQt5.
24
 For this you need Python 3 as well as PyQt5.
25
 
25
 
53
 
53
 
54
 The generated bundle will then be in 'build/mac/dist/OctoTray.app' as well as 'build/dist/OctoTray_Mac.zip'.
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
 ### Pre-Built Linux Binary
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
 To create it yourself, simply run:
60
 To create it yourself, simply run:
80
 
61
 
81
     ./build_linux.sh
62
     ./build_linux.sh
82
 
63
 
83
 The resulting binary will be in 'build/linux/dist' as well as 'build/dist/OctoTray_Linux.zip'.
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
 rm -rf build/archlinux
3
 rm -rf build/archlinux
4
 mkdir -p build/archlinux
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
 cp -r data/* build/archlinux/
9
 cp -r data/* build/archlinux/
8
 cp dist/PKGBUILD build/archlinux/
10
 cp dist/PKGBUILD build/archlinux/
9
 
11
 

+ 1
- 1
build_linux.sh View File

7
 cp data/* build/linux/
7
 cp data/* build/linux/
8
 
8
 
9
 cd build/linux
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
 cd dist
12
 cd dist
13
 zip -r OctoTray_Linux.zip *
13
 zip -r OctoTray_Linux.zip *

+ 0
- 5
build_unix.sh View File

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
 cp dist/setup_win.py build/win/
8
 cp dist/setup_win.py build/win/
9
 
9
 
10
 cd build/win
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
 cd ../..
13
 cd ../..
14
 mkdir -p build/dist/win
14
 mkdir -p build/dist/win

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

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

+ 1
- 1
dist/PKGBUILD View File

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

+ 2
- 2
dist/setup_mac.py View File

4
 from setuptools import setup
4
 from setuptools import setup
5
 
5
 
6
 APP_NAME = "OctoTray"
6
 APP_NAME = "OctoTray"
7
-APP = [ 'octotray.py' ]
7
+APP = [ 'main.py' ]
8
 DATA_FILES = [ 'octotray_icon.png' ]
8
 DATA_FILES = [ 'octotray_icon.png' ]
9
 VERSION="0.4.0"
9
 VERSION="0.4.0"
10
 
10
 
18
         'CFBundleIdentifier': "de.xythobuz.octotray",
18
         'CFBundleIdentifier': "de.xythobuz.octotray",
19
         'CFBundleVersion': VERSION,
19
         'CFBundleVersion': VERSION,
20
         'CFBundleShortVersionString': VERSION,
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

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

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

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
 
2
 
3
 # OctoTray Linux Qt System Tray OctoPrint client
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
 import json
9
 import json
13
 import sys
10
 import sys
14
-import os
15
 import time
11
 import time
16
-import string
17
 import urllib.parse
12
 import urllib.parse
18
 import urllib.request
13
 import urllib.request
19
-import signal
20
 import operator
14
 import operator
21
 import socket
15
 import socket
22
 from os import path
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
 class OctoTray():
25
 class OctoTray():
553
     name = "OctoTray"
26
     name = "OctoTray"
696
 
169
 
697
         self.iconPathName = None
170
         self.iconPathName = None
698
         for p in self.iconPaths:
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
                 self.iconPathName = path.join(p, self.iconName)
173
                 self.iconPathName = path.join(p, self.iconName)
701
                 break
174
                 break
702
         if self.iconPathName == None:
175
         if self.iconPathName == None:
1181
             self.trayIcon.setVisible(False)
654
             self.trayIcon.setVisible(False)
1182
         else:
655
         else:
1183
             self.mainWindow.setVisible(False)
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

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

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