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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # OctoTray.py
  5. #
  6. # Main application logic.
  7. import json
  8. import sys
  9. import time
  10. import urllib.parse
  11. import urllib.request
  12. import operator
  13. import socket
  14. from os import path
  15. from PyQt5 import QtNetwork
  16. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu, QMessageBox, QDesktopWidget
  17. from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices, QCursor
  18. from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
  19. from CamWindow import CamWindow
  20. from SettingsWindow import SettingsWindow
  21. from MainWindow import MainWindow
  22. class OctoTray():
  23. name = "OctoTray"
  24. vendor = "xythobuz"
  25. version = "0.4"
  26. iconName = "octotray_icon.png"
  27. iconPaths = [
  28. path.abspath(path.dirname(__file__)),
  29. "data",
  30. "/usr/share/pixmaps",
  31. ".",
  32. "..",
  33. "../data"
  34. ]
  35. networkTimeout = 2.0 # in s
  36. # list of lists, inner lists contain printer data:
  37. # first elements as in SettingsWindow.columns
  38. # 0=host 1=key 2=tool-preheat 3=bed-preheat
  39. # rest used for system-commands, menu, actions
  40. printers = []
  41. statesWithWarning = [
  42. "Printing", "Pausing", "Paused"
  43. ]
  44. camWindows = []
  45. settingsWindow = None
  46. # default, can be overridden in config
  47. jogMoveSpeed = 10 * 60 # in mm/min
  48. jogMoveLength = 10 # in mm
  49. def __init__(self, app, inSysTray):
  50. QCoreApplication.setApplicationName(self.name)
  51. self.app = app
  52. self.inSysTray = inSysTray
  53. self.manager = QtNetwork.QNetworkAccessManager()
  54. self.menu = QMenu()
  55. self.printers = self.readSettings()
  56. unknownCount = 0
  57. for p in self.printers:
  58. method = self.getMethod(p[0], p[1])
  59. print("Printer " + p[0] + " has method " + method)
  60. if method == "unknown":
  61. unknownCount += 1
  62. action = QAction(p[0])
  63. action.setEnabled(False)
  64. p.append(action)
  65. self.menu.addAction(action)
  66. continue
  67. commands = self.getSystemCommands(p[0], p[1])
  68. p.append(commands)
  69. menu = QMenu(self.getName(p[0], p[1]))
  70. p.append(menu)
  71. self.menu.addMenu(menu)
  72. if method == "psucontrol":
  73. action = QAction("Turn On PSU")
  74. action.triggered.connect(lambda chk, x=p: self.printerOnAction(x))
  75. p.append(action)
  76. menu.addAction(action)
  77. action = QAction("Turn Off PSU")
  78. action.triggered.connect(lambda chk, x=p: self.printerOffAction(x))
  79. p.append(action)
  80. menu.addAction(action)
  81. for i in range(0, len(commands)):
  82. action = QAction(commands[i].title())
  83. action.triggered.connect(lambda chk, x=p, y=i: self.printerSystemCommandAction(x, y))
  84. p.append(action)
  85. menu.addAction(action)
  86. if (p[2] != None) or (p[3] != None):
  87. menu.addSeparator()
  88. if p[2] != None:
  89. action = QAction("Preheat Tool")
  90. action.triggered.connect(lambda chk, x=p: self.printerHeatTool(x))
  91. p.append(action)
  92. menu.addAction(action)
  93. if p[3] != None:
  94. action = QAction("Preheat Bed")
  95. action.triggered.connect(lambda chk, x=p: self.printerHeatBed(x))
  96. p.append(action)
  97. menu.addAction(action)
  98. if (p[2] != None) or (p[3] != None):
  99. action = QAction("Cooldown")
  100. action.triggered.connect(lambda chk, x=p: self.printerCooldown(x))
  101. p.append(action)
  102. menu.addAction(action)
  103. menu.addSeparator()
  104. fileMenu = QMenu("Recent Files")
  105. p.append(fileMenu)
  106. menu.addMenu(fileMenu)
  107. files = self.getRecentFiles(p[0], p[1], 10)
  108. for f in files:
  109. fileName, filePath = f
  110. action = QAction(fileName)
  111. action.triggered.connect(lambda chk, x=p, y=filePath: self.printerFilePrint(x, y))
  112. p.append(action)
  113. fileMenu.addAction(action)
  114. action = QAction("Get Status")
  115. action.triggered.connect(lambda chk, x=p: self.printerStatusAction(x))
  116. p.append(action)
  117. menu.addAction(action)
  118. action = QAction("Show Webcam")
  119. action.triggered.connect(lambda chk, x=p: self.printerWebcamAction(x))
  120. p.append(action)
  121. menu.addAction(action)
  122. action = QAction("Open Web UI")
  123. action.triggered.connect(lambda chk, x=p: self.printerWebAction(x))
  124. p.append(action)
  125. menu.addAction(action)
  126. self.menu.addSeparator()
  127. self.settingsAction = QAction("&Settings")
  128. self.settingsAction.triggered.connect(self.showSettingsAction)
  129. self.menu.addAction(self.settingsAction)
  130. self.refreshAction = QAction("&Refresh")
  131. self.refreshAction.triggered.connect(self.restartApp)
  132. self.menu.addAction(self.refreshAction)
  133. self.quitAction = QAction("&Quit")
  134. self.quitAction.triggered.connect(self.exit)
  135. self.menu.addAction(self.quitAction)
  136. self.iconPathName = None
  137. for p in self.iconPaths:
  138. if path.isfile(path.join(p, self.iconName)):
  139. self.iconPathName = path.join(p, self.iconName)
  140. break
  141. if self.iconPathName == None:
  142. self.showDialog("OctoTray Error", "Icon file has not been found!", "", False, False, True)
  143. sys.exit(0)
  144. self.icon = QIcon()
  145. self.pic = QPixmap(32, 32)
  146. self.pic.load(self.iconPathName)
  147. self.icon = QIcon(self.pic)
  148. if self.inSysTray:
  149. self.trayIcon = QSystemTrayIcon(self.icon)
  150. self.trayIcon.setToolTip(self.name + " " + self.version)
  151. self.trayIcon.setContextMenu(self.menu)
  152. self.trayIcon.activated.connect(self.showHide)
  153. self.trayIcon.setVisible(True)
  154. else:
  155. self.mainWindow = MainWindow(self)
  156. self.mainWindow.show()
  157. self.mainWindow.activateWindow()
  158. screenGeometry = QDesktopWidget().screenGeometry()
  159. x = (screenGeometry.width() - self.mainWindow.width()) / 2
  160. y = (screenGeometry.height() - self.mainWindow.height()) / 2
  161. x += screenGeometry.x()
  162. y += screenGeometry.y()
  163. self.mainWindow.setGeometry(int(x), int(y), int(self.mainWindow.width()), int(self.mainWindow.height()))
  164. def showHide(self, activationReason):
  165. if activationReason == QSystemTrayIcon.Trigger:
  166. self.menu.popup(QCursor.pos())
  167. elif activationReason == QSystemTrayIcon.MiddleClick:
  168. if len(self.printers) > 0:
  169. self.printerWebcamAction(self.printers[0])
  170. def readSettings(self):
  171. settings = QSettings(self.vendor, self.name)
  172. js = settings.value("jog_speed")
  173. if js != None:
  174. self.jogMoveSpeed = int(js)
  175. jl = settings.value("jog_length")
  176. if jl != None:
  177. self.jogMoveLength = int(jl)
  178. printers = []
  179. l = settings.beginReadArray("printers")
  180. for i in range(0, l):
  181. settings.setArrayIndex(i)
  182. p = []
  183. p.append(settings.value("host"))
  184. p.append(settings.value("key"))
  185. p.append(settings.value("tool_preheat"))
  186. p.append(settings.value("bed_preheat"))
  187. printers.append(p)
  188. settings.endArray()
  189. return printers
  190. def writeSettings(self, printers):
  191. settings = QSettings(self.vendor, self.name)
  192. settings.setValue("jog_speed", self.jogMoveSpeed)
  193. settings.setValue("jog_length", self.jogMoveLength)
  194. settings.remove("printers")
  195. settings.beginWriteArray("printers")
  196. for i in range(0, len(printers)):
  197. p = printers[i]
  198. settings.setArrayIndex(i)
  199. settings.setValue("host", p[0])
  200. settings.setValue("key", p[1])
  201. settings.setValue("tool_preheat", p[2])
  202. settings.setValue("bed_preheat", p[3])
  203. settings.endArray()
  204. del settings
  205. def openBrowser(self, url):
  206. QDesktopServices.openUrl(QUrl("http://" + url))
  207. def showDialog(self, title, text1, text2 = "", question = False, warning = False, error = False):
  208. msg = QMessageBox()
  209. if error:
  210. msg.setIcon(QMessageBox.Critical)
  211. elif warning:
  212. msg.setIcon(QMessageBox.Warning)
  213. elif question:
  214. msg.setIcon(QMessageBox.Question)
  215. else:
  216. msg.setIcon(QMessageBox.Information)
  217. msg.setWindowTitle(title)
  218. msg.setText(text1)
  219. if text2 is not None:
  220. msg.setInformativeText(text2)
  221. if question:
  222. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  223. else:
  224. msg.setStandardButtons(QMessageBox.Ok)
  225. retval = msg.exec_()
  226. if retval == QMessageBox.Yes:
  227. return True
  228. else:
  229. return False
  230. def sendRequest(self, host, headers, path, content = None):
  231. url = "http://" + host + "/api/" + path
  232. if content == None:
  233. request = urllib.request.Request(url, None, headers)
  234. else:
  235. data = content.encode('ascii')
  236. request = urllib.request.Request(url, data, headers)
  237. try:
  238. with urllib.request.urlopen(request, None, self.networkTimeout) as response:
  239. text = response.read()
  240. return text
  241. except (urllib.error.URLError, urllib.error.HTTPError) as error:
  242. print("Error requesting URL \"" + url + "\": \"" + str(error) + "\"")
  243. return "error"
  244. except socket.timeout:
  245. print("Timeout waiting for response to \"" + url + "\"")
  246. return "timeout"
  247. def sendPostRequest(self, host, key, path, content):
  248. headers = {
  249. "Content-Type": "application/json",
  250. "X-Api-Key": key
  251. }
  252. return self.sendRequest(host, headers, path, content)
  253. def sendGetRequest(self, host, key, path):
  254. headers = {
  255. "X-Api-Key": key
  256. }
  257. return self.sendRequest(host, headers, path)
  258. def getTemperatureIsSafe(self, host, key):
  259. r = self.sendGetRequest(host, key, "printer")
  260. try:
  261. rd = json.loads(r)
  262. if "temperature" in rd:
  263. if ("tool0" in rd["temperature"]) and ("actual" in rd["temperature"]["tool0"]):
  264. if rd["temperature"]["tool0"]["actual"] > 50.0:
  265. return False
  266. if ("tool1" in rd["temperature"]) and ("actual" in rd["temperature"]["tool1"]):
  267. if rd["temperature"]["tool1"]["actual"] > 50.0:
  268. return False
  269. except json.JSONDecodeError:
  270. pass
  271. return True
  272. def getTemperatureString(self, host, key):
  273. r = self.sendGetRequest(host, key, "printer")
  274. s = ""
  275. try:
  276. rd = json.loads(r)
  277. if ("state" in rd) and ("text" in rd["state"]):
  278. s += rd["state"]["text"]
  279. if "temperature" in rd:
  280. s += " - "
  281. if "temperature" in rd:
  282. if "bed" in rd["temperature"]:
  283. if "actual" in rd["temperature"]["bed"]:
  284. s += "B"
  285. s += "%.1f" % rd["temperature"]["bed"]["actual"]
  286. if "target" in rd["temperature"]["bed"]:
  287. s += "/"
  288. s += "%.1f" % rd["temperature"]["bed"]["target"]
  289. s += " "
  290. if "tool0" in rd["temperature"]:
  291. if "actual" in rd["temperature"]["tool0"]:
  292. s += "T"
  293. s += "%.1f" % rd["temperature"]["tool0"]["actual"]
  294. if "target" in rd["temperature"]["tool0"]:
  295. s += "/"
  296. s += "%.1f" % rd["temperature"]["tool0"]["target"]
  297. s += " "
  298. if "tool1" in rd["temperature"]:
  299. if "actual" in rd["temperature"]["tool1"]:
  300. s += "T"
  301. s += "%.1f" % rd["temperature"]["tool1"]["actual"]
  302. if "target" in rd["temperature"]["tool1"]:
  303. s += "/"
  304. s += "%.1f" % rd["temperature"]["tool1"]["target"]
  305. s += " "
  306. except json.JSONDecodeError:
  307. pass
  308. return s.strip()
  309. def getState(self, host, key):
  310. r = self.sendGetRequest(host, key, "job")
  311. try:
  312. rd = json.loads(r)
  313. if "state" in rd:
  314. return rd["state"]
  315. except json.JSONDecodeError:
  316. pass
  317. return "Unknown"
  318. def getProgress(self, host, key):
  319. r = self.sendGetRequest(host, key, "job")
  320. try:
  321. rd = json.loads(r)
  322. if "progress" in rd:
  323. return rd["progress"]
  324. except json.JSONDecodeError:
  325. pass
  326. return "Unknown"
  327. def getName(self, host, key):
  328. r = self.sendGetRequest(host, key, "printerprofiles")
  329. try:
  330. rd = json.loads(r)
  331. if "profiles" in rd:
  332. p = next(iter(rd["profiles"]))
  333. if "name" in rd["profiles"][p]:
  334. return rd["profiles"][p]["name"]
  335. except json.JSONDecodeError:
  336. pass
  337. return host
  338. def getRecentFiles(self, host, key, count):
  339. r = self.sendGetRequest(host, key, "files?recursive=true")
  340. files = []
  341. try:
  342. rd = json.loads(r)
  343. if "files" in rd:
  344. t = [f for f in rd["files"] if "date" in f]
  345. fs = sorted(t, key=operator.itemgetter("date"), reverse=True)
  346. for f in fs[:count]:
  347. files.append((f["name"], f["origin"] + "/" + f["path"]))
  348. except json.JSONDecodeError:
  349. pass
  350. return files
  351. def getMethod(self, host, key):
  352. r = self.sendGetRequest(host, key, "plugin/psucontrol")
  353. if r == "timeout":
  354. return "unknown"
  355. try:
  356. rd = json.loads(r)
  357. if "isPSUOn" in rd:
  358. return "psucontrol"
  359. except json.JSONDecodeError:
  360. pass
  361. r = self.sendGetRequest(host, key, "system/commands/custom")
  362. if r == "timeout":
  363. return "unknown"
  364. try:
  365. rd = json.loads(r)
  366. for c in rd:
  367. if "action" in c:
  368. # we have some custom commands and no psucontrol
  369. # so lets try to use that instead of skipping
  370. # the printer completely with 'unknown'
  371. return "system"
  372. except json.JSONDecodeError:
  373. pass
  374. return "unknown"
  375. def getSystemCommands(self, host, key):
  376. l = []
  377. r = self.sendGetRequest(host, key, "system/commands/custom")
  378. try:
  379. rd = json.loads(r)
  380. if len(rd) > 0:
  381. print("system commands available for " + host + ":")
  382. for c in rd:
  383. if "action" in c:
  384. print(" - " + c["action"])
  385. l.append(c["action"])
  386. except json.JSONDecodeError:
  387. pass
  388. return l
  389. def setPSUControl(self, host, key, state):
  390. cmd = "turnPSUOff"
  391. if state:
  392. cmd = "turnPSUOn"
  393. return self.sendPostRequest(host, key, "plugin/psucontrol", '{ "command":"' + cmd + '" }')
  394. def setSystemCommand(self, host, key, cmd):
  395. cmd = urllib.parse.quote(cmd)
  396. return self.sendPostRequest(host, key, "system/commands/custom/" + cmd, '')
  397. def exit(self):
  398. QCoreApplication.quit()
  399. def printerSystemCommandAction(self, item, index):
  400. if "off" in item[2][index].lower():
  401. state = self.getState(item[0], item[1])
  402. if state in self.statesWithWarning:
  403. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to run '" + item[2][index] + "'?", True, True) == False:
  404. return
  405. safe = self.getTemperatureIsSafe(item[0], item[1])
  406. if safe == False:
  407. if self.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to turn it off?", True, True) == False:
  408. return
  409. self.setSystemCommand(item[0], item[1], item[2][index])
  410. def printerOnAction(self, item):
  411. self.setPSUControl(item[0], item[1], True)
  412. def printerOffAction(self, item):
  413. state = self.getState(item[0], item[1])
  414. if state in self.statesWithWarning:
  415. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == False:
  416. return
  417. safe = self.getTemperatureIsSafe(item[0], item[1])
  418. if safe == False:
  419. if self.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to turn it off?", True, True) == False:
  420. return
  421. self.setPSUControl(item[0], item[1], False)
  422. def printerHomingAction(self, item, axes = "xyz"):
  423. state = self.getState(item[0], item[1])
  424. if state in self.statesWithWarning:
  425. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to home it?", True, True) == False:
  426. return
  427. axes_string = ''
  428. for i in range(0, len(axes)):
  429. axes_string += '"' + str(axes[i]) + '"'
  430. if i < (len(axes) - 1):
  431. axes_string += ', '
  432. self.sendPostRequest(item[0], item[1], "printer/printhead", '{ "command": "home", "axes": [' + axes_string + '] }')
  433. def printerMoveAction(self, printer, axis, dist, relative = True):
  434. state = self.getState(printer[0], printer[1])
  435. if state in self.statesWithWarning:
  436. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to move it?", True, True) == False:
  437. return
  438. absolute = ''
  439. if relative == False:
  440. absolute = ', "absolute": true'
  441. self.sendPostRequest(printer[0], printer[1], "printer/printhead", '{ "command": "jog", "' + str(axis) + '": ' + str(dist) + ', "speed": ' + str(self.jogMoveSpeed) + absolute + ' }')
  442. def printerPauseResume(self, printer):
  443. state = self.getState(printer[0], printer[1])
  444. if state in self.statesWithWarning:
  445. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to pause/resume?", True, True) == False:
  446. return
  447. self.sendPostRequest(printer[0], printer[1], "job", '{ "command": "pause", "action": "toggle" }')
  448. def printerJobCancel(self, printer):
  449. state = self.getState(printer[0], printer[1])
  450. if state in self.statesWithWarning:
  451. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to cancel?", True, True) == False:
  452. return
  453. self.sendPostRequest(printer[0], printer[1], "job", '{ "command": "cancel" }')
  454. def printerWebAction(self, item):
  455. self.openBrowser(item[0])
  456. def printerStatusAction(self, item):
  457. progress = self.getProgress(item[0], item[1])
  458. s = item[0] + "\n"
  459. warning = False
  460. if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
  461. s += "%.1f%% Completion\n" % progress["completion"]
  462. s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
  463. s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
  464. elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
  465. s += "No job is currently running"
  466. else:
  467. s += "Could not read printer status!"
  468. warning = True
  469. t = self.getTemperatureString(item[0], item[1])
  470. if len(t) > 0:
  471. s += "\n" + t
  472. self.showDialog("OctoTray Status", s, None, False, warning)
  473. def printerFilePrint(self, item, path):
  474. self.sendPostRequest(item[0], item[1], "files/" + path, '{ "command": "select", "print": true }')
  475. def setTemperature(self, host, key, what, temp):
  476. path = "printer/bed"
  477. s = "{\"command\": \"target\", \"target\": " + temp + "}"
  478. if "tool" in what:
  479. path = "printer/tool"
  480. s = "{\"command\": \"target\", \"targets\": {\"" + what + "\": " + temp + "}}"
  481. if temp == None:
  482. temp = 0
  483. self.sendPostRequest(host, key, path, s)
  484. def printerHeatTool(self, p):
  485. self.setTemperature(p[0], p[1], "tool0", p[2])
  486. def printerHeatBed(self, p):
  487. self.setTemperature(p[0], p[1], "bed", p[3])
  488. def printerCooldown(self, p):
  489. state = self.getState(p[0], p[1])
  490. if state in self.statesWithWarning:
  491. if self.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to turn it off?", True, True) == False:
  492. return
  493. self.setTemperature(p[0], p[1], "tool0", 0)
  494. self.setTemperature(p[0], p[1], "bed", 0)
  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()) + 50)
  527. def removeSettingsWindow(self):
  528. self.settingsWindow = None
  529. def restartApp(self):
  530. QCoreApplication.exit(42)
  531. def closeAll(self):
  532. for cw in self.camWindows:
  533. cw.close()
  534. if self.settingsWindow != None:
  535. self.settingsWindow.close()
  536. if self.inSysTray:
  537. self.trayIcon.setVisible(False)
  538. else:
  539. self.mainWindow.setVisible(False)