• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-MSS / MSS / 16371583884

18 Jul 2025 01:19PM UTC coverage: 70.054% (-0.9%) from 70.952%
16371583884

push

github

web-flow
fix:fix for airspace access and shapely use (#2839)

* fix for airspace access and shapely use

* pep8 fixes

17 of 18 new or added lines in 1 file covered. (94.44%)

1738 existing lines in 41 files now uncovered.

14427 of 20594 relevant lines covered (70.05%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.3
/mslib/msui/editor.py
1
# -*- coding: utf-8 -*-
2
"""
3

4
    mslib.msui.editor
5
    ~~~~~~~~~~~~~~~~~~~~~~
6

7
    config editor for msui_settings.json.
8

9
    This file is part of MSS.
10

11
    :copyright: Copyright 2020 Vaibhav Mehra <veb7vmehra@gmail.com>
12
    :copyright: Copyright 2020-2025 by the MSS team, see AUTHORS.
13
    :license: APACHE-2.0, see LICENSE for details.
14

15
    Licensed under the Apache License, Version 2.0 (the "License");
16
    you may not use this file except in compliance with the License.
17
    You may obtain a copy of the License at
18

19
       http://www.apache.org/licenses/LICENSE-2.0
20

21
    Unless required by applicable law or agreed to in writing, software
22
    distributed under the License is distributed on an "AS IS" BASIS,
23
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
    See the License for the specific language governing permissions and
25
    limitations under the License.
26
"""
27
import collections.abc
1✔
28
import copy
1✔
29
import logging
1✔
30
import json
1✔
31
from pathlib import Path
1✔
32

33
from mslib.utils.qt import get_open_filename, get_save_filename, show_popup
1✔
34
from mslib.msui.qt5 import ui_configuration_editor_window as ui_conf
1✔
35
from PyQt5 import QtWidgets, QtCore, QtGui
1✔
36
from mslib.msui.constants import MSUI_SETTINGS
1✔
37
from mslib.msui.icons import icons
1✔
38
from mslib.utils.config import MSUIDefaultConfig as mss_default
1✔
39
from mslib.utils.config import config_loader, dict_raise_on_duplicates_empty, merge_dict
1✔
40
from mslib.utils.get_projection_params import get_projection_params
1✔
41

42
from mslib.support.qt_json_view import delegate
1✔
43
from mslib.support.qt_json_view.view import JsonView
1✔
44
from mslib.support.qt_json_view.model import JsonModel
1✔
45
from mslib.support.qt_json_view.datatypes import match_type, DataType, TypeRole, ListType
1✔
46

47

48
InvalidityRole = TypeRole + 1
1✔
49
DummyRole = TypeRole + 2
1✔
50
default_options = config_loader(default=True)
1✔
51

52

53
def get_root_index(index, parents=False):
1✔
54
    parent_list = []
1✔
55
    while index.parent().isValid():
1✔
56
        index = index.parent()
×
57
        parent_list.append(index)
×
58
    parent_list.reverse()
1✔
59
    if parents:
1✔
60
        return index, parent_list
1✔
61
    return index
×
62

63

64
class JsonDelegate(delegate.JsonDelegate):
1✔
65

66
    def paint(self, painter, option, index):
1✔
67
        """Use method from the data type or fall back to the default."""
68
        if index.column() == 0:
1✔
69
            source_model = index.model()
1✔
70
            if isinstance(source_model, QtCore.QAbstractProxyModel):
1✔
71
                source_model = source_model.sourceModel()
1✔
72
            data = source_model.serialize()
1✔
73

74
            # bold the key which has non-default value
75
            root_index, parents = get_root_index(index, parents=True)
1✔
76
            parents.append(index)
1✔
77
            key = root_index.data()
1✔
78
            if key in mss_default.list_option_structure or \
1✔
79
                key in mss_default.dict_option_structure or \
80
                key in mss_default.key_value_options:
81
                if root_index == index and data[key] != default_options[key]:
1✔
82
                    option.font.setWeight(QtGui.QFont.Bold)
1✔
83
            elif key in mss_default.fixed_dict_options:
1✔
84
                model_data = data[key]
1✔
85
                default_data = default_options[key]
1✔
86
                for parent in parents[1:]:
1✔
87
                    parent_data = parent.data()
×
88
                    if isinstance(default_data, list):
×
89
                        parent_data = int(parent.data())
×
90
                    model_data = model_data[parent_data]
×
91
                    default_data = default_data[parent_data]
×
92
                if model_data != default_data:
1✔
93
                    option.font.setWeight(QtGui.QFont.Bold)
1✔
94

95
            return super().paint(painter, option, index)
1✔
96

97
        type_ = index.data(TypeRole)
1✔
98
        if isinstance(type_, DataType):
1✔
99
            try:
1✔
100
                super().paint(painter, option, index)
1✔
101
                return type_.paint(painter, option, index)
1✔
102
            except NotImplementedError:
1✔
103
                pass
1✔
104
        return super().paint(painter, option, index)
1✔
105

106

107
class JsonSortFilterProxyModel(QtCore.QSortFilterProxyModel):
1✔
108

109
    def filterAcceptsRow(self, source_row, source_parent):
1✔
110
        # check if an item is currently accepted
111
        accepted = super().filterAcceptsRow(source_row, source_parent)
1✔
112
        if accepted:
1✔
113
            return True
1✔
114

115
        # checking if parent is accepted (works only for indexes with depth 2)
116
        src_model = self.sourceModel()
×
117
        index = src_model.index(source_row, self.filterKeyColumn(), source_parent)
×
118
        has_parent = src_model.itemFromIndex(index).parent()
×
119
        if has_parent:
×
120
            parent_index = self.mapFromSource(has_parent.index())
×
121
            return super().filterAcceptsRow(has_parent.row(), parent_index)
×
122

123
        return accepted
×
124

125

126
class ConfigurationEditorWindow(QtWidgets.QMainWindow, ui_conf.Ui_ConfigurationEditorWindow):
1✔
127
    """MSUI configuration editor class. Provides user interface elements for editing msui_settings.json
128
    """
129

130
    restartApplication = QtCore.pyqtSignal(name="restartApplication")
1✔
131

132
    def __init__(self, parent=None):
1✔
133
        super().__init__(parent)
1✔
134
        self.setupUi(self)
1✔
135

136
        options = config_loader()
1✔
137
        self.path = MSUI_SETTINGS
1✔
138
        self.last_saved = copy.deepcopy(options)
1✔
139

140
        self.optCb.addItem("All")
1✔
141
        for option in sorted(options.keys(), key=str.lower):
1✔
142
            self.optCb.addItem(option)
1✔
143

144
        # Create view and place in parent widget
145
        self.view = JsonView()
1✔
146
        self.view.setItemDelegate(JsonDelegate())
1✔
147
        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
1✔
148
        self.jsonWidget.setLayout(QtWidgets.QVBoxLayout())
1✔
149
        self.jsonWidget.layout().setContentsMargins(0, 0, 0, 0)
1✔
150
        self.jsonWidget.layout().addWidget(self.view)
1✔
151

152
        # Create proxy model for filtering
153
        self.proxy_model = JsonSortFilterProxyModel()
1✔
154
        self.json_model = JsonModel(data=options, editable_keys=True, editable_values=True)
1✔
155
        self.json_model.setHorizontalHeaderLabels(['Option', 'Value'])
1✔
156

157
        # Set view model
158
        self.proxy_model.setSourceModel(self.json_model)
1✔
159
        self.view.setModel(self.proxy_model)
1✔
160

161
        # Setting proxy model and view attributes
162
        self.proxy_model.setFilterKeyColumn(0)
1✔
163

164
        # Add actions to toolbar
165
        self.import_file_action = QtWidgets.QAction(
1✔
166
            QtGui.QIcon(icons("config_editor", "Folder-new.svg")), "Import config", self)
167
        self.import_file_action.setStatusTip("Import an external configuration file")
1✔
168
        self.toolBar.addAction(self.import_file_action)
1✔
169

170
        self.save_file_action = QtWidgets.QAction(
1✔
171
            QtGui.QIcon(icons("config_editor", "Document-save.svg")), "Save config", self)
172
        self.save_file_action.setStatusTip("Save current configuration")
1✔
173
        self.toolBar.addAction(self.save_file_action)
1✔
174

175
        self.export_file_action = QtWidgets.QAction(
1✔
176
            QtGui.QIcon(icons("config_editor", "Document-save-as.svg")), "Export config", self)
177
        self.export_file_action.setStatusTip("Export current configuration")
1✔
178
        self.toolBar.addAction(self.export_file_action)
1✔
179

180
        # Button signals
181
        self.optCb.currentIndexChanged.connect(self.set_option_filter)
1✔
182
        self.addOptBtn.clicked.connect(self.add_option_handler)
1✔
183
        self.removeOptBtn.clicked.connect(self.remove_option_handler)
1✔
184
        self.restoreDefaultsBtn.clicked.connect(self.restore_defaults)
1✔
185
        self.moveUpTb.clicked.connect(lambda: self.move_option(move=1))
1✔
186
        self.moveDownTb.clicked.connect(lambda: self.move_option(move=-1))
1✔
187

188
        # File action signals
189
        self.import_file_action.triggered.connect(self.import_config)
1✔
190
        self.save_file_action.triggered.connect(self.save_config)
1✔
191
        self.export_file_action.triggered.connect(self.export_config)
1✔
192

193
        # View/Model signals
194
        self.view.selectionModel().selectionChanged.connect(self.tree_selection_changed)
1✔
195
        self.json_model.dataChanged.connect(self.update_view)
1✔
196

197
        # set behaviour of widgets
198
        self.moveUpTb.hide()
1✔
199
        self.moveDownTb.hide()
1✔
200
        self.moveUpTb.setAutoRaise(True)
1✔
201
        self.moveUpTb.setArrowType(QtCore.Qt.UpArrow)
1✔
202
        self.moveUpTb.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
1✔
203
        self.moveDownTb.setAutoRaise(True)
1✔
204
        self.moveDownTb.setArrowType(QtCore.Qt.DownArrow)
1✔
205
        self.moveDownTb.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
1✔
206

207
        self.moveUpTb.setEnabled(False)
1✔
208
        self.moveDownTb.setEnabled(False)
1✔
209
        self.addOptBtn.setEnabled(False)
1✔
210
        self.removeOptBtn.setEnabled(False)
1✔
211
        self.restoreDefaultsBtn.setEnabled(False)
1✔
212

213
        # set tooltip and make keys non-editable
214
        self.set_noneditable_items(QtCore.QModelIndex())
1✔
215

216
        # json view attributes
217
        self.view.setAlternatingRowColors(True)
1✔
218
        self.view.setColumnWidth(0, self.view.width() // 2)
1✔
219

220
        # Add invalidity roles and update status of keys
221
        self.update_view()
1✔
222

223
        self.restart_on_save = True
1✔
224

225
    def set_noneditable_items(self, parent):
1✔
226
        for r in range(self.json_model.rowCount(parent)):
1✔
227
            index = self.json_model.index(r, 0, parent)
1✔
228
            item = self.json_model.itemFromIndex(index)
1✔
229
            item.setEditable(False)
1✔
230
            if item.text() in mss_default.fixed_dict_options + mss_default.fixed_list_options:
1✔
231
                self.set_noneditable_items(index)
1✔
232
            if item.text() in mss_default.config_descriptions:
1✔
233
                item.setData(mss_default.config_descriptions[item.text()], QtCore.Qt.ToolTipRole)
1✔
234

235
    def tree_selection_changed(self, selected, deselected):
1✔
236
        """Enable/Disable appropriate buttons based on selection in treeview
237
        """
238
        selection = self.view.selectionModel().selectedRows()
×
239
        # if no selection
240
        add, remove, restore_defaults, move = [False] * 4
×
241
        if len(selection) == 1:
×
242
            index = selection[0]
×
243
            if not index.parent().isValid():
×
244
                move = True
×
245
            root_index = get_root_index(index)
×
246
            if (root_index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options +
×
247
                    mss_default.fixed_list_options):
UNCOV
248
                add, move = True, True
×
249

250
            # display error message if key has invalid values
251
            if not index.parent().isValid():
×
252
                root_index = get_root_index(index)
×
253
                source_index = self.proxy_model.mapToSource(root_index)
×
254
                item = self.json_model.itemFromIndex(source_index)
×
255
                if any(item.data(InvalidityRole)):
×
256
                    invalidity = item.data(InvalidityRole)
×
257
                    errors = {"empty": invalidity[0], "duplicate": invalidity[1], "invalid": invalidity[2]}
×
258
                    msg = ", ".join([key for key in errors if errors[key]])
×
259
                    msg += " values found"
×
260
                    self.statusbar.showMessage(msg)
×
261
                elif item.data(DummyRole):
×
UNCOV
262
                    self.statusbar.showMessage("Dummy values found")
×
263
                else:
264
                    self.statusbar.showMessage("")
×
265
        if len(selection) >= 1:
×
266
            restore_defaults = True
×
267
            for index in selection:
×
268
                index = get_root_index(index)
×
UNCOV
269
                if index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options  \
×
270
                        + mss_default.fixed_list_options and self.proxy_model.rowCount(index) > 0:
271
                    remove = True
×
UNCOV
272
                    break
×
273

274
        self.addOptBtn.setEnabled(add)
×
275
        self.removeOptBtn.setEnabled(remove)
×
276
        self.restoreDefaultsBtn.setEnabled(restore_defaults)
×
277
        self.moveUpTb.setEnabled(move)
×
UNCOV
278
        self.moveDownTb.setEnabled(move)
×
279

280
    def update_view(self):
1✔
281
        """
282
        Set InvalidRole and DummyRole for all root items in the treeview and highlight appropriately.
283

284
        InvalidRole -> Boolean list indicating if item has Empty, Duplicate, Invalid values
285
        DummyRole -> Boolean value indicating if item has dummy value
286
        """
287
        source_model = self.json_model
1✔
288
        data = source_model.serialize()
1✔
289
        parent = QtCore.QModelIndex()
1✔
290
        for r in range(source_model.rowCount(parent)):
1✔
291
            root_index = source_model.index(r, 0, parent)
1✔
292
            root_item = source_model.itemFromIndex(root_index)
1✔
293

294
            empty, duplicate, invalid, dummy = [False] * 4
1✔
295
            color = QtCore.Qt.transparent
1✔
296
            key = root_index.data()
1✔
297
            if key in mss_default.dict_option_structure:
1✔
298
                child_keys = set()
1✔
299
                rows = source_model.rowCount(root_index)
1✔
300
                for row in range(rows):
1✔
301
                    child_key_data = source_model.index(row, 0, root_index).data()
1✔
302
                    child_keys.add(child_key_data)
1✔
303
                    if child_key_data == "":
1✔
UNCOV
304
                        empty = True
×
305

306
                # check for dummy values
307
                default = mss_default.dict_option_structure[key]
1✔
308
                values_dict = data[key]
1✔
309
                for value in values_dict:
1✔
310
                    if value in default:
1✔
311
                        if default[value] == values_dict[value]:
×
312
                            dummy = True
×
313
                            color = QtCore.Qt.gray
×
UNCOV
314
                            break
×
315

316
                # condition for checking duplicate and empty keys
317
                if len(child_keys) != rows or empty:
1✔
318
                    duplicate = True
×
UNCOV
319
                    color = QtCore.Qt.red
×
320
            elif key in mss_default.list_option_structure:
1✔
321
                values_list = data[key]
1✔
322
                # check if any dummy values
323
                if any([value == mss_default.list_option_structure[key][0] for value in values_list]):
1✔
324
                    dummy = True
×
UNCOV
325
                    color = QtCore.Qt.gray
×
326
                # check if any empty values
327
                if any([value == "" for value in values_list]):
1✔
328
                    empty = True
×
UNCOV
329
                    color = QtCore.Qt.red
×
330
                # check if any duplicate values
331
                if len(set([tuple(_x) if isinstance(_x, list) else _x for _x in values_list])) != len(values_list):
1✔
332
                    duplicate = True
×
UNCOV
333
                    color = QtCore.Qt.red
×
334
            elif key == 'filepicker_default':
1✔
335
                # ToDo remove filepicker_default
336
                if data[key] not in ['default', 'qt', 'fs']:
1✔
UNCOV
337
                    invalid = True
×
UNCOV
338
                    color = QtCore.Qt.red
×
339

340
            # set invalidityroles and dummyrole for key
341
            root_item.setData([empty, duplicate, invalid], InvalidityRole)
1✔
342
            root_item.setData(dummy, DummyRole)
1✔
343
            # set color for column 1
344
            item = source_model.itemFromIndex(root_index)
1✔
345
            item.setBackground(color)
1✔
346
            # set color for column 2
347
            source_index = source_model.index(r, 1, parent)
1✔
348
            item = source_model.itemFromIndex(source_index)
1✔
349
            item.setBackground(color)
1✔
350

351
    def set_option_filter(self, index):
1✔
352
        # By default FilterKeyColumn of the proxy model is set to 0
353
        if self.optCb.currentText() == "All":
×
354
            self.proxy_model.setFilterRegExp("")
×
355
            return
×
UNCOV
356
        self.proxy_model.setFilterRegExp(QtCore.QRegExp(f"^{self.optCb.currentText()}$"))
×
UNCOV
357
        self.view.expandAll()
×
358

359
    def add_option_handler(self):
1✔
360
        selection = self.view.selectionModel().selectedRows()
×
361
        if len(selection) == 0 or len(selection) > 1:
×
362
            logging.debug("zero or multiple selections while trying to add new value")
×
UNCOV
363
            self.statusbar.showMessage("Please select one option to add new value")
×
364
            return
×
365

366
        selected_index = get_root_index(selection[0])
×
367
        option = selected_index.data()
×
368
        parent = QtCore.QModelIndex()
×
369
        for r in range(self.json_model.rowCount(parent)):
×
370
            index = self.json_model.index(r, 0, parent)
×
371
            item = self.json_model.itemFromIndex(index)
×
UNCOV
372
            if index.data() == option:
×
373
                if option in mss_default.fixed_dict_options + mss_default.key_value_options:
×
374
                    # Cannot add options to fixed structure options
375
                    self.statusbar.showMessage(
×
376
                        "Option already exists. Please change value to your preference or restore to default.")
UNCOV
377
                    return
×
378
                elif option in mss_default.dict_option_structure:
×
379
                    # Append dummy value dict to options having a dictionary structure
380
                    json_data = mss_default.dict_option_structure[option]
×
381
                    type_ = match_type(json_data)
×
UNCOV
382
                    type_.next(model=self.json_model, data=json_data, parent=item)
×
383
                elif option in mss_default.list_option_structure:
×
384
                    # Append dummy value to options having a list structure
385
                    json_data = mss_default.list_option_structure[option]
×
UNCOV
386
                    type_ = match_type(json_data)
×
387
                    type_.next(model=self.json_model, data=json_data, parent=item)
×
388
                    # increase row count in view
389
                    rows = self.json_model.rowCount(index) - 1
×
390
                    new_item = self.json_model.itemFromIndex(self.json_model.index(rows, 0, index))
×
UNCOV
391
                    new_item.setData(rows, QtCore.Qt.DisplayRole)
×
392
                self.statusbar.showMessage("")
×
393
                # expand root item
UNCOV
394
                proxy_index = self.proxy_model.mapFromSource(index)
×
395
                self.view.expand(proxy_index)
×
396
                # expand, scroll to and select new item
397
                rows = self.json_model.rowCount(index) - 1
×
398
                new_index = self.json_model.index(rows, 0, index)
×
399
                proxy_index = self.proxy_model.mapFromSource(new_index)
×
400
                self.view.expand(proxy_index)
×
UNCOV
401
                self.view.scrollTo(proxy_index)
×
402
                self.view.selectionModel().select(
×
403
                    proxy_index, QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows)
404
                logging.debug("Added new value for %s", option)
×
UNCOV
405
                self.update_view()
×
UNCOV
406
                break
×
407

408
    def remove_option_handler(self):
1✔
409
        selection = self.view.selectionModel().selectedRows()
×
410
        if len(selection) == 0:
×
411
            logging.debug("zero selections while trying to remove option")
×
UNCOV
412
            self.statusbar.showMessage("Please select one/more options to remove")
×
UNCOV
413
            return
×
414

415
        # Collect all removable indexes from selected items
416
        non_removable = []
×
417
        removable_indexes = {}
×
418
        for index in selection:
×
419
            if not index.parent().isValid():
×
UNCOV
420
                if index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options:
×
UNCOV
421
                    removable_indexes[index] = set(range(self.proxy_model.rowCount(index)))
×
422
                else:
423
                    # cannot remove root item
UNCOV
424
                    non_removable.append(index)
×
425
            else:
426
                # find penultimate option key
427
                while index.parent().parent().isValid():
×
UNCOV
428
                    index = index.parent()
×
429
                root = index.parent()
×
430
                # enter only if root option not in fixed dictionary / key value options
431
                if root.data() not in mss_default.fixed_dict_options + mss_default.key_value_options:
×
UNCOV
432
                    if root in removable_indexes:
×
433
                        removable_indexes[root].add(index.row())
×
434
                    else:
435
                        removable_indexes[root] = {index.row()}
×
436
                else:
437
                    non_removable.append(index)
×
438

439
        if removable_indexes == {} and non_removable != []:
×
UNCOV
440
            self.statusbar.showMessage("Default options are not removable.")
×
UNCOV
441
            return
×
442

443
        # ToDo add confirmation dialog here
444

UNCOV
445
        options = "\n".join([index.data() for index in removable_indexes])
×
446
        logging.debug("Attempting to remove the following options\n%s", options)
×
447

448
        self.view.selectionModel().clearSelection()
×
449
        for index in removable_indexes:
×
450
            rows = sorted(list(removable_indexes[index]))
×
451
            for count, row in enumerate(rows):
×
UNCOV
452
                row = row - count
×
UNCOV
453
                self.proxy_model.removeRow(row, parent=index)
×
454

455
            # fix row number in list type options
456
            source_index = self.proxy_model.mapToSource(index)
×
457
            source_item = self.json_model.itemFromIndex(source_index)
×
458
            if isinstance(source_item.data(QtCore.Qt.UserRole + 1), ListType):
×
459
                for r in range(self.json_model.rowCount(source_index)):
×
460
                    child_index = self.json_model.index(r, 0, source_index)
×
UNCOV
461
                    item = self.json_model.itemFromIndex(child_index)
×
462
                    item.setData(r, QtCore.Qt.DisplayRole)
×
463

UNCOV
464
        self.statusbar.showMessage("Successfully removed values selected options")
×
UNCOV
465
        self.update_view()
×
466

467
    def restore_defaults(self):
1✔
468
        def update(data, option, value):
×
469
            """Function to update dict at a depth"""
470
            for k, v in data.items():
×
471
                if k == option:
×
472
                    data[k] = value
×
473
                    break
×
474
                if isinstance(v, collections.abc.Mapping):
×
UNCOV
475
                    data[k] = update(data.get(k, {}), option, value)
×
476
            return data
×
477

478
        selection = self.view.selectionModel().selectedRows()
×
479
        if len(selection) == 0:
×
480
            logging.debug("no selections while trying to restore defaults")
×
UNCOV
481
            self.statusbar.showMessage("Please select one/more options to restore defaults")
×
UNCOV
482
            return
×
483

484
        # get list of distinct indexes to restore
485
        model_data = self.json_model.serialize()
×
486
        selected_indexes = set()
×
487
        for index in selection:
×
488
            root_index, parent_list = get_root_index(index, parents=True)
×
489
            added = False
×
490
            data = model_data
×
491
            for parent in parent_list + [index]:
×
492
                data = data[parent.data()]
×
493
                if isinstance(data, list):
×
494
                    added = True
×
495
                    selected_indexes.add(parent)
×
496
                    break
×
UNCOV
497
            if not added:
×
UNCOV
498
                selected_indexes.add(index)
×
499

500
        # ToDo add confirmation dialog here
501

UNCOV
502
        options = "\n".join([index.data() for index in selected_indexes])
×
503
        logging.debug("Attempting to restore defaults for the following options\n%s", options)
×
504

505
        for index in selected_indexes:
×
506
            # check if root option and present in mss_default.key_value_options
507
            if not index.parent().isValid() and index.data() in mss_default.key_value_options:
×
508
                value_index = self.json_model.index(index.row(), 1, QtCore.QModelIndex())
×
UNCOV
509
                value_item = self.json_model.itemFromIndex(value_index)
×
UNCOV
510
                value_item.setData(default_options[index.data()], QtCore.Qt.DisplayRole)
×
511
                continue
512

513
            root_index, parent_list = get_root_index(index, parents=True)
×
514
            option = root_index.data()
×
515
            model_data = self.json_model.serialize()
×
516
            if option in mss_default.fixed_dict_options:
×
UNCOV
517
                if index == root_index:
×
518
                    json_data = default_options[option]
×
519
                else:
520
                    key = None
×
521
                    value = copy.deepcopy(default_options)
×
522
                    for parent in parent_list + [index]:
×
523
                        parent_data = parent.data()
×
524
                        if isinstance(value, list):
×
525
                            break
×
526
                        key = parent_data
×
527
                        value = value[parent_data]
×
UNCOV
528
                    data = copy.deepcopy(model_data[option])
×
529
                    json_data = update(data, key, value)
×
530
            else:
UNCOV
531
                json_data = default_options[option]
×
UNCOV
532
            if model_data[option] == json_data:
×
533
                continue
534
            # remove all rows
UNCOV
535
            for row in range(self.proxy_model.rowCount(root_index)):
×
536
                self.proxy_model.removeRow(0, parent=root_index)
×
537
            # add default values
538
            source_index = self.proxy_model.mapToSource(root_index)
×
539
            source_item = self.json_model.itemFromIndex(source_index)
×
UNCOV
540
            type_ = match_type(json_data)
×
541
            type_.next(model=self.json_model, data=json_data, parent=source_item)
×
542

543
        self.statusbar.showMessage("Defaults restored for selected options")
×
UNCOV
544
        self.view.clearSelection()
×
UNCOV
545
        self.update_view()
×
546

547
    def import_config(self):
1✔
548
        file_path = get_open_filename(self, "Import config", "", ";;".join(["JSON Files (*.json)", "All Files (*.*)"]))
×
UNCOV
549
        if not file_path:
×
UNCOV
550
            return
×
551

552
        if Path(file_path).exists():
×
553
            self.statusbar.showMessage("Importing config from path")
×
554
            file_content = Path(file_path).read_text(encoding="utf8")
×
555
            try:
×
556
                json_file_data = json.loads(file_content, object_pairs_hook=dict_raise_on_duplicates_empty)
×
557
            except json.JSONDecodeError as e:
×
558
                show_popup(self, "Error while loading file", e)
×
559
                logging.error("Error while loading json file %s", e)
×
560
                return
×
561
            except ValueError as e:
×
562
                show_popup(self, "Invalid keys detected", e)
×
563
                logging.error("Error while loading json file %s", e)
×
564
                return
×
565

566
        if json_file_data:
×
567
            json_model_data = self.json_model.serialize()
×
568
            options = merge_dict(copy.deepcopy(json_model_data), json_file_data)
×
569
            if options == json_model_data:
×
570
                self.statusbar.showMessage("No option with new values found")
×
571
                return
×
572
            # replace existing data with new data
573
            self.json_model.init(options, editable_keys=True, editable_values=True)
×
574
            self.view.setColumnWidth(0, self.view.width() // 2)
×
575
            self.set_noneditable_items(QtCore.QModelIndex())
×
576
            self.update_view()
×
577
            self.statusbar.showMessage("Successfully imported config")
×
578
            logging.debug("Imported new config data from file")
×
579
        else:
580
            self.statusbar.showMessage("No data found in the file")
×
581
            logging.debug("No data found in the file, using existing settings")
×
582

583
    def _save_to_path(self, filename):
1✔
584
        self.last_saved = self.json_model.serialize()
×
585
        json_data = copy.deepcopy(self.last_saved)
×
586
        save_data = copy.deepcopy(self.last_saved)
×
587

588
        for key in json_data:
×
589
            if json_data[key] == default_options[key] or json_data[key] == {} or json_data[key] == []:
×
590
                del save_data[key]
×
591

592
        # ToDo check errors keyword
593
        Path(filename).write_text(json.dumps(save_data, indent=4), encoding="utf8", errors="ignore")
×
594

595
    def validate_data(self):
1✔
596
        epsg_check, dummy = self.problem_in_map_sections()
1✔
597
        if epsg_check:
1✔
UNCOV
598
            return epsg_check, dummy
×
599
        invalid, dummy = False, False
1✔
600

601
        parent = QtCore.QModelIndex()
1✔
602
        for r in range(self.json_model.rowCount(parent)):
1✔
603
            index = self.json_model.index(r, 0, parent)
1✔
604
            item = self.json_model.itemFromIndex(index)
1✔
605
            invalid |= any(item.data(InvalidityRole))
1✔
606
            dummy |= item.data(DummyRole)
1✔
607

608
        return invalid, dummy
1✔
609

610
    def problem_in_map_sections(self):
1✔
611
        """
612
        Checks for failures in the predefined_map_sections, e.g. not supported epsg codes
613
        """
614
        key = "predefined_map_sections"
1✔
615
        source_model = self.json_model
1✔
616
        # set default color
617
        color = QtCore.Qt.black
1✔
618
        self.set_key_color(source_model, key, color)
1✔
619

620
        data = source_model.serialize()
1✔
621
        predefined_map_sections = data.get(key, dict())
1✔
622
        for name in predefined_map_sections:
1✔
623
            try:
1✔
624
                get_projection_params(predefined_map_sections[name]["CRS"])
1✔
UNCOV
625
            except ValueError:
×
626
                # visualize failure in section
627
                color = QtCore.Qt.red
×
UNCOV
628
                self.set_key_color(source_model, key, color)
×
629
                return True, True
×
630

631
        return False, False
1✔
632

633
    def set_key_color(self, source_model, key, color):
1✔
634
        """
635
        sets the foreground color of an item
636
        """
637
        parent = QtCore.QModelIndex()
1✔
638
        for r in range(source_model.rowCount(parent)):
1✔
639
            root_index = source_model.index(r, 0, parent)
1✔
640
            if key == root_index.data():
1✔
641
                item = source_model.itemFromIndex(root_index)
1✔
642
                item.setForeground(color)
1✔
643
                break
1✔
644

645
    def check_modified(self):
1✔
646
        return not self.last_saved == self.json_model.serialize()
1✔
647

648
    def save_config(self):
1✔
UNCOV
649
        invalid, dummy = self.validate_data()
×
UNCOV
650
        if invalid:
×
651
            show_popup(
×
652
                self,
653
                "Invalid values detected",
654
                "Please correct the invalid values (keys colored in red) to be able to save.")
UNCOV
655
            self.statusbar.showMessage("Please correct the values and try saving again")
×
UNCOV
656
            return False
×
657
        if dummy and self.check_modified():
×
658
            ret = QtWidgets.QMessageBox.warning(
659
                self, self.tr("Dummy values detected"),
660
                self.tr("Dummy values detected (keys colored in gray.)\n"
661
                        "Since they are dummy values you might face issues later on while working."
662
                        "\n\nDo you still want to continue to save?"),
663
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
664
                QtWidgets.QMessageBox.No)
UNCOV
665
            if ret == QtWidgets.QMessageBox.No:
×
UNCOV
666
                self.statusbar.showMessage("Please correct the values and try saving")
×
667
                return False
×
668

669
        if self.check_modified():
×
UNCOV
670
            if self.restart_on_save:
×
671
                ret = QtWidgets.QMessageBox.warning(
×
672
                    self, self.tr("Mission Support System"),
673
                    self.tr("Do you want to restart the application?\n"
674
                            "(This is necessary to apply changes)\n\n"
675
                            "Please note that clicking 'No' will not save the current configuration"),
676
                    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
677
                    QtWidgets.QMessageBox.No)
UNCOV
678
                if ret == QtWidgets.QMessageBox.Yes:
×
UNCOV
679
                    logging.debug("saving config file to: %s and restarting MSS", self.path)
×
680
                    self._save_to_path(self.path)
×
681
                    self.restartApplication.emit()
×
682
                    self.restart_on_save = False
×
683
                    self.close()
×
684
                else:
685
                    return
×
UNCOV
686
            self.restart_on_save = True
×
687
            logging.debug("saving config file to: %s", self.path)
×
688
            self._save_to_path(self.path)
×
689
        else:
690
            self.statusbar.showMessage("No values changed")
×
UNCOV
691
        return True
×
692

693
    def export_config(self):
1✔
UNCOV
694
        invalid, dummy = self.validate_data()
×
UNCOV
695
        if invalid:
×
696
            show_popup(
×
697
                self,
698
                "Invalid values detected",
699
                "Please correct the invalid values (keys colored in red) to be able to save.")
UNCOV
700
            self.statusbar.showMessage("Please correct the values and try exporting")
×
UNCOV
701
            return False
×
702

703
        if self.json_model.serialize() == default_options:
×
704
            msg = """Since the current configuration matches the default configuration, \
705
only an empty json file would be exported.\nDo you still want to continue?"""
UNCOV
706
            ret = QtWidgets.QMessageBox.warning(
×
707
                self, self.tr("Mission Support System"), self.tr(msg),
708
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
709
                QtWidgets.QMessageBox.No)
UNCOV
710
            if ret == QtWidgets.QMessageBox.No:
×
UNCOV
711
                return
×
712

713
        path = get_save_filename(self, "Export configuration", "msui_settings", "JSON files (*.json)")
×
UNCOV
714
        if path:
×
715
            self._save_to_path(path)
×
716

717
    def closeEvent(self, event):
1✔
718
        msg = ""
1✔
719
        invalid, dummy = self.validate_data()
1✔
720
        if invalid:
1✔
UNCOV
721
            msg = self.tr("Invalid keys/values found in config.\nDo you want to rectify and save changes?")
×
722
        elif dummy and not self.check_modified:
1✔
723
            msg = self.tr("Dummy keys/values found in config.\nDo you want to rectify and save changes?")
×
724
        elif self.check_modified():
1✔
725
            msg = self.tr(
×
726
                "Save Changes to default msui_settings.json?\nYou need to restart the gui for changes to take effect.")
727
        if msg != "":
1✔
UNCOV
728
            ret = QtWidgets.QMessageBox.warning(
×
729
                self, self.tr("Mission Support System"), self.tr(msg),
730
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
731
                QtWidgets.QMessageBox.No)
UNCOV
732
            if ret == QtWidgets.QMessageBox.Yes:
×
UNCOV
733
                if not self.save_config():
×
734
                    event.ignore()
×
735
                    return
×
736
        elif self.restart_on_save:
1✔
737
            ret = QtWidgets.QMessageBox.warning(
1✔
738
                self, self.tr("Mission Support System"),
739
                self.tr("Do you want to close the config editor?"),
740
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
741
                QtWidgets.QMessageBox.No)
742
            if ret == QtWidgets.QMessageBox.No:
1✔
UNCOV
743
                event.ignore()
×
UNCOV
744
                return
×
745

746
        event.accept()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc