f73b3741f0
Apply result of "git diff anaconda-18.37.11-1..anaconda-20.25.16-1" and resolve conflicts.
474 lines
18 KiB
Python
474 lines
18 KiB
Python
# Software selection spoke classes
|
|
#
|
|
# Copyright (C) 2011-2013 Red Hat, Inc.
|
|
#
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program; if not, write to the
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
# source code or documentation are not subject to the GNU General Public
|
|
# License and may only be used or replicated with the express permission of
|
|
# Red Hat, Inc.
|
|
#
|
|
# Red Hat Author(s): Chris Lumens <clumens@redhat.com>
|
|
#
|
|
|
|
from gi.repository import Gdk
|
|
|
|
from pyanaconda.flags import flags
|
|
from pyanaconda.i18n import _, N_
|
|
from pyanaconda.packaging import MetadataError
|
|
from pyanaconda.threads import threadMgr, AnacondaThread
|
|
from pyanaconda import constants
|
|
|
|
from pyanaconda.ui.communication import hubQ
|
|
from pyanaconda.ui.gui.spokes import NormalSpoke
|
|
from pyanaconda.ui.gui.spokes.lib.detailederror import DetailedErrorDialog
|
|
from pyanaconda.ui.gui.utils import enlightbox, gtk_action_wait
|
|
from pyanaconda.ui.gui.categories.software import SoftwareCategory
|
|
|
|
import sys
|
|
|
|
__all__ = ["SoftwareSelectionSpoke"]
|
|
|
|
class SoftwareSelectionSpoke(NormalSpoke):
|
|
builderObjects = ["addonStore", "environmentStore", "softwareWindow"]
|
|
mainWidgetName = "softwareWindow"
|
|
uiFile = "spokes/software.glade"
|
|
|
|
category = SoftwareCategory
|
|
|
|
icon = "package-x-generic-symbolic"
|
|
title = N_("_SOFTWARE SELECTION")
|
|
|
|
# Add-on selection states
|
|
# no user interaction with this add-on
|
|
_ADDON_DEFAULT = 0
|
|
# user selected
|
|
_ADDON_SELECTED = 1
|
|
# user de-selected
|
|
_ADDON_DESELECTED = 2
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
NormalSpoke.__init__(self, *args, **kwargs)
|
|
self._errorMsgs = None
|
|
self._tx_id = None
|
|
self._selectFlag = False
|
|
|
|
self.selectedGroups = []
|
|
self.excludedGroups = []
|
|
self.environment = None
|
|
|
|
self._addonStore = self.builder.get_object("addonStore")
|
|
self._environmentStore = self.builder.get_object("environmentStore")
|
|
|
|
# Used to determine which add-ons to display for each environment.
|
|
# The dictionary keys are environment IDs. The dictionary values are two-tuples
|
|
# consisting of lists of add-on group IDs. The first list is the add-ons specific
|
|
# to the environment, and the second list is the other add-ons possible for the
|
|
# environment.
|
|
self._environmentAddons = {}
|
|
|
|
# Used to store how the user has interacted with add-ons for the default add-on
|
|
# selection logic. The dictionary keys are group IDs, and the values are selection
|
|
# state constants. See refreshAddons for how the values are used.
|
|
self._addonStates = {}
|
|
|
|
# Used for detecting whether anything's changed in the spoke.
|
|
self._origAddons = []
|
|
self._origEnvironment = None
|
|
|
|
# We need to tell the addon view whether something is a separator or not.
|
|
self.builder.get_object("addonView").set_row_separator_func(self._addon_row_is_separator, None)
|
|
|
|
def _apply(self):
|
|
row = self._get_selected_environment()
|
|
if not row:
|
|
return
|
|
|
|
addons = self._get_selected_addons()
|
|
for group in addons:
|
|
if group not in self.selectedGroups:
|
|
self.selectedGroups.append(group)
|
|
|
|
self._selectFlag = False
|
|
self.payload.data.packages.groupList = []
|
|
self.payload.selectEnvironment(row[2])
|
|
self.environment = row[2]
|
|
for group in self.selectedGroups:
|
|
self.payload.selectGroup(group)
|
|
|
|
# And then save these values so we can check next time.
|
|
self._origAddons = addons
|
|
self._origEnvironment = self.environment
|
|
|
|
hubQ.send_not_ready(self.__class__.__name__)
|
|
hubQ.send_not_ready("SourceSpoke")
|
|
threadMgr.add(AnacondaThread(name=constants.THREAD_CHECK_SOFTWARE,
|
|
target=self.checkSoftwareSelection))
|
|
|
|
def apply(self):
|
|
self._apply()
|
|
self.data.packages.seen = True
|
|
|
|
def checkSoftwareSelection(self):
|
|
from pyanaconda.packaging import DependencyError
|
|
hubQ.send_message(self.__class__.__name__, _("Checking software dependencies..."))
|
|
try:
|
|
self.payload.checkSoftwareSelection()
|
|
except DependencyError as e:
|
|
self._errorMsgs = "\n".join(sorted(e.message))
|
|
hubQ.send_message(self.__class__.__name__, _("Error checking software dependencies"))
|
|
self._tx_id = None
|
|
else:
|
|
self._errorMsgs = None
|
|
self._tx_id = self.payload.txID
|
|
finally:
|
|
hubQ.send_ready(self.__class__.__name__, False)
|
|
hubQ.send_ready("SourceSpoke", False)
|
|
|
|
@property
|
|
def completed(self):
|
|
processingDone = not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and \
|
|
not self._errorMsgs and self.txid_valid
|
|
|
|
if flags.automatedInstall:
|
|
return processingDone and self.data.packages.seen
|
|
else:
|
|
return self._get_selected_environment() is not None and processingDone
|
|
|
|
@property
|
|
def changed(self):
|
|
row = self._get_selected_environment()
|
|
if not row:
|
|
return True
|
|
|
|
addons = self._get_selected_addons()
|
|
|
|
# Don't redo dep solving if nothing's changed.
|
|
if row[2] == self._origEnvironment and set(addons) == set(self._origAddons) and \
|
|
self.txid_valid:
|
|
return False
|
|
|
|
return True
|
|
|
|
@property
|
|
def mandatory(self):
|
|
return True
|
|
|
|
@property
|
|
def ready(self):
|
|
# By default, the software selection spoke is not ready. We have to
|
|
# wait until the installation source spoke is completed. This could be
|
|
# because the user filled something out, or because we're done fetching
|
|
# repo metadata from the mirror list, or we detected a DVD/CD.
|
|
|
|
return bool(not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and
|
|
not threadMgr.get(constants.THREAD_PAYLOAD_MD) and
|
|
not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and
|
|
self.payload.baseRepo is not None)
|
|
|
|
@property
|
|
def showable(self):
|
|
return not flags.livecdInstall and not self.data.method.method == "liveimg"
|
|
|
|
@property
|
|
def status(self):
|
|
if self._errorMsgs:
|
|
return _("Error checking software selection")
|
|
|
|
if not self.ready:
|
|
return _("Installation source not set up")
|
|
|
|
if not self.txid_valid:
|
|
return _("Source changed - please verify")
|
|
|
|
row = self._get_selected_environment()
|
|
if not row:
|
|
# Kickstart installs with %packages will have a row selected, unless
|
|
# they did an install without a desktop environment. This should
|
|
# catch that one case.
|
|
if flags.automatedInstall and self.data.packages.seen:
|
|
return _("Custom software selected")
|
|
|
|
return _("Nothing selected")
|
|
|
|
return self.payload.environmentDescription(row[2])[0]
|
|
|
|
def initialize(self):
|
|
NormalSpoke.initialize(self)
|
|
threadMgr.add(AnacondaThread(name=constants.THREAD_SOFTWARE_WATCHER,
|
|
target=self._initialize))
|
|
|
|
def _initialize(self):
|
|
hubQ.send_message(self.__class__.__name__, _("Downloading package metadata..."))
|
|
|
|
threadMgr.wait(constants.THREAD_PAYLOAD)
|
|
|
|
hubQ.send_message(self.__class__.__name__, _("Downloading group metadata..."))
|
|
|
|
# we have no way to select environments with kickstart right now
|
|
# so don't try.
|
|
if flags.automatedInstall and self.data.packages.seen:
|
|
# We don't want to do a full refresh, just join the metadata thread
|
|
threadMgr.wait(constants.THREAD_PAYLOAD_MD)
|
|
else:
|
|
# Grabbing the list of groups could potentially take a long time
|
|
# at first (yum does a lot of magic property stuff, some of which
|
|
# involves side effects like network access. We need to reference
|
|
# them here, outside of the main thread, to not block the UI.
|
|
try:
|
|
# pylint: disable-msg=W0104
|
|
self.payload.environments
|
|
# pylint: disable-msg=W0104
|
|
self.payload.groups
|
|
|
|
# Parse the environments and groups into the form we want
|
|
self._parseEnvironments()
|
|
except MetadataError:
|
|
hubQ.send_message(self.__class__.__name__,
|
|
_("No installation source available"))
|
|
return
|
|
|
|
# And then having done all that slow downloading, we need to do the first
|
|
# refresh of the UI here so there's an environment selected by default.
|
|
# This happens inside the main thread by necessity. We can't do anything
|
|
# that takes any real amount of time, or it'll block the UI from updating.
|
|
if not self._first_refresh():
|
|
return
|
|
|
|
self.payload.release()
|
|
|
|
hubQ.send_ready(self.__class__.__name__, False)
|
|
|
|
# If packages were provided by an input kickstart file (or some other means),
|
|
# we should do dependency solving here.
|
|
self._apply()
|
|
|
|
def _parseEnvironments(self):
|
|
self._environmentAddons = {}
|
|
|
|
for environment in self.payload.environments:
|
|
self._environmentAddons[environment] = ([], [])
|
|
|
|
# Determine which groups are specific to this environment and which other groups
|
|
# are available in this environment.
|
|
for grp in self.payload.groups:
|
|
if self.payload.environmentHasOption(environment, grp):
|
|
self._environmentAddons[environment][0].append(grp)
|
|
elif self.payload._isGroupVisible(grp) and self.payload._groupHasInstallableMembers(grp):
|
|
self._environmentAddons[environment][1].append(grp)
|
|
|
|
# Set all of the add-on selection states to the default
|
|
self._addonStates = {}
|
|
for grp in self.payload.groups:
|
|
self._addonStates[grp] = self._ADDON_DEFAULT
|
|
|
|
@gtk_action_wait
|
|
def _first_refresh(self):
|
|
try:
|
|
self.refresh()
|
|
return True
|
|
except MetadataError:
|
|
hubQ.send_message(self.__class__.__name__, _("No installation source available"))
|
|
return False
|
|
|
|
def refresh(self):
|
|
NormalSpoke.refresh(self)
|
|
|
|
threadMgr.wait(constants.THREAD_PAYLOAD_MD)
|
|
|
|
self._environmentStore.clear()
|
|
if self.environment not in self.payload.environments:
|
|
self.environment = None
|
|
|
|
firstEnvironment = True
|
|
for environment in self.payload.environments:
|
|
(name, desc) = self.payload.environmentDescription(environment)
|
|
|
|
itr = self._environmentStore.append([environment == self.environment, "<b>%s</b>\n%s" % (name, desc), environment])
|
|
# Either:
|
|
# (1) Select the environment given by kickstart or selected last
|
|
# time this spoke was displayed; or
|
|
# (2) Select the first environment given by display order as the
|
|
# default if nothing is selected.
|
|
if (environment == self.environment) or \
|
|
(not self.environment and firstEnvironment):
|
|
self.environment = environment
|
|
sel = self.builder.get_object("environmentSelector")
|
|
sel.select_iter(itr)
|
|
|
|
firstEnvironment = False
|
|
|
|
self.refreshAddons()
|
|
|
|
def _addon_row_is_separator(self, model, itr, *args):
|
|
# The last column of the model tells us if this row is a separator or not.
|
|
return model[itr][3]
|
|
|
|
def _addAddon(self, grp):
|
|
(name, desc) = self.payload.groupDescription(grp)
|
|
|
|
# If the add-on was previously selected by the user, select it
|
|
if self._addonStates[grp] == self._ADDON_SELECTED:
|
|
selected = True
|
|
# If the add-on was previously de-selected by the user, de-select it
|
|
elif self._addonStates[grp] == self._ADDON_DESELECTED:
|
|
selected = False
|
|
# Otherwise, use the default state
|
|
else:
|
|
selected = self.payload.environmentOptionIsDefault(self.environment, grp)
|
|
|
|
self._addonStore.append([selected, "<b>%s</b>\n%s" % (name, desc), grp, False])
|
|
|
|
def refreshAddons(self):
|
|
self._addonStore.clear()
|
|
if self.environment and (self.environment in self._environmentAddons):
|
|
# We have two lists: One of addons specific to this environment,
|
|
# and one of all the others. The environment-specific ones will be displayed
|
|
# first and then a separator, and then the generic ones. This is to make it
|
|
# a little more obvious that the thing on the left side of the screen and the
|
|
# thing on the right side of the screen are related.
|
|
#
|
|
# If a particular add-on was previously selected or de-selected by the user, that
|
|
# state will be used. Otherwise, the add-on will be selected if it is a default
|
|
# for this environment.
|
|
|
|
for grp in self._environmentAddons[self.environment][0]:
|
|
self._addAddon(grp)
|
|
|
|
# This marks a separator in the view.
|
|
self._addonStore.append([False, "", "", True])
|
|
|
|
for grp in self._environmentAddons[self.environment][1]:
|
|
self._addAddon(grp)
|
|
|
|
self._selectFlag = True
|
|
|
|
if self._errorMsgs:
|
|
self.set_warning(_("Error checking software dependencies. Click for details."))
|
|
else:
|
|
self.clear_info()
|
|
|
|
def _get_selected_addons(self):
|
|
return [row[2] for row in self._addonStore if row[0]]
|
|
|
|
# Returns the row in the store corresponding to what's selected on the
|
|
# left hand panel, or None if nothing's selected.
|
|
def _get_selected_environment(self):
|
|
environmentView = self.builder.get_object("environmentView")
|
|
itr = environmentView.get_selection().get_selected()[1]
|
|
if not itr:
|
|
return None
|
|
|
|
return self._environmentStore[itr]
|
|
|
|
@property
|
|
def txid_valid(self):
|
|
return self._tx_id == self.payload.txID
|
|
|
|
# Signal handlers
|
|
def on_environment_toggled(self, renderer, path):
|
|
if not self._selectFlag:
|
|
return
|
|
|
|
# First, mark every row as unselected so the radio button on whatever
|
|
# row was previously selected will be cleared out.
|
|
for row in self._environmentStore:
|
|
row[0] = False
|
|
|
|
# Then, remove all the groups that were selected by the previously
|
|
# selected environment.
|
|
for groupid in self.payload.environmentGroups(self.environment):
|
|
if groupid in self.selectedGroups:
|
|
self.selectedGroups.remove(groupid)
|
|
|
|
# Then mark the clicked environment as selected and update the screen.
|
|
self._environmentStore[path][0] = True
|
|
self.environment = self._environmentStore[path][2]
|
|
self.refreshAddons()
|
|
|
|
def on_environment_selection_changed(self, selection):
|
|
(model, itr) = selection.get_selected()
|
|
if not itr:
|
|
return
|
|
|
|
# Only do something if the row's not previously been selected.
|
|
if not model[itr][0]:
|
|
self.on_environment_toggled(None, model.get_path(itr))
|
|
|
|
def on_addon_toggled(self, renderer, path):
|
|
selected = not self._addonStore[path][0]
|
|
group = self._addonStore[path][2]
|
|
self._addonStore[path][0] = selected
|
|
if selected:
|
|
if group not in self.selectedGroups:
|
|
self.selectedGroups.append(group)
|
|
|
|
if group in self.excludedGroups:
|
|
self.excludedGroups.remove(group)
|
|
|
|
self._addonStates[group] = self._ADDON_SELECTED
|
|
|
|
else:
|
|
if group in self.selectedGroups:
|
|
self.selectedGroups.remove(group)
|
|
|
|
self._addonStates[group] = self._ADDON_DESELECTED
|
|
|
|
def on_addon_view_clicked(self, view, event, *args):
|
|
if event and not event.type in [Gdk.EventType.BUTTON_RELEASE, Gdk.EventType.KEY_RELEASE]:
|
|
return
|
|
|
|
if event and event.type == Gdk.EventType.KEY_RELEASE and \
|
|
event.keyval not in [Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_ISO_Enter, Gdk.KEY_KP_Enter, Gdk.KEY_KP_Space]:
|
|
return
|
|
|
|
selection = view.get_selection()
|
|
(model, itr) = selection.get_selected()
|
|
if not itr:
|
|
return
|
|
|
|
# If the user clicked on the first column, they've clicked on the checkbox which was
|
|
# handled separately from this signal handler. Handling it again here will result in
|
|
# the checkbox being toggled yet again. So, we need to return in that case.
|
|
col = view.get_cursor()[1]
|
|
if not col or col.get_title() == "Selected":
|
|
return
|
|
|
|
# Always do something here, since addons can be toggled.
|
|
self.on_addon_toggled(None, model.get_path(itr))
|
|
|
|
def on_info_bar_clicked(self, *args):
|
|
if not self._errorMsgs:
|
|
return
|
|
|
|
label = _("The following software marked for installation has errors. "
|
|
"This is likely caused by an error with\nyour installation source. "
|
|
"You can change your installation source or quit the installer.")
|
|
dialog = DetailedErrorDialog(self.data, buttons=[_("_Quit"), _("_Cancel"),
|
|
_("_Modify Software Source")],
|
|
label=label)
|
|
with enlightbox(self.window, dialog.window):
|
|
dialog.refresh(self._errorMsgs)
|
|
rc = dialog.run()
|
|
|
|
dialog.window.destroy()
|
|
|
|
if rc == 0:
|
|
# Quit.
|
|
sys.exit(0)
|
|
elif rc == 1:
|
|
# Close the dialog so the user can change selections.
|
|
pass
|
|
elif rc == 2:
|
|
# Send the user to the installation source spoke.
|
|
self.skipTo = "SourceSpoke"
|
|
self.window.emit("button-clicked")
|