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.

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