# -*- coding: utf-8 -*-
#
# Copyright Université Rennes 1 / INSERM
# Contributor: Raphael Weber
#
# Under CeCILL license
# http://www.cecill.info
"""
Module defining class :class:`.ConfigurationWindow`
"""
from PyQt5 import QtWidgets, QtCore, QtGui
import os
from configobj import ConfigObj
import numpy as np
from ..tools import pyqt_overlayer
from .Configuration import Configuration
[docs]class ConfigurationWindow():
def __init__(self, path=""):
"""
Class creating the window for configuring
:class:`.ViSiAnnoTLongRec`
:param path: path to the configuration file to load when
creating the window, it might be a configuration dictionary
:type path: str or dict
The window is composed of 5 main widgets:
- Video configuration
- Signal configuration
- Events annotation configuration
- General configuration
- Group box with load/save/done push buttons
Each one of the video configuration, signal configuration and event
annotation configuration is contained in an instance of
:class:`Configuration`.
The signal configuration has several configuration children (Interval,
Threshold and YRange), each one displayed in a separate window.
All the instances of :class:`Configuration` are stored in the
dictionary :attr:`.meta_dict`.
On the one hand, the attribute :attr:`ConfigurationWindow.lay` refers
to the first level of QGridLayout, meaning the layout filling the whole
window. On the other hand, the second level of QGridLayout fills the
first level layout ConfigurationWindow.lay.
These layouts are the configuration widgets (general =>
ConfigurationWindow.general_lay, video =>
ConfigurationWindow.meta_dict["Video"].lay, ...).
The different types of configuration are stored in 7 dictionaries:
- :attr:`ConfigurationWindow.general_dict`
- ``ConfigurationWindow.meta_dict["Video"].dict``
- ``ConfigurationWindow.meta_dict["Signal"].dict``
- ``ConfigurationWindow.meta_dict["Interval"].dict``
- ``ConfigurationWindow.meta_dict["Threshold"].dict``
- ``ConfigurationWindow.meta_dict["AnnotEvent"].dict``
- ``ConfigurationWindow.meta_dict["AnnotImage"].dict``
When values are modified manually in the window, these dictionaries are
not updated. The method :meth:`.set_configuration_dictionaries` must
be called to do so.
"""
#: (*dict*) Each value is an instances of :class:`Configuration`
#: contained in the window
#:
#: The keys are:
#:
#: - Video
#: - Signal
#: - Interval
#: - Threshold
#: - YRange
#: - AnnotEvent
#: - AnnotImage
#:
#: See :attr:`Configuration.dict` for details.
self.meta_dict = {}
#: (*list*) Types of configuration, each element is a string and must
#: be a key of :attr:`.meta_dict`
self.config_type_list = [
"Video", "Signal", "Interval", "Threshold", "YRange", "AnnotEvent",
"AnnotImage"
]
#: (*dict*) Same keys as :attr:`.general_dict`,
#: value is the list of positions of the corresponding widgets in the
#: general configuration widget
#: (:attr:`.general_lay`)
self.general_config_position_dict = {}
#: (*QtWidgets.QGridLayout*) Layout of the general configuration widget
self.general_lay = None
#: (*QtWidgets.QGroupBox*) Group box of the general configuration
#: widget
self.general_group_box = None
#: (*QtWidgets.QPushButton*) Push button for changing the directory of
#: annotations
self.general_push_button = None
# *********************** create window ***************************** #
#: (*QtWidgets.QApplication*) Display initializer
self.app = pyqt_overlayer.initialize_gui()
# set default font
self.app.setFont(QtGui.QFont("Times", 12))
#: (*QtWidgets.QWidget*) Container of main configuration window
self.win = None
#: (*QtWidgets.QGridLayout*) Layout of the main configuration window
self.lay = None
# create window
self.win, self.lay = pyqt_overlayer.create_window(
title="ViSiAnnoT configuration"
)
# create scroll area
scroll_lay, _ = pyqt_overlayer.add_scroll_area(self.lay, (0, 0))
# ******************** create video widget ************************** #
self.meta_dict["Video"] = Configuration(
self.lay,
(0, 0),
"Video",
1,
[
("edit", 1, {}), ("edit", 1, {}), ("edit", 1, {}),
("spin", 1, {"minimum": -2}), ("edit", 1, {})
],
['', '*.mp4', '_', 0, '%Y-%m-%dT%H-%M-%S'],
help_text="1. Directory\
2. File pattern\
3. Timestamp delimiter\
4. Timestamp position\
5. Timestamp format",
config_base_name="vid_",
pos_dir=0,
flag_dir_identical=True
)
# add configuration group box to scroll area
scroll_lay.addWidget(self.meta_dict["Video"].group_box)
# ******************** create signal widget ************************* #
self.meta_dict["Signal"] = Configuration(
self.lay,
(1, 0),
"Signal",
2,
[
("edit", 1, {}), ("edit", 1, {}), ("edit", 1, {}),
("spin", 1, {"minimum": -2}), ("edit", 1, {}), ("edit", 1, {}),
("edit_freq", 1, {}), ("edit_literal", 1, {})
],
[['', '*', '_', 0, '%Y-%m-%dT%H-%M-%S', '', 0, None]],
help_text="1. Signal widget name\
2. Directory\
3. File pattern\
4. Timestamp delimiter\
5. Timestamp position\
6. Timestamp format\
7. Key to access the data (for .mat or .h5), used as legend\
8. Frequency (0 if 2D data with timestamp, -1 if same as video)\
9. Plot style (default is None)",
pos_dir=0,
flag_dir_identical=True,
flag_key=True,
config_base_name="sig_",
children_config_name_list=["Interval", "Threshold", "YRange"]
)
# add configuration group box to scroll area
scroll_lay.addWidget(self.meta_dict["Signal"].group_box)
# listen to callback
self.meta_dict["Signal"].button_group_children.buttonClicked[int].connect(
self.show_child_window
)
# **************** create annotEvent widget ************************* #
self.meta_dict["AnnotEvent"] = Configuration(
self.lay,
(2, 0),
"AnnotEvent",
1,
[
("spin", 4, [
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 100}
])
],
(0, 0, 0, 50),
help_text="1. Label\
2. Color (RGBA)",
flag_key=True,
config_base_name="label_"
)
# add configuration group box to scroll area
scroll_lay.addWidget(self.meta_dict["AnnotEvent"].group_box)
# **************** create annotImage widget ************************* #
self.meta_dict["AnnotImage"] = Configuration(
self.lay, (3, 0), "AnnotImage", 1, [("edit", 1, {})], '',
help_text="1. Label", config_base_name="label_"
)
# add configuration group box to scroll area
scroll_lay.addWidget(self.meta_dict["AnnotImage"].group_box)
# ******************* create general widget ************************* #
self.create_widget_general((4, 0))
# add group box to scroll area
scroll_lay.addWidget(self.general_group_box)
# listen to callback
self.general_push_button.clicked.connect(
self.set_directory_annotations
)
# ***************** create load/save/done widget ******************** #
#: (*QtWidgets.QButtonGroup*) Group of buttons with the push buttons
#: Load/Save/Done
self.button_group_lsd = None
_, group_box_lsd, self.button_group_lsd = \
pyqt_overlayer.add_widget_button_group(
self.lay, (5, 0), ["Load", "Save", "Done"],
button_type="push", box_title="Configuration file", width=120
)
# set size
group_box_lsd.setMaximumHeight(80)
# listen to callback
self.button_group_lsd.buttonClicked[int].connect(self.call_lsd)
# ******************* create children windows ********************** #
#: (*list*) Instances of QtWidgets.QWidget containing the children
#: configuration windows
self.children_win_list = []
# interval configuration
self.create_child_configuration_window(
self.meta_dict["Signal"],
"Interval",
2,
[
("edit", 1, {}), ("edit", 1, {}), ("edit", 1, {}),
("spin", 1, {"minimum": -2}), ("edit", 1, {}), ("edit", 1, {}),
("edit_freq", 1, {}), ("spin", 4, [
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 100}
])
],
[['', '*', '_', 0, '%Y-%m-%dT%H-%M-%S', '', 0, (0, 0, 0, 50)]],
help_text="1. Directory\
2. File pattern\
3. Timestamp delimiter\
4. Timestamp position\
5. Timestamp format\
6. Key to access the data (for .mat or .h5)\
7. Frequency (0 if timestamps, -1 if same as signal)\
8. Color (RGBA)",
pos_dir=0,
flag_dir_identical=True,
win_size=(1200, 400)
)
# threshold configuration
self.create_child_configuration_window(
self.meta_dict["Signal"],
"Threshold",
2,
[
("edit_float", 1, {}),
("spin", 3, [
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255},
{"minimum": 0, "maximum": 255}
])
],
[[0, (0, 0, 0, 50)]],
help_text="1. Threhsold value\
2. Color (RGBA)",
win_size=(600, 400)
)
# Y range configuration
self.create_child_configuration_window(
self.meta_dict["Signal"],
"YRange",
1,
[("edit_float", 1, {}), ("edit_float", 1, {})],
[0, 0],
help_text="1. Min value\
2. Max value",
win_size=(500, 400)
)
# ************** initialize general configuration ******************* #
#: (*dict*) Default general configuration, see :attr:`.general_dict`
#: for details
self.general_dict_default = {
"flag_synchro": False,
"flag_pause_status": True,
"layout_mode": 0,
"zoom_factor": 2,
"max_points": 5000,
"nb_ticks": 10,
"trunc_duration": [0, 0],
"time_zone": "Europe/Paris",
"flag_annot_overlap": False,
"annot_dir": "Annotations",
"from_cursor_list": [],
"current_fmt": "%Y-%m-%dT%H:%M:%S.%s",
"range_fmt": "%H:%M:%S.%s",
"ticks_fmt": "%H:%M:%S.%s",
"ticks_size": 12,
"ticks_color": (93, 91, 89),
"ticks_offset": 5,
"y_ticks_width": 30,
"font_name": "Times",
"font_size": 12,
"font_size_title": 16,
"font_color": (0, 0, 0),
"nb_table_annot": 5,
"bg_color": (244, 244, 244),
"bg_color_plot": (255, 255, 255),
"height_widget_signal": 150
}
#: (*dict*) General configuration
#:
#: The keys and values are:
#:
#: - ``"flag_synchro"``: (*bool*) specify if the signals are
#: synchronized with video
#: - ``"flag_pause_status"``: (*bool*) specify if the video is
#: paused at launching
#: - ``"layout_mode"``: (*int*) either 0, 1 or 2
#: - ``"zoom_factor"``: (*int*)
#: - ``"max_points"``: (*int*)
#: - ``"nb_ticks"``: (*int*)
#: - ``"trunc_duration"``: (*list*) length 2 *(minute, second)*
#: - ``"time_zone"``: (*str*) time zone (complying with pytz
#: package)
#: - ``"flag_annot_overlap"``: (*bool*) specify if overlap of events
#: annotations is enabled
#: - ``"annot_dir"``: (*str*) directory of the annotations
#: - ``"from_cursor_list"``: (*list*) each element is a list of
#: length 2 *(minute, second)*
#: - ``"current_fmt"``: (*str*) datetime string format of the current
#: temporal position in progress bar, see keyword argument ``fmt`` of
#: :func:`.convert_datetime_to_string`
#: - ``"range_fmt"``: (*str*) datetime string format of the temporal
#: range duration in progress bar, see keyword argument ``fmt`` of
#: :func:`.convert_datetime_to_string`
#: - ``"ticks_fmt"``: (*str*) datetime string format of X axis ticks
#: text, see keyword argument ``fmt`` of
#: :func:`.convert_datetime_to_string`
#: - ``"ticks_size"``: (*int*) size of the ticks text in signal
#: plots
#: - ``"ticks_color"``: (*list*) RGB color of ticks
#: - ``"ticks_offset"``: (*int*) space in pixels between ticks and
#: associated values
#: - ``"y_ticks_width"``: (*int*) horizontal space in pixels for the
#: of Y axis ticks in signal widgets
#: - ``"font_name"``: (*str*) font of the text in ViSiAnnoT
#: - ``"font_size"``: (*int*) font size in ViSiAnnoT
#: - ``"font_size_title"``: (*int*) font size in ViSiAnnoT (title
#: of video widget and progress bar widget)
#: - ``"font_color"``: (*list*) RGB color of font in ViSiAnnoT
#: - ``"nb_table_annot"``: (*int*) maximum number of labels in one
#: row in annotation widgets
#: - ``"bg_color"``: (*list*) RGB color of background in ViSiAnnoT
#: - ``"bg_color_plot"``: (*list*) RGB color of background color of
#: signal plots
self.general_dict = None
# load configuration
self.load(path)
# infinite loop
pyqt_overlayer.infinite_loop_gui(self.app)
# *********************************************************************** #
# ******************** ConfigurationWindow methods ********************** #
# *********************************************************************** #
# *********************************************************************** #
# Group: Methods for widget creation
# *********************************************************************** #
[docs] def create_child_configuration_window(
self, parent_config, *args, win_size=(0, 0), **kwargs
):
"""
Creates a window for a child configuration
This method can be used when a configuration type is linked to another
one so that the keys in Configuration.dict must be the same.
For example, this applies to the signal configuration. We can specify
some threshold values to plot on the signal widgets. The thresholds
must be defined for an existing signal, so the keys in the
configuration dictionary are the same as in the signal configuration.
As a consequence, when a configuration grid is added in the child
configuration window, it has automatically the same key as in the
signal configuration and it cannot be changed. If the whole
configuration grid for a given key is deleted in the signal
configuration, then it is also deleted in the threshold configuration.
This method sets the following attributes:
- :attr:`.additional_win_list`.
- :attr:`.additional_lay_list`.
- :attr:`.meta_dict`.
:param parent_config: parent configuration
:type parent_config: :class:`.Configuration`
:param args: positional arguments of :class:`.Configuration`
constructor, starting from the third position
:param win_size: window size :math:`(width, height)`, set one value to
0 to maximize the window
:type win_size: tuple
:param kwargs: keyword arguments of :class:`.Configuration` constructor
(excepted ``parent_config`` which is the first positional argument
of this method)
"""
# create configuration window
win, lay = pyqt_overlayer.create_window(
size=win_size, title="%s configuration" % args[0], flag_show=False
)
# create scroll area
scroll_lay, scroll_area = pyqt_overlayer.add_scroll_area(lay, (0, 0))
# set size
scroll_area.setMinimumSize(win_size[0], win_size[1])
# create configuration
config = Configuration(
lay, (0, 0), *args, **kwargs, parent_config=parent_config
)
# add configuration group box to scroll area
scroll_lay.addWidget(config.group_box)
# add child to parent configuration
parent_config.add_child_configuration(config)
# create push button for hiding configuration window
push_button_ok = pyqt_overlayer.add_push_button(
lay, (1, 0), "Ok", width=80
)
# listen to callback
push_button_ok.clicked.connect(win.hide)
# update attributes
self.children_win_list.append(win)
self.meta_dict[args[0]] = config
# *********************************************************************** #
# End group
# *********************************************************************** #
# *********************************************************************** #
# Group: Methods for the management of callbacks
# *********************************************************************** #
[docs] def show_child_window(self, button_id):
"""
Callback method for opening child configuration window related to
signal configuration
Connected to the signal ``buttonClicked[int]`` of
``ConfigurationWindow.meta_dict["Signal"].button_group_children``.
The group button
``ConfigurationWindow.meta_dict["Signal"].button_group_children``
has as much buttons as children configurations (see
:attr:`.Configuration.children_configuration_list` of
``ConfigurationWindow.meta_dict["Signal"]``).
:param button_id: index of the button that has been pushed
:type button_id: int
"""
self.children_win_list[button_id].show()
[docs] def set_directory_annotations(self):
"""
Callback method for annotations directory selection
Connected to the signal ``clicked`` of
:attr:`.general_push_button`.
"""
# get text edit
pos = self.general_config_position_dict["annot_dir"][0]
edit_dir = self.general_lay.itemAtPosition(pos[0], pos[1]).widget()
# open dialog window
directory = QtWidgets.QFileDialog.getExistingDirectory(
None, "Select video directory", edit_dir.text(),
QtWidgets.QFileDialog.ShowDirsOnly
)
if directory != "":
# set text edit
edit_dir.setText(directory)
[docs] def call_lsd(self, button_id):
"""
Callback method for Load/Save/Done push buttons
Connected to the signal ``buttonClicked[int]`` of the button group
:attr:`.button_group_lsd`
The group button ConfigurationWindow.button_group_lsd has 3 buttons:
- button_id == 0 => load configuration file
- button_id == 1 => save configuration file
- button_id == 2 => set configuration dictionaries and close main
window
:param button_id: index of the button that has been pushed
:type button_id: int
"""
# load
if button_id == 0:
# open a dialog window in order to get the configuration file name
win = QtWidgets.QWidget()
path = QtWidgets.QFileDialog.getOpenFileName(
win, "Load configuration file", os.getcwd(), "*.ini"
)[0]
# load configuration file
if path != "":
self.load(path)
# save
elif button_id == 1:
# open dialog window in order to get where to save the
# configuration file
path = QtWidgets.QFileDialog.getSaveFileName(
None, "Save configuration file", os.getcwd(), "*.ini"
)[0]
# write configuration file
if path != '':
self.write_configobj_file(path)
print("Configuration file written")
# done
elif button_id == 2:
# set configuration dictionaries
self.set_configuration_dictionaries()
# close window(s)
self.win.close()
for win in self.children_win_list:
win.close()
print("Done with configuration")
# *********************************************************************** #
# End group
# *********************************************************************** #
# *********************************************************************** #
# Group: Methods for loading/saving configuration
# *********************************************************************** #
[docs] @staticmethod
def load_config_file(
path,
key_dict={
"General": 0,
"Video": 1,
"Signal": 2,
"Interval": 2,
"Threshold": 2,
"YRange": 1,
"AnnotEvent": 1,
"AnnotImage": 0
}
):
"""
Loads a configuration file
:param path: path to the configuration file to load
:type path: str
:param key_dict: each item corresponds to a configuration type to
retrieve, key is the configuration key, value is the number of
nesting levels to apply for converting a nested dictionary (that
may be retrieved by **ConfigObj**) to a nested list (equivalent to
:attr:`.nb_level` of corresponding instance of
:class:`.Configuration`)
:type key_dict: dict
:returns: configuration dictionary, with the same keys as ``key_dict``
:rtype: dict
"""
# load file
config = ConfigObj(path, unrepr=True)
# initialize output dictionary
config_dict = {}
# loop on configuration keys
for key, convert_level in key_dict.items():
if key in config.keys():
if convert_level == 0:
config_dict[key] = config[key]
else:
config_dict[key] = {}
for key_sub, dict_tmp in config[key].items():
config_dict[key][key_sub] = \
ConfigurationWindow.convert_dict_to_list_nested(
dict_tmp, level=convert_level
)
return config_dict
[docs] def load(self, path):
"""
Loads a configuration file and store it directly in the configuration
dictionaries
It calls the static method :meth:`.load_config_file`
and then sets the following attributes:
- :attr:`.general_dict`
- ``ConfigurationWindow.meta_dict[config_type].dict`` for config_type
in :attr:`.config_type_list`
:param path: path to the configuration file to load
:type path: str
"""
# check type
if isinstance(path, str):
# check if file exists
if os.path.isfile(path):
# load configuration file
config_dict = ConfigurationWindow.load_config_file(path)
print("Configuration file loaded")
else:
# default configuration
config_dict = {}
print("Configuration file not found, default configuration")
else:
# configuration dictionary passed as argument instead of a path
config_dict = path
# get general configuration
if "General" in config_dict:
self.general_dict = config_dict["General"]
# check if missing keys in general configuration
for key, default_value in self.general_dict_default.items():
if key not in self.general_dict.keys():
self.general_dict[key] = default_value
else:
# get default general configuration
self.general_dict = self.general_dict_default.copy()
# update meta_dict attribute
for config_type in self.config_type_list:
if config_type in config_dict.keys():
self.meta_dict[config_type].dict = config_dict[config_type]
else:
self.meta_dict[config_type].dict = {}
# display configuration
self.reset_display()
[docs] def reset_display(self):
"""
Deletes the whole configuration display and replaces it with the
current values of the attribute dictionaries
"""
# reset general
self.set_general_configuration(True)
# reset configurations
for config_type in self.config_type_list:
self.meta_dict[config_type].reset_display()
[docs] def write_configobj_file(self, path):
"""
Creates a .ini configuration file with the current configuration
Before writing the file, the method calls
:meth:`.set_configuration_dictionaries`.
:param path: path to the configuration file to create
:type path: str
"""
# set configuration dictionaries
self.set_configuration_dictionaries()
# remove configuration file if necessary
if os.path.isfile(path):
os.remove(path)
# create config object
config = ConfigObj(path, unrepr=True)
# convert general dictionary to config object
config["General"] = self.general_dict
# convert configuration dictionaries to config object
for config_type in self.config_type_list:
config[config_type] = self.meta_dict[config_type].dict
# write configuration file
config.write()
[docs] def set_configuration_dictionaries(self):
"""
Sets the configuration dictionaries with the values filled in the GUI
The configuration dictionaries are:
- :attr:`.general_dict`
- ``ConfigurationWindow.meta_dict[config_type].dict`` for config_type
in :attr:`.config_type_list`
"""
self.set_general_configuration(False)
for config_type in self.config_type_list:
self.meta_dict[config_type].set_dictionary()
[docs] def set_general_configuration(self, flag_display):
"""
Displays the values in the general configuration widget from
:attr:`.general_dict` or sets the values
of :attr:`.general_dict` from the values filled in
the general configuration widget (i.e.
:attr:`.general_lay`)
:param flag_display: specify if displaying values in the general
configuration widget, if ``False`` then it sets the values of
:attr:`.general_dict`
:type flag_display: bool
"""
# loop on elements
for key, pos_list in self.general_config_position_dict.items():
# check mode
if not flag_display:
# reset key in configuration dictionary
self.general_dict[key] = []
# loop on columns
for ite_col, pos in enumerate(pos_list):
# get widget
widget = self.general_lay.itemAtPosition(
pos[0], pos[1]
).widget()
# check element type
if isinstance(widget, QtWidgets.QCheckBox):
# get method name
method_0 = "setChecked"
method_1 = "isChecked"
elif isinstance(widget, QtWidgets.QSpinBox) or \
isinstance(widget, QtWidgets.QDoubleSpinBox):
# get method name
method_0 = "setValue"
method_1 = "value"
elif isinstance(widget, QtWidgets.QLineEdit):
# get method name
method_0 = "setText"
method_1 = "text"
else:
method_0, method_1 = None, None
# check method name
if method_0 is not None:
# check mode
if flag_display:
# check number of columns
if len(pos_list) > 1:
# set widget value
getattr(widget, method_0)(
self.general_dict[key][ite_col]
)
else:
# set widget value
getattr(widget, method_0)(self.general_dict[key])
else:
# update configuration dictionary
self.general_dict[key].append(
getattr(widget, method_1)()
)
# check mode
if method_0 is not None and not flag_display:
# check if single column
if len(pos_list) == 1:
# update configuration dictionary
self.general_dict[key] = self.general_dict[key][0]
# deal with list of list of spin boxes (trunc duration)
# get grid
key = "from_cursor_list"
pos = self.general_config_position_dict[key][0]
grid = self.general_lay.itemAtPosition(
pos[0], pos[1]
).widget().layout()
# get value list
value_list = self.general_dict[key]
# list of list of spin boxes
value_list = pyqt_overlayer.set_spin_box_table(
grid, value_list, flag_display
)
if not flag_display:
# delete empty durations (full of zeros)
value_array = np.array(value_list)
value_array = np.delete(
value_array,
np.where(value_array.sum(axis=1) == 0),
axis=0
)
value_list = []
for value in value_array:
value_list.append(list(value))
# update configuration dictionary
self.general_dict[key] = value_list
# *********************************************************************** #
# End group
# *********************************************************************** #
# *********************************************************************** #
# Group: Static methods for converting dictionary to list
# *********************************************************************** #
[docs] @staticmethod
def convert_dict_to_list(dic):
"""
Puts the values of a dictionary in a list
:param dic: dictionary
:type dic: dict
If dic is not a dictionary, then it returns ``dic``.
:returns: list
:rtype: list
"""
if isinstance(dic, dict):
return list(dic.values())
else:
return dic
[docs] @staticmethod
def convert_dict_to_list_nested(dic, level=1):
"""
Puts the values of a nested dictionary in a nested list
:param dic: dictionary
:type dic: dict
:returns: list of lists
:rtype: list
"""
dic_list = ConfigurationWindow.convert_dict_to_list(dic)
if level == 1 or not isinstance(dic_list, list):
return dic_list
else:
output = []
for dic_tmp in dic_list:
output.append(
ConfigurationWindow.convert_dict_to_list_nested(
dic_tmp, level=level - 1
)
)
return output
# *********************************************************************** #
# End group
# *********************************************************************** #