3 Commits

Author SHA1 Message Date
  Thomas Buck de7020a3cf added ability to list and print recent files. added actions to webcam window. 2 years ago
  Thomas Buck 45c343da22 update status with every 2nd webcam image. don't allow setting 0 delay for webcam / status. 2 years ago
  Thomas Buck 3e39a38fa8 support running in window, without system tray 2 years ago
2 changed files with 315 additions and 26 deletions
  1. 3
    1
      README.md
  2. 312
    25
      src/octotray.py

+ 3
- 1
README.md View File

@@ -9,7 +9,9 @@ Automatic builds are provided for Linux, Windows and macOS.
9 9
 
10 10
 For more [take a look at OctoTray on my website](https://www.xythobuz.de/octotray.html).
11 11
 
12
-## Building
12
+If the system tray is not available (or when passing the '-w' parameter) the main menu will instead be shown in a window.
13
+
14
+## Building / Running
13 15
 
14 16
 You have different options of building and running OctoTray:
15 17
 

+ 312
- 25
src/octotray.py View File

@@ -16,10 +16,13 @@ import time
16 16
 import string
17 17
 import urllib.parse
18 18
 import urllib.request
19
+import signal
20
+import operator
21
+import socket
19 22
 from os import path
20 23
 from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
21
-from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout, QTableWidget, QTableWidgetItem, QPushButton, QApplication
22
-from PyQt5.QtGui import QIcon, QPixmap, QImageReader, QDesktopServices, QFontDatabase, QCursor
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
23 26
 from PyQt5.QtCore import QCoreApplication, QSettings, QUrl, QTimer, QSize, Qt, QSettings
24 27
 
25 28
 class SettingsWindow(QWidget):
@@ -33,9 +36,33 @@ class SettingsWindow(QWidget):
33 36
         self.setWindowTitle(parent.name + " Settings")
34 37
         self.setWindowIcon(parent.icon)
35 38
 
39
+
36 40
         box = QVBoxLayout()
37 41
         self.setLayout(box)
38 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
+
39 66
         helpText = "Usage:\n"
40 67
         helpText += "1st Column: Printer Hostname or IP address\n"
41 68
         helpText += "2nd Column: OctoPrint API Key (32 char hexadecimal)\n"
@@ -124,6 +151,15 @@ class SettingsWindow(QWidget):
124 151
                     s = "0"
125 152
                 if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
126 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
+
127 163
         return (True, "")
128 164
 
129 165
     def closeEvent(self, event):
@@ -140,9 +176,14 @@ class SettingsWindow(QWidget):
140 176
                 self.parent.removeSettingsWindow()
141 177
                 return
142 178
 
143
-        if oldPrinters != newPrinters:
144
-            r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new list of printers?", "This will restart the application!", True, False, False)
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)
145 184
             if r == True:
185
+                self.parent.jogMoveSpeed = js
186
+                self.parent.jogMoveLength = jl
146 187
                 self.parent.writeSettings(newPrinters)
147 188
                 self.parent.restartApp()
148 189
 
@@ -226,7 +267,7 @@ class AspectRatioPixmapLabel(QLabel):
226 267
 
227 268
 class CamWindow(QWidget):
228 269
     reloadDelayDefault = 1000 # in ms
229
-    statusDelay = 5 * 1000 # in ms
270
+    statusDelayFactor = 2
230 271
     reloadOn = True
231 272
     sliderFactor = 100
232 273
 
@@ -250,15 +291,14 @@ class CamWindow(QWidget):
250 291
         box.addWidget(label, 0)
251 292
         box.setAlignment(label, Qt.AlignHCenter)
252 293
 
253
-        self.img = AspectRatioPixmapLabel()
254
-        self.img.setPixmap(QPixmap(640, 480))
255
-        box.addWidget(self.img, 1)
256
-
257 294
         slide = QHBoxLayout()
258 295
         box.addLayout(slide, 0)
259 296
 
297
+        self.slideStaticLabel = QLabel("Refresh")
298
+        slide.addWidget(self.slideStaticLabel, 0)
299
+
260 300
         self.slider = QSlider(Qt.Horizontal)
261
-        self.slider.setMinimum(int(0 / self.sliderFactor))
301
+        self.slider.setMinimum(int(100 / self.sliderFactor))
262 302
         self.slider.setMaximum(int(2000 / self.sliderFactor))
263 303
         self.slider.setTickInterval(int(100 / self.sliderFactor))
264 304
         self.slider.setPageStep(int(100 / self.sliderFactor))
