1
0

TableControlPage.qml 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import "../components"
  5. import "../components" as Components
  6. Page {
  7. id: page
  8. property var backend: null
  9. property string selectedPort: ""
  10. property bool isSerialConnected: false
  11. property bool autoPlayOnBoot: false
  12. // Backend signal connections
  13. Connections {
  14. target: backend
  15. function onSerialConnectionChanged(connected) {
  16. isSerialConnected = connected
  17. }
  18. function onCurrentPortChanged(port) {
  19. if (port) {
  20. selectedPort = port
  21. }
  22. }
  23. function onSettingsLoaded() {
  24. if (backend) {
  25. autoPlayOnBoot = backend.autoPlayOnBoot
  26. isSerialConnected = backend.serialConnected
  27. if (backend.currentPort) {
  28. selectedPort = backend.currentPort
  29. }
  30. }
  31. }
  32. }
  33. // backend is injected by the Loader after creation, so kick off the
  34. // initial mDNS browse (and settings load) when it arrives — at
  35. // Component.onCompleted it is still null and these would be no-ops.
  36. onBackendChanged: {
  37. if (backend) {
  38. refreshSerialPorts()
  39. loadSettings()
  40. }
  41. }
  42. function refreshSerialPorts() {
  43. if (backend) {
  44. backend.refreshSerialPorts()
  45. }
  46. }
  47. function loadSettings() {
  48. if (backend) {
  49. backend.loadControlSettings()
  50. }
  51. }
  52. Rectangle {
  53. anchors.fill: parent
  54. color: Components.ThemeManager.backgroundColor
  55. }
  56. ColumnLayout {
  57. anchors.fill: parent
  58. spacing: 0
  59. // Header
  60. Rectangle {
  61. Layout.fillWidth: true
  62. Layout.preferredHeight: Components.ThemeManager.headerHeight
  63. color: Components.ThemeManager.surfaceColor
  64. Rectangle {
  65. anchors.bottom: parent.bottom
  66. width: parent.width
  67. height: 1
  68. color: Components.ThemeManager.borderColor
  69. }
  70. RowLayout {
  71. anchors.fill: parent
  72. anchors.leftMargin: Components.ThemeManager.spaceLg
  73. anchors.rightMargin: Components.ThemeManager.spaceLg
  74. ConnectionStatus {
  75. backend: page.backend
  76. Layout.rightMargin: Components.ThemeManager.spaceSm
  77. }
  78. Label {
  79. text: "Control"
  80. font.family: Components.ThemeManager.fontDisplay
  81. font.pixelSize: Components.ThemeManager.fontSizeTitle
  82. color: Components.ThemeManager.textPrimary
  83. }
  84. Item {
  85. Layout.fillWidth: true
  86. }
  87. }
  88. }
  89. // Main content — two columns: the landscape panel wastes half its
  90. // width on full-page cards, so connection lives left and the
  91. // movement/device settings live right; both scroll together.
  92. ScrollView {
  93. Layout.fillWidth: true
  94. Layout.fillHeight: true
  95. contentWidth: availableWidth
  96. RowLayout {
  97. width: parent.width
  98. spacing: 0
  99. // ---- Left column: table connection ----
  100. ColumnLayout {
  101. Layout.fillWidth: true
  102. Layout.maximumWidth: Math.round(page.width * 0.47)
  103. Layout.alignment: Qt.AlignTop
  104. spacing: 0
  105. SettingsCard {
  106. Layout.rightMargin: Components.ThemeManager.spaceSm
  107. Layout.bottomMargin: Components.ThemeManager.spaceLg
  108. Layout.preferredHeight: connectionColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  109. ColumnLayout {
  110. id: connectionColumn
  111. anchors.fill: parent
  112. anchors.margins: Components.ThemeManager.spaceLg
  113. spacing: Components.ThemeManager.spaceMd
  114. SectionLabel {
  115. text: "Table connection"
  116. }
  117. // Connection status row — info rect + button as
  118. // siblings, so the button column lines up with
  119. // every other row's (Save, Refresh, Connect).
  120. RowLayout {
  121. Layout.fillWidth: true
  122. spacing: Components.ThemeManager.spaceSm
  123. Rectangle {
  124. Layout.fillWidth: true
  125. Layout.preferredHeight: 60
  126. radius: Components.ThemeManager.radiusSm
  127. color: isSerialConnected ? Components.ThemeManager.okSoft
  128. : Components.ThemeManager.cardColor
  129. border.color: isSerialConnected ? Components.ThemeManager.ok
  130. : Components.ThemeManager.borderColor
  131. border.width: 1
  132. Column {
  133. anchors.verticalCenter: parent.verticalCenter
  134. anchors.left: parent.left
  135. anchors.right: parent.right
  136. anchors.leftMargin: Components.ThemeManager.spaceLg
  137. anchors.rightMargin: Components.ThemeManager.spaceMd
  138. spacing: 2
  139. Label {
  140. text: isSerialConnected
  141. ? (backend && backend.tableName ? backend.tableName : "Connected")
  142. : "Not connected"
  143. color: isSerialConnected ? Components.ThemeManager.ok
  144. : Components.ThemeManager.textSecondary
  145. font.family: Components.ThemeManager.fontDisplay
  146. font.pixelSize: Components.ThemeManager.fontSizeBody
  147. elide: Text.ElideRight
  148. width: parent.width
  149. }
  150. Label {
  151. text: isSerialConnected
  152. ? selectedPort
  153. : (backend ? backend.reconnectStatus : "")
  154. visible: text !== ""
  155. color: Components.ThemeManager.textSecondary
  156. font.family: Components.ThemeManager.fontBody
  157. font.pixelSize: Components.ThemeManager.fontSizeCaption
  158. elide: Text.ElideRight
  159. width: parent.width
  160. }
  161. }
  162. }
  163. ModernControlButton {
  164. Layout.preferredWidth: 116
  165. Layout.preferredHeight: 44
  166. visible: isSerialConnected
  167. text: "Disconnect"
  168. outlined: true
  169. buttonColor: Components.ThemeManager.danger
  170. fontSize: 12
  171. onClicked: {
  172. if (backend) backend.disconnectSerial()
  173. }
  174. }
  175. }
  176. // Table password ($Sand/Password, sent as X-Sand-Key).
  177. // Empty + Save clears a stored password.
  178. RowLayout {
  179. Layout.fillWidth: true
  180. spacing: Components.ThemeManager.spaceSm
  181. TextField {
  182. id: tablePasswordField
  183. Layout.fillWidth: true
  184. Layout.preferredHeight: 44
  185. echoMode: TextInput.Password
  186. placeholderText: backend && backend.hasTablePassword
  187. ? "Table password (saved)"
  188. : "Table password (if set)"
  189. placeholderTextColor: Components.ThemeManager.textTertiary
  190. font.family: Components.ThemeManager.fontBody
  191. font.pixelSize: Components.ThemeManager.fontSizeBody
  192. color: Components.ThemeManager.textPrimary
  193. verticalAlignment: TextInput.AlignVCenter
  194. leftPadding: Components.ThemeManager.spaceLg
  195. rightPadding: Components.ThemeManager.spaceLg
  196. background: Rectangle {
  197. color: Components.ThemeManager.backgroundColor
  198. radius: 22
  199. border.color: tablePasswordField.activeFocus ? Components.ThemeManager.accent
  200. : Components.ThemeManager.borderColor
  201. border.width: 1
  202. }
  203. }
  204. ModernControlButton {
  205. Layout.preferredWidth: 116
  206. Layout.preferredHeight: 44
  207. text: "Save"
  208. buttonColor: Components.ThemeManager.accent
  209. fontSize: 12
  210. onClicked: {
  211. if (backend) {
  212. backend.setTablePassword(tablePasswordField.text)
  213. tablePasswordField.text = ""
  214. }
  215. }
  216. }
  217. }
  218. }
  219. }
  220. // Tables found on the network (mDNS)
  221. SettingsCard {
  222. Layout.rightMargin: Components.ThemeManager.spaceSm
  223. Layout.bottomMargin: Components.ThemeManager.spaceLg
  224. Layout.preferredHeight: networkColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  225. ColumnLayout {
  226. id: networkColumn
  227. anchors.fill: parent
  228. anchors.margins: Components.ThemeManager.spaceLg
  229. spacing: Components.ThemeManager.spaceMd
  230. RowLayout {
  231. Layout.fillWidth: true
  232. spacing: Components.ThemeManager.spaceSm
  233. SectionLabel {
  234. text: "Tables on your network"
  235. Layout.fillWidth: true
  236. }
  237. ModernControlButton {
  238. Layout.preferredWidth: 116
  239. Layout.preferredHeight: 44
  240. text: "Refresh"
  241. icon: "refresh"
  242. outlined: true
  243. buttonColor: Components.ThemeManager.textSecondary
  244. fontSize: 12
  245. onClicked: refreshSerialPorts()
  246. }
  247. }
  248. // The connected table already sits in the status
  249. // card above, so it is filtered from this list.
  250. Repeater {
  251. model: backend ? backend.discoveredTables : []
  252. delegate: RowLayout {
  253. required property var modelData
  254. readonly property bool isCurrent: isSerialConnected && modelData.url === selectedPort
  255. visible: !isCurrent
  256. Layout.fillWidth: true
  257. spacing: Components.ThemeManager.spaceSm
  258. Rectangle {
  259. Layout.fillWidth: true
  260. Layout.preferredHeight: 60
  261. radius: Components.ThemeManager.radiusSm
  262. color: Components.ThemeManager.cardColor
  263. border.color: Components.ThemeManager.borderColor
  264. border.width: 1
  265. Column {
  266. anchors.verticalCenter: parent.verticalCenter
  267. anchors.left: parent.left
  268. anchors.right: parent.right
  269. anchors.leftMargin: Components.ThemeManager.spaceLg
  270. anchors.rightMargin: Components.ThemeManager.spaceMd
  271. spacing: 2
  272. Label {
  273. text: modelData.name || modelData.url
  274. color: Components.ThemeManager.textPrimary
  275. font.family: Components.ThemeManager.fontDisplay
  276. font.pixelSize: Components.ThemeManager.fontSizeBody
  277. elide: Text.ElideRight
  278. width: parent.width
  279. }
  280. Label {
  281. text: modelData.url
  282. color: Components.ThemeManager.textSecondary
  283. font.family: Components.ThemeManager.fontBody
  284. font.pixelSize: Components.ThemeManager.fontSizeCaption
  285. elide: Text.ElideRight
  286. width: parent.width
  287. }
  288. }
  289. }
  290. ModernControlButton {
  291. Layout.preferredWidth: 116
  292. Layout.preferredHeight: 44
  293. text: "Connect"
  294. buttonColor: Components.ThemeManager.accent
  295. fontSize: 12
  296. onClicked: {
  297. if (backend) backend.connectSerial(modelData.url)
  298. }
  299. }
  300. }
  301. }
  302. Label {
  303. visible: !backend || backend.discoveredTables.length === 0
  304. text: "No tables found. Tap Refresh, or enter the address below."
  305. font.family: Components.ThemeManager.fontBody
  306. font.pixelSize: Components.ThemeManager.fontSizeCaption
  307. color: Components.ThemeManager.textTertiary
  308. Layout.fillWidth: true
  309. wrapMode: Text.WordWrap
  310. }
  311. // Manual address entry
  312. RowLayout {
  313. Layout.fillWidth: true
  314. spacing: Components.ThemeManager.spaceSm
  315. TextField {
  316. id: manualAddress
  317. Layout.fillWidth: true
  318. Layout.preferredHeight: 44
  319. placeholderText: "IP or host address"
  320. placeholderTextColor: Components.ThemeManager.textTertiary
  321. font.family: Components.ThemeManager.fontBody
  322. font.pixelSize: Components.ThemeManager.fontSizeBody
  323. color: Components.ThemeManager.textPrimary
  324. verticalAlignment: TextInput.AlignVCenter
  325. leftPadding: Components.ThemeManager.spaceLg
  326. rightPadding: Components.ThemeManager.spaceLg
  327. inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase
  328. background: Rectangle {
  329. color: Components.ThemeManager.backgroundColor
  330. radius: 22
  331. border.color: manualAddress.activeFocus ? Components.ThemeManager.accent
  332. : Components.ThemeManager.borderColor
  333. border.width: 1
  334. }
  335. }
  336. ModernControlButton {
  337. Layout.preferredWidth: 116
  338. Layout.preferredHeight: 44
  339. text: "Connect"
  340. buttonColor: Components.ThemeManager.accent
  341. fontSize: 12
  342. enabled: manualAddress.text.trim() !== ""
  343. onClicked: {
  344. if (backend) {
  345. backend.connectSerial(manualAddress.text.trim())
  346. manualAddress.text = ""
  347. }
  348. }
  349. }
  350. }
  351. }
  352. }
  353. }
  354. // ---- Right column: movement + device settings ----
  355. ColumnLayout {
  356. Layout.fillWidth: true
  357. Layout.alignment: Qt.AlignTop
  358. spacing: 0
  359. // The table itself: movement + auto play
  360. SettingsCard {
  361. Layout.leftMargin: Components.ThemeManager.spaceSm
  362. Layout.preferredHeight: movementColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  363. ColumnLayout {
  364. id: movementColumn
  365. anchors.fill: parent
  366. anchors.margins: Components.ThemeManager.spaceLg
  367. spacing: Components.ThemeManager.spaceMd
  368. SectionLabel {
  369. text: "Table"
  370. }
  371. RowLayout {
  372. Layout.fillWidth: true
  373. spacing: Components.ThemeManager.spaceSm
  374. ModernControlButton {
  375. Layout.fillWidth: true
  376. Layout.preferredHeight: Components.ThemeManager.touchTarget
  377. text: "Home"
  378. icon: "home"
  379. outlined: true
  380. buttonColor: Components.ThemeManager.accent
  381. fontSize: 13
  382. enabled: isSerialConnected
  383. onClicked: {
  384. if (backend) backend.sendHome()
  385. }
  386. }
  387. ModernControlButton {
  388. Layout.fillWidth: true
  389. Layout.preferredHeight: Components.ThemeManager.touchTarget
  390. text: "Center"
  391. icon: "adjust"
  392. outlined: true
  393. buttonColor: Components.ThemeManager.accent
  394. fontSize: 13
  395. enabled: isSerialConnected
  396. onClicked: {
  397. if (backend) backend.moveToCenter()
  398. }
  399. }
  400. ModernControlButton {
  401. Layout.fillWidth: true
  402. Layout.preferredHeight: Components.ThemeManager.touchTarget
  403. text: "Edge"
  404. icon: "radio_unchecked"
  405. outlined: true
  406. buttonColor: Components.ThemeManager.accent
  407. fontSize: 13
  408. enabled: isSerialConnected
  409. onClicked: {
  410. if (backend) backend.moveToPerimeter()
  411. }
  412. }
  413. }
  414. Rectangle {
  415. Layout.fillWidth: true
  416. Layout.preferredHeight: 1
  417. color: Components.ThemeManager.borderLight
  418. }
  419. RowLayout {
  420. Layout.fillWidth: true
  421. spacing: Components.ThemeManager.spaceSm
  422. ColumnLayout {
  423. Layout.fillWidth: true
  424. spacing: 2
  425. Label {
  426. text: "Auto play"
  427. font.family: Components.ThemeManager.fontMedium
  428. font.pixelSize: Components.ThemeManager.fontSizeBody
  429. color: Components.ThemeManager.textPrimary
  430. }
  431. Label {
  432. text: "Start playing when the table powers on"
  433. font.family: Components.ThemeManager.fontBody
  434. font.pixelSize: Components.ThemeManager.fontSizeCaption
  435. color: Components.ThemeManager.textSecondary
  436. wrapMode: Text.WordWrap
  437. Layout.fillWidth: true
  438. }
  439. }
  440. DwSwitch {
  441. id: autoPlaySwitch
  442. checked: autoPlayOnBoot
  443. onToggled: {
  444. autoPlayOnBoot = checked
  445. if (backend) {
  446. backend.setAutoPlayOnBoot(checked)
  447. }
  448. }
  449. // A user toggle breaks the declarative binding;
  450. // this keeps the switch following loaded settings.
  451. Binding {
  452. target: autoPlaySwitch
  453. property: "checked"
  454. value: autoPlayOnBoot
  455. }
  456. }
  457. }
  458. Rectangle {
  459. Layout.fillWidth: true
  460. Layout.preferredHeight: 1
  461. color: Components.ThemeManager.borderLight
  462. }
  463. // Reboots the FluidNC controller ($Bye soft-reset);
  464. // the board re-homes on the way back up.
  465. ModernControlButton {
  466. Layout.fillWidth: true
  467. Layout.preferredHeight: Components.ThemeManager.touchTarget
  468. text: "Restart table"
  469. icon: "restart"
  470. outlined: true
  471. buttonColor: Components.ThemeManager.textSecondary
  472. fontSize: 13
  473. enabled: isSerialConnected
  474. onClicked: {
  475. if (backend) backend.restartBackend()
  476. }
  477. }
  478. }
  479. }
  480. // This screen: sleep, theme, power
  481. SettingsCard {
  482. Layout.leftMargin: Components.ThemeManager.spaceSm
  483. Layout.bottomMargin: Components.ThemeManager.spaceLg
  484. Layout.preferredHeight: screenColumn.implicitHeight + 2 * Components.ThemeManager.spaceLg
  485. ColumnLayout {
  486. id: screenColumn
  487. anchors.fill: parent
  488. anchors.margins: Components.ThemeManager.spaceLg
  489. spacing: Components.ThemeManager.spaceMd
  490. SectionLabel {
  491. text: "This screen"
  492. }
  493. Label {
  494. text: "Sleeps after"
  495. font.family: Components.ThemeManager.fontMedium
  496. font.pixelSize: Components.ThemeManager.fontSizeCaption
  497. color: Components.ThemeManager.textSecondary
  498. }
  499. RowLayout {
  500. id: timeoutGrid
  501. Layout.fillWidth: true
  502. spacing: Components.ThemeManager.spaceSm
  503. property string currentSelection: backend ? backend.getCurrentScreenTimeoutOption() : "5 minutes"
  504. Connections {
  505. target: backend
  506. function onScreenTimeoutChanged() {
  507. if (backend) {
  508. timeoutGrid.currentSelection = backend.getCurrentScreenTimeoutOption()
  509. }
  510. }
  511. }
  512. Repeater {
  513. model: [
  514. { label: "30 s", value: "30 seconds" },
  515. { label: "1 m", value: "1 minute" },
  516. { label: "5 m", value: "5 minutes" },
  517. { label: "10 m", value: "10 minutes" },
  518. { label: "Never", value: "Never" }
  519. ]
  520. ChoiceChip {
  521. required property var modelData
  522. Layout.fillWidth: true
  523. Layout.preferredHeight: Components.ThemeManager.touchTarget
  524. label: modelData.label
  525. selected: timeoutGrid.currentSelection === modelData.value
  526. onClicked: {
  527. if (backend) {
  528. backend.setScreenTimeoutByOption(modelData.value)
  529. timeoutGrid.currentSelection = modelData.value
  530. }
  531. }
  532. }
  533. }
  534. }
  535. Rectangle {
  536. Layout.fillWidth: true
  537. Layout.preferredHeight: 1
  538. color: Components.ThemeManager.borderLight
  539. }
  540. RowLayout {
  541. Layout.fillWidth: true
  542. spacing: Components.ThemeManager.spaceSm
  543. Label {
  544. text: "Night mode"
  545. font.family: Components.ThemeManager.fontMedium
  546. font.pixelSize: Components.ThemeManager.fontSizeBody
  547. color: Components.ThemeManager.textPrimary
  548. Layout.fillWidth: true
  549. }
  550. DwSwitch {
  551. id: darkModeSwitch
  552. checked: Components.ThemeManager.darkMode
  553. onToggled: {
  554. Components.ThemeManager.darkMode = checked
  555. }
  556. }
  557. }
  558. Rectangle {
  559. Layout.fillWidth: true
  560. Layout.preferredHeight: 1
  561. color: Components.ThemeManager.borderLight
  562. }
  563. ModernControlButton {
  564. Layout.fillWidth: true
  565. Layout.preferredHeight: Components.ThemeManager.touchTarget
  566. text: "Shut down Pi"
  567. icon: "power"
  568. outlined: true
  569. buttonColor: Components.ThemeManager.danger
  570. fontSize: 13
  571. onClicked: {
  572. if (backend) backend.shutdownPi()
  573. }
  574. }
  575. }
  576. }
  577. }
  578. }
  579. }
  580. }
  581. }