QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
AppSettings.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5import QGroundControl
6import QGroundControl.Controls
7import QGroundControl.AppSettings
8
9Rectangle {
10 id: settingsView
11 color: qgcPal.window
12 z: QGroundControl.zOrderTopMost
13
14 readonly property real _defaultTextHeight: ScreenTools.defaultFontPixelHeight
15 readonly property real _defaultTextWidth: ScreenTools.defaultFontPixelWidth
16 readonly property real _horizontalMargin: _defaultTextWidth / 2
17 readonly property real _verticalMargin: _defaultTextHeight / 2
18
19 property bool _first: true
20 property bool _commingFromRIDSettings: false
21 property int _selectedPageIndex: -1
22 property int _selectedSectionIndex: -1
23 property var _expandedPages: ({}) // pageIndex -> bool
24 property int _expandedRevision: 0 // bumped to trigger re-evaluation
25 property string _searchQuery: ""
26
27 function _setExpanded(pageIndex, value) {
28 _expandedPages[pageIndex] = value
29 _expandedRevision++
30 }
31
32 function _isExpanded(pageIndex) {
33 void _expandedRevision // create binding dependency
34 return !!_expandedPages[pageIndex]
35 }
36
37 // Search: returns array of matching section indices for a page, or empty if no match
38 function _matchingSections(pageIndex) {
39 var query = _searchQuery.toLowerCase().trim()
40 if (query === "") return [] // empty = no filtering
41
42 var entry = settingsPagesModel.get(pageIndex)
43 if (!entry) return []
44
45 // Check English search terms
46 var termsStr = entry.searchTerms
47 var matches = []
48 var matched = {}
49 if (termsStr && termsStr !== "") {
50 try {
51 var terms = JSON.parse(termsStr)
52 for (var i = 0; i < terms.length; i++) {
53 if (terms[i].terms.indexOf(query) !== -1) {
54 matched[terms[i].section] = true
55 matches.push(terms[i].section)
56 }
57 }
58 } catch(e) { console.warn("AppSettings: JSON parse error in searchTerms:", e) }
59 }
60
61 // Check translatable terms (translated at runtime)
62 var trStr = entry.translatableTerms
63 if (trStr && trStr !== "") {
64 try {
65 var trTerms = JSON.parse(trStr)
66 for (var j = 0; j < trTerms.length; j++) {
67 if (matched[trTerms[j].section]) continue
68 var ctx = trTerms[j].context
69 var tList = trTerms[j].terms
70 for (var k = 0; k < tList.length; k++) {
71 if (qsTranslate(ctx, tList[k]).toLowerCase().indexOf(query) !== -1) {
72 matched[trTerms[j].section] = true
73 matches.push(trTerms[j].section)
74 break
75 }
76 }
77 }
78 } catch(e) { console.warn("AppSettings: JSON parse error in translatableTerms:", e) }
79 }
80
81 return matches
82 }
83
84 // Does this page have any search matches? (or is search empty = show all)
85 function _pageMatchesSearch(pageIndex) {
86 if (_searchQuery.trim() === "") return true
87 return _matchingSections(pageIndex).length > 0
88 }
89
90 function _navigateTo(pageIndex, sectionIndex) {
91 var entry = settingsPagesModel.get(pageIndex)
92 if (!entry || entry.name === "Divider") return
93
94 var url = entry.url
95 _selectedSectionIndex = sectionIndex
96
97 if (_selectedPageIndex !== pageIndex) {
98 _selectedPageIndex = pageIndex
99 rightPanel.source = url
100 }
101
102 // Apply section filter after the page is loaded
103 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
104 rightPanel.item.sectionFilter = sectionIndex
105 }
106 }
107
108 function showSettingsPage(settingsPage) {
109 for (var i = 0; i < settingsPagesModel.count; i++) {
110 var entry = settingsPagesModel.get(i)
111 if (entry && entry.name === settingsPage) {
112 _navigateTo(i, -1)
113 break
114 }
115 }
116 }
117
118 // This need to block click event leakage to underlying map.
119 DeadMouseArea {
120 anchors.fill: parent
121 }
122
123 QGCPalette { id: qgcPal }
124
125 Component.onCompleted: {
126 // Find and select the default page
127 var targetUrl = globals.commingFromRIDIndicator
128 ? "qrc:/qml/QGroundControl/AppSettings/RemoteIDSettings.qml"
129 : "qrc:/qml/QGroundControl/AppSettings/GeneralSettings.qml"
130 globals.commingFromRIDIndicator = false
131
132 for (var i = 0; i < settingsPagesModel.count; i++) {
133 var entry = settingsPagesModel.get(i)
134 if (entry && entry.url === targetUrl) {
135 _navigateTo(i, -1)
136 break
137 }
138 }
139 }
140
141 Connections {
142 target: rightPanel
143 function onLoaded() {
144 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
145 rightPanel.item.sectionFilter = _selectedSectionIndex
146 }
147 }
148 }
149
150 SettingsPagesModel { id: settingsPagesModel }
151
152 ColumnLayout {
153 id: leftPanel
154 width: Math.max(buttonColumn.implicitWidth + _horizontalMargin, ScreenTools.defaultFontPixelWidth * 22)
155 anchors.topMargin: _verticalMargin
156 anchors.top: parent.top
157 anchors.bottom: parent.bottom
158 anchors.leftMargin: _horizontalMargin
159 anchors.left: parent.left
160 spacing: _verticalMargin / 2
161
162 QGCTextField {
163 id: searchField
164 Layout.fillWidth: true
165 placeholderText: qsTr("Search settings...")
166
167 onTextChanged: {
168 settingsView._searchQuery = text
169 }
170 }
171
172 QGCFlickable {
173 id: buttonList
174 objectName: "settings_buttonList"
175 Layout.fillWidth: true
176 Layout.fillHeight: true
177 contentHeight: buttonColumn.height + _verticalMargin
178 flickableDirection: Flickable.VerticalFlick
179 clip: true
180
181 ColumnLayout {
182 id: buttonColumn
183 spacing: 0
184
185 Repeater {
186 id: buttonRepeater
187 model: settingsPagesModel
188
189 ColumnLayout {
190 id: pageColumn
191 spacing: 0
192 Layout.fillWidth: true
193
194 required property int index
195 required property var model
196
197 property string pageName: model.name ?? ""
198 property string pageUrl: model.url ?? ""
199 property string pageIconUrl: model.iconUrl ?? ""
200 property var pageVisible: model.pageVisible ?? function() { return true }
201 property var pageSections: {
202 try {
203 var trStr = model.translatableTerms
204 if (trStr && trStr !== "") {
205 var trTerms = JSON.parse(trStr)
206 return trTerms.map(function(t) {
207 if (!t.terms || t.terms.length === 0) return ""
208 return qsTranslate(t.context, t.terms[0])
209 }).filter(function(s) { return s !== "" })
210 }
211 var s = model.sections
212 return (s && s !== "") ? JSON.parse(s) : []
213 } catch(e) {
214 console.warn("AppSettings: JSON parse error in pageSections:", e)
215 return []
216 }
217 }
218 property bool isSelected: settingsView._selectedPageIndex === index
219 property bool hasMultipleSections: pageSections.length > 1
220 property bool isSearching: settingsView._searchQuery.trim() !== ""
221 property bool matchesSearch: settingsView._pageMatchesSearch(index)
222 property bool isExpanded: hasMultipleSections && (isSearching ? matchesSearch : settingsView._isExpanded(index))
223
224 visible: {
225 if (pageName === "Divider") return !isSearching
226 if (!pageVisible()) return false
227 if (isSearching) return matchesSearch
228 return true
229 }
230
231 // Divider
232 Item {
233 Layout.fillWidth: true
234 height: ScreenTools.defaultFontPixelHeight / 2
235 visible: pageName === "Divider"
236 }
237
238 // Page button
239 SettingsButton {
240 Layout.fillWidth: true
241 objectName: "settingsButton_" + (model.nameKey ?? pageName)
242 text: pageName
243 icon.source: pageIconUrl
244 expandable: hasMultipleSections
245 expanded: isExpanded
246 checked: isSelected && settingsView._selectedSectionIndex === -1
247 visible: pageName !== "Divider" && pageVisible()
248
249 onClicked: {
250 if (mainWindow.allowViewSwitch()) {
251 settingsView._navigateTo(index, -1)
252 if (hasMultipleSections) {
253 // Toggle expand/collapse when re-clicking the same page
254 if (isSelected && isExpanded) {
255 settingsView._setExpanded(index, false)
256 } else if (!isExpanded) {
257 settingsView._setExpanded(index, true)
258 }
259 }
260 }
261 }
262
263 onToggleExpand: {
264 if (!mainWindow.allowViewSwitch()) {
265 return
266 }
267 var expanding = !isExpanded
268 settingsView._setExpanded(index, expanding)
269 if (!expanding && isSelected) {
270 settingsView._navigateTo(index, -1)
271 }
272 }
273 }
274
275 // Section sub-items (indented, shown when page is expanded)
276 Repeater {
277 model: isExpanded ? pageSections : []
278
279 Button {
280 id: sectionBtn
281 Layout.fillWidth: true
282 padding: ScreenTools.defaultFontPixelWidth * 0.75
283 leftPadding: ScreenTools.defaultFontPixelWidth * 3
284 hoverEnabled: !ScreenTools.isMobile
285
286 property int sectionIndex: index
287 property bool sectionChecked: pageColumn.isSelected && settingsView._selectedSectionIndex === sectionIndex
288 property bool sectionMatchesSearch: {
289 if (!pageColumn.isSearching) return true
290 var matches = settingsView._matchingSections(pageColumn.index)
291 return matches.indexOf(sectionIndex) !== -1
292 }
293 property bool sectionContentVisible: {
294 if (!pageColumn.isSelected) return true
295 if (!rightPanel.item) return true
296 if (typeof rightPanel.item.sectionVisible !== "function") return true
297 return rightPanel.item.sectionVisible(sectionIndex)
298 }
299 property color textColor: sectionChecked || pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText
300 visible: sectionMatchesSearch && sectionContentVisible
301
302 background: Rectangle {
303 color: qgcPal.buttonHighlight
304 opacity: sectionBtn.sectionChecked || sectionBtn.pressed ? 1 : sectionBtn.enabled && sectionBtn.hovered ? 0.2 : 0
305 radius: ScreenTools.defaultFontPixelWidth / 2
306 }
307
308 contentItem: QGCLabel {
309 text: modelData
310 color: sectionBtn.textColor
311 font.pointSize: ScreenTools.defaultFontPointSize * 0.9
312 horizontalAlignment: Text.AlignLeft
313 }
314
315 onClicked: {
316 if (mainWindow.allowViewSwitch()) {
317 settingsView._navigateTo(pageColumn.index, sectionIndex)
318 }
319 }
320 }
321 }
322 }
323 }
324 }
325 }
326 }
327
328 Rectangle {
329 id: divider
330 anchors.topMargin: _verticalMargin
331 anchors.bottomMargin: _verticalMargin
332 anchors.leftMargin: _horizontalMargin
333 anchors.left: leftPanel.right
334 anchors.top: parent.top
335 anchors.bottom: parent.bottom
336 width: 1
337 color: qgcPal.windowShade
338 }
339
340 //-- Panel Contents
341 Loader {
342 id: rightPanel
343 objectName: "settings_rightPanel"
344 anchors.leftMargin: _horizontalMargin
345 anchors.rightMargin: _horizontalMargin
346 anchors.topMargin: _verticalMargin
347 anchors.bottomMargin: _verticalMargin
348 anchors.left: divider.right
349 anchors.right: parent.right
350 anchors.top: parent.top
351 anchors.bottom: parent.bottom
352 }
353}