@@ -271,13 +311,150 @@ class CamWindow(QWidget):
271 311
         self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
272 312
         slide.addWidget(self.slideLabel, 0)
273 313
 
314
+        self.img = AspectRatioPixmapLabel()
315
+        self.img.setPixmap(QPixmap(640, 480))
316
+        box.addWidget(self.img, 1)
317
+
274 318
         self.statusLabel = QLabel("Status: unavailable")
275 319
         box.addWidget(self.statusLabel, 0)
276
-        box.setAlignment(label, Qt.AlignHCenter)
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)
277 395
 
278 396
         self.loadImage()
279 397
         self.loadStatus()
280 398
 
399
+    def moveXP(self):
400
+        self.parent.printerMoveAction(self.printer, "x", int(self.parent.jogMoveLength), True)
401
+
402
+    def moveXM(self):
403
+        self.parent.printerMoveAction(self.printer, "x", -1 * int(self.parent.jogMoveLength), True)
404
+
405
+    def moveYP(self):
406
+        self.parent.printerMoveAction(self.printer, "y", int(self.parent.jogMoveLength), True)
407
+
408
+    def moveYM(self):
409
+        self.parent.printerMoveAction(self.printer, "y", -1 * int(self.parent.jogMoveLength), True)
410
+
411
+    def moveZP(self):
412
+        self.parent.printerMoveAction(self.printer, "z", int(self.parent.jogMoveLength), True)
413
+
414
+    def moveZM(self):
415
+        self.parent.printerMoveAction(self.printer, "z", -1 * int(self.parent.jogMoveLength), True)
416
+
417
+    def homeX(self):
418
+        self.parent.printerHomingAction(self.printer, "x")
419
+
420
+    def homeY(self):
421
+        self.parent.printerHomingAction(self.printer, "y")
422
+
423
+    def homeZ(self):
424
+        self.parent.printerHomingAction(self.printer, "z")
425
+
426
+    def homeAll(self):
427
+        self.parent.printerHomingAction(self.printer, "xyz")
428
+
429
+    def turnOn(self):
430
+        if self.method == "psucontrol":
431
+            self.parent.printerOnAction(self.printer)
432
+        elif self.method == "system":
433
+            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
434
+            for cmd in cmds:
435
+                if "on" in cmd:
436
+                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
437
+                    break
438
+
439
+    def turnOff(self):
440
+        if self.method == "psucontrol":
441
+            self.parent.printerOffAction(self.printer)
442
+        elif self.method == "system":
443
+            cmds = self.parent.getSystemCommands(self.printer[0], self.printer[1])
444
+            for cmd in cmds:
445
+                if "off" in cmd:
446
+                    self.parent.setSystemCommand(self.printer[0], self.printer[1], cmd)
447
+                    break
448
+
449
+    def cooldown(self):
450
+        self.parent.printerCooldown(self.printer)
451
+
452
+    def preheatTool(self):
453
+        self.parent.printerHeatTool(self.printer)
454
+
455
+    def preheatBed(self):
456
+        self.parent.printerHeatBed(self.printer)
457
+
281 458
     def getHost(self):
282 459
         return self.host
283 460
 
@@ -295,7 +472,7 @@ class CamWindow(QWidget):
295 472
 
296 473
     def scheduleLoadStatus(self):
297 474
         if self.reloadOn:
298
-            QTimer.singleShot(self.statusDelay, self.loadStatus)
475
+            QTimer.singleShot(self.slider.value() * self.sliderFactor * self.statusDelayFactor, self.loadStatus)
299 476
 
300 477
     def loadImage(self):
301 478
         url = QUrl(self.url)
@@ -337,6 +514,24 @@ class CamWindow(QWidget):
337 514
             else:
338 515
                 print("Error loading image: " + reply.errorString())
339 516
 
517
+class MainWindow(QWidget):
518
+    def __init__(self, parent, *args, **kwargs):
519
+        super(MainWindow, self).__init__(*args, **kwargs)
520
+        self.parent = parent
521
+
522
+        self.mainLayout = QVBoxLayout()
523
+        self.setLayout(self.mainLayout)
524
+        self.mainLayout.addWidget(self.parent.menu)
525
+
526
+        self.parent.menu.aboutToHide.connect(self.aboutToHide)
527
+
528
+    def aboutToHide(self):
529
+        self.parent.menu.show()
530
+
531
+    def closeEvent(self, event):
532
+        self.parent.exit()
533
+        event.accept()
534
+
340 535
 class OctoTray():
