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 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # depends on:
  5. # - python-pyqt5
  6. #
  7. # see also:
  8. # https://doc.qt.io/qt-5/qtwidgets-widgets-imageviewer-example.html
  9. # https://stackoverflow.com/a/22618496
  10. import json
  11. import sys
  12. import os
  13. import time
  14. import urllib.parse
  15. import urllib.request
  16. from PyQt5 import QtWidgets, QtGui, QtCore, QtNetwork
  17. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QDesktopWidget, QSizePolicy, QSlider, QLayout, QTableWidget, QTableWidgetItem, QPushButton
  18. from PyQt5.QtGui import QIcon, QPixmap, QImageReader, QDesktopServices
  19. from PyQt5.QtCore import QCoreApplication, QSettings, QUrl, QTimer, QSize, Qt, QSettings
  20. class SettingsWindow(QWidget):
  21. def __init__(self, parent, *args, **kwargs):
  22. super(SettingsWindow, self).__init__(*args, **kwargs)
  23. self.parent = parent
  24. self.setWindowTitle(parent.name + " Settings")
  25. self.setWindowIcon(parent.icon)
  26. box = QVBoxLayout()
  27. self.setLayout(box)
  28. buttons = QHBoxLayout()
  29. box.addLayout(buttons, 0)
  30. self.add = QPushButton("&Add Printer")
  31. self.add.clicked.connect(self.addPrinter)
  32. buttons.addWidget(self.add)
  33. self.remove = QPushButton("&Remove Printer")
  34. self.remove.clicked.connect(self.removePrinter)
  35. buttons.addWidget(self.remove)
  36. printers = self.parent.readSettings()
  37. self.rows = len(printers)
  38. self.table = QTableWidget(self.rows, 2)
  39. box.addWidget(self.table, 1)
  40. for i in range(0, self.rows):
  41. p = printers[i]
  42. for j in range(0, 2):
  43. item = QTableWidgetItem(p[j])
  44. self.table.setItem(i, j, item)
  45. self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
  46. self.table.resizeColumnsToContents()
  47. def tableToList(self):
  48. printers = []
  49. for i in range(0, self.rows):
  50. p = [self.table.item(i, 0).text(), self.table.item(i, 1).text()]
  51. printers.append(p)
  52. return printers
  53. def closeEvent(self, event):
  54. oldPrinters = [item[0:2] for item in self.parent.printers]
  55. newPrinters = self.tableToList()
  56. if oldPrinters != newPrinters:
  57. 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)
  58. if r == True:
  59. self.parent.writeSettings(newPrinters)
  60. QCoreApplication.exit(42)
  61. self.parent.removeSettingsWindow()
  62. def addPrinter(self):
  63. self.rows += 1
  64. self.table.setRowCount(self.rows)
  65. self.table.setItem(self.rows - 1, 0, QTableWidgetItem("HOSTNAME"))
  66. self.table.setItem(self.rows - 1, 1, QTableWidgetItem("API_KEY"))
  67. def removePrinter(self):
  68. r = self.table.currentRow()
  69. if (r >= 0) and (r < self.rows):
  70. self.rows -= 1
  71. self.table.removeRow(r)
  72. class AspectRatioPixmapLabel(QLabel):
  73. def __init__(self, *args, **kwargs):
  74. super(AspectRatioPixmapLabel, self).__init__(*args, **kwargs)
  75. self.setMinimumSize(1, 1)
  76. self.setScaledContents(False)
  77. self.pix = QPixmap(0, 0)
  78. def setPixmap(self, p):
  79. self.pix = p
  80. super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
  81. def heightForWidth(self, width):
  82. if self.pix.isNull():
  83. return self.height()
  84. else:
  85. return (self.pix.height() * width) / self.pix.width()
  86. def sizeHint(self):
  87. w = self.width()
  88. return QSize(int(w), int(self.heightForWidth(w)))
  89. def scaledPixmap(self):
  90. return self.pix.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
  91. def resizeEvent(self, e):
  92. if not self.pix.isNull():
  93. super(AspectRatioPixmapLabel, self).setPixmap(self.scaledPixmap())
  94. class CamWindow(QWidget):
  95. reloadDelayDefault = 1000 # in ms
  96. statusDelay = 10 * 1000 # in ms
  97. addSize = 100
  98. reloadOn = True
  99. def __init__(self, parent, printer, *args, **kwargs):
  100. super(CamWindow, self).__init__(*args, **kwargs)
  101. self.app = parent.app
  102. self.manager = parent.manager
  103. self.manager.finished.connect(self.handleResponse)
  104. self.parent = parent
  105. self.printer = printer
  106. self.host = self.printer[0]
  107. self.url = "http://" + self.host + ":8080/?action=snapshot"
  108. self.setWindowTitle(parent.name + " Webcam Stream")
  109. self.setWindowIcon(parent.icon)
  110. box = QVBoxLayout()
  111. self.setLayout(box)
  112. label = QLabel(self.url)
  113. box.addWidget(label, 0)
  114. box.setAlignment(label, Qt.AlignHCenter)
  115. self.img = AspectRatioPixmapLabel()
  116. self.img.setPixmap(QPixmap(640, 480))
  117. box.addWidget(self.img, 1)
  118. slide = QHBoxLayout()
  119. box.addLayout(slide, 0)
  120. self.slider = QSlider(Qt.Horizontal)
  121. self.slider.setMinimum(0)
  122. self.slider.setMaximum(2000)
  123. self.slider.setTickInterval(100)
  124. self.slider.setPageStep(100)
  125. self.slider.setSingleStep(100)
  126. self.slider.setTickPosition(QSlider.TicksBelow)
  127. self.slider.setValue(self.reloadDelayDefault)
  128. self.slider.valueChanged.connect(self.sliderChanged)
  129. slide.addWidget(self.slider, 1)
  130. self.slideLabel = QLabel(str(self.reloadDelayDefault) + "ms")
  131. slide.addWidget(self.slideLabel, 0)
  132. self.statusLabel = QLabel("Status: unavailable")
  133. box.addWidget(self.statusLabel, 0)
  134. box.setAlignment(label, Qt.AlignHCenter)
  135. size = self.size()
  136. size.setHeight(size.height() + self.addSize)
  137. self.resize(size)
  138. self.loadImage()
  139. self.loadStatus()
  140. def getHost(self):
  141. return self.host
  142. def sliderChanged(self):
  143. self.slideLabel.setText(str(self.slider.value()) + "ms")
  144. def closeEvent(self, event):
  145. self.reloadOn = False
  146. self.url = ""
  147. self.parent.removeWebcamWindow(self)
  148. def scheduleLoadImage(self):
  149. if self.reloadOn:
  150. QTimer.singleShot(self.slider.value(), self.loadImage)
  151. def scheduleLoadStatus(self):
  152. if self.reloadOn:
  153. QTimer.singleShot(self.statusDelay, self.loadStatus)
  154. def loadImage(self):
  155. url = QUrl(self.url)
  156. request = QtNetwork.QNetworkRequest(url)
  157. self.manager.get(request)
  158. def loadStatus(self):
  159. s = "Status: "
  160. t = self.parent.getTemperatureString(self.host, self.printer[1])
  161. if len(t) > 0:
  162. s += t
  163. else:
  164. s += "Unknown"
  165. progress = self.parent.getProgress(self.host, self.printer[1])
  166. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  167. s += " - %.1f%%" % progress["completion"]
  168. s += " - runtime "
  169. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
  170. s += " - "
  171. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  172. self.statusLabel.setText(s)
  173. self.scheduleLoadStatus()
  174. def handleResponse(self, reply):
  175. if reply.url().url() == self.url:
  176. if reply.error() == QtNetwork.QNetworkReply.NoError:
  177. reader = QImageReader(reply)
  178. reader.setAutoTransform(True)
  179. image = reader.read()
  180. if image != None:
  181. if image.colorSpace().isValid():
  182. image.convertToColorSpace(QColorSpace.SRgb)
  183. self.img.setPixmap(QPixmap.fromImage(image))
  184. self.scheduleLoadImage()
  185. else:
  186. print("Error decoding image: " + reader.errorString())
  187. else:
  188. print("Error loading image: " + reply.errorString())
  189. class OctoTray():
  190. name = "OctoTray"
  191. vendor = "xythobuz"
  192. version = "0.3"
  193. iconPaths = [ "./", "../", "data/", "../data/", "/usr/share/pixmaps/" ]
  194. iconName = "octotray_icon.png"
  195. networkTimeout = 2.0 # in s
  196. # list of lists, inner lists contain printer data:
  197. # 0=host 1=key (2=system-commands 3=menu 4+=actions)
  198. printers = []
  199. statesWithWarning = [
  200. "Printing", "Pausing", "Paused"
  201. ]
  202. camWindows = []
  203. settingsWindow = None
  204. def __init__(self, app):
  205. QCoreApplication.setApplicationName(self.name)
  206. self.app = app
  207. if not QSystemTrayIcon.isSystemTrayAvailable():
  208. self.showDialog("OctoTray Error", "System Tray is not available on this platform!", "", False, False, True)
  209. sys.exit(0)
  210. self.manager = QtNetwork.QNetworkAccessManager()
  211. self.menu = QMenu()
  212. self.printers = self.readSettings()
  213. unknownCount = 0
  214. for p in self.printers:
  215. method = self.getMethod(p[0], p[1])
  216. print("Printer " + p[0] + " has method " + method)
  217. if method == "unknown":
  218. unknownCount += 1
  219. continue
  220. commands = self.getSystemCommands(p[0], p[1])
  221. p.append(commands)
  222. menu = QMenu(self.getName(p[0], p[1]))
  223. p.append(menu)
  224. self.menu.addMenu(menu)
  225. if method == "psucontrol":
  226. action = QAction("Turn On PSU")
  227. action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
  228. p.append(action)
  229. menu.addAction(action)
  230. action = QAction("Turn Off PSU")
  231. action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
  232. p.append(action)
  233. menu.addAction(action)
  234. for i in range(0, len(commands)):
  235. action = QAction(commands[i].title())
  236. action.triggered.connect(lambda chk, x=p, y=i: self.printerSystemCommandAction(x, y))
  237. p.append(action)
  238. menu.addAction(action)
  239. action = QAction("Get Status")
  240. action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
  241. p.append(action)
  242. menu.addAction(action)
  243. action = QAction("Show Webcam")
  244. action.triggered.connect(lambda chk, x=p: self.printerWebcamAction(x))
  245. p.append(action)
  246. menu.addAction(action)
  247. action = QAction("Open Web UI")
  248. action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
  249. p.append(action)
  250. menu.addAction(action)
  251. self.settingsAction = QAction("&Settings")
  252. self.settingsAction.triggered.connect(self.showSettingsAction)
  253. self.menu.addAction(self.settingsAction)
  254. self.quitAction = QAction("&Quit")
  255. self.quitAction.triggered.connect(self.exit)
  256. self.menu.addAction(self.quitAction)
  257. self.iconPathName = None
  258. for p in self.iconPaths:
  259. if os.path.isfile(p + self.iconName):
  260. self.iconPathName = p + self.iconName
  261. break
  262. if self.iconPathName == None:
  263. self.showDialog("OctoTray Error", "Icon file has not been found! found", "", False, False, True)
  264. sys.exit(0)
  265. self.icon = QIcon()
  266. self.pic = QPixmap(32, 32)
  267. self.pic.load(self.iconPathName)
  268. self.icon = QIcon(self.pic)
  269. self.trayIcon = QSystemTrayIcon(self.icon)
  270. self.trayIcon.setToolTip(self.name + " " + self.version)
  271. self.trayIcon.setContextMenu(self.menu)
  272. self.trayIcon.setVisible(True)
  273. def readSettings(self):
  274. settings = QSettings(self.vendor, self.name)
  275. printers = []
  276. l = settings.beginReadArray("printers")
  277. for i in range(0, l):
  278. settings.setArrayIndex(i)
  279. p = []
  280. p.append(settings.value("host"))
  281. p.append(settings.value("key"))
  282. printers.append(p)
  283. settings.endArray()
  284. return printers
  285. def writeSettings(self, printers):
  286. settings = QSettings(self.vendor, self.name)
  287. settings.remove("printers")
  288. settings.beginWriteArray("printers")
  289. for i in range(0, len(printers)):
  290. p = printers[i]
  291. settings.setArrayIndex(i)
  292. settings.setValue("host", p[0])
  293. settings.setValue("key", p[1])
  294. settings.endArray()
  295. del settings
  296. def openBrowser(self, url):
  297. QDesktopServices.openUrl(QUrl("http://" + url))
  298. def showDialog(self, title, text1, text2 = "", question = False, warning = False, error = False):
  299. msg = QMessageBox()
  300. if error:
  301. msg.setIcon(QMessageBox.Critical)
  302. elif warning:
  303. msg.setIcon(QMessageBox.Warning)
  304. elif question:
  305. msg.setIcon(QMessageBox.Question)
  306. else:
  307. msg.setIcon(QMessageBox.Information)
  308. msg.setWindowTitle(title)
  309. msg.setText(text1)
  310. if text2 is not None:
  311. msg.setInformativeText(text2)
  312. if question:
  313. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  314. else:
  315. msg.setStandardButtons(QMessageBox.Ok)
  316. retval = msg.exec_()
  317. if retval == QMessageBox.Yes:
  318. return True
  319. else:
  320. return False
  321. def sendRequest(self, host, headers, path, content = None):
  322. url = "http://" + host + "/api/" + path
  323. if content == None:
  324. request = urllib.request.Request(url, None, headers)
  325. else:
  326. data = content.encode('ascii')
  327. request = urllib.request.Request(url, data, headers)
  328. try:
  329. with urllib.request.urlopen(request, None, self.networkTimeout) as response:
  330. text = response.read()
  331. return text
  332. except urllib.error.URLError:
  333. pass
  334. except urllib.error.HTTPError:
  335. pass
  336. return ""
  337. def sendPostRequest(self, host, key, path, content):
  338. headers = {
  339. "Content-Type": "application/json",
  340. "X-Api-Key": key
  341. }
  342. return self.sendRequest(host, headers, path, content)
  343. def sendGetRequest(self, host, key, path):
  344. headers = {
  345. "X-Api-Key": key
  346. }
  347. return self.sendRequest(host, headers, path)
  348. def getTemperatureString(self, host, key):
  349. r = self.sendGetRequest(host, key, "printer")
  350. s = ""
  351. try:
  352. rd = json.loads(r)
  353. if ("state" in rd) and ("text" in rd["state"]):
  354. s += rd["state"]["text"]
  355. if "temperature" in rd:
  356. s += " - "
  357. if "temperature" in rd:
  358. if "bed" in rd["temperature"]:
  359. if "actual" in rd["temperature"]["bed"]:
  360. s += "B"
  361. s += "%.1f" % rd["temperature"]["bed"]["actual"]
  362. if "target" in rd["temperature"]["bed"]:
  363. s += "/"
  364. s += "%.1f" % rd["temperature"]["bed"]["target"]
  365. s += " "
  366. if "tool0" in rd["temperature"]:
  367. if "actual" in rd["temperature"]["tool0"]:
  368. s += "T"
  369. s += "%.1f" % rd["temperature"]["tool0"]["actual"]
  370. if "target" in rd["temperature"]["tool0"]:
  371. s += "/"
  372. s += "%.1f" % rd["temperature"]["tool0"]["target"]
  373. s += " "
  374. if "tool1" in rd["temperature"]:
  375. if "actual" in rd["temperature"]["tool1"]:
  376. s += "T"
  377. s += "%.1f" % rd["temperature"]["tool1"]["actual"]
  378. if "target" in rd["temperature"]["tool1"]:
  379. s += "/"
  380. s += "%.1f" % rd["temperature"]["tool1"]["target"]
  381. s += " "
  382. except json.JSONDecodeError:
  383. pass
  384. return s.strip()
  385. def getState(self, host, key):
  386. r = self.sendGetRequest(host, key, "job")
  387. try:
  388. rd = json.loads(r)
  389. if "state" in rd:
  390. return rd["state"]
  391. except json.JSONDecodeError:
  392. pass
  393. return "Unknown"
  394. def getProgress(self, host, key):
  395. r = self.sendGetRequest(host, key, "job")
  396. try:
  397. rd = json.loads(r)
  398. if "progress" in rd:
  399. return rd["progress"]
  400. except json.JSONDecodeError:
  401. pass
  402. return "Unknown"
  403. def getName(self, host, key):
  404. r = self.sendGetRequest(host, key, "printerprofiles")
  405. try:
  406. rd = json.loads(r)
  407. if "profiles" in rd:
  408. p = next(iter(rd["profiles"]))
  409. if "name" in rd["profiles"][p]:
  410. return rd["profiles"][p]["name"]
  411. except json.JSONDecodeError:
  412. pass
  413. return host
  414. def getMethod(self, host, key):
  415. r = self.sendGetRequest(host, key, "plugin/psucontrol")
  416. try:
  417. rd = json.loads(r)
  418. if "isPSUOn" in rd:
  419. return "psucontrol"
  420. except json.JSONDecodeError:
  421. pass
  422. r = self.sendGetRequest(host, key, "system/commands/custom")
  423. try:
  424. rd = json.loads(r)
  425. for c in rd:
  426. if "action" in c:
  427. # we have some custom commands and no psucontrol
  428. # so lets try to use that instead of skipping
  429. # the printer completely with 'unknown'
  430. return "system"
  431. except json.JSONDecodeError:
  432. pass
  433. return "unknown"
  434. def getSystemCommands(self, host, key):
  435. l = []
  436. r = self.sendGetRequest(host, key, "system/commands/custom")
  437. try:
  438. rd = json.loads(r)
  439. if len(rd) > 0:
  440. print("system commands available for " + host + ":")
  441. for c in rd:
  442. if "action" in c:
  443. print(" - " + c["action"])
  444. l.append(c["action"])
  445. except json.JSONDecodeError:
  446. pass
  447. return l
  448. def setPSUControl(self, host, key, state):
  449. cmd = "turnPSUOff"
  450. if state:
  451. cmd = "turnPSUOn"
  452. return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
  453. def setSystemCommand(self, host, key, cmd):
  454. cmd = urllib.parse.quote(cmd)
  455. return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
  456. def exit(self):
  457. QCoreApplication.quit()
  458. def printerSystemCommandAction(self, item, index):
  459. if "off" in item[2][index].lower():
  460. state = self.getState(item[0], item[1])
  461. if state in self.statesWithWarning:
  462. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to run '" + item[2][index] + "'?", True, True) == True:
  463. self.setSystemCommand(item[0], item[1], item[2][index])
  464. else:
  465. return
  466. self.setSystemCommand(item[0], item[1], item[2][index])
  467. def printerOnAction(self, item):
  468. self.setPSUControl(item[0], item[1], True)
  469. def printerOffAction(self, item):
  470. state = self.getState(item[0], item[1])
  471. if state in self.statesWithWarning:
  472. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == True:
  473. self.setPSUControl(item[0], item[1], False)
  474. else:
  475. self.setPSUControl(item[0], item[1], False)
  476. def printerWebAction(self, item):
  477. self.openBrowser(item[0])
  478. def printerStatusAction(self, item):
  479. progress = self.getProgress(item[0], item[1])
  480. s = ""
  481. warning = False
  482. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  483. s = "%.1f%% Completion\n" % progress["completion"]
  484. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  485. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  486. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  487. s = "No job is currently running"
  488. else:
  489. s = "Could not read printer status!"
  490. warning = True
  491. t = self.getTemperatureString(item[0], item[1])
  492. if len(t) > 0:
  493. s += "\n" + t
  494. self.showDialog("OctoTray Status", s, None, False, warning)
  495. def printerWebcamAction(self, item):
  496. for cw in self.camWindows:
  497. if cw.getHost() == item[0]:
  498. cw.show()
  499. cw.activateWindow()
  500. return
  501. window = CamWindow(self, item)
  502. self.camWindows.append(window)
  503. window.show()
  504. window.activateWindow()
  505. screenGeometry = QDesktopWidget().screenGeometry()
  506. x = (screenGeometry.width() - window.width()) / 2
  507. y = (screenGeometry.height() - window.height()) / 2
  508. x += screenGeometry.x()
  509. y += screenGeometry.y()
  510. window.setGeometry(int(x), int(y), int(window.width()), int(window.height()))
  511. def removeWebcamWindow(self, window):
  512. self.camWindows.remove(window)
  513. def showSettingsAction(self):
  514. if self.settingsWindow != None:
  515. self.settingsWindow.show()
  516. self.settingsWindow.activateWindow()
  517. return
  518. self.settingsWindow = SettingsWindow(self)
  519. self.settingsWindow.show()
  520. self.settingsWindow.activateWindow()
  521. screenGeometry = QDesktopWidget().screenGeometry()
  522. x = (screenGeometry.width() - self.settingsWindow.width()) / 2
  523. y = (screenGeometry.height() - self.settingsWindow.height()) / 2
  524. x += screenGeometry.x()
  525. y += screenGeometry.y()
  526. self.settingsWindow.setGeometry(int(x), int(y), int(self.settingsWindow.width()), int(self.settingsWindow.height()))
  527. def removeSettingsWindow(self):
  528. self.settingsWindow = None
  529. def closeAll(self):
  530. for cw in self.camWindows:
  531. cw.close()
  532. if self.settingsWindow != None:
  533. self.settingsWindow.close()
  534. self.trayIcon.setVisible(False)
  535. if __name__ == "__main__":
  536. app = QtWidgets.QApplication(sys.argv)
  537. tray = OctoTray(app)
  538. rc = app.exec_()
  539. while rc == 42:
  540. tray.closeAll()
  541. tray = OctoTray(app)
  542. rc = app.exec_()
  543. sys.exit(rc)