6import QGroundControl.Controls
7import QGroundControl.AppSettings
12 z: QGroundControl.zOrderTopMost
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
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: ""
27 function _setExpanded(pageIndex, value) {
28 _expandedPages[pageIndex] = value
32 function _isExpanded(pageIndex) {
33 void _expandedRevision // create binding dependency
34 return !!_expandedPages[pageIndex]
37 function _pageSections(entry) {
38 return entry && typeof entry.sections === "function" ? entry.sections() : []
41 function _pageAvailable(entry) {
42 if (!entry || entry.name === "Divider" ||
43 (typeof entry.pageVisible === "function" && !entry.pageVisible())) {
47 var sections = _pageSections(entry)
48 return sections.length === 0 || sections.some(function(section) { return section.visible })
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
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() !== "") {
66 let hasPageBefore = false
67 for (let i = pageIndex - 1; i >= 0; i--) {
68 let entry = settingsPagesModel.get(i)
69 if (entry.name === "Divider") {
72 if (_pageAvailable(entry)) {
81 for (let j = pageIndex + 1; j < settingsPagesModel.count; j++) {
82 if (_pageAvailable(settingsPagesModel.get(j))) {
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
94 var entry = settingsPagesModel.get(pageIndex)
95 if (!_pageAvailable(entry)) return []
97 var sections = _pageSections(entry)
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)
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
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
124 _selectedSectionIndex = sectionIndex
126 if (_selectedPageIndex !== pageIndex) {
127 _selectedPageIndex = pageIndex
128 rightPanel.source = url
131 // Apply section filter after the page is loaded
132 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
133 rightPanel.item.sectionFilter = sectionIndex
137 function _navigateToFirstAvailablePage() {
138 for (var i = 0; i < settingsPagesModel.count; i++) {
139 if (_pageAvailable(settingsPagesModel.get(i))) {
145 _selectedPageIndex = -1
146 _selectedSectionIndex = -1
147 rightPanel.source = ""
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) {
161 // This need to block click event leakage to underlying map.
166 QGCPalette { id: qgcPal }
168 Component.onCompleted: {
169 if (globals.commingFromRIDIndicator) {
170 globals.commingFromRIDIndicator = false
171 showSettingsPage("Remote ID")
174 if (_selectedPageIndex === -1) {
175 _navigateToFirstAvailablePage()
181 function onLoaded() {
182 if (rightPanel.item && typeof rightPanel.item.sectionFilter !== "undefined") {
183 rightPanel.item.sectionFilter = _selectedSectionIndex
188 SettingsPagesModel { id: settingsPagesModel }
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
202 objectName: "settings_searchField"
203 Layout.fillWidth: true
204 placeholderText: qsTr("Search settings...")
207 settingsView._searchQuery = text
213 objectName: "settings_buttonList"
214 Layout.fillWidth: true
215 Layout.fillHeight: true
216 contentHeight: buttonColumn.height + _verticalMargin
217 flickableDirection: Flickable.VerticalFlick
222 width: buttonList.width
227 model: settingsPagesModel
232 Layout.fillWidth: true
234 required property int index
235 required property var model
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
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))
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)
263 onPageAvailableChanged: {
264 if (isSelected && !pageAvailable) {
265 settingsView._navigateToFirstAvailablePage()
270 if (pageName === "Divider") return settingsView._dividerVisible(index)
271 if (!pageAvailable) return false
272 if (isSearching) return matchesSearch
277 objectName: pageName === "Divider" ? "settingsDivider_" + index : ""
278 visible: pageName === "Divider"
283 Layout.fillWidth: true
284 objectName: "settingsButton_" + (model.nameKey ?? pageName)
286 icon.source: pageIconUrl
287 expandable: hasMultipleSections
289 checked: isSelected && settingsView._selectedSectionIndex === -1
290 visible: pageName !== "Divider" && pageAvailable
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)
307 if (!mainWindow.allowViewSwitch()) {
310 var expanding = !isExpanded
311 settingsView._setExpanded(index, expanding)
312 if (!expanding && isSelected) {
313 settingsView._navigateTo(index, -1)
318 // Section sub-items (indented, shown when page is expanded)
320 model: isExpanded ? visiblePageSections : []
324 Layout.fillWidth: true
325 padding: ScreenTools.defaultFontPixelWidth * 0.75
326 leftPadding: ScreenTools.defaultFontPixelWidth * 3
327 hoverEnabled: !ScreenTools.isMobile
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
336 property color textColor: sectionChecked || pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText
337 visible: sectionMatchesSearch
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
345 contentItem: QGCLabel {
347 color: sectionBtn.textColor
348 font.pointSize: ScreenTools.defaultFontPointSize * 0.9
349 horizontalAlignment: Text.AlignLeft
353 if (mainWindow.allowViewSwitch()) {
354 settingsView._navigateTo(pageColumn.index, sectionIndex)
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
374 color: qgcPal.windowShade
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