Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DelayedProgressPopup for File Conflicts Progress during Package Installation [master] #1252

Merged
merged 15 commits into from Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
242 changes: 242 additions & 0 deletions library/general/src/lib/ui/delayed_progress_popup.rb
@@ -0,0 +1,242 @@
# ------------------------------------------------------------------------------
# Copyright (c) 2022 SUSE LLC
#
# 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.
#
# ------------------------------------------------------------------------------

require "yast2/system_time"
require "yast"

module Yast
# Progress popup dialog that only opens after a certain delay, so it never
# opens for very short operations (< 4 seconds by default), only when an
# operation takes long enough to actually give feedback to the user.
#
# This is less disruptive than a progress dialog that always opens, and in
# most cases, flashes by so fast that the user can't recognize what it says.
#
# The tradeoff is that it takes a few seconds until there is any visual
# feedback (until the delay is expired).
#
# Notice that this does not use an active timer; the calling application has
# to trigger the check for the timeout by calling progress() in regular
# intervals.
#
# You can change the delay by changing the delay_seconds member variable, you
# can force the dialog to open with open!, and you can stop and (re-) start
# the timer.
#
# In any case, when done with this progress reporting, call close(). You
# don't need to check if it ever opened; close() does that automatically.
#
# see examples/delayed_progress_1.rb for a usage example.
#
class DelayedProgressPopup
include Yast::UIShortcuts
include Yast::Logger

# @return [String] Text for the dialog heading. Default: nil.
attr_accessor :heading

# @return [Integer] Delay (timeout) in seconds.
attr_accessor :delay_seconds

# @return [Integer] Percent (0..100) that are considered "almost done"
# so the dialog is not opened anymore if it isn't already. Default: 80
# Set this to 100 to disable that.
attr_accessor :almost_done_percent

# @return [Boolean] Add a "Cancel" button to the dialog. Default: true.
attr_accessor :use_cancel_button

# Constructor.
#
# If `auto_start` is `true` (default), this also starts the timer with a
# default (4 seconds) timeout.
#
# The `close` method must be explicitly called at the end when the progress
# is finished.
#
# @param delay [Integer,nil] optional delay in seconds
# @param auto_start [Boolean] start the timer immediately
# @param heading [String,nil] optional popup heading
def initialize(delay: nil, auto_start: true, heading: nil)
Yast.import "UI"
Yast.import "Label"

@delay_seconds = delay || 4
@heading = heading
@use_cancel_button = true
@almost_done_percent = 80
@is_open = false
start_timer if auto_start
log.info "Created delayed progress popup"
end

# A static variant with block, it automatically closes the popup at the end.
#
# @param delay [Integer,nil] optional delay in seconds
# @param heading [String,nil] optional popup heading
# @example
# Yast::DelayedProgressPopup.run(delay: 5, heading: "Working...") do |popup|
# 10.times do |sec|
# popup.progress(10 * sec, "Working #{sec}")
# sleep(1)
# end
# end
def self.run(delay: nil, auto_start: true, heading: nil, &block)
popup = new(delay: delay, auto_start: auto_start, heading: heading)
block.call(popup)
ensure
popup&.close
end

# Update the progress.
#
# If the dialog is not open yet, this opens it if the timeout is expired;
# unless the whole process is almost done anyway, i.e. at the time when the
# dialog would be opened, progress_percent is already at the
# @almost_done_percent threshold, so it would just open and then close
# almost immediately again.
#
# @param [Integer] progress_percent numeric progress bar value
# @param [nil|String] progress_text optional progress bar label text
#
def progress(progress_percent, progress_text = nil)
log.info "progress_percent: #{progress_percent}"
open_if_needed unless progress_percent >= @almost_done_percent
return unless open?

update_progress(progress_percent, progress_text)
end

# Open the dialog if needed, i.e. if it's not already open and if the timer
# expired.
#
# Notice that progress() does this automatically.
#
def open_if_needed
return if open?

open! if timer_expired?
end

# Open the dialog unconditionally.
def open!
log.info "Opening the delayed progress popup"
UI.OpenDialog(dialog_widgets)
@is_open = true
stop_timer
end

# Close the dialog if it is open. Only stop the timer if it is not (because
# the timer didn't expire).
#
# Do not call this if another dialog was opened on top of this one in the
# meantime: Just like a normal UI.CloseDialog call, this closes the topmost
# dialog; which in that case might not be the right one.
#
def close
stop_timer
return unless open?

UI.CloseDialog
@is_open = false
end

# Start or restart the timer.
def start_timer
@start_time = Yast2::SystemTime.uptime
end

# Stop the timer.
def stop_timer
@start_time = nil
end

# Check if the dialog is open.
def open?
@is_open
end

# Check if the timer expired.
def timer_expired?
return false unless timer_running?

now = Yast2::SystemTime.uptime
now > @start_time + delay_seconds
end

# Check if the timer is running.
def timer_running?
!@start_time.nil?
end

protected

