yast/yast-yast2

View on GitHub
library/cwm/src/modules/CWM.rb

Summary

Maintainability
F
1 wk
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
#
# ***************************************************************************
# File:  modules/CWM.ycp
# Package:  Common widget manipulation
# Summary:  Routines for common widget manipulation
# Authors:  Jiri Srain <jsrain@suse.cz>
#
# $Id$
#
require "yast"

require "cwm/abstract_widget"

module Yast
  class CWMClass < Module
    def main
      Yast.import "UI"
      textdomain "base"

      Yast.import "Label"
      Yast.import "Report"
      Yast.import "Wizard"

      # local variables

      # Widget that is being currently processed
      @processed_widget = {}

      # All widgets of the current dialog, in the correct order
      @current_dialog_widgets = []

      # stack of settings of nested calls of CWM
      @settings_stack = []

      # Handler to be called after validation of a dialog fails
      @validation_failed_handler = nil
    end

    # UI containers, layout helpers that contain other widgets.  Used by
    # functions that recurse through "contents" to decide whether to go
    # deeper.
    CONTAINER_WIDGETS = [
      :Frame,
      :RadioButtonGroup,
      :VBox,
      :HBox,
      :MarginBox,
      :MinWidth,
      :MinHeight,
      :MinSize,
      :Left,
      :Right,
      :Top,
      :Bottom,
      :HCenter,
      :VCenter,
      :HVCenter,
      :HSquash,
      :VSquash,
      :HVSquash,
      :HWeight,
      :VWeight,
      :DumbTab,
      :ReplacePoint
    ].freeze

    # local functions

    # Push the settings of the currently run dialog to the stack
    def PushSettings
      @settings_stack = Builtins.prepend(
        @settings_stack,
        "widgets" => @current_dialog_widgets
      )

      nil
    end

    # Pop the settings of the currently run dialog from the stack
    def PopSettings
      current_dialog = Ops.get(@settings_stack, 0, {})
      Ops.set(@settings_stack, 0, nil)
      @settings_stack = Builtins.filter(@settings_stack) { |e| !e.nil? }
      @current_dialog_widgets = Ops.get_list(current_dialog, "widgets", [])

      nil
    end

    # Process term with the dialog, replace strings in the term with
    # appropriate widgets
    # @param term [::CWM::StringTerm] term dialog containing strings
    # @param widgets [Hash{String => ::CWM::WidgetHash}] widget name -> widget description
    # @return [::CWM::UITerm] updated term ready to be used as a dialog
    def ProcessTerm(term, widgets)
      return term if term.empty?

      ret = Yast::Term.new(term.value)

      is_frame = term.value == :Frame
      is_id_frame = false

      term.each.with_index do |arg, index|
        if is_frame && index == 0 # no action
          # frame can have id and also label, so mark if id is used
          is_id_frame = arg.is_a?(Yast::Term) && arg.value == :id
          Builtins.y2debug("Leaving untouched %1", arg)
        elsif is_frame && index == 1 && is_id_frame && arg.is_a?(::String) # no action
          Builtins.y2debug("Leaving untouched %1", arg)
        elsif Ops.is_term?(arg) # recurse
          arg = ProcessTerm(arg, widgets) if CONTAINER_WIDGETS.include?(arg.value)
        elsif Ops.is_string?(arg) # action
          Builtins.y2error("find string '#{arg}' without associated widget in StringTerm #{term.inspect}") unless widgets[arg]
          Builtins.y2milestone("Known widgets #{widgets.inspect}") unless widgets[arg]

          arg = widgets.fetch(arg, {}).fetch("widget") { VBox() }
          arg = ProcessTerm(arg, widgets) if CONTAINER_WIDGETS.include?(arg.value)
        end
        ret << arg
      end
      ret
    end

    # Process term with the dialog, return all strings.
    # To be used as an argument for widget_names until they are obsoleted.
    # @param term [::CWM::StringTerm] term dialog containing strings
    # @return [Array<String>] found in the term
    def StringsOfTerm(term)
      term = deep_copy(term)
      rets = []
      args = Builtins.size(term)
      index = 0
      while Ops.less_than(index, args)
        arg = Ops.get(term, index)
        current = Builtins.symbolof(term)
        if current == :Frame && index == 0 # no action
          Builtins.y2debug("Leaving untouched %1", arg)
        elsif Ops.is_term?(arg)
          rets = Ops.add(rets, StringsOfTerm(Convert.to_term(arg))) if CONTAINER_WIDGETS.include?(arg.value)
        elsif Ops.is_string?(arg) # action
          rets = Builtins.add(rets, Convert.to_string(arg))
        end
        index = Ops.add(index, 1)
      end
      deep_copy(rets)
    end

    # Validate the value against the basic type
    # @param [Object] value any a value to validate
    # @param [String] type string type information
    # @return [Boolean] true on success or if do not know how to validate
    def ValidateBasicType(value, type)
      return Ops.is_term?(value) if type == "term"
      return Ops.is_string?(value) if type == "string"
      return Ops.is_symbol?(value) if type == "symbol"
      return Ops.is_list?(value) if type == "list"
      return Ops.is_map?(value) if type == "map"
      return Ops.is_boolean?(value) if type == "boolean"
      return Ops.is_integer?(value) if type == "integer"

      Builtins.y2error("Unknown value type %1", type)
      true
    end

    # Validate type of entry of the widget/option description map
    # Also checks option description maps if present
    # @param [String] key string key of the map entry
    # @param [Object] value any value of the map entry
    # @param [String] widget any name of the widget/option
    # @return [Boolean] true if validation succeeded
    def ValidateValueType(key, value, widget)
      types = {
        # general
        "widget"        => "symbol",
        "custom_widget" => "term",
        "handle_events" => "list",
        "label"         => "string",
        "opt"           => "list",
        "ui_timeout"    => "integer",
        "validate_type" => "symbol",
        # int field
        "minimum"       => "integer",
        "maximum"       => "integer",
        "_cwm_attrib"   => "map",
        "fallback"      => "map"
      }
      type = Ops.get(types, key)
      success = if type.nil?
        case key
        when "widget_func" then Ops.is(value, "term ()")
        when "init", "cleanup" then Ops.is(value, "void (string)")
        when "handle" then Ops.is(value, "symbol (string, map)")
        when "store" then Ops.is(value, "void (string, map)")
        when "validate_function" then Ops.is(value, "boolean (string, map)")
        when "items" then Ops.is(value, "list <list <string>>")
        when "_cwm_do_validate" then Ops.is(value, "boolean (string, map <string, any>)")
        when "help"
          # help can be static string or dynamic callback, that allows to recompute help for
          # widgets with dynamic contents like replace_point
          Ops.is(value, "string") || Ops.is(value, "string ()")
        else
          # unknown key, always valid
          true
        end
      else
        ValidateBasicType(value, type)
      end

      if !success
        Builtins.y2error(
          "Wrong type of option %1 in description map of %2",
          key,
          widget
        )
      end

      success
    end

    # Validate value of entry of the widget/option description map
    # Also checks option description maps if present
    # @param [String] key string key of the map entry
    # @param [Object] value any value of the map entry
    # @param [String] widget any name of the widget/option
    # @return [Boolean] true if validation succeeded
    def ValidateValueContents(key, value, widget)
      error = ""
      case key
      when "label"
        s = Convert.to_string(value)
        if s.nil? || Builtins.size(s) == 0
          error = "Empty label"
        elsif Builtins.size(Builtins.filterchars(s, "&")) != 1
          error = "Label has no shortcut or more than 1 shortcuts"
        end
      when "widget"
        s = Convert.to_symbol(value)
        error = "No widget specified" if s.nil?
      when "custom_widget"
        s = Convert.to_term(value)
        error = "No custom widget specified" if s.nil?
      end

      return true if error == ""

      Builtins.y2error("Error on key %1 of widget %2: %3", key, widget, error)
      false
    end

    # @param widgets [Array<::CWM::WidgetHash>] list of widget maps
    # @return [Integer] the lowest 'ui_timeout' value, or 0
    def GetLowestTimeout(widgets)
      minimum = 0
      Builtins.foreach(widgets) do |w|
        timeout = Ops.get_integer(w, "ui_timeout", 0)
        if (Ops.less_than(timeout, minimum) && Ops.greater_than(timeout, 0)) ||
            minimum == 0
          minimum = timeout
        end
      end
      minimum
    end

    # Add fallback functions to a widget
    # global only because of testsuites
    # @param [Array<::CWM::WidgetHash>] widgets a list of widget desctiption maps
    # @param [Hash] functions map of functions
    # @return a list of modified widget description maps
    def mergeFunctions(widgets, functions)
      widgets = deep_copy(widgets)
      functions = deep_copy(functions)
      functions = Builtins.filter(functions) { |k, _v| Ops.is_string?(k) }
      fallback_functions = Convert.convert(
        functions,
        from: "map",
        to:   "map <string, any>"
      )
      Builtins.maplist(widgets) do |w|
        Convert.convert(
          Builtins.union(fallback_functions, w),
          from: "map",
          to:   "map <string, any>"
        )
      end
    end

    # Set widgets according to internally stored settings
    # global only because of testsuites
    # @param [Array<::CWM::WidgetHash>] widgets list of maps representing widgets
    def initWidgets(widgets)
      widgets = deep_copy(widgets)
      Builtins.foreach(widgets) do |w|
        # set initial properties
        valid_chars = Ops.get_string(w, "valid_chars")
        if !valid_chars.nil?
          UI.ChangeWidget(
            Id(Ops.get_string(w, "_cwm_key", "")),
            :ValidChars,
            valid_chars
          )
        end
        # set initial values
        self.processed_widget = deep_copy(w)
        toEval = Convert.convert(
          Ops.get(w, "init"),
          from: "any",
          to:   "void (string)"
        )
        toEval&.call(Ops.get_string(w, "_cwm_key", ""))
      end

      nil
    end

    # Handle change of widget after event generated
    # global only because of testsuites
    # @param [Array<::CWM::WidgetHash>] widgets list of maps represenging widgets
    # @param [Hash] event_descr map event that occured
    # @return [Symbol] modified action (sometimes may be needed) or nil
    def handleWidgets(widgets, event_descr)
      event_descr = deep_copy(event_descr)
      ret = nil
      Builtins.foreach(widgets) do |w|
        if ret.nil?
          self.processed_widget = deep_copy(w)
          events = Ops.get_list(w, "handle_events", [])
          toEval = Convert.convert(
            Ops.get(w, "handle"),
            from: "any",
            to:   "symbol (string, map)"
          )
          if !toEval.nil? &&
              (events == [] ||
                Builtins.contains(events, Ops.get(event_descr, "ID")))
            ret = toEval.call(Ops.get_string(w, "_cwm_key", ""), event_descr)
          end
        end
      end
      ret
    end

    # Save changes of widget after event generated
    # global only because of testsuites
    # CWMTab uses it too
    # @param [Array<::CWM::WidgetHash>] widgets list of maps represenging widgets
    # @param [Hash] event map event that occured
    def saveWidgets(widgets, event)
      widgets = deep_copy(widgets)
      event = deep_copy(event)
      Builtins.foreach(widgets) do |w|
        self.processed_widget = deep_copy(w)
        toEval = Convert.convert(
          Ops.get(w, "store"),
          from: "any",
          to:   "void (string, map)"
        )
        toEval&.call(Ops.get_string(w, "_cwm_key", ""), event)
      end

      nil
    end

    # Cleanup after dialog was finished (independently on what event)
    # global only because of testsuites
    # @param [Array<::CWM::WidgetHash>] widgets list of maps represenging widgets
    def cleanupWidgets(widgets)
      widgets = deep_copy(widgets)
      Builtins.foreach(widgets) do |w|
        self.processed_widget = deep_copy(w)
        toEval = Convert.convert(
          Ops.get(w, "cleanup"),
          from: "any",
          to:   "void (string)"
        )
        toEval&.call(Ops.get_string(w, "_cwm_key", ""))
      end

      nil
    end

    # functions

    # Return description map of currently processed widget
    # @return [Hash] description map of currently processed widget
    def GetProcessedWidget
      deep_copy(@processed_widget)
    end

    # Create a term with OK and Cancel buttons placed horizontally
    # @return [::CWM::UITerm] the term (HBox)
    def OkCancelBox
      ButtonBox(
        PushButton(
          Id(:_tp_ok),
          Opt(:key_F10, :default, :okButton),
          Label.OKButton
        ),
        PushButton(
          Id(:_tp_cancel),
          Opt(:key_F9, :cancelButton),
          Label.CancelButton
        )
      )
    end

    # Validate widget description map, check for maps structure
    # Also checks option description maps if present
    # @param [Hash{String => ::CWM::WidgetHash}] widgets map widgets description map
    # @return [Boolean] true on success
    def ValidateMaps(widgets)
      widgets = deep_copy(widgets)
      ret = true
      Builtins.foreach(widgets) do |k, v|
        Builtins.foreach(v) do |kk, vv|
          ret = ValidateValueType(kk, vv, k) && ret
        end
        to_check = case Ops.get(v, "widget")
        when :custom
          ["custom_widget"]
        when :empty
          []
        else
          ["label", "widget"]
        end
        Builtins.foreach(to_check) do |key|
          if key != "label" ||
              (Ops.get(v, "widget") != :radio_buttons &&
                  Ops.get(v, "widget") != :custom &&
                  Ops.get(v, "widget") != :rich_text &&
                  Ops.get(v, "widget") != :func)
            ret = ValidateValueContents(key, Ops.get(v, key), k) && ret
          end
        end
        if Ops.get(v, "widget") == :custom
          ret = ValidateValueContents(
            "custom_widget",
            Ops.get(v, "custom_widget"),
            k
          ) && ret
        end
        # validate widget-specific entries
        if Builtins.haskey(v, "_cwm_do_validate")
          val_func = Convert.convert(
            Ops.get(v, "_cwm_do_validate"),
            from: "any",
            to:   "boolean (string, map <string, any>)"
          )
          ret = val_func.call(k, v) && ret if !val_func.nil?
        end
      end
      ret
    end

    # Prepare a widget for usage
    # @param [::CWM::WidgetHash] widget_descr map widget description map
    # @return [::CWM::WidgetHash] modified widget description map
    #    where "widget" key is a {::CWM::UITerm}
    def prepareWidget(widget_descr)
      widget_descr = deep_copy(widget_descr)
      w = deep_copy(widget_descr)
      widget = Ops.get_symbol(w, "widget", :inputfield)

      if Ops.get(w, "widget") == :empty
        Ops.set(w, "widget", VBox())
      elsif Ops.get(w, "widget") == :custom &&
          Ops.get(w, "custom_widget")
        Ops.set(w, "widget", Ops.get_term(w, "custom_widget") { VSpacing(0) })
      elsif Ops.get(w, "widget") == :func
        toEval = Convert.convert(
          Ops.get(w, "widget_func"),
          from: "any",
          to:   "term ()"
        )
        if toEval.nil?
          Ops.set(w, "widget", VBox())
        else
          Ops.set(w, "widget", toEval.call)
        end
      else
        id_term = Id(Ops.get_string(w, "_cwm_key", ""))
        opt_term = Opt()
        Builtins.foreach(Ops.get_list(w, "opt", [])) do |o|
          opt_term = Builtins.add(opt_term, o)
        end
        label = Ops.get_string(w, "label", Ops.get_string(w, "_cwm_key", ""))

        case widget
        when :inputfield, :textentry
          # backward compatibility
          opt_term = Builtins.add(opt_term, :hstretch) if !Builtins.contains(Builtins.argsof(opt_term), :hstretch)
          Ops.set(w, "widget", InputField(id_term, opt_term, label))
        when :password
          Ops.set(w, "widget", Password(id_term, opt_term, label))
        when :checkbox
          Ops.set(w, "widget", CheckBox(id_term, opt_term, label))
        when :combobox
          Ops.set(
            w,
            "widget",
            ComboBox(
              id_term,
              opt_term,
              label,
              Builtins.maplist(Ops.get_list(w, "items", [])) do |i|
                Item(Id(Ops.get(i, 0, "")), Ops.get(i, 1, Ops.get(i, 0, "")))
              end
            )
          )
        when :selection_box
          Ops.set(
            w,
            "widget",
            SelectionBox(
              id_term,
              opt_term,
              label,
              Builtins.maplist(Ops.get_list(w, "items", [])) do |i|
                Item(Id(Ops.get(i, 0, "")), Ops.get(i, 1, Ops.get(i, 0, "")))
              end
            )
          )
        when :multi_selection_box
          Ops.set(
            w,
            "widget",
            MultiSelectionBox(
              id_term,
              opt_term,
              label,
              Builtins.maplist(Ops.get_list(w, "items", [])) do |i|
                Item(Id(Ops.get(i, 0, "")), Ops.get(i, 1, Ops.get(i, 0, "")))
              end
            )
          )
        when :intfield
          min = Ops.get_integer(w, "minimum", 0)
          max = Ops.get_integer(w, "maximum", (2**31) - 1) # libyui support only signed int
          Ops.set(
            w,
            "widget",
            IntField(id_term, opt_term, label, min, max, min)
          )
        when :radio_buttons
          hspacing = Ops.get_integer(w, "hspacing", 0)
          vspacing = Ops.get_integer(w, "vspacing", 0)
          buttons = VBox(VSpacing(vspacing))
          Builtins.foreach(Ops.get_list(w, "items", [])) do |i|
            buttons = Builtins.add(
              buttons,
              Left(
                RadioButton(
                  Id(Ops.get(i, 0, "")),
                  opt_term,
                  Ops.get(i, 1, Ops.get(i, 0, ""))
                )
              )
            )
            buttons = Builtins.add(buttons, VSpacing(vspacing))
          end
          Ops.set(
            w,
            "widget",
            Frame(
              label,
              HBox(
                HSpacing(hspacing),
                RadioButtonGroup(id_term, buttons),
                HSpacing(hspacing)
              )
            )
          )
        when :radio_button
          Ops.set(w, "widget", RadioButton(id_term, opt_term, label))
        when :push_button
          Ops.set(w, "widget", PushButton(id_term, opt_term, label))
        when :menu_button
          Ops.set(
            w,
            "widget",
            MenuButton(
              id_term,
              opt_term,
              label,
              Builtins.maplist(Ops.get_list(w, "items", [])) do |i|
                Item(Id(Ops.get(i, 0, "")), Ops.get(i, 1, Ops.get(i, 0, "")))
              end
            )
          )
        when :multi_line_edit
          Ops.set(w, "widget", MultiLineEdit(id_term, opt_term, label))
        when :richtext
          Ops.set(w, "widget", RichText(id_term, opt_term, ""))
        when :date_field
          Ops.set(w, "widget", DateField(id_term, opt_term, label))
        when :time_field
          Ops.set(w, "widget", TimeField(id_term, opt_term, label))
        end
      end
      Ops.set(w, "custom_widget", nil) # not needed any more
      deep_copy(w)
    end

    # Validate single widget
    # @param [::CWM::WidgetHash] widget widget description map
    # @param [Hash] event map event that caused validation
    # @param [String] key widget key for validation by function
    # @return true if validation succeeded
    def validateWidget(widget, event, key)
      widget = deep_copy(widget)
      event = deep_copy(event)
      self.processed_widget = deep_copy(widget)
      failed = false
      val_type = Ops.get_symbol(widget, "validate_type")
      case val_type
      when :function, :function_no_popup
        toEval = Convert.convert(
          Ops.get(widget, "validate_function"),
          from: "any",
          to:   "boolean (string, map)"
        )
        failed = !toEval.call(key, event) if !toEval.nil?
      when :regexp
        regexp = Ops.get_string(widget, "validate_condition", "")
        if !Builtins.regexpmatch(
          Convert.to_string(UI.QueryWidget(Id(:_tp_value), :Value)),
          regexp
        )
          failed = true
        end
      when :list
        possible = Ops.get_list(widget, "validate_condition", [])
        failed = true if !Builtins.contains(possible, UI.QueryWidget(Id(:_tp_value), :Value))
      end

      if failed && val_type != :function
        error = Ops.get_string(widget, "validate_help", "")
        if error == ""
          wname = Ops.get_string(
            widget,
            "label",
            Ops.get_string(widget, "_cwm_key", "")
          )
          wname = Builtins.deletechars(wname, "&")
          # message popup, %1 is a label of some widget
          error = Builtins.sformat(_("The value of %1 is invalid."), wname)
        end
        UI.SetFocus(Id(Ops.get_string(widget, "_cwm_key", "")))
        Report.Error(error)
      end
      !failed
    end

    # Validate dialog contents for allow it to be saved
    # @param [Array<::CWM::WidgetHash>] widgets list of widgets to validate
    # @param [Hash] event map event that caused validation
    # @return [Boolean] true if everything is OK, false  if something is wrong
    def validateWidgets(widgets, event)
      widgets = deep_copy(widgets)
      event = deep_copy(event)
      result = true
      Builtins.foreach(widgets) do |w|
        widget_key = Ops.get_string(w, "_cwm_key", "")
        result &&= validateWidget(w, event, widget_key)
      end
      @validation_failed_handler.call if !result && !@validation_failed_handler.nil?
      result
    end

    # Read widgets with listed names
    # @param [Array<String>] names a list of strings/symbols names of widgets
    # @param [Hash <String, ::CWM::WidgetHash] source a map containing the widgets
    # @return [Array<::CWM::WidgetHash>] of maps representing widgets
    def CreateWidgets(names, source)
      names = deep_copy(names)
      source = deep_copy(source)
      ValidateMaps(source) # FIXME: find better place
      ret = Builtins.maplist(names) do |w|
        m = Ops.get(source, w, {})
        # leave add here in order to make a copy of the structure
        # eval isn't usable because the map may contain terms, that can't
        # be evaluated here
        m = Builtins.add(m, "_cwm_key", w)
        deep_copy(m)
      end
      ret = Builtins.maplist(ret) { |w| prepareWidget(w) }
      deep_copy(ret)
    end

    # Merge helps from the widgets
    # @param [Array<::CWM::WidgetHash>] widgets a list of widget description maps
    # @return [String] merged helps of the widgets
    def MergeHelps(widgets)
      return "" unless widgets

      helps = widgets.map do |widget|
        help = widget["help"]
        help.respond_to?(:call) ? help.call : help
      end
      helps.compact!
      helps.join("\n")
    end

    # Prepare the dialog, replace strings in the term with appropriate
    # widgets
    # @param dialog  [::CWM::StringTerm] term dialog containing strings
    # @param widgets [Array<::CWM::WidgetHash>] list of widget description maps
    # @return [::CWM::UITerm] updated term ready to be used as a dialog
    def PrepareDialog(dialog, widgets)
      return dialog.clone if dialog.empty?

      m = widgets.map do |w|
        widget_key = w.fetch("_cwm_key", "")
        [widget_key, w]
      end.to_h
      ProcessTerm(dialog, m)
    end

    # Replace help for a particular widget
    # @param [String] widget string widget ID of widget to replace help,
    # if nil is passed, then just regenerate help. Useful for refresh dynamic help content.
    # @param [String] help string new help to the widget. If widget is nil, then this argument is ignored
    #
    # @example change help content for widget w which had static help
    #   Yast::CWM.ReplaceWidgetHelp("my_widget", "my new free-cool-in help")
    # @example refresh help for widget with dynamic content
    #   Yast::CWM.ReplaceWidgetHelp
    def ReplaceWidgetHelp(widget = nil, help = "")
      if widget
        @current_dialog_widgets = Builtins.maplist(@current_dialog_widgets) do |w|
          Ops.set(w, "help", help) if Ops.get_string(w, "_cwm_key", "") == widget
          deep_copy(w)
        end
      end
      help = MergeHelps(@current_dialog_widgets)
      Wizard.RestoreHelp(help)

      nil
    end

    # A hook to handle Alt-Ctrl-Shift-D
    def handleDebug
      Builtins.y2debug("Handling a debugging event")

      nil
    end

    # Generic function to create dialog and handle it's events
    # @param [Array<::CWM::WidgetHash>] widgets list of widget maps
    # @param [Hash] functions map initialize/save/handle fallbacks if not specified
    #   with the widgets.
    # @param [Array<Object>] skip_store_for list of events for which the value of the widget will not be stored
    #   Useful mainly for non-standard redraw of widgets, like :reset or :redraw. It will skip also
    #   validation, because it is not needed as nothing is stored.
    # @return [Symbol] wizard sequencer symbol
    def Run(widgets, functions, skip_store_for: [])
      widgets = deep_copy(widgets)
      functions = deep_copy(functions)
      widgets = mergeFunctions(widgets, functions)
      PushSettings()
      @current_dialog_widgets = deep_copy(widgets)
      initWidgets(widgets)

      # allow a handler to enable/disable widgets before the first real
      # UserInput takes place
      UI.FakeUserInput("ID" => "_cwm_wakeup")

      ret = nil
      save_exits = [:next, :ok]
      save = false
      event_descr = {}
      timeout = GetLowestTimeout(widgets)
      while ret != :back && ret != :abort && !save
        event_descr = if Ops.greater_than(timeout, 0)
          UI.WaitForEvent(timeout)
        else
          UI.WaitForEvent
        end
        ret = Ops.get(event_descr, "ID")
        handleDebug if Ops.get_string(event_descr, "EventType", "") == "DebugEvent"
        handle_ret = handleWidgets(widgets, event_descr)
        if !handle_ret.nil? ||
            (Ops.is_symbol?(ret) && Builtins.contains(save_exits, ret))
          save = true
          if !handle_ret.nil?
            ret = handle_ret
            Ops.set(event_descr, "ID", ret)
          end
        end

        ret = :abort if ret == :cancel

        if ret == :next && functions[:next]
          ret = nil unless functions[:next].call
          save = false if ret.nil?
        elsif ret == :back && functions[:back]
          ret = nil unless functions[:back].call
        elsif ret == :abort && functions[:abort]
          ret = nil unless functions[:abort].call
        end

        next if ret.nil?

        # ok, so what happens here? event want to save widgets, so check that there is no explicit
        # skip of  storing for this event and there is a widget containing invalid value.
        # In such case do not save and clear ret, so we are still in loop
        if save && !skip_store_for.include?(ret) && !validateWidgets(widgets, event_descr)
          ret = nil
          save = false
        end
      end
      saveWidgets(widgets, event_descr) if save && !skip_store_for.include?(ret)
      cleanupWidgets(widgets)
      PopSettings()
      Convert.to_symbol(ret)
    end

    # Disable given bottom buttons of the wizard sequencer
    # @param buttons [Array<String>] list of buttons to be disabled:
    #   "back_button", "abort_button", "next_button"
    def DisableButtons(buttons)
      Builtins.foreach(buttons) do |button|
        Wizard.DisableBackButton if button == "back_button"
        Wizard.DisableAbortButton if button == "abort_button"
        Wizard.DisableNextButton if button == "next_button"
      end

      nil
    end

    # Adjust the labels of the bottom buttons of the wizard sequencer
    # @param [String] next_ label of the "Next" button
    # @param [String] back string label of the "Back" button
    # @param [String] abort string label of the "Abort" button
    # @param [String] _help unused parameter since help button cannot be hide anyway
    def AdjustButtons(next_, back, abort, _help)
      next_ = "" if next_.nil?
      back = "" if back.nil?
      abort = "" if abort.nil?
      if next_ == ""
        Wizard.HideNextButton
      else
        Wizard.SetNextButton(:next, next_)
      end

      if abort == ""
        Wizard.HideAbortButton
      else
        Wizard.SetAbortButton(:abort, abort)
      end

      if back == ""
        Wizard.HideBackButton
      else
        Wizard.SetBackButton(:back, back)
      end

      nil
    end

    # Set handler to be called after validation of a dialog failed
    # @param [void ()] handler a function reference to be caled. If nil, nothing is called
    def SetValidationFailedHandler(handler)
      @validation_failed_handler = deep_copy(handler)

      nil
    end

    # Display the dialog and run its event loop using new widget API
    # @param contents [::CWM::WidgetTerm] UI term including instances of {CWM::AbstractWidget}
    # @param caption [String] caption of dialog
    # @param back_button [String, nil] label for dialog back button,
    #   `nil` to use the default label, `""` to omit the button
    # @param next_button [String, nil] label for dialog next button,
    #   `nil` to use the default label, `""` to omit the button
    # @param abort_button [String, nil] label for dialog abort button,
    #   `nil` to use the default label, `""` to omit the button
    # @param skip_store_for [Array] list of events for which the value of the widget will not be stored.
    #   Useful mainly when some widget returns an event that should not trigger the storing,
    #   like a reset button or a redrawing.  It will skip also validation, because it is not needed
    #   as nothing is stored.
    # @param disable_buttons [Array<String>] buttons to disable:
    #   "back_button", "abort_button", "next_button"
    # @param next_handler [Proc] handler that is called after clicking on next. If it returns false,
    #   then it does not go next. If it returns true, then :next symbol is returned. If handler is not
    #   defined, then it acts like if it returns true.
    # @param back_handler [Proc] handler that is called after clicking on back. If it returns false,
    #   then it does not go back. If it returns true, then :back symbol is returned. If handler is not
    #   defined, then it acts like if it returns true.
    # @param abort_handler [Proc] handler that is called after clicking on abort. If it returns false,
    #   then it stops abort. If it returns true, then :abort symbol is returned. If handler is not
    #   defined, then it acts like if it returns true.
    #
    # @return [Symbol] wizard sequencer symbol
    def show(contents, caption: nil, back_button: nil, next_button: nil, abort_button: nil, skip_store_for: [],
      disable_buttons: [], next_handler: nil, back_handler: nil, abort_handler: nil)
      widgets = widgets_in_contents(contents)
      options = {
        "contents"     => widgets_contents(contents),
        "widget_names" => widgets.map(&:widget_id),
        "widget_descr" => widgets.map { |w| [w.widget_id, w.cwm_definition] }.to_h
      }
      options["caption"] = caption if caption
      options["back_button"] = back_button if back_button
      options["next_button"] = next_button if next_button
      options["abort_button"] = abort_button if abort_button
      options["skip_store_for"] = skip_store_for
      options["disable_buttons"] = disable_buttons
      options["fallback_functions"] = {}
      options["fallback_functions"][:next] = Yast.fun_ref(next_handler, "boolean ()") if next_handler
      options["fallback_functions"][:back] = Yast.fun_ref(back_handler, "boolean ()") if back_handler
      options["fallback_functions"][:abort] = Yast.fun_ref(abort_handler, "boolean ()") if abort_handler

      ShowAndRun(options)
    end

    # Display the dialog and run its event loop
    # @param [Hash<String, Object>] settings a map of all settings needed to run the dialog
    # @option settings [Array<CWM::AbstractWidget>] "widgets" list of widgets used in CWM,
    #   it is auto added to `"widget_names"` and `"widget_descr"`
    def ShowAndRun(settings)
      settings = deep_copy(settings)
      if settings["widgets"]
        widgets = settings["widgets"]
        settings["widget_names"] ||= []
        settings["widget_names"] += widgets.map(&:widget_id)
        settings["widget_descr"] ||= {}
        settings["widget_descr"] = widgets.map { |w| [w.widget_id, w.cwm_definition] }.to_h
      end
      widget_descr = Ops.get_map(settings, "widget_descr", {})
      contents = Ops.get_term(settings, "contents", VBox())
      widget_names = Convert.convert(
        Ops.get(settings, "widget_names") { StringsOfTerm(contents) },
        from: "any",
        to:   "list <string>"
      )
      caption = Ops.get_string(settings, "caption", "")
      back_button = Ops.get_string(settings, "back_button") { Label.BackButton }
      next_button = Ops.get_string(settings, "next_button") { Label.NextButton }
      abort_button = Ops.get_string(settings, "abort_button") do
        Label.AbortButton
      end
      fallback = Ops.get_map(settings, "fallback_functions", {})

      w = CreateWidgets(widget_names, widget_descr)
      help = MergeHelps(w)
      contents = PrepareDialog(contents, w)
      Wizard.SetContentsButtons(
        caption,
        contents,
        help,
        back_button,
        next_button
      )
      AdjustButtons(next_button, back_button, abort_button, nil)
      DisableButtons(Ops.get_list(settings, "disable_buttons", []))

      skip_store_for = settings["skip_store_for"] || []
      Run(w, fallback, skip_store_for: skip_store_for)
    end

    # Display the dialog and run its event loop
    # @param [Array<String>] widget_names list of names of widgets that will be used in the
    #   dialog
    # @param [Hash{String => ::CWM::WidgetHash}] widget_descr map description map of all widgets
    # @param contents [::CWM::StringTerm] contents of the dialog, identifiers instead of
    #   widgets
    # @param [String] caption string dialog caption
    # @param [String] back_button string label of the back button
    # @param [String] next_button string label of the next button
    # @param [Hash] fallback map initialize/save/handle fallbacks if not specified
    #   with the widgets.
    # @return [Symbol] wizard sequencer symbol
    # @deprecated Use {#show} or {#ShowAndRun}
    def ShowAndRunOrig(widget_names, widget_descr, contents, caption, back_button, next_button, fallback)
      widget_names = deep_copy(widget_names)
      widget_descr = deep_copy(widget_descr)
      contents = deep_copy(contents)
      fallback = deep_copy(fallback)
      ShowAndRun(
        "widget_names"       => widget_names,
        "widget_descr"       => widget_descr,
        "contents"           => contents,
        "caption"            => caption,
        "back_button"        => back_button,
        "next_button"        => next_button,
        "fallback_functions" => fallback
      )
    end

    # useful handlers

    # Do-nothing replacement for a widget initialization function.
    # Used for push buttons if all the other widgets have a fallback.
    # @param [String] _key id of the widget
    def InitNull(_key)
      nil
    end

    # Do-nothing replacement for a widget storing function.
    # Used for push buttons if all the other widgets have a fallback.
    # @param [String] _key  id of the widget
    # @param [Hash] _event  the event being handled
    def StoreNull(_key, _event)
      nil
    end

    # Saves changes of all the widgets in the current dialog
    #
    # @param [Hash] event map event that triggered the saving
    def save_current_widgets(event)
      saveWidgets(@current_dialog_widgets, event)
    end

    # Validates all the widgets in the current dialog
    #
    # @param [Hash] event map event that caused validation
    # @return [Boolean] true if everything is OK, false  if something is wrong
    def validate_current_widgets(event)
      validateWidgets(@current_dialog_widgets, event)
    end

    publish function: :StringsOfTerm, type: "list <string> (term)"
    publish function: :ValidateBasicType, type: "boolean (any, string)"
    publish function: :ValidateValueType, type: "boolean (string, any, string)"
    publish function: :mergeFunctions, type: "list <map <string, any>> (list <map <string, any>>, map)"
    publish function: :initWidgets, type: "void (list <map <string, any>>)"
    publish function: :handleWidgets, type: "symbol (list <map <string, any>>, map)"
    publish function: :saveWidgets, type: "void (list <map <string, any>>, map)"
    publish function: :cleanupWidgets, type: "void (list <map <string, any>>)"
    publish function: :GetProcessedWidget, type: "map <string, any> ()"
    publish function: :OkCancelBox, type: "term ()"
    publish function: :ValidateMaps, type: "boolean (map <string, map <string, any>>)"
    publish function: :prepareWidget, type: "map <string, any> (map <string, any>)"
    publish function: :validateWidget, type: "boolean (map <string, any>, map, string)"
    publish function: :validateWidgets, type: "boolean (list <map <string, any>>, map)"
    publish function: :CreateWidgets, type: "list <map <string, any>> (list <string>, map <string, map <string, any>>)"
    publish function: :MergeHelps, type: "string (list <map <string, any>>)"
    publish function: :PrepareDialog, type: "term (term, list <map <string, any>>)"
    publish function: :ReplaceWidgetHelp, type: "void (string, string)"
    publish function: :Run, type: "symbol (list <map <string, any>>, map)"
    publish function: :DisableButtons, type: "void (list <string>)"
    publish function: :AdjustButtons, type: "void (string, string, string, string)"
    publish function: :SetValidationFailedHandler, type: "void (void ())"
    publish function: :ShowAndRun, type: "symbol (map <string, any>)"
    publish function: :ShowAndRunOrig, type: "symbol (list <string>, map <string, map <string, any>>, term, string, string, string, map)"
    publish function: :InitNull, type: "void (string)"
    publish function: :StoreNull, type: "void (string, map)"

    # @return [Array<::CWM::AbstractWidget>]
    def widgets_in_contents(contents)
      contents.each_with_object([]) do |arg, res|
        case arg
        when ::CWM::AbstractWidget
          res << arg
          # if widget have its own content, also search it
          res.concat(widgets_in_contents(arg.contents)) if arg.respond_to?(:contents)
        when Yast::Term then res.concat(widgets_in_contents(arg))
        end
      end
    end

    # @param  [::CWM::WidgetTerm] contents
    # @return [::CWM::StringTerm]
    def widgets_contents(contents)
      res = contents.clone

      (0..(res.size - 1)).each do |index|
        case contents[index]
        when ::CWM::AbstractWidget then res[index] = res[index].widget_id
        when Yast::Term then res[index] = widgets_contents(res[index])
        end
      end

      res
    end

  private

    # such an accessor enables testing
    def processed_widget=(widget)
      @processed_widget = widget
    end
  end

  CWM = CWMClass.new
  CWM.main
end