ModernPatternListPage.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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 patternModel
  9. property var backend
  10. property var stackView
  11. property bool searchExpanded: false
  12. property bool isRefreshing: false
  13. property int patternCount: patternModel ? patternModel.rowCount() : 0
  14. // Handle pattern refresh completion from backend
  15. Connections {
  16. target: backend
  17. function onPatternsRefreshCompleted(success, message) {
  18. if (patternModel) {
  19. patternModel.refresh()
  20. }
  21. isRefreshing = false
  22. }
  23. }
  24. // Update pattern count when model resets (rowCount() is not reactive)
  25. Connections {
  26. target: patternModel
  27. function onModelReset() {
  28. patternCount = patternModel.rowCount()
  29. }
  30. }
  31. Rectangle {
  32. anchors.fill: parent
  33. color: Components.ThemeManager.backgroundColor
  34. }
  35. ColumnLayout {
  36. anchors.fill: parent
  37. spacing: 0
  38. // Header with integrated search
  39. Rectangle {
  40. Layout.fillWidth: true
  41. Layout.preferredHeight: Components.ThemeManager.headerHeight
  42. color: Components.ThemeManager.surfaceColor
  43. Rectangle {
  44. anchors.bottom: parent.bottom
  45. width: parent.width
  46. height: 1
  47. color: Components.ThemeManager.borderColor
  48. }
  49. RowLayout {
  50. anchors.fill: parent
  51. anchors.leftMargin: Components.ThemeManager.spaceLg
  52. anchors.rightMargin: Components.ThemeManager.spaceMd
  53. spacing: Components.ThemeManager.spaceSm
  54. ConnectionStatus {
  55. backend: page.backend
  56. visible: !searchExpanded
  57. }
  58. Label {
  59. text: "Browse"
  60. font.family: Components.ThemeManager.fontDisplay
  61. font.pixelSize: Components.ThemeManager.fontSizeTitle
  62. color: Components.ThemeManager.textPrimary
  63. visible: !searchExpanded
  64. }
  65. Label {
  66. text: patternCount + " patterns"
  67. font.family: Components.ThemeManager.fontBody
  68. font.pixelSize: Components.ThemeManager.fontSizeCaption
  69. color: Components.ThemeManager.textTertiary
  70. visible: !searchExpanded
  71. }
  72. // Refresh button
  73. Rectangle {
  74. Layout.preferredWidth: 40
  75. Layout.preferredHeight: 40
  76. radius: 20
  77. color: refreshMouseArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  78. visible: !searchExpanded
  79. Components.Icon {
  80. id: refreshIcon
  81. anchors.centerIn: parent
  82. name: "refresh"
  83. size: 20
  84. color: isRefreshing ? Components.ThemeManager.accent
  85. : Components.ThemeManager.textSecondary
  86. SequentialAnimation on opacity {
  87. running: isRefreshing
  88. loops: Animation.Infinite
  89. NumberAnimation { to: 0.4; duration: 500 }
  90. NumberAnimation { to: 1.0; duration: 500 }
  91. }
  92. }
  93. MouseArea {
  94. id: refreshMouseArea
  95. anchors.fill: parent
  96. enabled: !isRefreshing
  97. onClicked: {
  98. if (backend) {
  99. isRefreshing = true
  100. backend.refreshPatterns()
  101. }
  102. }
  103. }
  104. }
  105. Item {
  106. Layout.fillWidth: true
  107. visible: !searchExpanded
  108. }
  109. // Expandable search pill
  110. Rectangle {
  111. Layout.fillWidth: searchExpanded
  112. Layout.preferredWidth: searchExpanded ? parent.width - 60 : 130
  113. Layout.preferredHeight: 40
  114. radius: 20
  115. color: Components.ThemeManager.backgroundColor
  116. border.color: searchExpanded || searchField.hasUnappliedSearch
  117. ? Components.ThemeManager.accent
  118. : Components.ThemeManager.borderColor
  119. border.width: 1
  120. Behavior on Layout.preferredWidth {
  121. NumberAnimation { duration: 200 }
  122. }
  123. RowLayout {
  124. anchors.fill: parent
  125. anchors.leftMargin: Components.ThemeManager.spaceMd
  126. anchors.rightMargin: Components.ThemeManager.spaceMd
  127. spacing: Components.ThemeManager.spaceSm
  128. Components.Icon {
  129. name: "search"
  130. size: 17
  131. color: searchExpanded ? Components.ThemeManager.accent
  132. : Components.ThemeManager.textSecondary
  133. }
  134. TextField {
  135. id: searchField
  136. Layout.fillWidth: true
  137. placeholderText: searchExpanded ? "Search patterns, then press Enter" : "Search"
  138. placeholderTextColor: Components.ThemeManager.textTertiary
  139. font.family: Components.ThemeManager.fontBody
  140. font.pixelSize: Components.ThemeManager.fontSizeBody
  141. color: Components.ThemeManager.textPrimary
  142. visible: searchExpanded || text.length > 0
  143. property string lastSearchText: ""
  144. property bool hasUnappliedSearch: text !== lastSearchText && text.length > 0
  145. background: Rectangle {
  146. color: "transparent"
  147. }
  148. // Only filter when user presses Enter or field loses focus
  149. onAccepted: {
  150. patternModel.filter(text)
  151. lastSearchText = text
  152. Qt.inputMethod.hide()
  153. focus = false
  154. }
  155. // Enable virtual keyboard
  156. activeFocusOnPress: true
  157. selectByMouse: true
  158. inputMethodHints: Qt.ImhNoPredictiveText
  159. // Direct MouseArea for touch events
  160. MouseArea {
  161. anchors.fill: parent
  162. onPressed: {
  163. searchField.forceActiveFocus()
  164. Qt.inputMethod.show()
  165. mouse.accepted = false // Pass through to TextField
  166. }
  167. }
  168. onActiveFocusChanged: {
  169. if (activeFocus) {
  170. searchExpanded = true
  171. // Force virtual keyboard to show
  172. Qt.inputMethod.show()
  173. } else {
  174. // Apply search when focus is lost
  175. if (text !== lastSearchText) {
  176. patternModel.filter(text)
  177. lastSearchText = text
  178. }
  179. }
  180. }
  181. Keys.onReturnPressed: {
  182. // onAccepted runs; just hide keyboard and unfocus
  183. Qt.inputMethod.hide()
  184. focus = false
  185. }
  186. Keys.onEscapePressed: {
  187. text = ""
  188. lastSearchText = ""
  189. patternModel.filter("")
  190. Qt.inputMethod.hide()
  191. focus = false
  192. }
  193. }
  194. Text {
  195. text: "Search"
  196. font.family: Components.ThemeManager.fontBody
  197. font.pixelSize: Components.ThemeManager.fontSizeCaption
  198. color: Components.ThemeManager.textTertiary
  199. visible: !searchExpanded && searchField.text.length === 0
  200. }
  201. }
  202. MouseArea {
  203. anchors.fill: parent
  204. enabled: !searchExpanded
  205. onClicked: {
  206. searchExpanded = true
  207. searchField.forceActiveFocus()
  208. Qt.inputMethod.show()
  209. }
  210. }
  211. }
  212. // Close button when expanded
  213. Rectangle {
  214. Layout.preferredWidth: 40
  215. Layout.preferredHeight: 40
  216. radius: 20
  217. visible: searchExpanded
  218. color: searchCloseArea.pressed ? Components.ThemeManager.pressedColor : "transparent"
  219. Components.Icon {
  220. anchors.centerIn: parent
  221. name: "close"
  222. size: 20
  223. color: Components.ThemeManager.textSecondary
  224. }
  225. MouseArea {
  226. id: searchCloseArea
  227. anchors.fill: parent
  228. onClicked: {
  229. searchExpanded = false
  230. searchField.text = ""
  231. searchField.lastSearchText = ""
  232. searchField.focus = false
  233. patternModel.filter("")
  234. }
  235. }
  236. }
  237. }
  238. }
  239. // Content - Pattern Grid
  240. GridView {
  241. id: gridView
  242. Layout.fillWidth: true
  243. Layout.fillHeight: true
  244. cellWidth: 200
  245. cellHeight: 220
  246. model: patternModel
  247. clip: true
  248. visible: patternCount > 0
  249. // Recycle delegates and pre-build the next rows: creating a card
  250. // mid-flick is the other half of the scroll stutter.
  251. reuseItems: true
  252. cacheBuffer: 440
  253. ScrollBar.vertical: ScrollBar {
  254. active: true
  255. policy: ScrollBar.AsNeeded
  256. }
  257. delegate: Item {
  258. width: gridView.cellWidth
  259. height: gridView.cellHeight
  260. ModernPatternCard {
  261. anchors.centerIn: parent
  262. width: gridView.cellWidth - 10
  263. height: gridView.cellHeight - 10
  264. name: model.name
  265. preview: model.preview
  266. onClicked: {
  267. if (stackView && backend) {
  268. stackView.push("PatternDetailPage.qml", {
  269. patternName: model.name,
  270. patternPath: model.path,
  271. patternPreview: model.preview,
  272. backend: backend
  273. })
  274. }
  275. }
  276. }
  277. }
  278. add: Transition {
  279. NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 300 }
  280. NumberAnimation { property: "scale"; from: 0.8; to: 1; duration: 300 }
  281. }
  282. }
  283. // Empty state — no search results, or no patterns at all
  284. Item {
  285. Layout.fillWidth: true
  286. Layout.fillHeight: true
  287. visible: patternCount === 0
  288. property bool searching: searchField.text !== ""
  289. Column {
  290. anchors.centerIn: parent
  291. spacing: Components.ThemeManager.spaceLg
  292. Components.Icon {
  293. name: parent.parent.searching ? "search" : "radio_unchecked"
  294. size: 44
  295. anchors.horizontalCenter: parent.horizontalCenter
  296. color: Components.ThemeManager.textTertiary
  297. }
  298. Label {
  299. text: parent.parent.searching ? "No patterns found" : "No patterns yet"
  300. anchors.horizontalCenter: parent.horizontalCenter
  301. color: Components.ThemeManager.textSecondary
  302. font.family: Components.ThemeManager.fontDisplay
  303. font.pixelSize: Components.ThemeManager.fontSizeTitle
  304. }
  305. Label {
  306. text: parent.parent.searching
  307. ? "Try a different search term"
  308. : "Connect to a table on the Control page to load its patterns"
  309. anchors.horizontalCenter: parent.horizontalCenter
  310. color: Components.ThemeManager.textTertiary
  311. font.family: Components.ThemeManager.fontBody
  312. font.pixelSize: Components.ThemeManager.fontSizeBody
  313. horizontalAlignment: Text.AlignHCenter
  314. }
  315. }
  316. }
  317. }
  318. }