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

Open-MSS / MSS / 14997546987

13 May 2025 01:12PM UTC coverage: 69.887% (+0.01%) from 69.875%
14997546987

Pull #2815

github

web-flow
Merge cc8ab5ac6 into 84c28f149
Pull Request #2815: fixed timeout definition by list_operation_structure

2 of 4 new or added lines in 2 files covered. (50.0%)

5 existing lines in 1 file now uncovered.

14477 of 20715 relevant lines covered (69.89%)

0.7 hits per line

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

41.78
/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 fs
1✔
30
import logging
1✔
31
import json
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)
×
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)
×
NEW
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):
×
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)
×
NEW
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
×
272
                    break
×
273

274
        self.addOptBtn.setEnabled(add)
×
275
        self.removeOptBtn.setEnabled(remove)
×
276
        self.restoreDefaultsBtn.setEnabled(restore_defaults)
×
277
        self.moveUpTb.setEnabled(move)
×
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✔
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
×
314
                            break
×
315

316
                # condition for checking duplicate and empty keys
317
                if len(child_keys) != rows or empty:
1✔
318
                    duplicate = True
×
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✔
UNCOV
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
×
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✔
UNCOV
332
                    duplicate = True
×
UNCOV
333
                    color = QtCore.Qt.red
×
334
            elif key == 'filepicker_default':
1✔
335
                if data[key] not in ['default', 'qt', 'fs']:
1✔
336
                    invalid = True
×
337
                    color = QtCore.Qt.red
×
338

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

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

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

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

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

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

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

442
        # ToDo add confirmation dialog here
443

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

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

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

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

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

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

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

499
        # ToDo add confirmation dialog here
500

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

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

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

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

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

551
        # load data from selected file
552
        dir_name, file_name = fs.path.split(file_path)
×
553
        with fs.open_fs(dir_name) as _fs:
×
554
            if _fs.exists(file_name):
×
555
                file_content = _fs.readtext(file_name)
×
556
                try:
×
557
                    json_file_data = json.loads(file_content, object_pairs_hook=dict_raise_on_duplicates_empty)
×
558
                except json.JSONDecodeError as e:
×
559
                    show_popup(self, "Error while loading file", e)
×
560
                    logging.error("Error while loading json file %s", e)
×
561
                    return
×
562
                except ValueError as e:
×
563
                    show_popup(self, "Invalid keys detected", e)
×
564
                    logging.error("Error while loading json file %s", e)
×
565
                    return
×
566

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

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

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

593
        filename = filename.replace('\\', '/')
×
594
        dir_name, file_name = fs.path.split(filename)
×
595
        with fs.open_fs(dir_name) as _fs:
×
596
            _fs.writetext(file_name, json.dumps(save_data, indent=4))
×
597

598
    def validate_data(self):
1✔
599
        epsg_check, dummy = self.problem_in_map_sections()
1✔
600
        if epsg_check:
1✔
601
            return epsg_check, dummy
×
602
        invalid, dummy = False, False
1✔
603

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

611
        return invalid, dummy
1✔
612

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

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

634
        return False, False
1✔
635

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

648
    def check_modified(self):
1✔
649
        return not self.last_saved == self.json_model.serialize()
1✔
650

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

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

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

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

716
        path = get_save_filename(self, "Export configuration", "msui_settings", "JSON files (*.json)")
×
717
        if path:
×
718
            self._save_to_path(path)
×
719

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

749
        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