341 536
     name = "OctoTray"
342 537
     vendor = "xythobuz"
@@ -367,13 +562,14 @@ class OctoTray():
367 562
     camWindows = []
368 563
     settingsWindow = None
369 564
 
370
-    def __init__(self, app):
565
+    # default, can be overridden in config
566
+    jogMoveSpeed = 10 * 60 # in mm/min
567
+    jogMoveLength = 10 # in mm
568
+
569
+    def __init__(self, app, inSysTray):
371 570
         QCoreApplication.setApplicationName(self.name)
372 571
         self.app = app
373
-
374
-        if not QSystemTrayIcon.isSystemTrayAvailable():
375
-            self.showDialog("OctoTray Error", "System Tray is not available on this platform!", "", False, False, True)
376
-            sys.exit(0)
572
+        self.inSysTray = inSysTray
377 573
 
378 574
         self.manager = QtNetwork.QNetworkAccessManager()
379 575
         self.menu = QMenu()
@@ -440,6 +636,18 @@ class OctoTray():
440 636
 
441 637
             menu.addSeparator()
442 638
 
639
+            fileMenu = QMenu("Recent Files")
640
+            p.append(fileMenu)
641
+            menu.addMenu(fileMenu)
642
+
643
+            files = self.getRecentFiles(p[0], p[1], 10)
644
+            for f in files:
645
+                fileName, filePath = f
646
+                action = QAction(fileName)
647
+                action.triggered.connect(lambda chk, x=p, y=filePath: self.printerFilePrint(x, y))
648
+                p.append(action)
649
+                fileMenu.addAction(action)
650
+
443 651
             action = QAction("Get Status")
444 652
             action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
445 653
             p.append(action)
@@ -483,18 +691,41 @@ class OctoTray():
483 691
         self.pic.load(self.iconPathName)
484 692
         self.icon = QIcon(self.pic)
485 693
 
486
-        self.trayIcon = QSystemTrayIcon(self.icon)
487
-        self.trayIcon.setToolTip(self.name + " " + self.version)
488
-        self.trayIcon.setContextMenu(self.menu)
489
-        self.trayIcon.activated.connect(self.showHide)
490
-        self.trayIcon.setVisible(True)
694
+        if self.inSysTray:
695
+            self.trayIcon = QSystemTrayIcon(self.icon)
696
+            self.trayIcon.setToolTip(self.name + " " + self.version)
697
+            self.trayIcon.setContextMenu(self.menu)
698
+            self.trayIcon.activated.connect(self.showHide)
699
+            self.trayIcon.setVisible(True)
700
+        else:
701
+            self.mainWindow = MainWindow(self)
702
+            self.mainWindow.show()
703
+            self.mainWindow.activateWindow()
704
+            screenGeometry = QDesktopWidget().screenGeometry()
705
+            x = (screenGeometry.width() - self.mainWindow.width()) / 2
706
+            y = (screenGeometry.height() - self.mainWindow.height()) / 2
707
+            x += screenGeometry.x()
708
+            y += screenGeometry.y()
709
+            self.mainWindow.setGeometry(int(x), int(y), int(self.mainWindow.width()), int(self.mainWindow.height()))
491 710
 
492 711
     def showHide(self, activationReason):
493 712
         if activationReason == QSystemTrayIcon.Trigger:
494 713
             self.menu.popup(QCursor.pos())
714
+        elif activationReason == QSystemTrayIcon.MiddleClick:
715
+            if len(self.printers) > 0:
716
+                self.printerWebcamAction(self.printers[0])
495 717
 
496 718
     def readSettings(self):
497 719
         settings = QSettings(self.vendor, self.name)
720
+
721
+        js = settings.value("jog_speed")
722
+        if js != None:
723
+            self.jogMoveSpeed = int(js)
724
+
725
+        jl = settings.value("jog_length")
726
+        if jl != None:
727
+            self.jogMoveLength = int(jl)
728
+
498 729
         printers = []
499 730
         l = settings.beginReadArray("printers")
500 731
         for i in range(0, l):
@@ -510,6 +741,10 @@ class OctoTray():
510 741
 
511 742
     def writeSettings(self, printers):
512 743
         settings = QSettings(self.vendor, self.name)
744
+
745
+        settings.setValue("jog_speed", self.jogMoveSpeed)
746
+        settings.setValue("jog_length", self.jogMoveLength)
747
+
513 748
         settings.remove("printers")
