library/packages/src/modules/PackagesUI.rb
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# Module: PackagesUI.ycp
#
# Authors: Gabriele Strattner (gs@suse.de)
# Ladislav Slezák <lslezak@novell.com>
#
# Purpose: Provides common dialogs related to
# the package management.
#
# $Id$
require "yast"
require "cgi"
require "packages/commit_result"
require "packages/update_messages_view"
module Yast
class PackagesUIClass < Module
def main
Yast.import "Pkg"
Yast.import "UI"
textdomain "base"
Yast.import "Label"
Yast.import "Wizard"
Yast.import "HTML"
Yast.import "String"
Yast.import "Popup"
Yast.import "Report"
@package_summary = {}
end
def GetPackageSummary
deep_copy(@package_summary)
end
def SetPackageSummary(summary)
summary = deep_copy(summary)
if summary.nil?
Builtins.y2error("Cannot set nil package summary!")
return
end
Builtins.y2debug("Setting package summary: %1", summary)
@package_summary = deep_copy(summary)
nil
end
def ResetPackageSummary
Builtins.y2debug("Resetting package summary")
@package_summary = {}
nil
end
def SetPackageSummaryItem(name, value)
value = deep_copy(value)
if name.nil? || name == ""
Builtins.y2error("Invalid item name: '%1'", name)
return
end
Builtins.y2debug("Package summary '%1': %2", name, value)
Ops.set(@package_summary, name, value)
nil
end
#
# Popup displays helptext
#
def DisplayHelpMsg(headline, helptext, color, vdim)
helptext = deep_copy(helptext)
dia_opt = Opt(:decorated)
case color
when :warncolor
dia_opt = Opt(:decorated, :warncolor)
when :infocolor
dia_opt = Opt(:decorated, :infocolor)
end
header = Empty()
header = Left(Heading(headline)) if headline != ""
UI.OpenDialog(
dia_opt,
HBox(
VSpacing(vdim),
VBox(
HSpacing(50),
header,
VSpacing(0.2),
helptext, # e.g. `Richtext()
PushButton(Id(:ok_help), Opt(:default), Label.OKButton)
)
)
)
UI.SetFocus(Id(:ok_help))
r = UI.UserInput
UI.CloseDialog
deep_copy(r)
end
# Display unconfirmed licenses of the selected packages.
# @return [Boolean] true when all licenses were accepted (or there was no license to confirm)
def ConfirmLicenses
ret = true
to_install = Pkg.GetPackages(:selected, true)
licenses = Pkg.PkgGetLicensesToConfirm(to_install)
Builtins.y2milestone("Licenses to confirm: %1", Builtins.size(licenses))
Builtins.y2debug("Licenses to confirm: %1", licenses)
display_info = UI.GetDisplayInfo
size_x = Builtins.tointeger(Ops.get_integer(display_info, "Width", 800))
size_y = Builtins.tointeger(Ops.get_integer(display_info, "Height", 600))
if Ops.greater_or_equal(size_x, 800) && Ops.greater_or_equal(size_y, 600)
size_x = 80
size_y = 20
else
size_x = 54
size_y = 15
end
Builtins.foreach(licenses) do |package, license|
popup = VBox(
HSpacing(size_x),
# dialog heading, %1 is package name
Heading(Builtins.sformat(_("Confirm Package License: %1"), package)),
HBox(VSpacing(size_y), RichText(Id(:lic), format_license(license))),
VSpacing(1),
HBox(
PushButton(Id(:help), Label.HelpButton),
HStretch(),
# push button
PushButton(Id(:accept), _("I &Agree")),
# push button
PushButton(Id(:deny), _("I &Disagree"))
)
)
UI.OpenDialog(popup)
ui = nil
while ui.nil?
ui = Convert.to_symbol(UI.UserInput)
next if ui != :help
ui = nil
# help text
help = _(
"<p><b><big>License Confirmation</big></b><br>\n" \
"The package in the headline of the dialog requires an explicit confirmation\n" \
"of acceptance of its license.\n" \
"If you reject the license of the package, the package will not be installed.\n" \
"<br>\n" \
"To accept the license of the package, click <b>I Agree</b>.\n" \
"To reject the license of the package, click <b>I Disagree</b></p>."
)
UI.OpenDialog(
HBox(
VSpacing(18),
VBox(
HSpacing(70),
RichText(help),
HBox(
HStretch(),
# push button
PushButton(Id(:close), Label.CloseButton),
HStretch()
)
)
)
)
UI.UserInput
UI.CloseDialog
end
UI.CloseDialog
Builtins.y2milestone(
"License of package %1 accepted: %2",
package,
ui == :accept
)
if ui == :accept
Pkg.PkgMarkLicenseConfirmed(package)
else
Pkg.PkgTaboo(package)
ret = false
end
end
ret
end
# format the license so it's displayed the same way as in the libyui-qt-pkg dialog,
# (see https://github.com/libyui/libyui-qt-pkg/blob/master/src/YQPkgObjList.cc#L1411
# https://github.com/libyui/libyui-qt-pkg/blob/master/src/YQPkgTextDialog.cc#L295 )
# @param [String] license the raw license text obtained from libzypp
# @return [String] formatted license for displaying in a RichText widget
def format_license(license)
# check the flag for a preformatted HTML
return license.dup if license.include?("<!-- DT:Rich -->")
ret = CGI.escapeHTML(license)
# two empty lines mean a new paragraph
ret.gsub!("\n\n", "</p><p>")
"<p>" + ret + "</p>"
end
# Run helper function, reads the display_support_status feature from the control file
# @return [Boolean] the read value
def ReadSupportStatus
# Load the control file
Yast.import "ProductControl"
Yast.import "ProductFeatures"
ret = ProductFeatures.GetBooleanFeature(
"software",
"display_support_status"
)
Builtins.y2milestone("Feature display_support_status: %1", ret)
ret
end
# Start the detailed package selection.
# @param [Hash{String => Object}] options options passed to the widget. All options are optional,
# if an option is missing or is nil the default value will be used. All options:
# $[ "enable_repo_mgr" : boolean // display the repository management menu,
# // default: false (disabled)
# "enable_online_search": boolean // enable the online search feature
# "display_support_status" : boolean // display the support status summary dialog,
# // default: depends on the Product Feature "software", "display_support_status"
# "mode" : symbol // package selector mode, no default value, supported values:
# `youMode (online update mode),
# `updateMode (update mode),
# `searchMode (search filter view),
# `summaryMode (installation summary filter view),
# `repoMode (repositories filter view
# ]
#
# @return [Symbol] Returns `accept or `cancel .
def RunPackageSelector(options)
options = deep_copy(options)
Builtins.y2milestone("Called RunPackageSelector(%1)", options)
enable_repo_mgr = Ops.get_boolean(options, "enable_repo_mgr")
display_support_status = Ops.get_boolean(
options,
"display_support_status"
)
mode = Ops.get_symbol(options, "mode")
# set the defaults if the option is missing or nil
display_support_status = ReadSupportStatus() if display_support_status.nil?
if enable_repo_mgr.nil?
# disable repository management by default
enable_repo_mgr = false
end
Builtins.y2milestone(
"Running package selection, mode: %1, options: display repo management: %2, display support status: %3",
mode,
enable_repo_mgr,
display_support_status
)
widget_options = Opt()
widget_options = Builtins.add(widget_options, mode) if !mode.nil?
widget_options = Builtins.add(widget_options, :repoMgr) if !enable_repo_mgr.nil? && enable_repo_mgr
widget_options = Builtins.add(widget_options, :confirmUnsupported) if !display_support_status.nil? && display_support_status
widget_options = Builtins.add(widget_options, :onlineSearch) if options["enable_online_search"]
Builtins.y2milestone(
"Options for the package selector widget: %1",
widget_options
)
# exception text
raise _("Opening package selector failed.") if !UI.OpenDialog(
Opt(:defaultsize),
if widget_options.empty?
PackageSelector(Id(:packages), "")
else
PackageSelector(Id(:packages), widget_options, "")
end
)
result = Convert.to_symbol(UI.RunPkgSelection(Id(:packages)))
UI.CloseDialog
Builtins.y2milestone("Package selector returned %1", result)
result
end
# Start the pattern selection dialog. If the UI does not support the
# PatternSelector, start the detailed selection with "patterns" as the
# initial view.
# @return [Symbol] Return `accept or `cancel
#
#
def RunPatternSelector(enable_back: false, cancel_label: Label.CancelButton)
Builtins.y2milestone("Running pattern selection dialog")
if !UI.HasSpecialWidget(:PatternSelector) ||
UI.WizardCommand(term(:Ping)) != true
return RunPackageSelector({}) # Fallback: detailed selection
end
# Help text for software patterns / selections dialog
help_text = _(
"<p>\n" \
"\t\t This dialog allows you to define this system's tasks and what software to install.\n" \
"\t\t Available tasks and software for this system are shown by category in the left\n" \
"\t\t column. To view a description for an item, select it in the list.\n" \
"\t\t </p>"
) +
_(
"<p>\n" \
"\t\t Change the status of an item by clicking its status icon\n" \
"\t\t or right-click any icon for a context menu.\n" \
"\t\t With the context menu, you can also change the status of all items.\n" \
"\t\t </p>"
) +
_(
"<p>\n" \
"\t\t <b>Details</b> opens the detailed software package selection\n" \
"\t\t where you can view and select individual software packages.\n" \
"\t\t </p>"
) +
_(
"<p>\n" \
"\t\t The disk usage display in the lower right corner shows the remaining disk space\n" \
"\t\t after all requested changes will have been performed.\n" \
"\t\t Hard disk partitions that are full or nearly full can degrade\n" \
"\t\t system performance and in some cases even cause serious problems.\n" \
"\t\t The system needs some available disk space to run properly.\n" \
"\t\t </p>"
)
# bugzilla #298056
# [ Back ] [ Cancel ] [ Accept ] buttons with [ Back ] disabled
Wizard.OpenNextBackDialog
Wizard.SetBackButton(:back, Label.BackButton)
Wizard.SetAbortButton(:cancel, cancel_label)
Wizard.SetNextButton(:accept, Label.OKButton)
enable_back ? Wizard.EnableBackButton : Wizard.DisableBackButton
Wizard.SetContents(
# Dialog title
# Hint for German translation: "Softwareauswahl und Einsatzzweck des Systems"
_("Software Selection and System Tasks"),
PatternSelector(Id(:patterns)),
help_text,
enable_back,
true
) # has_next
result = nil
loop do
result = Convert.to_symbol(UI.RunPkgSelection(Id(:patterns)))
Builtins.y2milestone("Pattern selector returned %1", result)
if result == :details
result = RunPackageSelector({})
if result == :cancel
# don't get all the way out - the user might just have
# been scared of the gory details.
result = nil
end
end
break if [:cancel, :accept, :back].include?(result)
end
Wizard.CloseDialog
Builtins.y2milestone("Pattern selector returned %1", result)
result
end
def FormatPackageList(pkgs, link)
pkgs = deep_copy(pkgs)
ret = ""
if Ops.greater_than(Builtins.size(pkgs), 8)
head = Builtins.sublist(pkgs, 0, 8)
ret = Builtins.sformat(
"%1... %2",
Builtins.mergestring(head, ", "),
HTML.Link(_("(more)"), link)
)
else
ret = Builtins.mergestring(pkgs, ", ")
end
ret
end
def InstallationSummary(summary)
ret = ""
if Builtins.haskey(summary, "success")
ret = HTML.Para(
HTML.Heading(
if Ops.get_boolean(summary, "success", true)
_("Installation Successfully Finished")
else
_("Package Installation Failed")
end
)
)
end
if Builtins.haskey(summary, "error")
ret = Ops.add(
ret,
HTML.List(
[
Builtins.sformat(
_("Error Message: %1"),
HTML.Colorize(Ops.get_string(summary, "error", ""), "red")
)
]
)
)
end
items = []
failed_packs = Builtins.size(Ops.get_list(summary, "failed", []))
if Ops.greater_than(failed_packs, 0)
items = Builtins.add(
items,
Ops.add(
Ops.add(
HTML.Colorize(
Builtins.sformat(_("Failed Packages: %1"), failed_packs),
"red"
),
"<BR>"
),
FormatPackageList(
Builtins.lsort(Ops.get_list(summary, "failed", [])),
"failed_packages"
)
)
)
end
if Ops.greater_than(Ops.get_integer(summary, "installed", 0), 0)
items = Builtins.add(
items,
Ops.add(
Ops.add(
Builtins.sformat(
_("Installed Packages: %1"),
Ops.get_integer(summary, "installed", 0)
),
"<BR>"
),
FormatPackageList(
Builtins.lsort(Ops.get_list(summary, "installed_list", [])),
"installed_packages"
)
)
)
end
if Ops.greater_than(Ops.get_integer(summary, "updated", 0), 0)
items = Builtins.add(
items,
Ops.add(
Ops.add(
Builtins.sformat(
_("Updated Packages: %1"),
Ops.get_integer(summary, "updated", 0)
),
"<BR>"
),
FormatPackageList(
Builtins.lsort(Ops.get_list(summary, "updated_list", [])),
"updated_packages"
)
)
)
end
if Ops.greater_than(Ops.get_integer(summary, "removed", 0), 0)
items = Builtins.add(
items,
Ops.add(
Ops.add(
Builtins.sformat(
_("Removed Packages: %1"),
Ops.get_integer(summary, "removed", 0)
),
"<BR>"
),
FormatPackageList(
Builtins.lsort(Ops.get_list(summary, "removed_list", [])),
"removed_packages"
)
)
)
end
if Ops.greater_than(
Builtins.size(Ops.get_list(summary, "remaining", [])),
0
)
items = Builtins.add(
items,
Ops.add(
Ops.add(
Builtins.sformat(
_("Not Installed Packages: %1"),
Builtins.size(Ops.get_list(summary, "remaining", []))
),
"<BR>"
),
FormatPackageList(
Builtins.lsort(Ops.get_list(summary, "remaining", [])),
"remaining_packages"
)
)
)
end
if Ops.greater_than(Builtins.size(items), 0)
ret = Ops.add(
ret,
HTML.Para(Ops.add(HTML.Heading(_("Packages")), HTML.List(items)))
)
end
# reset the items list
items = []
if Ops.greater_than(Ops.get_integer(summary, "time_seconds", 0), 0)
items = Builtins.add(
items,
Builtins.sformat(
_("Elapsed Time: %1"),
String.FormatTime(Ops.get_integer(summary, "time_seconds", 0))
)
)
end
if Ops.greater_than(Ops.get_integer(summary, "installed_bytes", 0), 0)
items = Builtins.add(
items,
Builtins.sformat(
_("Total Installed Size: %1"),
String.FormatSize(Ops.get_integer(summary, "installed_bytes", 0))
)
)
end
if Ops.greater_than(Ops.get_integer(summary, "downloaded_bytes", 0), 0)
items = Builtins.add(
items,
Builtins.sformat(
_("Total Downloaded Size: %1"),
String.FormatSize(Ops.get_integer(summary, "downloaded_bytes", 0))
)
)
end
if Ops.greater_than(Builtins.size(items), 0)
ret = Ops.add(
ret,
HTML.Para(Ops.add(HTML.Heading(_("Statistics")), HTML.List(items)))
)
end
items = []
if Builtins.haskey(summary, "install_log") &&
Ops.greater_than(
Builtins.size(Ops.get_string(summary, "install_log", "")),
0
)
items = Builtins.add(
items,
HTML.Link(_("Installation log"), "install_log")
)
end
if Ops.greater_than(Builtins.size(items), 0)
ret = Ops.add(
ret,
HTML.Para(Ops.add(HTML.Heading(_("Details")), HTML.List(items)))
)
end
Builtins.y2milestone("Installation summary: %1", ret)
ret
end
def ShowDetailsString(heading, text)
Popup.LongText(heading, RichText(Opt(:plainText), text), 70, 20)
nil
end
def ShowDetailsList(heading, pkgs)
pkgs = deep_copy(pkgs)
ShowDetailsString(
heading,
Builtins.mergestring(Builtins.lsort(pkgs), "\n")
)
nil
end
def ShowInstallationSummaryMap(summary)
summary_str = InstallationSummary(summary)
if summary["installed"] == 0 && summary["updated"] == 0 && summary["removed"] == 0 && summary["remaining"] == []
Builtins.y2warning("No summary, skipping summary dialog")
return :next
end
Builtins.y2milestone("Displaying installation report: #{summary.inspect}")
wizard_opened = false
# open a new wizard dialog if needed
if !Wizard.IsWizardDialog
Wizard.OpenNextBackDialog
wizard_opened = true
end
current_action = SCR.Read(path(".sysconfig.yast2.PKGMGR_ACTION_AT_EXIT"))
dialog = VBox(
RichText(Id(:rtext), summary_str),
Left(
ComboBox(Id(:action), _("After Installing Packages"),
[
Item(Id("summary"), _("Show This Report"), current_action == "summary"),
Item(Id("close"), _("Finish"), current_action == "close"),
Item(Id("restart"), _("Continue in the Software Manager"), current_action == "restart")
])
)
)
help_text = _(
"<P><BIG><B>Installation Report</B></BIG><BR>Here is a summary of installed or removed packages.</P>"
)
Wizard.SetNextButton(:next, Label.FinishButton)
Wizard.SetBackButton(:back, Label.ContinueButton)
Wizard.SetContents(
_("Installation Report"),
dialog,
help_text,
true,
true
)
result = nil
loop do
result = UI.UserInput
Builtins.y2milestone("input: %1", result)
# handle detail requests (clicking a link in the summary)
if Ops.is_string?(result)
# display installation log
case result
when "install_log"
ShowDetailsString(
_("Installation log"),
Ops.get_string(summary, "install_log", "")
)
when "installed_packages"
ShowDetailsList(
_("Installed Packages"),
Ops.get_list(summary, "installed_list", [])
)
when "updated_packages"
ShowDetailsList(
_("Updated Packages"),
Ops.get_list(summary, "updated_list", [])
)
when "removed_packages"
ShowDetailsList(
_("Removed Packages"),
Ops.get_list(summary, "removed_list", [])
)
when "remaining_packages"
ShowDetailsList(
_("Remaining Packages"),
Ops.get_list(summary, "remaining", [])
)
else
Builtins.y2error("Unknown input: %1", result)
end
elsif Ops.is_symbol?(result)
# close by WM
result = :abort if result == :cancel
end
break if [:next, :abort, :back].include?(result)
end
Builtins.y2milestone("Installation Summary result: %1", result)
new_action = UI.QueryWidget(Id(:action), :Value)
# the combobox value has been changed, save the new value
if result == :next && current_action != new_action
if new_action != "summary"
# disabling installation report dialog, inform the user how to enable it back
Popup.Message(_("If you want to show this report dialog again edit\n\n"\
"System > Yast2 > GUI > PKGMGR_ACTION_AT_EXIT\n\n" \
"value in the YaST sysconfig editor."))
end
Builtins.y2milestone("Changing PKGMGR_ACTION_AT_EXIT from #{current_action.inspect} to #{new_action.inspect}")
SCR.Write(path(".sysconfig.yast2.PKGMGR_ACTION_AT_EXIT"), new_action)
# flush
SCR.Write(path(".sysconfig.yast2"), nil)
end
Wizard.RestoreNextButton
Wizard.RestoreBackButton
if wizard_opened
# close the opened window
Wizard.CloseDialog
end
Convert.to_symbol(result)
end
def ShowInstallationSummary
ShowInstallationSummaryMap(@package_summary)
end
# Show messages coming from libzypp about installed packages
#
# This messages are retrieved from libzypp.
#
# @param [Array] result Result from package commit (as it comes from PkgCommit)
def show_update_messages(result)
return false if result.nil?
commit_result = ::Packages::CommitResult.from_result(result)
return false if commit_result.update_messages.empty?
view = ::Packages::UpdateMessagesView.new(commit_result.update_messages)
Report.LongMessage(view.richtext)
true
end
publish function: :GetPackageSummary, type: "map <string, any> ()"
publish function: :SetPackageSummary, type: "void (map <string, any>)"
publish function: :ResetPackageSummary, type: "void ()"
publish function: :SetPackageSummaryItem, type: "void (string, any)"
publish function: :DisplayHelpMsg, type: "any (string, term, symbol, integer)"
publish function: :ConfirmLicenses, type: "boolean ()"
publish function: :RunPackageSelector, type: "symbol (map <string, any>)"
publish function: :RunPatternSelector, type: "symbol ()"
publish function: :InstallationSummary, type: "string (map <string, any>)"
publish function: :ShowInstallationSummaryMap, type: "symbol (map <string, any>)"
publish function: :ShowInstallationSummary, type: "symbol ()"
end
PackagesUI = PackagesUIClass.new
PackagesUI.main
end