# Return a widget term for the dialog widgets.
# Reimplement this in inherited classes for a different dialog content.
#
def dialog_widgets
placeholder_label = " " # at least one blank
heading_spacing = @heading.nil? ? 0 : 0.4
MinWidth(
40,
VBox(
MarginBox(
1, 0.4,
VBox(
dialog_heading,
VSpacing(heading_spacing),
VCenter(
ProgressBar(Id(:progress_bar), placeholder_label, 100, 0)
)
)
),
VSpacing(0.4),
dialog_buttons
)
)
end

# Return a widget term for the dialog heading.
def dialog_heading
return Empty() if @heading.nil?

Left(Heading(@heading))
end

# Return a widget term for the dialog buttons.
# Reimplement this in inherited classes for different buttons.
#
# Notice that the buttons only do anything if the calling application
# handles them, e.g. with UI.PollInput().
#
# Don't forget that in the Qt UI, every window has a WM_CLOSE button (the
# [x] icon in the window title bar that is meant for closing the window)
# that returns :cancel in UI.UserInput() / UI.PollInput().
#
def dialog_buttons
return Empty() unless @use_cancel_button

ButtonBox(
PushButton(Id(:cancel), Opt(:cancelButton), Yast::Label.CancelButton)
)
end

# Update the progress bar.
def update_progress(progress_percent, progress_text = nil)
return unless UI.WidgetExists(:progress_bar)

UI.ChangeWidget(Id(:progress_bar), :Value, progress_percent)
UI.ChangeWidget(Id(:progress_bar), :Label, progress_text) unless progress_text.nil?
end
end
end
37 changes: 37 additions & 0 deletions library/general/src/lib/ui/examples/delayed_progress_1.rb
@@ -0,0 +1,37 @@
# Example for the DelayedProgressPopup
#
# Start with:
#
# y2start ./delayed_progress_1.rb qt
# or
# y2start ./delayed_progress_1.rb ncurses
#

require "yast"
require "ui/delayed_progress_popup"

popup = Yast::DelayedProgressPopup.new

# All those parameters are optional;
# comment out or uncomment to experiment.
popup.heading = "Deep Think Mode"
popup.delay_seconds = 2
# popup.use_cancel_button = false

puts("Nothing happens for #{popup.delay_seconds} seconds, then the popup opens.")

10.times do |sec|
puts "#{sec} sec"
popup.progress(10 * sec, "Working #{sec}")
if popup.open?
# Checking for popup.open? is only needed here because otherwise there is
# no window at all yet, so UI.WaitForEvent() throws an exception. Normal
# applications have a main window at this point.

event = Yast::UI.WaitForEvent(1000) # implicitly sleeps
break if event["ID"] == :cancel
else
sleep(1)
end
end
popup.close
35 changes: 35 additions & 0 deletions library/general/src/lib/ui/examples/delayed_progress_2.rb
@@ -0,0 +1,35 @@
# Example for the DelayedProgressPopup
#
# Start with:
#
# y2start ./delayed_progress_2.rb qt
# or
# y2start ./delayed_progress_2.rb ncurses
#

require "yast"
require "ui/delayed_progress_popup"

Yast::DelayedProgressPopup.run(delay: 2, heading: "Deep Think Mode") do |popup|
# All those parameters are optional;
# comment out or uncomment to experiment.
# popup.heading = "Deep Think Mode"
# popup.use_cancel_button = false

puts("Nothing happens for #{popup.delay_seconds} seconds, then the popup opens.")

10.times do |sec|
puts "#{sec} sec"
popup.progress(10 * sec, "Working #{sec}")
if popup.open?
# Checking for popup.open? is only needed here because otherwise there is
# no window at all yet, so UI.WaitForEvent() throws an exception. Normal
# applications have a main window at this point.

event = Yast::UI.WaitForEvent(1000) # implicitly sleeps
break if event["ID"] == :cancel
else
sleep(1)
end
end
end
@@ -0,0 +1,36 @@
# Example for the DelayedProgressPopup
#
# Start with:
#
# y2start ./delayed_progress_almost_done.rb qt
# or
# y2start ./delayed_progress_almost_done.rb ncurses
#

require "yast"
require "ui/delayed_progress_popup"

Yast::DelayedProgressPopup.run(delay: 3, heading: "Deep Think Mode") do |popup|
# All those parameters are optional;
# comment out or uncomment to experiment.
# popup.heading = "Deep Think Mode"
# popup.use_cancel_button = false

puts("This will never open, not even after the #{popup.delay_seconds} sec delay.")

5.times do |sec|
percent = 80 + sec
puts "#{sec} sec; progress: #{percent}%"
popup.progress(percent, "Working #{sec}")
if popup.open?
# Checking for popup.open? is only needed here because otherwise there is
# no window at all yet, so UI.WaitForEvent() throws an exception. Normal
# applications have a main window at this point.

event = Yast::UI.WaitForEvent(1000) # implicitly sleeps
break if event["ID"] == :cancel
else
sleep(1)
end
end
end