514 749
         settings.beginWriteArray("printers")
515 750
         for i in range(0, len(printers)):
@@ -677,6 +912,20 @@ class OctoTray():
677 912
             pass
678 913
         return host
679 914
 
915
+    def getRecentFiles(self, host, key, count):
916
+        r = self.sendGetRequest(host, key, "files?recursive=true")
917
+        files = []
918
+        try:
919
+            rd = json.loads(r)
920
+            if "files" in rd:
921
+                t = [f for f in rd["files"] if "date" in f]
922
+                fs = sorted(t, key=operator.itemgetter("date"), reverse=True)
923
+                for f in fs[:count]:
924
+                    files.append((f["name"], f["origin"] + "/" + f["path"]))
925
+        except json.JSONDecodeError:
926
+            pass
927
+        return files
928
+
680 929
     def getMethod(self, host, key):
681 930
         r = self.sendGetRequest(host, key, "plugin/psucontrol")
682 931
         if r == "timeout":
@@ -766,6 +1015,32 @@ class OctoTray():
766 1015
 
767 1016
         self.setPSUControl(item[0], item[1], False)
768 1017
 
1018
+    def printerHomingAction(self, item, axes = "xyz"):
1019
+        state = self.getState(item[0], item[1])
1020
+        if state in self.statesWithWarning:
1021
+            if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to home it?", True, True) == False:
1022
+                return
1023
+
1024
+        axes_string = ''
1025
+        for i in range(0, len(axes)):
1026
+            axes_string += '"' + str(axes[i]) + '"'
1027
+            if i < (len(axes) - 1):
1028
+                axes_string += ', '
1029
+
1030
+        self.sendPostRequest(item[0], item[1], "printer/printhead", '{ "command": "home", "axes": [' + axes_string + '] }')
1031
+
1032
+    def printerMoveAction(self, printer, axis, dist, relative = True):
1033
+        state = self.getState(printer[0], printer[1])
1034
+        if state in self.statesWithWarning:
1035
+            if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to move it?", True, True) == False:
1036
+                return
1037
+
1038
+        absolute = ''
1039
+        if relative == False:
1040
+            absolute = ', "absolute": true'
1041
+
1042
+        self.sendPostRequest(printer[0], printer[1], "printer/printhead", '{ "command": "jog", "' + str(axis) + '": ' + str(dist) + ', "speed": ' + str(self.jogMoveSpeed) + absolute + ' }')
1043
+
769 1044
     def printerWebAction(self, item):
770 1045
         self.openBrowser(item[0])
771 1046
 
@@ -787,6 +1062,9 @@ class OctoTray():
787 1062
             s += "\n" + t
788 1063
         self.showDialog("OctoTray Status", s, None, False, warning)
789 1064
 
1065
+    def printerFilePrint(self, item, path):
1066
+        self.sendPostRequest(item[0], item[1], "files/" + path, '{ "command": "select", "print": true }')
1067
+
790 1068
     def setTemperature(self, host, key, what, temp):
791 1069
         path = "printer/bed"
792 1070
         s = "{\"command\": \"target\", \"target\": " + temp + "}"
@@ -868,18 +1146,27 @@ class OctoTray():
868 1146
         if self.settingsWindow != None:
869 1147
             self.settingsWindow.close()
870 1148
 
871
-        self.trayIcon.setVisible(False)
1149
+        if self.inSysTray:
1150
+            self.trayIcon.setVisible(False)
1151
+        else:
1152
+            self.mainWindow.setVisible(False)
872 1153
 
873 1154
 if __name__ == "__main__":
874 1155
     app = QApplication(sys.argv)
875 1156
     app.setQuitOnLastWindowClosed(False)
876 1157
 
877
-    tray = OctoTray(app)
1158
+    signal.signal(signal.SIGINT, signal.SIG_DFL)
1159
+
1160
+    inSysTray = QSystemTrayIcon.isSystemTrayAvailable()
1161
+    if ("windowed" in sys.argv) or ("--windowed" in sys.argv) or ("-w" in sys.argv):
1162
+        inSysTray = False
1163
+
1164
+    tray = OctoTray(app, inSysTray)
878 1165
     rc = app.exec_()
879 1166
 
880 1167
     while rc == 42:
881 1168
         tray.closeAll()
882
-        tray = OctoTray(app)
1169
+        tray = OctoTray(app, inSysTray)
883 1170
         rc = app.exec_()
884 1171
 
885 1172
     sys.exit(rc)

Loading…
Cancel
Save