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 function _pageSections(entry) {
38 return entry && typeof entry.sections === "function" ? entry.sections() : []
39 }
40
41 function _pageAvailable(entry) {
42 if (!entry || entry.name === "Divider" ||
43 (typeof entry.pageVisible === "function" && !entry.pageVisible())) {
44 return false
45 }
46
47 var sections = _pageSections(entry)
48 return sections.length === 0 || sections.some(function(section) { return section.visible })
49 }
50
51 function _sectionAvailable(sections, sectionIndex) {
52 if (sectionIndex === -1) return true
53 for (var i = 0; i < sections.length; i++) {
54 if (sections[i].index === sectionIndex) return sections[i].visible
55 }
56 return false
57 }
58
59 // A divider shows only between two available pages. When consecutive dividers have no
60 // available page between them, only the first one shows.
61 function _dividerVisible(pageIndex) {
62 if (_searchQuery.trim() !== "") {
63 return false
64 }
65
66 let hasPageBefore = false
67 for (let i = pageIndex - 1; i >= 0; i--) {
68 let entry = settingsPagesModel.get(i)
69 if (entry.name === "Divider") {
70 return false
71 }
72 if (_pageAvailable(entry)) {
73 hasPageBefore = true
74 break
75 }
76 }
77 if (!hasPageBefore) {
78 return false
79 }
80
81 for (let j = pageIndex + 1; j < settingsPagesModel.count; j++) {
82 if (_pageAvailable(settingsPagesModel.get(j))) {
83 return true
84 }
85 }
86 return false
87 }
88
89 // Search: returns array of matching section indices for a page, or empty if no match
90 function _matchingSections(pageIndex) {
91 var query = _searchQuery.toLowerCase().trim()
92 if (query === "") return [] // empty = no filtering
93
94 var entry = settingsPagesModel.get(pageIndex)
95 if (!_pageAvailable(entry)) return []
96
97 var sections = _pageSections(entry)
98 var matches = []
99 for (var i = 0; i < sections.length; i++) {
100 if (!sections[i].visible) continue
101 for (var j = 0; j < sections[i].searchTerms.length; j++) {
102 if (sections[i].searchTerms[j].indexOf(query) !== -1) {
103 matches.push(sections[i].index)
104 break
105 }
106 }
107 }
108 return matches
109 }
110
111 // Does this page have any search matches? (or is search empty = show all)
112 function _pageMatchesSearch(pageIndex) {
113 if (_searchQuery.trim() === "") return true
114 return _matchingSections(pageIndex).length > 0
115 }
116
117 function _navigateTo(pageIndex, sectionIndex) {
118 var entry = settingsPagesModel.get(pageIndex)
119 if (!entry || entry.name === "Divider") return
120 if (!_pageAvailable(entry)) return
121 if (!_sectionAvailable(_pageSections(entry), sectionIndex)) return
122
123 var url = entry.url
124 _selectedSectionIndex = sectionIndex
125
126 if (_selectedPageIndex !== pageIndex) {
127 _selectedPageIndex = pageIndex
128 rightPanel.source = url
129 }
130
131 // Apply section filter after the page is loaded
132 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
133 rightPanel.item.sectionFilter = sectionIndex
134 }
135 }
136
137 function _navigateToFirstAvailablePage() {
138 for (var i = 0; i < settingsPagesModel.count; i++) {
139 if (_pageAvailable(settingsPagesModel.get(i))) {
140 _navigateTo(i, -1)
141 return
142 }
143 }
144
145 _selectedPageIndex = -1
146 _selectedSectionIndex = -1
147 rightPanel.source = ""
148 }
149
150 // settingsPage is the untranslated page name from SettingsPages.json
151 function showSettingsPage(settingsPage) {
152 for (var i = 0; i < settingsPagesModel.count; i++) {
153 var entry = settingsPagesModel.get(i)
154 if (entry && entry.nameKey === settingsPage) {
155 _navigateTo(i, -1)
156 break
157 }
158 }
159 }
160
161 // This need to block click event leakage to underlying map.
162 DeadMouseArea {
163 anchors.fill: parent
164 }
165
166 QGCPalette { id: qgcPal }
167
168 Component.onCompleted: {
169 if (globals.commingFromRIDIndicator) {
170 globals.commingFromRIDIndicator = false
171 showSettingsPage("Remote ID")
172 }
173
174 if (_selectedPageIndex === -1) {
175 _navigateToFirstAvailablePage()
176 }
177 }
178
179 Connections {
180 target: rightPanel
181 function onLoaded() {
182 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
183 rightPanel.item.sectionFilter = _selectedSectionIndex
184 }
185 }
186 }
187
188 SettingsPagesModel { id: settingsPagesModel }
189
190 ColumnLayout {
191 id: leftPanel
192 width: Math.max(buttonColumn.implicitWidth + _horizontalMargin, ScreenTools.defaultFontPixelWidth * 22)
193 anchors.topMargin: _verticalMargin
194 anchors.top: parent.top
195 anchors.bottom: parent.bottom
196 anchors.leftMargin: _horizontalMargin
197 anchors.left: parent.left
198 spacing: _verticalMargin / 2
199
200 QGCTextField {
201 id: searchField
202 objectName: "settings_searchField"
203 Layout.fillWidth: true
204 placeholderText: qsTr("Search settings...")
205
206 onTextChanged: {
207 settingsView._searchQuery = text
208 }
209 }
210
211 QGCFlickable {
212 id: buttonList
213 objectName: "settings_buttonList"
214 Layout.fillWidth: true
215 Layout.fillHeight: true
216 contentHeight: buttonColumn.height + _verticalMargin
217 flickableDirection: Flickable.VerticalFlick
218 clip: true
219
220 ColumnLayout {
221 id: buttonColumn
222 width: buttonList.width
223 spacing: 0
224
225 Repeater {
226 id: buttonRepeater
227 model: settingsPagesModel
228
229 ColumnLayout {
230 id: pageColumn
231 spacing: 0
232 Layout.fillWidth: true
233
234 required property int index
235 required property var model
236
237 property string pageName: model.name ?? ""
238 property string pageUrl: model.url ?? ""
239 property string pageIconUrl: model.iconUrl ?? ""
240 property var pageVisible: model.pageVisible ?? function() { return true }
241 property var pageSections: pageVisible() ? settingsView._pageSections(model) : []
242 property var visiblePageSections: pageSections.filter(function(section) {
243 return section.visible
244 })
245 property bool pageAvailable: pageVisible() &&
246 (pageSections.length === 0 || visiblePageSections.length > 0)
247 property bool isSelected: settingsView._selectedPageIndex === index
248 property bool hasMultipleSections: visiblePageSections.length > 1
249 property bool isSearching: settingsView._searchQuery.trim() !== ""
250 property bool matchesSearch: pageAvailable && settingsView._pageMatchesSearch(index)
251 property bool isExpanded: hasMultipleSections && (isSearching ? matchesSearch : settingsView._isExpanded(index))
252
253 onPageSectionsChanged: {
254 let sections = pageSections ?? []
255 let visibleCount = sections.filter(function(section) { return section.visible }).length
256 if (isSelected && pageAvailable && settingsView._selectedSectionIndex !== -1 &&
257 (visibleCount <= 1 ||
258 !settingsView._sectionAvailable(sections, settingsView._selectedSectionIndex))) {
259 settingsView._navigateTo(index, -1)
260 }
261 }
262
263 onPageAvailableChanged: {
264 if (isSelected && !pageAvailable) {
265 settingsView._navigateToFirstAvailablePage()
266 }
267 }
268
269 visible: {
270 if (pageName === "Divider") return settingsView._dividerVisible(index)
271 if (!pageAvailable) return false
272 if (isSearching) return matchesSearch
273 return true
274 }
275
276 SidebarDivider {
277 objectName: pageName === "Divider" ? "settingsDivider_" + index : ""
278 visible: pageName === "Divider"
279 }
280
281 // Page button
282 SettingsButton {
283 Layout.fillWidth: true
284 objectName: "settingsButton_" + (model.nameKey ?? pageName)
285 text: pageName
286 icon.source: pageIconUrl
287 expandable: hasMultipleSections
288 expanded: isExpanded
289 checked: isSelected && settingsView._selectedSectionIndex === -1
290 visible: pageName !== "Divider" && pageAvailable
291
292 onClicked: {
293 if (mainWindow.allowViewSwitch()) {
294 settingsView._navigateTo(index, -1)
295 if (hasMultipleSections) {
296 // Toggle expand/collapse when re-clicking the same page
297 if (isSelected && isExpanded) {
298 settingsView._setExpanded(index, false)
299 } else if (!isExpanded) {
300 settingsView._setExpanded(index, true)
301 }
302 }
303 }
304 }
305
306 onToggleExpand: {
307 if (!mainWindow.allowViewSwitch()) {
308 return
309 }
310 var expanding = !isExpanded
311 settingsView._setExpanded(index, expanding)
312 if (!expanding && isSelected) {
313 settingsView._navigateTo(index, -1)
314 }
315 }
316 }
317
318 // Section sub-items (indented, shown when page is expanded)
319 Repeater {
320 model: isExpanded ? visiblePageSections : []
321
322 Button {
323 id: sectionBtn
324 Layout.fillWidth: true
325 padding: ScreenTools.defaultFontPixelWidth * 0.75
326 leftPadding: ScreenTools.defaultFontPixelWidth * 3
327 hoverEnabled: !ScreenTools.isMobile
328
329 property int sectionIndex: modelData.index
330 property bool sectionChecked: pageColumn.isSelected && settingsView._selectedSectionIndex === sectionIndex
331 property bool sectionMatchesSearch: {
332 if (!pageColumn.isSearching) return true
333 var matches = settingsView._matchingSections(pageColumn.index)
334 return matches.indexOf(sectionIndex) !== -1
335 }
336 property color textColor: sectionChecked || pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText
337 visible: sectionMatchesSearch
338
339 background: Rectangle {
340 color: qgcPal.buttonHighlight
341 opacity: sectionBtn.sectionChecked || sectionBtn.pressed ? 1 : sectionBtn.enabled && sectionBtn.hovered ? 0.2 : 0
342 radius: ScreenTools.defaultFontPixelWidth / 2
343 }
344
345 contentItem: QGCLabel {
346 text: modelData.name
347 color: sectionBtn.textColor
348 font.pointSize: ScreenTools.defaultFontPointSize * 0.9
349 horizontalAlignment: Text.AlignLeft
350 }
351
352 onClicked: {
353 if (mainWindow.allowViewSwitch()) {
354 settingsView._navigateTo(pageColumn.index, sectionIndex)
355 }
356 }
357 }
358 }
359 }
360 }
361 }
362 }
363 }
364
365 Rectangle {
366 id: divider
367 anchors.topMargin: _verticalMargin
368 anchors.bottomMargin: _verticalMargin
369 anchors.leftMargin: _horizontalMargin
370 anchors.left: leftPanel.right
371 anchors.top: parent.top
372 anchors.bottom: parent.bottom
373 width: 1
374 color: qgcPal.windowShade
375 }
376
377 //-- Panel Contents
378 Loader {
379 id: rightPanel
380 objectName: "settings_rightPanel"
381 anchors.leftMargin: _horizontalMargin
382 anchors.rightMargin: _horizontalMargin
383 anchors.topMargin: _verticalMargin
384 anchors.bottomMargin: _verticalMargin
385 anchors.left: divider.right
386 anchors.right: parent.right
387 anchors.top: parent.top
388 anchors.bottom: parent.bottom
389 }
390}