yast/yast-yast2

View on GitHub
library/packages/src/modules/SlideShow.rb

Summary

Maintainability
D
2 days
Test Coverage
# ***************************************************************************
#
# 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:    SlideShow.ycp
#
# Purpose:    Slide show during installation
#
# Author:    Stefan Hundhammer <sh@suse.de>
#      Stanislav Visnovsky <visnov@suse.cz>
#
# $Id$
#
# Usage:
# This is a generic module for handling global progress bar with optional slideshow/release notes etc.
#
# Global progress
# ===============
# The basic idea is that the progress consists of "stages" - during a new install,
# there are 3: disk preparation, image deployment and package installation.
#
# Before the first client using the unified progress, the stages need to be set
# up, e.g. :
#
# list< map<string,any> > stages = [
#     $[
#     "name" : "disk",
#     "description": _("Preparing disks..."),
#     "value" : Mode::update() ? 0 : 120, // 2 minutes, who needs more? ;-)
#     "units" : `sec,
#     ],
#     $[
#     "name" : "images",
#     "description": _("Deploying Images..."),
#     "value" : ImageInstallation::TotalSize() / 1024, // kilobytes
#     "units" : `kb,
#     ],
#     $[
#     "name" : "packages",
#     "description": _("Installing Packages..."),
#     // here, we do a hack, because until images are deployed, we cannot
# determine how many
#     // packages will be really installed additionally
#     "value" : (PackageSlideShow::total_size_to_install -
# ImageInstallation::TotalSize()) / 1024 , // kilobytes
#     "units" : `kb,
#     ],
#   ];
#
#   SlideShow::Setup( stages );
#
# The function will calculate the partitioning of the unified progress based on
# estimate of a needed time. A stage can provide the estimate of time or an
# amount of data to be transferred (the constants used are based on assumption
# of 15 min install time and that the data are downloaded and written to disk).
# The logic is no rocket science as the only goal for a progress bar is to have
# it move somewhat regularly. Also, the function resets timers and other
# progress status information, including which parts are shown. See \ref SlideShow::Reset.
#
# A client using the new unified progress will do basically 2 things:
#
# 1) calls SlideShow::MoveToStage( stage-id )
# - this will move the global progress to a proper position for start of the
# stage and updates also the label ("description" entry in the map)
#
# 2) calls regularly SlideShow::StageProgress( new_percent, new_label )
# - new_percent is the progress inside of the current stage, the library will
# recompute this to get a global progress percents.
# - if new_label is nil, label is not updated.
#
# SlideShow dialogs
# =================
# // SlideShow language must be set before opening the dialog
# SlideShow::SetLanguage( Language::language );
# SlideShow::OpenDialog ();
# ... <update stages, progress ...> ...
# SlideShow::CloseDialog ();
#
# More functionality
# ==================
# The SlideShow dialog contains the following functionality:
# - global progress (see above)
# - release notes viewer
require "yast"
require "yast2/system_time"

