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

Open-MSS / MSS / 14956428468

11 May 2025 01:46PM UTC coverage: 69.89% (+0.02%) from 69.875%
14956428468

Pull #2815

github

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

14477 of 20714 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:
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
                add, move = True, True
×
248

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

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

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

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

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

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

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

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

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

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

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

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

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

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

441
        # ToDo add confirmation dialog here
442

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

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

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

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

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

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

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

498
        # ToDo add confirmation dialog here
499

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

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

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

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

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

550
        # load data from selected file
551
        dir_name, file_name = fs.path.split(file_path)
×
552
        with fs.open_fs(dir_name) as _fs:
×
553
            if _fs.exists(file_name):
×
554
                file_content = _fs.readtext(file_name)
×
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
        filename = filename.replace('\\', '/')
×
593
        dir_name, file_name = fs.path.split(filename)
×
594
        with fs.open_fs(dir_name) as _fs:
×
595
            _fs.writetext(file_name, json.dumps(save_data, indent=4))
×
596

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

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

610
        return invalid, dummy
1✔
611

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

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

633
        return False, False
1✔
634

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

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

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

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

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

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

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

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

748
        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