From 8e249123f6eeda0346d0173a98eea55de89302b6 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Fri, 10 Nov 2017 13:13:15 +0100 Subject: [PATCH] new generation of popup library --- .../general/example/popup_manual_tester.sh | 1 + library/general/example/popup_params.rb | 43 +++ library/general/example/popup_serie.rb | 56 +++ .../general/example/popup_series_tester.sh | 1 + library/general/src/Makefile.am | 6 +- library/general/src/lib/yast2/popup.rb | 318 ++++++++++++++++++ library/general/src/lib/yast2/popup_rspec.rb | 20 ++ library/general/test/popup_rspec_test.rb | 28 ++ 8 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 library/general/example/popup_manual_tester.sh create mode 100644 library/general/example/popup_params.rb create mode 100644 library/general/example/popup_serie.rb create mode 100644 library/general/example/popup_series_tester.sh create mode 100644 library/general/src/lib/yast2/popup.rb create mode 100644 library/general/src/lib/yast2/popup_rspec.rb create mode 100644 library/general/test/popup_rspec_test.rb diff --git a/library/general/example/popup_manual_tester.sh b/library/general/example/popup_manual_tester.sh new file mode 100644 index 000000000..ec1232bfb --- /dev/null +++ b/library/general/example/popup_manual_tester.sh @@ -0,0 +1 @@ +Y2DIR=$(dirname "$0")/../src /sbin/yast2 $(dirname "$0")/popup_params.rb diff --git a/library/general/example/popup_params.rb b/library/general/example/popup_params.rb new file mode 100644 index 000000000..06bd13dc9 --- /dev/null +++ b/library/general/example/popup_params.rb @@ -0,0 +1,43 @@ +# rubocop:disable all +require "yast" + +require "yast2/popup" + +Yast.import "UI" + +include Yast::UIShortcuts + +def code(params) + "Yast2::Popup.show(#{params})" +end + +content = VBox( + InputField(Id(:params), Opt(:notify), "Popup Params:"), + VSpacing(1), + InputField(Id(:code), Opt(:disable), "Code to run:"), + VSpacing(1), + PushButton(Id(:call), "Show") +) + +Yast::UI.OpenDialog(content) +loop do + ret = Yast::UI.UserInput + case ret + when :params + value = Yast::UI.QueryWidget(:params, :Value) + Yast::UI.ChangeWidget(:code, :Value, code(value)) + when :cancel + break + when :call + begin + value = Yast::UI.QueryWidget(:params, :Value) + eval(code(value)) + rescue => e + Yast2::Popup.show("Failed with #{e.message}") + end + end +end + +Yast::UI.CloseDialog + +nil diff --git a/library/general/example/popup_serie.rb b/library/general/example/popup_serie.rb new file mode 100644 index 000000000..287e0a52e --- /dev/null +++ b/library/general/example/popup_serie.rb @@ -0,0 +1,56 @@ +require "yast" + +require "yast2/popup" + +Yast2::Popup.show("Simple text") + +Yast2::Popup.show("Long text\n" * 50) + +Yast2::Popup.show("Simple text with details", details: "More details here") + +Yast2::Popup.show("Simple text with timeout", timeout: 10) + +Yast2::Popup.show("Continue/Cancel buttons", buttons: :continue_cancel) + +Yast2::Popup.show("Yes/No buttons", buttons: :yes_no) + +Yast2::Popup.show("Yes/No buttons with No focused", buttons: :yes_no, focus: :no) + +Yast2::Popup.show("Yes/No buttons with timeout returning focused item", buttons: :yes_no, focus: :no, timeout: 10) + +Yast2::Popup.show("Own buttons", buttons: { button1: "button 1", button2: "button 2" }, focus: :button2) + +Yast2::Popup.show("Richtext is set to false", richtext: false) + +Yast2::Popup.show("Richtext is set to :auto", richtext: :auto) + +Yast2::Popup.show("Long text. Richtext is set to :auto\n" * 50, richtext: :auto) + +Yast2::Popup.show("Richtext is set to true", richtext: true) + +Yast2::Popup.show("Long text. Richtext is set to true
" * 50, richtext: true) + +Yast2::Popup.show("Long text with newlines. Richtext is set to true\n" * 50, richtext: true) + +Yast2::Popup.show(":notice style", style: :notice) + +Yast2::Popup.show(":important style", style: :important) + +Yast2::Popup.show(":warning style", style: :warning) + +Yast2::Popup.show("Headline set", headline: "Headline") + +Yast2::Popup.show("Headline set to :error", headline: :error) + +Yast2::Popup.show( + "All options", + headline: "Headline", + details: "details", + timeout: 10, + buttons: :yes_no, + focus: :no, + richtext: false, + style: :important +) + +Yast2::Popup.feedback("feedback for time consuming operation", headline: "Syncing...") { sleep(5) } diff --git a/library/general/example/popup_series_tester.sh b/library/general/example/popup_series_tester.sh new file mode 100644 index 000000000..f6f82bffd --- /dev/null +++ b/library/general/example/popup_series_tester.sh @@ -0,0 +1 @@ +Y2DIR=$(dirname "$0")/../src /sbin/yast2 $(dirname "$0")/popup_serie.rb diff --git a/library/general/src/Makefile.am b/library/general/src/Makefile.am index 28fe5d389..e64642716 100644 --- a/library/general/src/Makefile.am +++ b/library/general/src/Makefile.am @@ -93,7 +93,11 @@ ylib2_DATA = \ lib/ui/text_helpers.rb \ lib/ui/widgets.rb -EXTRA_DIST = $(module_DATA) $(client_DATA) $(scrconf_DATA) $(agent_SCRIPTS) $(ydata_DATA) $(fillup_DATA) $(ylib_DATA) $(ylib2_DATA) +ylib3dir = "${yast2dir}/lib/yast2" +ylib3_DATA = \ + lib/yast2/popup.rb + +EXTRA_DIST = $(module_DATA) $(client_DATA) $(scrconf_DATA) $(agent_SCRIPTS) $(ydata_DATA) $(fillup_DATA) $(ylib_DATA) $(ylib2_DATA) $(ylib3_DATA) include $(top_srcdir)/Makefile.am.common diff --git a/library/general/src/lib/yast2/popup.rb b/library/general/src/lib/yast2/popup.rb new file mode 100644 index 000000000..157bcf853 --- /dev/null +++ b/library/general/src/lib/yast2/popup.rb @@ -0,0 +1,318 @@ +require "yast" + +require "erb" + +Yast.import "Label" +Yast.import "UI" + + +module Yast2 + + # Class responsible for showing popups. It have small, but consistent API. + # Intended as replacement for Yast::Popup module. + # @note as UI is not easy to test, it is recommended after modifications to this class run + # examples/popup_series_tester.sh which tests common combination of options. + # @note for rspec tests use popup_rspec where is helper for easier mocking that still does + # argument verifications. + class Popup + class << self + include Yast::I18n + + # Number of lines for richtext: :auto to switch to richtext widget + LINES_THRESHOLD = 20 + + RICHTEXT_WIDTH = 60 + RICHTEXT_HEIGHT = 10 + + # Shows popup and return which symbol of pressed button + # @param message [String] message to show + # @param details [String] hidden details that can be shown, if empty then it is nil shown + # @param headline [String, :error, :warning] sets popup headline. When String is passed it is shown as it is. + # When empty string is passed no headline is shown. + # If Symbol is passed it have to be from predefined set of symbols. + # Note: Symbol it is just predefined strings, does not modify style of popup. + # @param timeout [Integer] how long wait till autoclose dialog. 0 means wait forever. + # @param buttons [Hash, Symbol] specified which buttons popup will have. + # it can be hash in format button_id => button_text, showed in same order as in hash. + # Beware that :details and :stop id is reserved. + # The second option is symbol that specify one of predefined set of buttons. + # Current set of predefined buttons are + # `:ok` -> `{ ok: Label.OKButton}` + # `continue_cancel` -> `{ continue: Label.ContinueButton, cancel: Label.CancelButton }` + # `:yes_no` -> `{ yes: Label.YesButton, no: Label.NoButton }` + # @param focus [Symbol, nil] what button focus. + # Also it is button which is returned if timeout exceed. + # if it is nil, then choose the first button. See buttons parameter. + # @param richtext [Boolean, :auto] if use richtext widget. + # Useful when report contain richtext tags or is long, so it have to be scrollable. + # If :auto is used, then it detect too long text and use richtext for it and! escape + # richtext tags, so if text is really richtext then use true. + # @param style [:notice, :important, :warning] popup dialog styling. :notice is common one, + # :important is brighter and :warning is style when something goes wrong. + # See Yast::UI.OpenDialog options :infocolor and :warncolor. + # @return [Symbol] symbol of pressed button. If timeout appear, + # then button set in focus parameter is returned. If user click on 'x' button in window + # then `:cancel` symbol is returned. + # + # @example pair of old and new API calls + # Yast::Popup.Message(text) + # Yast2::Popup.show(text) + # + # Yast::Popup.MessageDetails(text, details) + # Yast2::Popup.show(text, details: details) + # + # Yast::Popup.TimedError(text, seconds) + # Yast2::Popup.show(text, headline: :error, timeout: seconds) + # + # Yast::Popup.TimedErrorAnyQuestion(headline, message, yes_button_message, no_button_message, focus, timeout_seconds) + # Yast2::Popup.show(message, headline: headline, timeout: timeout_seconds, buttons: { yes: yes_button_message, no: no_button_message), focus: :yes) + + # Yast::Popup.TimedLongNotify(message, timeout_seconds) + # Yast2::Popup.show(message, richtext: true, timeout: timeout_seconds) + def show(message, details: "", headline: "", timeout: 0, focus: nil, buttons: :ok, richtext: :auto, style: :notice) + textdomain "base" + buttons = generate_buttons(buttons) + headline = generate_headline(headline) + # add default focus button before adding details, as details should not be focussed + focus = buttons.keys.first if focus.nil? + buttons = add_details_button(buttons) unless details.empty? + buttons = add_stop_button(buttons) if timeout > 0 + check_arguments!(message, details, timeout, focus, buttons) + content_res = content(body(headline, message, richtext, timeout), buttons) + + event_loop(content_res, focus, timeout, details, style) + end + + # Shows feedback till given block does not finish. + # @param message [String] message to show + # @param headline [String] sets popup headline. String is shown. + # If empty string is passed no headline is shown. + # @yield [] operation that needs to show feedback + # @return result of block + def feedback(message, headline: "", &block) + headline = generate_headline(headline) + if !message.is_a?(::String) + raise ArgumentError, "Invalid value #{message.inspect} of parameter message" + end + + body = VBox( + VSpacing(0.4), + *headline_widgets(headline), + Left(Label(message)) + ) + content_res = content(body, {}) + + res = Yast::UI.OpenDialog(content_res) + raise "Failed to open dialog, see logs." unless res + begin + block.call + ensure + Yast::UI.CloseDialog + end + end + + private + + include Yast::UIShortcuts + + def check_arguments!(message, details, timeout, focus, buttons) + if !message.is_a?(::String) + raise ArgumentError, "Invalid value #{message.inspect} of parameter message" + end + + if !details.is_a?(::String) + raise ArgumentError, "Invalid value #{details.inspect} of parameter details" + end + + if !timeout.is_a?(::Integer) + raise ArgumentError, "Invalid value #{timeout.inspect} of parameter timeout" + end + + if !buttons.key?(focus) + raise ArgumentError, "Invalid value #{focus.inspect} for parameter focus. " \ + "Known buttons: #{buttons.keys}." + end + end + + def generate_buttons(buttons) + case buttons + when ::Hash + buttons + when :ok + { ok: Yast::Label.OKButton } + when :continue_cancel + { continue: Yast::Label.ContinueButton, cancel: Yast::Label.CancelButton } + when :yes_no + { yes: Yast::Label.YesButton, no: Yast::Label.NoButton } + else + raise ArgumentError, "Invalid value #{buttons.inspect} for parameter buttons." + end + end + + def generate_headline(headline) + case headline + when ::String + headline + when :warning + Yast::Label.WarningMsg + when :error + Yast::Label.ErrorMsg + else + raise ArgumentError, "Invalid value #{headline.inspect} for parameter headline." + end + end + + def add_details_button(buttons) + # use this way merge to have details as first place button + { details: _("&Details...")}.merge(buttons) + end + + def add_stop_button(buttons) + # use this way merge to have details as first place button + buttons.merge({ stop: Yast::Label.StopButton }) + end + + def headline_widgets(headline) + if headline.empty? + [Empty()] + else + [Left(Heading(headline)), VSpacing(0.2)] + end + end + + def timeout_widget(timeout) + if timeout > 0 + Label(Id(:label), timeout.to_s) + else + Empty() + end + end + + def plain_to_richtext(text) + ERB::Util.html_escape(text).gsub("\n", "
") + end + + def message_widget(message, richtext) + case richtext + when true + HBox( + VSpacing(RICHTEXT_HEIGHT), + VBox( + HSpacing(RICHTEXT_WIDTH), + RichText(message), + ) + ) + when false + Left(Label(message)) + when :auto + if message.lines.size >= LINES_THRESHOLD + message_widget(plain_to_richtext(message), true) + else + message_widget(message, false) + end + else + raise ArgumentError, "Invalid richtext paramter #{richtext.inspect}" + end + end + + def body(headline, message, richtext, timeout) + VBox( + VSpacing(0.4), + *headline_widgets(headline), + message_widget(message, richtext), + VSpacing(0.2), + timeout_widget(timeout) + ) + end + + def content(body, buttons) + HBox( + HSpacing(1), + VBox( + VSpacing(0.2), + body, + VSpacing(1), + button_box(buttons), + VSpacing(0.2) + ), + HSpacing(1), + ) + end + + def button_box(buttons) + push_buttons = buttons.map do |id, label| + # lets auto detect options for button box + opt = case id + when :ok, :yes, :continue + Opt(:key_F10, :okButton) + when :cancel, :no + Opt(:key_F9, :cancelButton) + else + Opt(:customButton) + end + + PushButton(Id(id), opt, label) + end + + return Empty() if push_buttons.empty? + + # relax sanity check as there can be situation where is + # e.g. OK and details with `show(text, details: text2)` + ButtonBox(Opt(:relaxSanityCheck), *push_buttons) + end + + def event_loop(content, focus, timeout, details, style) + res = Yast::UI.OpenDialog(dialog_options(style), content) + remaining_time = timeout + raise "Failed to open dialog, see logs." unless res + begin + Yast::UI.SetFocus(focus) + loop do + res = timeout > 0 ? Yast::UI.TimeoutUserInput(1000) : Yast::UI.UserInput + remaining_time -= 1 + res = handle_event(res, details, remaining_time, focus) + return res if res + end + ensure + Yast::UI.CloseDialog + end + end + + def dialog_options(style) + case style + when :notice + Opt() + when :important + Opt(:infocolor) + when :warning + Opt(:warncolor) + else + raise ArgumentError, "Invalid style parameter #{style.inspect}" + end + end + + def handle_event(res, details, remaining_time, focus) + case res + when :details + show(details) + nil + when :timeout + if remaining_time <= 0 + :focus + else + Yast::UI.ChangeWidget(:label, :Value, remaining_time.to_s) + nil + end + when :stop + loop do + res = Yast::UI.UserInput + res = handle_event(res, details, remaining_time, focus) + return res if res + end + else + res + end + end + end + end +end diff --git a/library/general/src/lib/yast2/popup_rspec.rb b/library/general/src/lib/yast2/popup_rspec.rb new file mode 100644 index 000000000..e75e9645f --- /dev/null +++ b/library/general/src/lib/yast2/popup_rspec.rb @@ -0,0 +1,20 @@ +require "yast" + +Yast.import "UI" + +require "yast2/popup" + +def expect_to_show_popup_which_return(output) + expect(Yast2::Popup).to receive(:show).and_call_original + allow(Yast::UI).to receive(:OpenDialog).and_return true + allow(Yast::UI).to receive(:CloseDialog) + allow(Yast::UI).to receive(:SetFocus).and_return true + allow(Yast::UI).to receive(:UserInput).and_return output + allow(Yast::UI).to receive(:TimeoutUserInput).and_return output +end + +def expect_to_show_feedback + expect(Yast2::Popup).to receive(:feedback).and_call_original + allow(Yast::UI).to receive(:OpenDialog).and_return true + allow(Yast::UI).to receive(:CloseDialog) +end diff --git a/library/general/test/popup_rspec_test.rb b/library/general/test/popup_rspec_test.rb new file mode 100644 index 000000000..9827f7f08 --- /dev/null +++ b/library/general/test/popup_rspec_test.rb @@ -0,0 +1,28 @@ +require_relative "test_helper" + +require "yast2/popup" +require "yast2/popup_rspec" + +describe "expect_to_show_popup_which_return" do + it "returns parameter from popup" do + expect_to_show_popup_which_return(:test) + expect(Yast2::Popup.show("test")).to eq :test + end + + it "let popup raise argument error if wrong arguments are passed" do + expect_to_show_popup_which_return(:test) + expect{Yast2::Popup.show("test", buttons: nil)}.to raise_error(ArgumentError) + end +end + +describe "expect_to_show_feedback" do + it "returns value from block" do + expect_to_show_feedback + expect(Yast2::Popup.feedback("test") { :test }).to eq :test + end + + it "let feedback raise argument error if wrong arguments are passed" do + expect_to_show_feedback + expect{Yast2::Popup.feedback(nil)}.to raise_error(ArgumentError) + end +end