Skip to content

Commit

Permalink
Add UI to enable optional distros when launching
Browse files Browse the repository at this point in the history
This is a gui interface allowing configs to define common optional distros
and allowing a user to enable those distros. This interfaces with the
`--requirement` hab cli argument.
  • Loading branch information
MHendricks committed Jul 3, 2024
1 parent fc39f82 commit 1d3d2d6
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 5 deletions.
3 changes: 2 additions & 1 deletion hab_gui/resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ Please make sure to update the sources table when adding or updating images.

| File | Source | Notes | Author |
|---|---|---|---|
| ![](hab_gui/resources/arrow-left-top-bold.svg) [arrow-left-top-bold.svg](hab_gui/resources/arrow-left-top-bold.svg) | https://pictogrammers.com/library/mdi/icon/arrow-left-top-bold/ | | [Colton Wiscombe](https://pictogrammers.com/contributor/Xenomorph99/) |
| ![](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/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/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/contributor/Xenomorph99/) |
| ![](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 |
Expand Down
1 change: 1 addition & 0 deletions hab_gui/resources/arrow-left-top-bold.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions hab_gui/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ def verbosity(self, value):
user_prefs.save()
logger.debug(f"User prefs verbosity saved to {user_prefs.filename}")

def user_pref(self, key, default=None):
"""Returns the value for a specific user_prefs setting or default."""
user_prefs = self.resolver.user_prefs()
if user_prefs.enabled:
user_prefs.load()
return user_prefs.get(key, default)
return default

def set_user_pref(self, key, value):
"""Update a specific user_pref and save prefs to disk."""
user_prefs = self.resolver.user_prefs()
if user_prefs.enabled:
user_prefs.load()
user_prefs[key] = value
user_prefs.save()
return True
return False

@property
def uri(self):
return self._uri
Expand Down
30 changes: 30 additions & 0 deletions hab_gui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,33 @@ def load_ui(filename, widget, ui_name=""):

filename = filename.parent / "ui" / f"{ui_name}.ui"
QtCompat.loadUi(filename, widget)


@contextmanager
def block_signals(objs):
"""Block Qt signals while inside this with statement.
Example::
with utils.block_signals(combo):
combo.setCurrentIndex(0)
Args:
objs (list): List of Qt.QtCore.QObject's to block signals for.
Yields:
List of (bool, QWidget) tuples where the bool is the widget's previous
blocking state.
"""

# Store previous state
blocked = [(o, o.signalsBlocked()) for o in objs]

for o in objs:
o.blockSignals(True)

try:
yield list(blocked)
finally:
for o, b in blocked:
o.blockSignals(b)
4 changes: 0 additions & 4 deletions hab_gui/widgets/alias_button_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ def refresh(self):
for button_name, button_coord in button_coords.items():
button = self.button_cls(cfg, button_name)
self.grid_layout.addWidget(button, button_coord[0], button_coord[1])
self.spacer_item = QtWidgets.QSpacerItem(
20, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
)
self.grid_layout.addItem(self.spacer_item, self.grid_layout.rowCount(), 0, 1, 1)

def clear(self):
while self.grid_layout.count():
Expand Down
73 changes: 73 additions & 0 deletions hab_gui/widgets/distro_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from hab.solvers import Solver
from hab.utils import NotSet
from Qt.QtCore import QTimer

from .. import utils
from .name_picker import NamePicker


class DistroPicker(NamePicker):
"""A widget for picking from a list of optional distros.
This displays the `optional_distros` config setting. Any distros checked use
`hab.Resolver.forced_requirements` to load those distros. See the cli argument
`--requirement` for more info.
"""

pref_name = "distro_picker"

def __init__(self, settings, title="Options", label="Distro", parent=None):
super().__init__(settings, title=title, label=label, parent=parent)
# This widget needs to update the hab resolver before other widgets
# are updated. This signal is used to update forced_requirements.
self.settings.uri_changing.connect(self.refresh)

def item_changed(self, item, column):
"""Called when a item is modified, saves the user prefs when a checked
state is updated and updates the displayed aliases."""
super().item_changed(item, column)
# Ensure the UI is updated with the new forced_requirements
self.update_requirements()
QTimer.singleShot(0, self.uri_changed)

def reset_to_default(self):
"""Reset the checked state of names to the default values, clearing
saved user_prefs for the current URI.
"""
super().reset_to_default()
# Refresh the alias button widget
self.update_requirements()
self.uri_changed()

def update_requirements(self):
# Ensure the UI is updated with the new forced_requirements
selected = self.selected()
forced_requirements = Solver.simplify_requirements(list(selected))

# Preserve any requirements passed via the cli.
cli_reqs = self.settings.resolver.__forced_requirements__
if cli_reqs:
# If the same distro is specified, the GUI's requirement should win.
forced_requirements = dict(cli_reqs, **forced_requirements)

self.settings.resolver.forced_requirements = forced_requirements

def uri_changed(self):
"""Work function that forces the gui to update its aliases."""

# The refresh method is called when this signal is emitted, don't
# double process it by blocking signals.
with utils.block_signals([self.name_tree]):
self.settings.uri_changed.emit(self.settings.uri)

def refresh(self, uri):
resolver = self.settings.resolver
cfg = resolver.resolve(uri)
optional = cfg.optional_distros
if optional is NotSet:
optional = {}
self.set_names(optional, uri=uri)

# Ensure the alias_buttons widget has the updated requirements before
# it refreshes from the `uri_changed` signal emited later.
self.update_requirements()
142 changes: 142 additions & 0 deletions hab_gui/widgets/name_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from hab.parsers import HabBase
from Qt import QtCore, QtWidgets

from .. import utils


class NamePicker(QtWidgets.QGroupBox):
"""A widget for selecting from a set of names with a description.
Provides a list view of names the user can check. A description can be shown
next to each name.
"""

pref_name = None
"""Defines the name of the user_pref key used to store selected names. If not
specified then saving is disabled for this class. If enabled then user_prefs
are saved when the check state of any item in this tree is updated. The saved
names are stored per modified URI, see :py:meth:`standardize_uri` for details.
"""

def __init__(self, settings, title="Options", label="Distro", parent=None):
super().__init__(parent=parent)
self.settings = settings
self.default_selection = set()
utils.load_ui(__file__, self)
self.setTitle(title)
self.name_tree.setHeaderLabel(label)
self.reset_to_default_btn.setIcon(utils.Paths.icon("arrow-left-top-bold"))
self.name_tree.itemChanged.connect(self.item_changed)
self.reset_to_default_btn.released.connect(self.reset_to_default)

def item_changed(self, item, column):
"""Called when a item is modified, saves the user prefs when a checked
state is updated."""
if column != 0:
return
self.save_user_selection()

def names(self):
"""Returns a dict of the current state of this widget.
The key is the name shown in column 1. The value is a 2 item list containing
the description shown to the user in column 2 and if name is checked by
default.
"""
ret = {}
for index in range(self.name_tree.topLevelItemCount()):
item = self.name_tree.topLevelItem(index)
checked = item.checkState(0) == QtCore.Qt.Checked
ret[item.text(0)] = [item.text(1), checked]
return ret

def set_names(self, names, uri=None):
self.name_tree.clear()
# Reset the default selection
self.default_selection = set()
with utils.block_signals([self.name_tree]):
for name, settings in names.items():
item = QtWidgets.QTreeWidgetItem(self.name_tree, [name, settings[0]])
item.setToolTip(1, settings[0])
# Build the `default_selection` set for the current URI
if len(settings) > 1 and settings[1]:
self.default_selection.add(name)
item.setCheckState(0, QtCore.Qt.Unchecked)

self.name_tree.resizeColumnToContents(0)
user_selection = self.user_selection(uri)
if user_selection is None:
self.set_selected(self.default_selection)
self.reset_to_default_btn.setDisabled(True)
else:
self.set_selected(user_selection)
self.reset_to_default_btn.setDisabled(False)

def reset_to_default(self):
"""Reset the checked state of names to the default values, clearing
saved user_prefs for the current URI.
"""
self.set_selected(self.default_selection)
self.save_user_selection(reset=True)

def selected(self):
"""Returns the checked names as a list."""
ret = set()
for index in range(self.name_tree.topLevelItemCount()):
item = self.name_tree.topLevelItem(index)
if item.checkState(0) == QtCore.Qt.Checked:
ret.add(item.text(0))
return ret

def set_selected(self, selected):
"""Update the checked state to just these names."""
with utils.block_signals([self.name_tree]):
for index in range(self.name_tree.topLevelItemCount()):
item = self.name_tree.topLevelItem(index)
name = item.text(0)
item.setCheckState(
0, QtCore.Qt.Checked if name in selected else QtCore.Qt.Unchecked
)

def sizeHint(self): # noqa: N802
return QtCore.QSize(0, 160)

def standardize_uri(self, uri):
"""Modify the URI for saving in user_prefs. This implementation discards
all but the top level item."""
return uri.split(HabBase.separator)[0]

def user_selection(self, uri):
"""Returns the names selected for the current URI saved in user_prefs
as a dict or None if user_prefs are disabled.
"""
if not self.pref_name:
return None

if uri is None:
uri = self.settings.uri

uri = self.standardize_uri(uri)
return self.settings.user_pref(self.pref_name, {}).get(uri, None)

def save_user_selection(self, reset=False):
"""Saves the currently selected names into user_prefs if enabled.
Also always updates the enabled state of the reset button.
"""
uri = self.standardize_uri(self.settings.uri)
user_selections = self.settings.user_pref(self.pref_name, {})
selected = self.selected()
is_default = selected == self.default_selection
if reset and is_default:
# Only remove the URI from user_prefs if reset was pressed so we
# can save having all optional dependencies disabled.
if uri in user_selections:
del user_selections[uri]
self.reset_to_default_btn.setDisabled(True)
else:
user_selections[uri] = list(selected)
self.reset_to_default_btn.setDisabled(False)

if self.pref_name:
self.settings.set_user_pref(self.pref_name, user_selections)
60 changes: 60 additions & 0 deletions hab_gui/widgets/ui/name_picker.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>name_picker</class>
<widget class="QGroupBox" name="name_picker">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>395</width>
<height>104</height>
</rect>
</property>
<property name="title">
<string>Options</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeWidget" name="name_tree">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Distro</string>
</property>
</column>
<column>
<property name="text">
<string>Description</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QToolButton" name="reset_to_default_btn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Reset to defaults</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
Loading

0 comments on commit 1d3d2d6

Please sign in to comment.