Linux PyQt tray application to control OctoPrint instances
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OctoTray.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # OctoTray.py
  5. #
  6. # Main application logic.
  7. import sys
  8. from os import path
  9. from PyQt5 import QtNetwork
  10. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QDesktopWidget
  11. from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices, QCursor
  12. from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
  13. from CamWindow import CamWindow
  14. from SettingsWindow import SettingsWindow
  15. from SettingsWindow import Printer
  16. from MainWindow import MainWindow
  17. from APIOctoprint import APIOctoprint
  18. from APIMoonraker import APIMoonraker
  19. class OctoTray():
  20. name = "OctoTray"
  21. vendor = "xythobuz"
  22. version = "0.5"
  23. iconName = "octotray_icon.png"
  24. iconPaths = [
  25. path.abspath(path.dirname(__file__)),
  26. "data",
  27. "/usr/share/pixmaps",
  28. ".",
  29. "..",
  30. "../data"
  31. ]
  32. networkTimeout = 2.0 # in s
  33. # list of Printer objects
  34. printers = []
  35. camWindows = []
  36. settingsWindow = None
  37. # default, can be overridden in config
  38. jogMoveSpeedDefault = 10 * 60 # in mm/min
  39. jogMoveLengthDefault = 10 # in mm
  40. def __init__(self, app, inSysTray):
  41. QCoreApplication.setApplicationName(self.name)
  42. self.app = app
  43. self.inSysTray = inSysTray
  44. self.manager = QtNetwork.QNetworkAccessManager()
  45. self.menu = QMenu()
  46. self.printers = self.readSettings()
  47. unknownCount = 0
  48. for p in self.printers:
  49. p.menus = []
  50. if p.apiType.lower() == "octoprint":
  51. p.api = APIOctoprint(self, p.host, p.key)
  52. elif p.apiType.lower() == "moonraker":
  53. p.api = APIMoonraker(self, p.host, p.webcam)
  54. else:
  55. print("Unsupported API type " + p.apiType)
  56. unknownCount += 1
  57. action = QAction(p.host)
  58. action.setEnabled(False)
  59. p.menus.append(action)
  60. self.menu.addAction(action)
  61. continue
  62. commands = p.api.getAvailableCommands()
  63. # don't populate menu when no methods are available
  64. if len(commands) == 0:
  65. unknownCount += 1
  66. action = QAction(p.host)
  67. action.setEnabled(False)
  68. p.menus.append(action)
  69. self.menu.addAction(action)
  70. continue
  71. # top level menu for this printer
  72. menu = QMenu(p.api.getName())
  73. p.menus.append(menu)
  74. self.menu.addMenu(menu)
  75. # create action for all available commands
  76. for cmd in commands:
  77. name, func = cmd
  78. action = QAction(name)
  79. action.triggered.connect(lambda chk, n=name, f=func: f(n))
  80. p.menus.append(action)
  81. menu.addAction(action)
  82. if (p.tempTool != None) or (p.tempBed != None):
  83. menu.addSeparator()
  84. if p.tempTool != None:
  85. action = QAction("Preheat Tool")
  86. action.triggered.connect(lambda chk, p=p: p.api.printerHeatTool(p.tempTool))
  87. p.menus.append(action)
  88. menu.addAction(action)
  89. if p.tempBed != None:
  90. action = QAction("Preheat Bed")
  91. action.triggered.connect(lambda chk, p=p: p.api.printerHeatBed(p.tempBed))
  92. p.menus.append(action)
  93. menu.addAction(action)
  94. if (p.tempTool != None) or (p.tempBed != None):
  95. action = QAction("Cooldown")
  96. action.triggered.connect(lambda chk, p=p: p.api.printerCooldown())
  97. p.menus.append(action)
  98. menu.addAction(action)
  99. menu.addSeparator()
  100. fileMenu = QMenu("Recent Files")
  101. p.menus.append(fileMenu)
  102. menu.addMenu(fileMenu)
  103. files = p.api.getRecentFiles(10)
  104. for f in files:
  105. fileName, filePath = f
  106. action = QAction(fileName)
  107. action.triggered.connect(lambda chk, p=p, f=filePath: p.api.printFile(f))
  108. p.menus.append(action)
  109. fileMenu.addAction(action)
  110. action = QAction("Get Status")
  111. action.triggered.connect(lambda chk, p=p: p.api.statusDialog())
  112. p.menus.append(action)
  113. menu.addAction(action)
  114. action = QAction("Show Webcam")
  115. action.triggered.connect(lambda chk, x=p: self.printerWebcamAction(x))
  116. p.menus.append(action)
  117. menu.addAction(action)
  118. action = QAction("Open Web UI")
  119. action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
  120. p.menus.append(action)
  121. menu.addAction(action)
  122. self.menu.addSeparator()
  123. self.settingsAction = QAction("&Settings")
  124. self.settingsAction.triggered.connect(self.showSettingsAction)
  125. self.menu.addAction(self.settingsAction)
  126. self.refreshAction = QAction("&Refresh")
  127. self.refreshAction.triggered.connect(self.restartApp)
  128. self.menu.addAction(self.refreshAction)
  129. self.quitAction = QAction("&Quit")
  130. self.quitAction.triggered.connect(self.exit)
  131. self.menu.addAction(self.quitAction)
  132. self.iconPathName = None
  133. for p in self.iconPaths:
  134. if path.isfile(path.join(p, self.iconName)):
  135. self.iconPathName = path.join(p, self.iconName)
  136. break
  137. if self.iconPathName == None:
  138. self.showDialog("OctoTray Error", "Icon file has not been found!", "", False, False, True)
  139. sys.exit(0)
  140. self.icon = QIcon()
  141. self.pic = QPixmap(32, 32)
  142. self.pic.load(self.iconPathName)
  143. self.icon = QIcon(self.pic)
  144. if self.inSysTray:
  145. self.trayIcon = QSystemTrayIcon(self.icon)
  146. self.trayIcon.setToolTip(self.name + " " + self.version)
  147. self.trayIcon.setContextMenu(self.menu)
  148. self.trayIcon.activated.connect(self.showHide)
  149. self.trayIcon.setVisible(True)
  150. else:
  151. self.mainWindow = MainWindow(self)
  152. self.mainWindow.show()
  153. self.mainWindow.activateWindow()
  154. screenGeometry = QDesktopWidget().screenGeometry()
  155. x = (screenGeometry.width() - self.mainWindow.width()) / 2
  156. y = (screenGeometry.height() - self.mainWindow.height()) / 2
  157. x += screenGeometry.x()
  158. y += screenGeometry.y()
  159. self.mainWindow.setGeometry(int(x), int(y), int(self.mainWindow.width()), int(self.mainWindow.height()))
  160. def showHide(self, activationReason):
  161. if activationReason == QSystemTrayIcon.Trigger:
  162. self.menu.popup(QCursor.pos())
  163. elif activationReason == QSystemTrayIcon.MiddleClick:
  164. if len(self.printers) > 0:
  165. self.printerWebcamAction(self.printers[0])
  166. def readSettings(self):
  167. settings = QSettings(self.vendor, self.name)
  168. printers = []
  169. l = settings.beginReadArray("printers")
  170. for i in range(0, l):
  171. settings.setArrayIndex(i)
  172. p = Printer()
  173. # Generic settings
  174. p.host = settings.value("host", "octopi.local")
  175. p.apiType = settings.value("api_type", "OctoPrint")
  176. p.tempTool = settings.value("tool_preheat", "0")
  177. p.tempBed = settings.value("bed_preheat", "0")
  178. p.jogSpeed = settings.value("jog_speed", self.jogMoveSpeedDefault)
  179. p.jogLength = settings.value("jog_length", self.jogMoveLengthDefault)
  180. # Octoprint specific settings
  181. p.key = settings.value("key", "")
  182. # Moonraker specific settings
  183. p.webcam = settings.value("webcam", "0")
  184. print("readSettings() " + str(i) + ":\n" + str(p) + "\n")
  185. printers.append(p)
  186. settings.endArray()
  187. return printers
  188. def writeSettings(self, printers):
  189. settings = QSettings(self.vendor, self.name)
  190. settings.remove("printers")
  191. settings.beginWriteArray("printers")
  192. for i in range(0, len(printers)):
  193. p = printers[i]
  194. print("writeSettings() " + str(i) + ":\n" + str(p) + "\n")
  195. settings.setArrayIndex(i)
  196. # Generic settings
  197. settings.setValue("host", p.host)
  198. settings.setValue("api_type", p.apiType)
  199. settings.setValue("tool_preheat", p.tempTool)
  200. settings.setValue("bed_preheat", p.tempBed)
  201. settings.setValue("jog_speed", p.jogSpeed)
  202. settings.setValue("jog_length", p.jogLength)
  203. # Octoprint specific settings
  204. settings.setValue("key", p.key)
  205. # Moonraker specific settings
  206. settings.setValue("webcam", p.webcam)
  207. settings.endArray()
  208. del settings
  209. def openBrowser(self, url):
  210. QDesktopServices.openUrl(QUrl("http://" + url))
  211. def showDialog(self, title, text1, text2 = "", question = False, warning = False, error = False):
  212. msg = QMessageBox()
  213. if error:
  214. msg.setIcon(QMessageBox.Critical)
  215. elif warning:
  216. msg.setIcon(QMessageBox.Warning)
  217. elif question:
  218. msg.setIcon(QMessageBox.Question)
  219. else:
  220. msg.setIcon(QMessageBox.Information)
  221. msg.setWindowTitle(title)
  222. msg.setText(text1)
  223. if text2 is not None:
  224. msg.setInformativeText(text2)
  225. if question:
  226. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  227. else:
  228. msg.setStandardButtons(QMessageBox.Ok)
  229. retval = msg.exec_()
  230. if retval == QMessageBox.Yes:
  231. return True
  232. else:
  233. return False
  234. def exit(self):
  235. QCoreApplication.quit()
  236. def printerWebAction(self, item):
  237. self.openBrowser(item.host)
  238. def printerWebcamAction(self, item):
  239. for cw in self.camWindows:
  240. if cw.getHost() == item.host:
  241. cw.show()
  242. cw.activateWindow()
  243. return
  244. window = CamWindow(self, item)
  245. self.camWindows.append(window)
  246. window.show()
  247. window.activateWindow()
  248. screenGeometry = QDesktopWidget().screenGeometry()
  249. x = (screenGeometry.width() - window.width()) / 2
  250. y = (screenGeometry.height() - window.height()) / 2
  251. x += screenGeometry.x()
  252. y += screenGeometry.y()
  253. window.setGeometry(int(x), int(y), int(window.width()), int(window.height()))
  254. def removeWebcamWindow(self, window):
  255. self.camWindows.remove(window)
  256. def showSettingsAction(self):
  257. if self.settingsWindow != None:
  258. self.settingsWindow.show()
  259. self.settingsWindow.activateWindow()
  260. return
  261. self.settingsWindow = SettingsWindow(self)
  262. self.settingsWindow.show()
  263. self.settingsWindow.activateWindow()
  264. screenGeometry = QDesktopWidget().screenGeometry()
  265. x = (screenGeometry.width() - self.settingsWindow.width()) / 2
  266. y = (screenGeometry.height() - self.settingsWindow.height()) / 2
  267. x += screenGeometry.x()
  268. y += screenGeometry.y()
  269. self.settingsWindow.setGeometry(int(x), int(y), int(self.settingsWindow.width()), int(self.settingsWindow.height()) + 50)
  270. def removeSettingsWindow(self):
  271. self.settingsWindow = None
  272. def restartApp(self):
  273. QCoreApplication.exit(42)
  274. def closeAll(self):
  275. for cw in self.camWindows:
  276. cw.close()
  277. if self.settingsWindow != None:
  278. self.settingsWindow.close()
  279. if self.inSysTray:
  280. self.trayIcon.setVisible(False)
  281. else:
  282. self.mainWindow.setVisible(False)