-
Notifications
You must be signed in to change notification settings - Fork 44
/
popup.rb
290 lines (257 loc) · 9.91 KB
/
popup.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
require "yast"
require "erb"
Yast.import "Label"
Yast.import "UI"
module Yast2
# Class responsible for showing popups. It has a small but consistent API.
# Intended as a replacement for {Yast::Popup} module.
# @note as the UI is not easy to test, it is recommended to run
# `examples/popup_series_tester.sh` after modifying this code,
# to tests common combinations of options.
# @note for RSpec tests, `require "yast2/popup_rspec"` for easier mocking
# that still does argument verifications.
class Popup
class << self
include Yast::I18n
# Number of lines to switch to richtext widget for richtext: false
LINES_THRESHOLD = 20
RICHTEXT_WIDTH = 60
RICHTEXT_HEIGHT = 10
# Minimum width for auto wrapped labels. This value is used when
# the label has longer lines.
LABEL_MINWIDTH = 60
# Show a popup, wait for a button press (or a timeout), return the button ID.
# @param message [String] message to show. The only mandatory argument.
# @param details [String] details that will be shown in another popup
# on pressing a "Details..." button. (`""` -> no button)
# @param headline [String, :error, :warning] sets popup headline.
# A String is shown as is (`""` -> no headline shown).
# `:error` and `:warning` produce the corresponding translated string.
# Note: a Symbol means just predefined strings, not affecting popup style.
# @param timeout [Integer] how many seconds until autoclosing. 0 means forever.
# @param buttons [Hash<Symbol, String>, Symbol] buttons shown.
# - Explicit way: a **Hash** button_id => button_text,
# shown in source code order.
# Beware that symbols starting with `:__` are reserved.
# - Shorthand way: a **Symbol**, one of:
# - `:ok` -> `{ ok: Label.OKButton}`
# - `:continue_cancel` -> `{ continue: Label.ContinueButton,
# cancel: Label.CancelButton }`
# - `:yes_no` -> `{ yes: Label.YesButton, no: Label.NoButton }`
# @param focus [Symbol, nil] which button gets the focus.
# Also it is the button which is returned if the timeout is exceeded.
# `nil` means the first button. See buttons parameter.
# @param richtext [Boolean] whether to interpret richtext tags in message. If it it true,
# then always Richtext widget is used. If it is false, for short text Label widget is used
# and for long text Richtext widget is used, but richtext tags are not interpreted.
# @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: false, 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?
add_details_button(buttons) unless details.empty?
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
private
include Yast::UIShortcuts
def check_arguments!(message, details, timeout, focus, buttons)
raise ArgumentError, "Invalid value #{message.inspect} of parameter message" if !message.is_a?(::String)
raise ArgumentError, "Invalid value #{details.inspect} of parameter details" if !details.is_a?(::String)
raise ArgumentError, "Invalid value #{timeout.inspect} of parameter timeout" if !timeout.is_a?(::Integer)
if !buttons.key?(focus)
raise ArgumentError, "Invalid value #{focus.inspect} for parameter focus. " \
"Known buttons: #{buttons.keys}."
end
nil
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
buttons[:__details] = _("&Details...")
end
def add_stop_button(buttons)
# use this way merge to have details as first place button
buttons[:__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(:__timeout_label), timeout.to_s)
else
Empty()
end
end
def plain_to_richtext(text)
ERB::Util.html_escape(text).gsub("\n", "<br>")
end
def message_widget(message, richtext)
if richtext
HBox(
VSpacing(RICHTEXT_HEIGHT),
VBox(
HSpacing(RICHTEXT_WIDTH),
RichText(message)
)
)
elsif message.lines.size >= LINES_THRESHOLD
message_widget(plain_to_richtext(message), true)
else
width = [LABEL_MINWIDTH, message.lines.map(&:size).max].min
MinWidth(width, Left(Label(Opt(:autoWrap), message)))
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)
raise "Failed to open dialog, see logs." unless res
begin
remaining_time = timeout
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(:__timeout_label, :Value, remaining_time.to_s)
nil
end
when :__stop
loop do
Yast::UI.ChangeWidget(:__stop, :Enabled, false)
Yast::UI.SetFocus(focus)
res = Yast::UI.UserInput
res = handle_event(res, details, remaining_time, focus)
return res if res
end
else
res
end
end
end
end
end