"""Logic for the application settings UI"""
import logging
import json
from qtpy.QtWidgets import QDialog
from qtpy.QtCore import Slot, QAbstractItemModel, QModelIndex, Qt
from friendlypics2.misc.gui_helpers import load_ui
# TODO: fix pylint warnings with this class - it's only used internally by the model below
[docs]class SettingsItem:
"""Interface to a single application setting or group of application settings
Loosely based on example code found
`here <https://github.com/pyside/Examples/blob/master/examples/itemviews/simpletreemodel/simpletreemodel.py>`__
"""
def __init__(self, data, parent=None):
"""
Args:
data (tuple):
fields associated with this setting, providing data for each column in the view
parent (SettingsItem):
parent setting that owns this one. Set to None to indicate this item is a root
setting with no parent
"""
self._log = logging.getLogger(__name__)
self._parent_item = parent
self._item_data = data
self._child_items = list()
def __str__(self):
return json.dumps(self._item_data, indent=4)
[docs] def set_data(self, value):
"""Modifies the data value for this setting
Args:
value (str):
new value for this setting
"""
self._item_data = (self._item_data[0], value)
@property
def child_items(self):
"""list (SettingsItem): list of children owned by this item"""
return self._child_items
[docs] def appendChild(self, item): # pylint: disable=invalid-name
"""Adds a new child element to this setting in the tree
Args:
item (SettingsItem):
child item related to this one
"""
self._child_items.append(item)
[docs] def child(self, row):
"""Gets a specific child item from this parent
Args:
row (int):
index of the child item to retrieve
Returns:
SettingsItem: child item with the given index
"""
return self._child_items[row]
[docs] def childCount(self): # pylint: disable=invalid-name
"""int: number of children contained within this item"""
return len(self._child_items)
[docs] def columnCount(self): # pylint: disable=invalid-name
"""int: number of columns or fields associated with this item"""
return len(self._item_data)
[docs] def data(self, column):
"""str: data associated with this item for the given field / column.
May be None if no data for the specified column exists."""
if column > len(self._item_data):
return None
return self._item_data[column]
[docs] def parent(self):
"""SettingItem: parent object that owns this item, or None if this is a root item"""
return self._parent_item
[docs] def row(self):
"""int: gets the relative offset of this item with respect to its siblings"""
if self._parent_item:
return self._parent_item.child_items.index(self)
return 0
[docs]class AppSettingsModel(QAbstractItemModel):
"""Interface for rendering application settings stored in a hierarchical format
Loosely based on example code found
`here <https://github.com/pyside/Examples/blob/master/examples/itemviews/simpletreemodel/simpletreemodel.py>`__
"""
def __init__(self, data):
"""
Args:
data (AppSettings):
reference to the application settings to be managed by this dialog
"""
super().__init__(None)
self._log = logging.getLogger(__name__)
self._settings = data
self._root_item = SettingsItem(("Setting", "Value"))
self._setup_model_data(self._settings.data, self._root_item)
@property
def root_item(self):
"""SettingsItem: gets the root node of our settings tree"""
return self._root_item
def _setup_model_data(self, data, parent):
"""Helper method used to populate our model data
Args:
data (dict):
dictionary of settings to be managed by our model
nodes in the dictionary that contain nested dictionaries are assumed to be settings groups,
and all other nodes are assumed to be simple settings / data values
parent (SettingsItem):
reference to the parent item that will own all child items defined by the given data block
"""
for cur_key, cur_val in data.items():
if isinstance(cur_val, dict):
temp = SettingsItem((cur_key, ""), parent)
else:
temp = SettingsItem((cur_key, cur_val), parent)
parent.appendChild(temp)
if isinstance(cur_val, dict):
self._setup_model_data(cur_val, temp)
[docs] def columnCount(self, parent): # pylint: disable=invalid-name
"""retrieves the number of fields associated with a given item
Args:
parent (QModelIndex):
index of the item to retrieve information for
contains a reference to the SettingsItem it manages
Returns:
int: number of fields associated with the given item
"""
if parent.isValid():
return parent.internalPointer().columnCount()
return self._root_item.columnCount()
[docs] def data(self, index, role): # pylint: disable=no-self-use
"""retrieves data for a specific setting given a specific role
Args:
index (QModelIndex):
index for the setting to query
role:
`ItemDataRole <https://doc.qt.io/qt-5/qt.html#ItemDataRole-enum>`__
of the role within the view where the data will be used
Returns:
str:
data for the specified setting within the given context, or None if no
data suitable for the given role can be found
"""
if not index.isValid() or role != Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())
[docs] def setData(self, index, value, role): # pylint: disable=invalid-name
"""Modifies the value of a settings
Loosely based on example code found
`here <https://doc.qt.io/qtforpython/overviews/model-view-programming.html#an-editable-model>`__
Args:
index (QModelIndex):
index of the app setting to modify
value (str):
new data value for the setting
role:
`ItemDataRole <https://doc.qt.io/qt-5/qt.html#ItemDataRole-enum>`__
of the role within the view where the data is being edited
Returns:
bool: True if the data was successfully modified, False if not
"""
if not index.isValid() or role != Qt.EditRole:
return False
item = index.internalPointer()
item.set_data(value)
# resync the data in the view
self.dataChanged.emit(index, index)
return True
[docs] def flags(self, index): # pylint: disable=no-self-use
"""gets bit-field describing how a given setting should be rendered
Args:
index (QModelIndex):
index of the setting to be analysed
Returns:
int: bit-field encoded set of flags describing how the given item should be displayed
"""
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
[docs] def index(self, row, column, parent):
"""Gets the index for a given element in the data managed by the model
Args:
row (int):
row of the data set to query
within our context, this is the ID of the setting to query
column (int):
column of the data set to query
within our context, this is the ID of the field of a specific setting to query
parent (QModelIndex):
index of the node / setting containing the children to query
Returns:
QModelIndex: reference to the setting associated with the given context
"""
if not self.hasIndex(row, column, parent):
return QModelIndex()
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
return QModelIndex()
[docs] def parent(self, index):
"""Gets the index of the parent item for a given setting
Args:
index (QModelIndex):
index of the setting to query
Returns:
QModelIndex:
reference to the setting owning the given item, or an empty index if the
given item has no parent
"""
if not index.isValid():
return QModelIndex()
child_item = index.internalPointer()
parent_item = child_item.parent()
if parent_item == self._root_item:
return QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
[docs] def rowCount(self, parent): # pylint: disable=invalid-name
"""Number of direct children owned by a given index
Args:
parent (QModelIndex):
reference to the setting to query
Returns:
int: number of children owned by this parent
"""
if parent.column() > 0:
self._log.debug(f"Returning default for {parent.internalPointer()}")
return 0
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
return parent_item.childCount()
[docs]class SettingsDialog(QDialog):
"""Logic for managing application settings dialog"""
def __init__(self, parent, settings):
"""
Args:
parent (QWidget):
Parent widget / dialog that owns the settings dialog
settings (AppSettings):
reference to the application settings to be managed by this dialog
"""
super().__init__(parent)
self._settings = settings
self._log = logging.getLogger(__name__)
self._load_ui()
def _load_ui(self):
"""Internal helper method that configures the UI for the dialog"""
load_ui("settings_dlg.ui", self)
self.cancel_button.clicked.connect(self.close)
self.save_button.clicked.connect(self._save_clicked)
model = AppSettingsModel(self._settings)
self.settings_view.setModel(model)
# Center the about box on the parent window
parent_geom = self.parent().geometry()
self.move(parent_geom.center() - self.rect().center())
def _load_data(self, item):
"""helper method that dumps the raw data from the settings data model to a dictionary
Args:
item(SettingsItem):
reference to the item to retrieve settings data from. Data will be generated
recursively for all children owned by the settings item.
Returns:
dict: updated settings data loaded from the data model
"""
retval = dict()
for cur_child in item.child_items:
if cur_child.child_items:
temp_data = self._load_data(cur_child)
else:
temp_data = cur_child.data(1)
retval[cur_child.data(0)] = temp_data
return retval
@Slot()
def _save_clicked(self):
"""Callback for when the user clicks the save button"""
new_data = self._load_data(self.settings_view.model().root_item)
self._settings.data = new_data
self._settings.save()
self.close()