3 コミット

作成者 SHA1 メッセージ 日付
  Thomas Buck de7020a3cf added ability to list and print recent files. added actions to webcam window. 2年前
  Thomas Buck 45c343da22 update status with every 2nd webcam image. don't allow setting 0 delay for webcam / status. 2年前
  Thomas Buck 3e39a38fa8 support running in window, without system tray 2年前
2個のファイルの変更315行の追加26行の削除
  1. 3
    1
      README.md
  2. 312
    25
      src/octotray.py

+ 3
- 1
README.md ファイルの表示

@@ -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 ファイルの表示

@@ -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)

読み込み中…
キャンセル
保存