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.

SettingsWindow.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #!/usr/bin/env python3
  2. # OctoTray Linux Qt System Tray OctoPrint client
  3. #
  4. # SettingsWindow.py
  5. #
  6. # UI for changes to application configuration.
  7. import string
  8. import pprint
  9. from PyQt5 import QtWidgets
  10. from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLineEdit, QGridLayout, QComboBox
  11. from PyQt5.QtGui import QFontDatabase, QIntValidator
  12. from PyQt5.QtCore import Qt
  13. class Printer(object):
  14. # field 'api' for actual I/O
  15. # field 'host' etc. for settings
  16. def __repr__(self):
  17. return pprint.pformat(vars(self))
  18. class SettingsWindow(QWidget):
  19. genericColumns = [
  20. ( "Hostname", "octopi.local" ),
  21. ( "Interface", "OctoPrint" ),
  22. ( "Tool Temp", "0" ),
  23. ( "Bed Temp", "0" ),
  24. ( "Jog Speed", "600" ),
  25. ( "Jog Length", "10" ),
  26. ]
  27. apiColumns = [
  28. ( "OctoPrint", [
  29. ( "API Key", "000000000_API_KEY_HERE_000000000", [] )
  30. ]),
  31. ( "Moonraker", [
  32. ( "Webcam", "0", [] )
  33. ]),
  34. ]
  35. def __init__(self, parent, *args, **kwargs):
  36. super(SettingsWindow, self).__init__(*args, **kwargs)
  37. self.parent = parent
  38. self.setWindowTitle(parent.name + " Settings")
  39. self.setWindowIcon(parent.icon)
  40. box = QVBoxLayout()
  41. self.setLayout(box)
  42. self.openWeb = QPushButton("&Open Web UI of selected")
  43. self.openWeb.clicked.connect(self.openWebUI)
  44. box.addWidget(self.openWeb, 0)
  45. buttons = QHBoxLayout()
  46. box.addLayout(buttons, 0)
  47. self.add = QPushButton("&Add Printer")
  48. self.add.clicked.connect(self.addPrinter)
  49. buttons.addWidget(self.add)
  50. self.remove = QPushButton("&Remove Printer")
  51. self.remove.clicked.connect(self.removePrinter)
  52. buttons.addWidget(self.remove)
  53. buttons2 = QHBoxLayout()
  54. box.addLayout(buttons2, 0)
  55. self.up = QPushButton("Move &Up")
  56. self.up.clicked.connect(self.moveUp)
  57. buttons2.addWidget(self.up)
  58. self.down = QPushButton("Move &Down")
  59. self.down.clicked.connect(self.moveDown)
  60. buttons2.addWidget(self.down)
  61. # Printer data from OctoTray settings
  62. self.data = self.parent.readSettings()
  63. self.originalData = self.data.copy()
  64. # Table of printers
  65. self.printerCount = len(self.data)
  66. self.printers = QTableWidget(self.printerCount, len(self.genericColumns))
  67. box.addWidget(self.printers, 1)
  68. # Populate table of printers
  69. for i in range(0, self.printerCount):
  70. p = self.data[i]
  71. # hostname in first column
  72. item = QTableWidgetItem(p.host)
  73. self.printers.setItem(i, 0, item)
  74. font = item.font()
  75. font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
  76. item.setFont(font)
  77. # API selection in second column
  78. item = QComboBox()
  79. for api, options in self.apiColumns:
  80. item.addItem(api)
  81. if p.apiType == api:
  82. item.setCurrentText(api)
  83. item.currentIndexChanged.connect(self.selectionChanged)
  84. self.printers.setCellWidget(i, 1, item)
  85. # Tool Temp in third column
  86. item = QTableWidgetItem(p.tempTool)
  87. self.printers.setItem(i, 2, item)
  88. # Bed Temp in fourth column
  89. item = QTableWidgetItem(p.tempBed)
  90. self.printers.setItem(i, 3, item)
  91. # Jog Speed in fifth column
  92. item = QTableWidgetItem(p.jogSpeed)
  93. self.printers.setItem(i, 4, item)
  94. # Jog Length in sixth column
  95. item = QTableWidgetItem(p.jogLength)
  96. self.printers.setItem(i, 5, item)
  97. self.apiColumns[0][1][0][2].append(p.key)
  98. self.apiColumns[1][1][0][2].append(p.webcam)
  99. # Table of settings
  100. self.settings = QTableWidget(1, 2)
  101. box.addWidget(self.settings, 1)
  102. # Callback to update settings when printers selection changes
  103. self.printers.itemSelectionChanged.connect(self.selectionChanged)
  104. self.settings.itemChanged.connect(self.settingsChanged)
  105. # Put usage hint in settings table
  106. self.populateDefaultSettings()
  107. # Setup tables
  108. self.setupTableHeaders()
  109. # Initialize empty entry when none are available
  110. if len(self.data) <= 0:
  111. self.addPrinter()
  112. def setupTableHeaders(self):
  113. for t, tc in [
  114. ( self.printers, [ i[0] for i in self.genericColumns ] ),
  115. ( self.settings, [ "Option", "Value" ] )
  116. ]:
  117. t.setHorizontalHeaderLabels(tc)
  118. t.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
  119. t.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
  120. t.resizeColumnsToContents()
  121. def settingsChanged(self, item):
  122. printer = self.printers.currentRow()
  123. if (printer < 0) or (item.column() < 1):
  124. return
  125. apiType = self.printers.cellWidget(printer, 1).currentText()
  126. if apiType == self.apiColumns[0][0]:
  127. self.apiColumns[0][1][item.row()][2][printer] = item.text()
  128. elif apiType == self.apiColumns[1][0]:
  129. self.apiColumns[1][1][item.row()][2][printer] = item.text()
  130. def populateDefaultSettings(self):
  131. self.settings.clear()
  132. self.settings.setRowCount(1)
  133. self.settings.setColumnCount(2)
  134. item = QTableWidgetItem("Select printer for")
  135. self.settings.setItem(0, 0, item)
  136. item = QTableWidgetItem("detailed settings")
  137. self.settings.setItem(0, 1, item)
  138. self.setupTableHeaders()
  139. self.settings.resizeColumnsToContents()
  140. def selectionChanged(self):
  141. i = self.printers.currentRow()
  142. apiType = self.printers.cellWidget(i, 1).currentText()
  143. for api, nv in self.apiColumns:
  144. if api == apiType:
  145. self.settings.clear()
  146. self.settings.setRowCount(len(nv))
  147. self.settings.setColumnCount(2)
  148. n = 0
  149. for name, value, data in nv:
  150. item = QTableWidgetItem(name)
  151. self.settings.setItem(n, 0, item)
  152. item = QTableWidgetItem(data[i])
  153. self.settings.setItem(n, 1, item)
  154. n += 1
  155. self.setupTableHeaders()
  156. self.settings.resizeColumnsToContents()
  157. return
  158. self.populateDefaultSettings()
  159. def printersToList(self):
  160. printers = []
  161. for i in range(0, self.printerCount):
  162. p = Printer()
  163. p.host = self.printers.item(i, 0).text()
  164. p.apiType = self.printers.cellWidget(i, 1).currentText()
  165. p.tempTool = self.printers.item(i, 2).text()
  166. p.tempBed = self.printers.item(i, 3).text()
  167. p.jogSpeed = self.printers.item(i, 4).text()
  168. p.jogLength = self.printers.item(i, 5).text()
  169. p.key = self.apiColumns[0][1][0][2][i]
  170. p.webcam = self.apiColumns[1][1][0][2][i]
  171. printers.append(p)
  172. return printers
  173. def settingsValid(self, printers):
  174. for p in printers:
  175. # p.host needs to be valid hostname or IP
  176. # TODO
  177. # p.apiType
  178. # TODO
  179. if p.apiType == self.apiColumns[0][0]:
  180. # p.key only for octoprint
  181. # p.key needs to be valid API key (hexadecimal, 32 chars)
  182. if (len(p.key) != 32) or not all(c in string.hexdigits for c in p.key):
  183. return (False, "API Key not 32-digit hexadecimal")
  184. elif p.apiType == self.apiColumns[1][0]:
  185. # p.webcam only for moonraker
  186. if (len(p.webcam) < 1) or (len(p.webcam) > 1) or not all(c in string.digits for c in p.webcam):
  187. return (False, "Webcam ID not a number from 0...9")
  188. # p.tempTool and p.tempBed need to be integer temperatures (0...999)
  189. for s in [ p.tempTool, p.tempBed ]:
  190. if s == None:
  191. s = "0"
  192. if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
  193. return (False, "Temperature not a number from 0...999")
  194. js = p.jogSpeed
  195. if js == None:
  196. js = "0"
  197. if (len(js) < 1) or (len(js) > 3) or not all(c in string.digits for c in js) or (int(js) < 0) or (int(js) > 6000):
  198. return (False, "Jog Speed not a number from 0...6000")
  199. jl = p.jogLength
  200. if jl == None:
  201. jl = "0"
  202. if (len(jl) < 1) or (len(jl) > 3) or not all(c in string.digits for c in jl) or (int(jl) < 0) or (int(jl) > 100):
  203. return (False, "Jog Length not a number from 0...100")
  204. return (True, "")
  205. def printerDiffers(self, a, b):
  206. if (a.host != b.host) or (a.key != b.key) or (a.tempTool != b.tempTool) or (a.tempBed != b.tempBed) or (a.jogSpeed != b.jogSpeed) or (a.jogLength != b.jogLength) or (a.webcam != b.webcam):
  207. return True
  208. return False
  209. def printersDiffer(self, a, b):
  210. if (len(a) != len(b)):
  211. return True
  212. for i in range(0, len(a)):
  213. if self.printerDiffers(a[i], b[i]):
  214. return True
  215. return False
  216. def closeEvent(self, event):
  217. oldPrinters = self.parent.printers
  218. newPrinters = self.printersToList()
  219. valid, errorText = self.settingsValid(newPrinters)
  220. if valid == False:
  221. r = self.parent.showDialog(self.parent.name + " Settings Invalid", errorText + "!", "Do you want to edit it again?", True, True, False)
  222. if r == True:
  223. event.ignore()
  224. return
  225. else:
  226. self.parent.removeSettingsWindow()
  227. return
  228. if self.printersDiffer(oldPrinters, newPrinters):
  229. 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)
  230. if r == True:
  231. self.parent.writeSettings(newPrinters)
  232. self.parent.removeSettingsWindow()
  233. self.parent.restartApp()
  234. self.parent.removeSettingsWindow()
  235. def addPrinter(self):
  236. self.printerCount += 1
  237. self.printers.setRowCount(self.printerCount)
  238. for i in range(0, len(self.genericColumns)):
  239. if i != 1:
  240. item = QTableWidgetItem(self.genericColumns[i][1])
  241. self.printers.setItem(self.printerCount - 1, i, item)
  242. if i == 0:
  243. font = item.font()
  244. font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
  245. item.setFont(font)
  246. else:
  247. item = QComboBox()
  248. for api, options in self.apiColumns:
  249. item.addItem(api)
  250. if self.genericColumns[i][1] == api:
  251. item.setCurrentText(api)
  252. item.currentIndexChanged.connect(self.selectionChanged)
  253. self.printers.setCellWidget(self.printerCount - 1, i, item)
  254. # add default values for api specific settings
  255. self.apiColumns[0][1][0][2].append(self.apiColumns[0][1][0][1])
  256. self.apiColumns[1][1][0][2].append(self.apiColumns[1][1][0][1])
  257. self.printers.resizeColumnsToContents()
  258. self.printers.setCurrentItem(self.printers.item(self.printerCount - 1, 0))
  259. def removePrinter(self):
  260. r = self.printers.currentRow()
  261. if (r >= 0) and (r < self.printerCount):
  262. self.printerCount -= 1
  263. self.printers.removeRow(r)
  264. self.printers.setCurrentItem(self.printers.item(min(r, self.printerCount - 1), 0))
  265. # also remove values for api specific settings
  266. del self.apiColumns[0][1][0][2][r]
  267. del self.apiColumns[1][1][0][2][r]
  268. def moveUp(self):
  269. i = self.printers.currentRow()
  270. if i <= 0:
  271. return
  272. for c in range(0, self.printers.columnCount()):
  273. if c != 1:
  274. a = self.printers.takeItem(i, c)
  275. b = self.printers.takeItem(i - 1, c)
  276. self.printers.setItem(i, c, b)
  277. self.printers.setItem(i - 1, c, a)
  278. else:
  279. a = self.printers.cellWidget(i, c).currentText()
  280. b = self.printers.cellWidget(i - 1, c).currentText()
  281. self.printers.cellWidget(i, c).setCurrentText(b)
  282. self.printers.cellWidget(i - 1, c).setCurrentText(a)
  283. # also move values for api specific settings
  284. for v in [ self.apiColumns[0][1][0][2], self.apiColumns[1][1][0][2] ]:
  285. a = v[i]
  286. b = v[i - 1]
  287. v[i] = b
  288. v[i - 1] = a
  289. self.printers.setCurrentItem(self.printers.item(i - 1, 0))
  290. def moveDown(self):
  291. i = self.printers.currentRow()
  292. if i >= (self.printerCount - 1):
  293. return
  294. for c in range(0, self.printers.columnCount()):
  295. if c != 1:
  296. a = self.printers.takeItem(i, c)
  297. b = self.printers.takeItem(i + 1, c)
  298. self.printers.setItem(i, c, b)
  299. self.printers.setItem(i + 1, c, a)
  300. else:
  301. a = self.printers.cellWidget(i, c).currentText()
  302. b = self.printers.cellWidget(i + 1, c).currentText()
  303. self.printers.cellWidget(i, c).setCurrentText(b)
  304. self.printers.cellWidget(i + 1, c).setCurrentText(a)
  305. # also move values for api specific settings
  306. for v in [ self.apiColumns[0][1][0][2], self.apiColumns[1][1][0][2] ]:
  307. a = v[i]
  308. b = v[i + 1]
  309. v[i] = b
  310. v[i + 1] = a
  311. self.printers.setCurrentItem(self.printers.item(i + 1, 0))
  312. def openWebUI(self):
  313. host = self.printers.item(self.printers.currentRow(), 0).text()
  314. self.parent.openBrowser(host)