module Yast
  class SlideShowClass < Module
    include Yast::Logger

    def main
      Yast.import "UI"

      textdomain "base"

      Yast.import "Label"
      Yast.import "Stage"
      Yast.import "Wizard"
      Yast.import "Mode"
      Yast.import "Popup"
      Yast.import "Slides"

      @language = "en"
      @widgets_created = false
      @opened_own_wizard = false
      @user_abort = false

      # we need to remember the values for tab switching
      # these are the initial values
      @total_progress_label = _("Installing...")
      @total_progress_value = 0

      @relnotes = nil # forward declaration

      @_stages = {} # list of the configured stages
      @_current_stage = nil # current stage

      @_rn_tabs = {} # tabs with release notes
      @_relnotes = {} # texts with release notes, product -> text
      @_base_product = "" # base product for release notes ordering
    end

    # Set the flag that user requested abort of the installation
    # @param [Boolean] abort  new state of the abort requested flag (true = abort requested)
    def SetUserAbort(abort)
      @user_abort = abort

      nil
    end

    # Get the status of the flag that user requested abort of the installation
    # @return [Boolean]   state of the abort requested flag (true = abort requested)
    def GetUserAbort
      @user_abort
    end

    # Start the internal (global) timer.
    #
    # FIXME: Obsolete published method
    def StartTimer
      nil
    end

    # Reset the internal (global) timer.
    #
    # FIXME: Obsolete published method
    def ResetTimer
      nil
    end

    # Stop the internal (global) timer and account elapsed time.
    #
    # FIXME: Obsolete published method
    def StopTimer
      nil
    end

    # Check if currently the "Details" page is shown
    # @return true if showing details, false otherwise
    #
    def ShowingDetails
      @widgets_created && UI.WidgetExists(:detailsPage)
    end

    # Check if currently the "Slide Show" page is shown
    # @return true if showing details, false otherwise
    #
    # FIXME: Obsolete published method
    def ShowingSlide
      false
    end

    # Check if currently the "Release Notes" page is shown
    # @return true if showing details, false otherwise
    #
    def ShowingRelNotes(id)
      @widgets_created && UI.WidgetExists(id)
    end

    def ProductRelNotesID(product)
      ("rn_" + product).to_sym
    end

    # Restart the subprogress of the slideshow. This means the
    # label will be set to given text, value to 0.
    # @param [String] text  new label for the subprogress
    #
    # FIXME: Obsolete published method
    def SubProgressStart(text); end

    # Updates status of the sub-progress in slide show. The new value and label
    # will be set to values given as parametes. If a given parameter contains *nil*,
    # respective value/label will not be updated.
    #
    # @param [Fixnum] value  new value for the subprogress
    # @param [String] label  new label for the subprogress
    #
    # FIXME: Obsolete published method
    def SubProgress(value, label); end

    # Restart the global progress of the slideshow. This means the
    # label will be set to given text, value to 0.
    #
    # @param [String] text  new label for the global progress
    #
    # FIXME: Obsolete published method
    def GlobalProgressStart(text)
      UpdateGlobalProgress(0, text)
    end

    # Updates status of the global progress in slide show. The new value and label
    # will be set to values given as parametes. If a given parameter contains *nil*,
    # respective value/label will not be updated.
    #
    # @param [Fixnum] value  new value for the global progress
    # @param [String] label  new label for the global progress
    def UpdateGlobalProgress(value, label)
      value ||= @total_progress_value
      label ||= @total_progress_label

      if UI.WidgetExists(:progressTotal)
        if @total_progress_value != value
          @total_progress_value = value
          UI.ChangeWidget(:progressTotal, :Value, value)
        end

        if @total_progress_label != label
          @total_progress_label = label
          UI.ChangeWidget(:progressTotal, :Label, label)
        end
      else
        log.warn "progressTotal widget missing"
      end

      nil
    end

    # Return the description for the current stage.
    # @return [String]  localized string description
    def CurrentStageDescription
      Ops.get_locale(@_current_stage, "description", _("Installing..."))
    end

    # Move the global progress to the beginning of the given stage.
    # @param [String] stage_name  id of the stage to move to
    def MoveToStage(stage_name)
      if !Builtins.haskey(@_stages, stage_name)
        Builtins.y2error("Unknown progress stage \"%1\"", stage_name)
        return
      end

      @_current_stage = Ops.get(@_stages, stage_name)

      Builtins.y2milestone(
        "Moving to stage %1 (%2)",
        stage_name,
        Ops.get_integer(@_stages, [stage_name, "start"], 0)
      )
      # translators: default global progress bar label
      UpdateGlobalProgress(
        Ops.get_integer(@_stages, [stage_name, "start"], 0),
        Ops.get_locale(@_current_stage, "description", _("Installing..."))
      )

      nil
    end

    # Update the global progress according to the progress in the current stage.
    # The new value will be set to the per cent of the current stage according to  \param value.The
    # value must be lower that 100 (or it's corrected to 100).
    # If the \text is not nil, the label will be updated
    # to this text as well. Otherwise label will not change.
    #
    # @param [Fixnum] value  new value for the stage progress in per cents
    # @param [String] text  new label for the global progress
    def StageProgress(value, text)
      if Ops.greater_than(value, 100)
        Builtins.y2error("Stage progress value larger than expected: %1", value)
        value = 100
      end

      UpdateGlobalProgress(
        Ops.add(
          Ops.get_integer(@_current_stage, "start", 0),
          Ops.divide(
            Ops.multiply(value, Ops.get_integer(@_current_stage, "size", 1)),
            100
          )
        ),
        text
      )

      nil
    end

    # Sets the current global progress label.
    #
    # @param [String]  new label
    def SetGlobalProgressLabel(text)
      UpdateGlobalProgress(nil, text)

      nil
    end

    # Append message to the installation log.
    # @param [String] msg  message to be added, without trailing eoln
    def AppendMessageToInstLog(_msg)
      nil
    end

    # Check if the dialog is currently set up so the user could switch to the slide page.
    #
    def HaveSlideWidget
      UI.WidgetExists(:dumbTab)
    end

    # Check if the slide show is available. This must be called before trying
    # to access any slides; some late initialization is done here.
    #
    # FIXME: Obsolete
    def CheckForSlides
      nil
    end

    # Set the slide show text.
    # @param [String] text
    #
    # FIXME: Obsolete
    def SetSlideText(_text)
      nil
    end

    # Set the curent language. Must be called once during initialization.
    #
    def SetLanguage(new_language)
      @language = new_language
      Builtins.y2milestone("New SlideShow language: %1", @language)

      nil
    end

    # Create one single item for the CD statistics table
    #
    # FIXME: Obsolete published method
    def TableItem(id, col1, col2, col3, col4)
      Item(Id(id), col1, col2, col3, col4)
    end

    # Load a slide image + text.
    # @param [Fixnum] slide_no number of slide to load
    #
    # FIXME: Obsolete
    def LoadSlide(_slide_no)
      nil
    end

    # Check if the current slide needs to be changed and do that if
    # necessary.
    #
    # FIXME: Obsolete
    def ChangeSlideIfNecessary
      nil
    end

    # Widgets for the progress bar tab
    # @return  A term describing the widgets
    #
    def progress_widgets
      MarginBox(
        4, 1, # hor/vert
        VBox(
          product_name_widgets,
          VCenter(
            ProgressBar(
              Id(:progressTotal),
              @total_progress_label,
              100,
              @total_progress_value
            )
          )
        )
      )
    end

    # Widgets for the product name
    # @return A term describing the widgets
    def product_name_widgets
      text = product_name
      return Empty() if text.nil? || text.empty?

      MarginBox(
        0, 1, # hor/vert
        Left(
          Label(Id(:productName), text)
        )
      )
    end

    # Name of the base product that is or will be installed
    # @return [String,nil] Display name of the base product
    def product_name
      # Avoid expensive operation in the installed system where this will
      # always return 'nil' anyway.
      return nil if Mode.normal

      # lazy load product spec as it lives in packager and maybe is not available
      require "y2packager/product_spec"
      product = Y2Packager::ProductSpec.selected_base
      return nil if product.nil?

      product.display_name
    rescue LoadError
      log.info "product spec not available. Probably yast2-packager is missing"

      nil
    end

    # Construct widgets describing a page with the real slide show
    # (the RichText / HTML page)
    #
    # @return  A term describing the widgets
    #
    def SlidePageWidgets; end

    def DetailsTableWidget; end

    # Construct widgets for the "details" page
    #
    # @return  A term describing the widgets
    #
    def DetailsPageWidgets; end

    # Construct widgets for the "release notes" page
    #
    # @return  A term describing the widgets
    #
    def RelNotesPageWidgets(id); end

    # Switch from the 'details' view to the 'slide show' view.
    #
    # FIXME: Obsolete
    def SwitchToSlideView
      return if ShowingSlide()

      if UI.WidgetExists(:tabContents)
        UI.ChangeWidget(:dumbTab, :CurrentItem, :showSlide)
        UI.ReplaceWidget(:tabContents, SlidePageWidgets())
        # UpdateTotalProgress(false);    // FIXME: this breaks other stages!
      end

      nil
    end

    # Rebuild the details page.
    # FIXME: Obsolete
    def RebuildDetailsView
      nil
    end

    # Switch from the 'slide show' view to the 'details' view.
    #
    # FIXME: Obsolete
    def SwitchToDetailsView
      nil
    end

    # Switch to the 'release notes' view.
    #
    def SwitchToReleaseNotesView(id)
      return if ShowingRelNotes(id)

      if UI.WidgetExists(:tabContents)
        UI.ChangeWidget(:dumbTab, :CurrentItem, id)
        UI.ReplaceWidget(:tabContents, RelNotesPageWidgets(id))
        # UpdateTotalProgress(false);
      end

      nil
    end

    # Help text for the dialog
    def HelpText
      # Help text while software packages are being installed (displayed only in rare cases)
      _("<p>Packages are being installed.</p>") +
        _(
          "<P><B>Aborting Installation</B> Package installation can be aborted using the <B>Abort</B> button. However, the system then can be in an inconsistent or unusable state or it may not boot if the basic system component is not installed.</P>"
        )
    end

    # set the release notes for slide show
    # @param [map<string,string>] map product name -> release notes text
    # @param [string] base product name
    def SetReleaseNotes(relnotes, base_product)
      @_relnotes = relnotes
      @_base_product = base_product
    end

    def add_relnotes_for_product(product, relnotes, tabs)
      id = ProductRelNotesID product
      # Translators: Tab name, keep short, %s is product name, e.g. SLES
      tabs << Item(Id(id), _("%s Release Notes") % product)
      @_rn_tabs[id] = relnotes
    end

    # Rebuild the dialog. Useful if slides become available post-creating the dialog.
    #
    # @param [Boolean] show_release_notes release notes tab will be shown.
    def RebuildDialog(_show_release_notes = false)
      log.info "Rebuilding partitioning/RPM_installation progress"
      contents = progress_widgets

      Builtins.y2milestone("SlideShow contents: %1", contents)

      Wizard.SetContents(
        if Mode.update
          # Dialog heading - software packages are being upgraded
          _("Performing Upgrade")
        else
          # Dialog heading - software packages are being installed
          _("Performing Installation")
        end,
        contents,
        HelpText(),
        false, # no back button
        false  # no next button
      )

      @widgets_created = true

      nil
    end

    # Redrawing the complete slide show if needed.
    #
    def Redraw
      nil
    end

    # Open the slide show base dialog with empty work area (placeholder for
    # the image) and CD statistics.
    #
    def OpenSlideShowBaseDialog
      if !Wizard.IsWizardDialog # If there is no Wizard dialog open already, open one
        Wizard.OpenNextBackDialog
        @opened_own_wizard = true
      end

      UI.WizardCommand(term(:ProtectNextButton, false))
      Wizard.RestoreBackButton
      Wizard.RestoreAbortButton
      Wizard.EnableAbortButton
      Wizard.RestoreNextButton

      Wizard.SetContents(
        # Dialog heading while software packages are being installed
        _("Package Installation"),
        Empty(), # Wait until InitPkgData() is called from outside
        HelpText(),
        false,
        false
      ) # has_back, has_next

      RebuildDialog(true)

      # reset abort status
      SetUserAbort(false)

      nil
    end

    # Initialize generic data to default values
    def Reset
      nil
    end

    # Process (slide show) input (button press).
    #
    def HandleInput(button)
      SwitchToReleaseNotesView(button) if @_rn_tabs.key?(button) && !ShowingRelNotes(button)
      # NOTE: `abort is handled in SlideShowCallbacks::HandleInput()

      nil
    end

    # Check for user button presses and handle them. Generic handling to be used in the
    # progress handlers.
    #
    def GenericHandleInput
      button = UI.PollInput

      # in case of cancel ask user if he really wants to quit installation
      if [:abort, :cancel].include?(button)
        if Mode.normal
          SetUserAbort(
            Popup.AnyQuestion(
              Popup.NoHeadline,
              # popup yes-no
              _("Do you really want\nto quit the installation?"),
              Label.YesButton,
              Label.NoButton,
              :focus_no
            )
          )
        elsif Stage.initial
          SetUserAbort(Popup.ConfirmAbort(:unusable)) # Mode::update (), Stage::cont ()
        else
          SetUserAbort(Popup.ConfirmAbort(:incomplete))
        end

        AppendMessageToInstLog(_("Aborted")) if GetUserAbort()
      else
        HandleInput(button)
      end

      nil
    end

    # Open the slide show dialog.
    #
    def OpenDialog
      # call SlideShowCallbacks::InstallSlideShowCallbacks()
      WFM.call("wrapper_slideshow_callbacks", ["InstallSlideShowCallbacks"])

      OpenSlideShowBaseDialog()

      nil
    end

    # Close the slide show dialog.
    #
    def CloseDialog
      Wizard.CloseDialog if @opened_own_wizard

      # call SlideShowCallbacks::RemoveSlideShowCallbacks()
      WFM.call("wrapper_slideshow_callbacks", ["RemoveSlideShowCallbacks"])

      nil
    end

    def ShowTable
      nil
    end

    def HideTable
      nil
    end

    def UpdateTable(_items)
      nil
    end

    #  Prepare the stages for the global progressbar. Will compute the total estimate of time and
    #  partition the global 100% to given stages based on their estimates. Can compute out of
    #  time and size to download.
    #
    #  The stages description list example:
    #  [
    #      $[
    #    "name" : "disk",
    #    "description" : "Prepare disk...",
    #    "value" : 85,    // disk speed can be guessed by the storage, thus passing time
    #    "units" : `sec
    #       ],
    #      $[
    #    "name" : "images";
    #    "description" : "Deploying images...",
    #    "value" : 204800,  // amount of kb to be downloaded/installed
    #    "units" : `kb
    #       ],
    #  ]
    def Setup(stages)
      stages = deep_copy(stages)
      log.info "SlideShow stages: #{stages}"
      # initiliaze the generic counters
      Reset()

      # gather total amount of time need
      total_time = 0

      Builtins.foreach(stages) do |stage|
        total_time = if Ops.get_symbol(stage, "units", :sec) == :sec
          Ops.add(total_time, Ops.get_integer(stage, "value", 0)) # assume kilobytes
        else
          # assume 15 minutes for installation of openSUSE 11.0, giving 3495 as the constant for kb/s
          Ops.add(
            total_time,
            Ops.divide(Ops.get_integer(stage, "value", 0), 3495)
          )
        end
      end

      # avoid division by zero, set at least 1 second
      total_time = 1 if total_time == 0

      Builtins.y2milestone("Total estimated time: %1", total_time)

      start = 0 # value where the current stage starts

      @_stages = {} # prepare a new stages description

      total_size = 0
      # distribute the total time to stages as per cents
      Builtins.foreach(stages) do |stage|
        if Ops.get_symbol(stage, "units", :sec) == :sec
          Ops.set(
            stage,
            "size",
            Ops.divide(
              Ops.multiply(Ops.get_integer(stage, "value", 0), 100),
              total_time
            )
          )
          Ops.set(stage, "start", start)
        else
          # assume 15 minutes for installation of openSUSE 11.0, giving 3495 as the constant
          Ops.set(
            stage,
            "size",
            Ops.divide(
              Ops.divide(
                Ops.multiply(Ops.get_integer(stage, "value", 0), 100),
                3495
              ),
              total_time
            )
          )
          Ops.set(stage, "start", start)
          if Ops.greater_than(
            Ops.add(Ops.get_integer(stage, "size", 0), start),
            100
          )
            Ops.set(stage, "size", Ops.subtract(100, start))
          end
        end

        start = Ops.add(start, Ops.get_integer(stage, "size", 0))
        total_size += stage["size"]
        Ops.set(@_stages, Ops.get_string(stage, "name", ""), stage)
        # setup first stage
        @_current_stage = deep_copy(stage) if @_current_stage.nil?
      end

      # Because of using integers in the calculation above the sum of the sizes
      # might not be 100% due to rounding. Update the last stage so the
      # total installation progress is 100%.
      if total_size != 100
        log.info "Total global progress: #{total_size}%, adjusting to 100%..."

        # find the last stage and adjust it
        updated_stage_name = stages.last["name"]
        updated_stage = @_stages[updated_stage_name]

        new_size = 100 - total_size + updated_stage["size"]
        log.info "Updating '#{updated_stage_name}' stage size from " \
                 "#{updated_stage["size"]}% to #{new_size}%"

        updated_stage["size"] = new_size
        @_stages[updated_stage_name] = updated_stage
      end

      Builtins.y2milestone("Global progress bar: %1", @_stages)

      nil
    end

    # Returns the current setup defined by Setup().
    #
    # @return [Hash <String, Hash{String => Object>}] stages
    # @see #Setup()
    #
    # **Structure:**
    #
    #     $[ stage_name : $[ stage_setup ], ... ]
    def GetSetup
      deep_copy(@_stages)
    end

    publish variable: :language, type: "string"
    publish variable: :widgets_created, type: "boolean"
    publish variable: :opened_own_wizard, type: "boolean"
    publish variable: :relnotes, type: "string"
    publish function: :ChangeSlideIfNecessary, type: "void ()"
    publish function: :SetUserAbort, type: "void (boolean)"
    publish function: :GetUserAbort, type: "boolean ()"
    publish function: :StartTimer, type: "void ()"
    publish function: :ResetTimer, type: "void ()"
    publish function: :StopTimer, type: "void ()"
    publish function: :ShowingDetails, type: "boolean ()"
    publish function: :ShowingSlide, type: "boolean ()"
    publish function: :ShowingRelNotes, type: "boolean (symbol)"
    publish function: :SubProgressStart, type: "void (string)"
    publish function: :SubProgress, type: "void (integer, string)"
    publish function: :GlobalProgressStart, type: "void (string)"
    publish function: :CurrentStageDescription, type: "string ()"
    publish function: :MoveToStage, type: "void (string)"
    publish function: :StageProgress, type: "void (integer, string)"
    publish function: :SetGlobalProgressLabel, type: "void (string)"
    publish function: :AppendMessageToInstLog, type: "void (string)"
    publish function: :HaveSlideWidget, type: "boolean ()"
    publish function: :SetLanguage, type: "void (string)"
    publish function: :TableItem, type: "term (string, string, string, string, string)"
    publish function: :SwitchToReleaseNotesView, type: "void (symbol)"
    publish function: :RebuildDialog, type: "void ()"
    publish function: :Reset, type: "void ()"
    publish function: :HandleInput, type: "void (any)"
    publish function: :GenericHandleInput, type: "void ()"
    publish function: :OpenDialog, type: "void ()"
    publish function: :CloseDialog, type: "void ()"
    publish function: :ShowTable, type: "void ()"
    publish function: :HideTable, type: "void ()"
    publish function: :UpdateTable, type: "void (list <term>)"
    publish function: :Setup, type: "void (list <map <string, any>>)"
    publish function: :GetSetup, type: "map <string, map <string, any>> ()"
    publish function: :SetReleaseNotes, type: "void (map<string, string>, string)"
  end

  SlideShow = SlideShowClass.new
  SlideShow.main
end