Skip to content

Commit

Permalink
Merge pull request #1252 from yast/huha-dl-progress-master
Browse files Browse the repository at this point in the history
DelayedProgressPopup for File Conflicts Progress during Package Installation [master]
  • Loading branch information
shundhammer committed Apr 7, 2022
2 parents 769d508 + 450a254 commit 161f0cc
Show file tree
Hide file tree
Showing 8 changed files with 449 additions and 89 deletions.
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

0 comments on commit 161f0cc

Please sign in to comment.