Skip to content

Commit

Permalink
Add gui for editing custom variables added in hab==0.35.0
Browse files Browse the repository at this point in the history
  • Loading branch information
MHendricks committed Feb 19, 2024
1 parent 731f31e commit 6ec8c99
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 1 deletion.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ to take hab out of the shell.
- [habw](#habwexe) command allows using hab without popup consoles on windows.
- Customization of hab-gui using [entry_points](#hab-gui-entry-points) defined
in hab site json files.
- Gui for [editing](hab_gui/widgets/custom_variable_editor/custom_variable_editor.py)
hab [custom variables](https://github.com/blurstudio/hab?tab=readme-ov-file#custom-variables).
It can be added to the [menu](hab_gui/actions/edit_custom_variables_action.py) using entry points.

# Quickstart

Expand Down
41 changes: 41 additions & 0 deletions hab_gui/actions/edit_custom_variables_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from Qt import QtWidgets

from .. import utils
from ..widgets.custom_variable_editor import CustomVariableEditor


class EditCustomVariablesAction(QtWidgets.QAction):
"""A QAction that allows the user to edit custom variables.
Shows a dialog showing any config/distro json files that have editing enabled
by setting the top level dict variable `variable_editor` to `True`. Users can
then add or remove variables, and edit their keys and values.
Args:
resolver (hab.Resolver): The resolver used for settings.
hab_widget (QWidget): The URI widget menu operations are performed on.
verbosity (int, optional): The current verbosity setting.
parent (Qt.QtWidgets.QWidget, optional): Define a parent for this widget.
"""

def __init__(self, resolver, hab_widget, verbosity=0, parent=None):
super().__init__(
utils.Paths.icon("pencil-box-outline.svg"),
"Edit Custom Variables",
parent,
)
self.hab_widget = hab_widget
self.resolver = resolver
self.verbosity = verbosity
self.setObjectName("edit_custom_variables")

self.triggered.connect(self.edit_custom_variables)

def edit_custom_variables(self):
dlg = CustomVariableEditor.create_dialog(
self.resolver, verbosity=self.verbosity, parent=self.parent()
)
dlg.exec_()

# Ensure the hab_gui respects any changes the user may have made
self.hab_widget.refresh_cache()
6 changes: 5 additions & 1 deletion hab_gui/resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ Please make sure to update the sources table when adding or updating images.

| File | Source | Notes | Author |
|---|---|---|---|
| ![](hab_gui/resources/content-save.svg) [content-save.svg](hab_gui/resources/content-save.svg) | https://pictogrammers.com/library/mdi/icon/content-save/ | | Google |
| ![](hab_gui/resources/habihat-white.svg) [habihat-white.svg](hab_gui/resources/habihat-white.svg) | | | Blur Studio |
| ![](hab_gui/resources/habihat.svg) [habihat.svg](hab_gui/resources/habihat.svg) | | | Blur Studio |
| ![](hab_gui/resources/menu.svg) [menu.svg](hab_gui/resources/menu.svg) | https://pictogrammers.com/library/mdi/icon/menu/ | | Google |
| ![](hab_gui/resources/pin-off-outline.svg) [pin-off-outline.svg](hab_gui/resources/pin-off-outline.svg) | https://pictogrammers.com/library/mdi/icon/pin-off-outline/ | | Google |
| ![](hab_gui/resources/minus-thick.svg) [minus-thick.svg](hab_gui/resources/minus-thick.svg) | https://pictogrammers.com/library/mdi/icon/minus-thick/ | | [Colton Wiscombe](https://pictogrammers.com/library/mdi/icon/minus-thick/) |
| ![](hab_gui/resources/pencil-box-outline.svg) [pencil-box-outline.svg](hab_gui/resources/pencil-box-outline.svg) | https://pictogrammers.com/library/mdi/icon/pencil-box-outline/ | | [Austin Andrews](https://pictogrammers.com/contributor/Templarian/) |
| ![](hab_gui/resources/pin-off-outline.svg) [pin-off-outline.svg](hab_gui/resources/pin-off-outline.svg) | https://pictogrammers.com/library/mdi/icon/pin-off-outline/ | | [At Abbey's side](https://pictogrammers.com/library/mdi/icon/pin-off-outline/) |
| ![](hab_gui/resources/pin-outline.svg) [pin-outline.svg](hab_gui/resources/pin-outline.svg) | https://pictogrammers.com/library/mdi/icon/pin-outline/ | | Google |
| ![](hab_gui/resources/plus-thick.svg) [plus-thick.svg](hab_gui/resources/plus-thick.svg) | https://pictogrammers.com/library/mdi/icon/plus-thick/ | | [Austin Andrews](https://pictogrammers.com/contributor/Templarian/) |
| ![](hab_gui/resources/refresh.svg) [refresh.svg](hab_gui/resources/refresh.svg) | https://pictogrammers.com/library/mdi/icon/refresh/ | | Google |
1 change: 1 addition & 0 deletions hab_gui/resources/content-save.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions hab_gui/resources/minus-thick.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions hab_gui/resources/pencil-box-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions hab_gui/resources/plus-thick.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions hab_gui/widgets/custom_variable_editor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .custom_variable_editor import CustomVariableEditor # noqa: F401
166 changes: 166 additions & 0 deletions hab_gui/widgets/custom_variable_editor/custom_variable_editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import logging

from Qt import QtCore, QtWidgets

from ... import utils
from .file_tree_widget_item import FileTreeWidgetItem
from .variable_tree_widget_item import VariableTreeWidgetItem

logger = logging.getLogger(__name__)


class CustomVariableEditor(QtWidgets.QWidget):
"""A widget that can view and edit custom variables in hab configs/distros.
This widget will only show config/distro files that have `variable_editor`
set to `True` in the top level dict.
Args:
resolver (hab.Resolver): The resolver to change verbosity settings on.
verbosity (int): Change the verbosity setting to this value. If None is passed,
all results are be shown without any filtering.
parent (Qt.QtWidgets.QWidget, optional): Define a parent for this widget.
"""

def __init__(self, resolver, verbosity=0, parent=None):
super().__init__(parent)
self._refresh_on_show = True
self.resolver = resolver
self.verbosity = verbosity
utils.load_ui(__file__, self)
self.setWindowIcon(utils.Paths.icon("habihat.svg"))

self.uiAddVariableBTN.setIcon(utils.Paths.icon("plus-thick.svg"))
self.uiEditCurrentItemBTN.setIcon(utils.Paths.icon("pencil-box-outline.svg"))
self.uiResetBTN.setIcon(utils.Paths.icon("refresh.svg"))
self.uiRemoveVariableBTN.setIcon(utils.Paths.icon("minus-thick.svg"))
self.uiSaveBTN.setIcon(utils.Paths.icon("content-save.svg"))

# Configure editing of widget items. _is_refreshing is used to only
# prevent updating the model while refreshing, not other signals.
self._is_refreshing = False
self.uiVariableTREE.model().dataChanged.connect(self.editing_finished)

def add_variable(self):
"""Add a new variable to the selected FileTreeWidgetItem"""
item = self.uiVariableTREE.currentItem()
if "Undefined" in item.parser.variables:
QtWidgets.QMessageBox.information(
self,
"Variable already defined",
"You already have a variable named Undefined. Change the name "
"of that variable before adding a new one.",
)
else:
item.parser.variables["Undefined"] = "Undefined"
VariableTreeWidgetItem(item, "Undefined")
# Mark the item as dirty
item.dirty = True

@property
def dirty(self):
"""Generator that yields any FileTreeWidgetItem's that are modified."""
for index in range(self.uiVariableTREE.topLevelItemCount()):
child = self.uiVariableTREE.topLevelItem(index)
if child.dirty:
yield child

def edit_cell(self):
"""Edit the currently selected cell"""
index = self.uiVariableTREE.currentIndex()
self.uiVariableTREE.edit(index)

def editing_finished(self, top_left, bottom_right, roles):
if self._is_refreshing:
return

if QtCore.Qt.EditRole in roles:
item = self.uiVariableTREE.itemFromIndex(top_left)
column = top_left.column()
if column == 0:
item.variable_name = item.text(column)
elif column == 1:
item.value = item.text(column)

def current_changed(self, current=None, previous=None):
"""Enable buttons based on the current selection."""
item = self.uiVariableTREE.currentItem()
is_file = isinstance(item, FileTreeWidgetItem)
self.uiAddVariableBTN.setEnabled(is_file)
self.uiEditCurrentItemBTN.setEnabled(not is_file)
self.uiRemoveVariableBTN.setEnabled(not is_file)

@utils.cursor_override()
def refresh(self):
self._is_refreshing = True
try:
self.uiVariableTREE.clear()

for forest in (self.resolver.configs, self.resolver.distros):
for row in self.resolver.dump_forest(forest, attr=None):
parser = row.node
if parser.filename:
if parser.load(parser.filename).get("variable_editor", False):
FileTreeWidgetItem(self.uiVariableTREE, parser)

self.uiVariableTREE.expandAll()
self.uiVariableTREE.resizeColumnToContents(0)

self.current_changed()
finally:
self._is_refreshing = False

@property
def refresh_on_show(self):
"""Should this automatically refresh when the widget is shown."""
return self._refresh_on_show

@refresh_on_show.setter
def refresh_on_show(self, state):
self._refresh_on_show = state

def remove_variable(self):
"""Remove the currently selected variable"""
item = self.uiVariableTREE.currentItem()
item.remove_variable()
parent = item.parent()
idx = parent.indexOfChild(item)
parent.takeChild(idx)
parent.dirty = True

def reset(self):
"""Revert any un-saved changes."""
self.resolver.clear_caches()
self.refresh()

def save(self):
"""Save all changes to disk"""
for parser in self.dirty:
parser.save()

# Re-display the saved data
self.reset()

def showEvent(self, event): # noqa: N802
super().showEvent(event)
if self.refresh_on_show:
self.refresh()

@classmethod
def create_dialog(cls, resolver, verbosity=0, title="Edit Variables", parent=None):
"""Create a simple standalone QDialog containing this widget.
Args:
resolver (hab.Resolver): The resolver to change verbosity settings on.
verbosity (int): Change the verbosity setting to this value. If None is passed,
all results are be shown without any filtering.
title (str, optional): The window title of the created dialog.
parent (Qt.QtWidgets.QWidget, optional): Define a parent for this widget.
"""
dlg = QtWidgets.QDialog(parent=parent)
dlg.setWindowTitle(title)
dlg.setWindowIcon(utils.Paths.icon("pencil-box-outline.svg"))
layout = QtWidgets.QVBoxLayout(dlg)
dlg.uiVariableWGT = cls(resolver, verbosity=verbosity, parent=dlg)
layout.addWidget(dlg.uiVariableWGT)
return dlg
86 changes: 86 additions & 0 deletions hab_gui/widgets/custom_variable_editor/file_tree_widget_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json

import hab.utils
from Qt import QtCore, QtWidgets

from .variable_tree_widget_item import VariableTreeWidgetItem


class FileTreeWidgetItem(QtWidgets.QTreeWidgetItem):
"""A QTreeWidgetItem used to show a given config/distro and its custom variables."""

def __init__(self, parent, parser):
super().__init__(parent)
self.parser = parser
# Add a tracking variable to tell if the parser is dirty
if not hasattr(self.parser, "dirty"):
self.parser.dirty = False

# Add a child item that shows the filename. It should not be editable.
self.filename_item = QtWidgets.QTreeWidgetItem(self)
self.filename_item.setFlags(QtCore.Qt.NoItemFlags)

self.refresh()

@property
def dirty(self):
return self.parser.dirty

@dirty.setter
def dirty(self, state):
changed = state != self.parser.dirty
self.parser.dirty = state
if changed:
self.setText(0, self.name)

@property
def name(self):
name = self.parser.name
if self.dirty:
return f"{name}*"
return name

def refresh(self):
self.setText(0, self.name)
self.filename_item.setText(0, "Filename")
self.filename_item.setText(1, str(self.parser.filename))

for index, variable_name in enumerate(self.parser.variables):
# Get the existing variable item if possible. Index 0 is the
# filename item.
item = self.child(index + 1)
if item:
item.variable_name = variable_name
item.refresh()
else:
# Otherwise add a new item
VariableTreeWidgetItem(self, variable_name)

# If any variables were removed, remove their tree widget items
variable_count = len(self.parser.variables) + 1
for _ in range(variable_count, self.childCount() + 1):
self.removeChild(self.child(variable_count))

def save(self):
"""Save the variable changes to disk.
NOTE: This saves the data as regular json data not json5. Any comments,
etc will be cleared by calling this method.
Returns:
bool: Returns if this was dirty and updated data was saved to disk.
"""
if not self.dirty:
return False

# Reload data from disk
raw_data = hab.utils.load_json_file(self.parser.filename)

# Update the variables section with the changes.
raw_data["variables"] = self.parser.variables

# Save changes over top of the existing file.
with self.parser.filename.open("w") as fle:
json.dump(raw_data, fle, indent=4, cls=hab.utils.HabJsonEncoder)

return True
Loading

0 comments on commit 6ec8c99

Please sign in to comment.