-
Notifications
You must be signed in to change notification settings - Fork 43
/
multi_status_selector.rb
485 lines (426 loc) · 13.6 KB
/
multi_status_selector.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# Copyright (c) [2020] SUSE LLC
#
# 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 SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
require "cwm"
require "abstract_method"
module CWM
# Custom widget to manage multi status selection items
#
# It uses a RichText to emulate the multi selection list meeting following requirements:
#
# - Allow to select more than one item.
# - Able to represent multiple statuses: no selected, selected, auto selected.
# - Items can be enable or disabled.
# - Emit different events to distinguish the interaction through check box input or its label.
# - Automatic text wrapping.
# - Keep the vertical scroll.
#
# If you want to see it in action, have a look at yast2-registration or yast2-packager modules.
#
# TODO: make possible to use it more than once in the same dialog, maybe by using the parent
# widget_id as a prefix for the event_id. See {CWM::MultiStatusSelector#handle} and
# {CWM::MultiStatusSelector::Item.event_id}.
#
# @example Defining a MultiStatusSelector to manage products selection (with dependencies)
#
# require "cwm/multi_status_selector"
#
# class MyMultiStatusSelector < CWM::MultiStatusSelector
# attr_reader :items
#
# def initialize(products)
# @products = products
# @items = products.map { |p| Item.new(p) }
# end
#
# def contents
# VBox(
# VWeight(60, super),
# VWeight(40, details)
# )
# end
#
# def toggle(item)
# item.toggle
# select_dependencies
# label_event_handler(item)
# end
#
# private
#
# attr_accessor :products
#
# def details
# @details ||= CWM::RichText.new
# end
#
# def select_dependencies
# # logic to recalculate auto selected items
# end
#
# def label_event_handler(item)
# details.value = item.description
# end
#
# class Item < CWM::MultiStateSelector::Item
# attr_reader :status, :dependencies
#
# def initialize(product)
# @product = product
# @status = product.status || UNSELECTED
# @dependencies = product.dependencies || []
# end
#
# def id
# product.id
# end
#
# def label
# product.friendly_name || product.name
# end
#
# def description
# # build the item description
# end
#
# private
#
# attr_reader :product
# end
# end
class MultiStatusSelector < CustomWidget
# @!method items
# The items collection
# @return [Array<Item>] the collection of available items
abstract_method :items
# @macro seeAbstractWidget
def init
refresh
end
# @macro seeAbstractWidget
def contents
HBox(content)
end
# Updates the content based on items list
def refresh
new_value = items.map do |item|
item_content = item.to_richtext
if Yast::UI.TextMode
"#{item_content}<br>"
else
"<p>#{item_content}</p>"
end
end
content.value = new_value.join
end
# @macro seeAbstractWidget
def handle(event)
if event["ID"].to_s.include?(Item.event_id)
id, fired_by = event["ID"].split(Item.event_id)
item = find_item(id)
send("#{fired_by}_event_handler", item)
end
nil
end
# Toggles the status of given item
#
# Redefine it if needed to perform additional actions before or after toggling the item, like
# calculating dependencies for auto selection.
#
# @param item [Item] item to toggle the status
def toggle(item)
item.toggle
end
private
# @macro seeAbstractWidget
def handle_all_events
true
end
# Handles the event fired by the Item check box input
#
# @param item [Item] the item that fired the event
def input_event_handler(item)
toggle(item)
refresh
end
# Handles the event fired by the Item check box label
#
# @param [Item] the item that fired the event
def label_event_handler(item)
log.debug("Unhandled label event fired by #{item.inspect}")
end
# Returns the item with given id
#
# @param needle [#to_s] any object that responds to `#to_s`
# @return [Item, nil] the item which id matches with given object#to_s
def find_item(needle)
items.find { |i| i.id.to_s == needle.to_s }
end
# Convenience widget to keep the content updated
#
# @return [ContentArea]
def content
@content ||= Content.new
end
# A CWM::RichText able to keep the vertical scroll after updating its value
class Content < RichText
# @macro seeAbstractWidget
def opt
[:notify]
end
# @macro seeRichText
def keep_scroll?
true
end
end
# A plain Ruby object in charge to build an item "check box" representation
#
# It already provides a default state (always enabled) and the logic to deal with the status
# (selected, unselected or auto selected) but it can be extended by redefining the #status,
# #toggle, and/or #enabled? methods.
#
# Derived classes must define #id and #label attributes/methods.
#
# See the {MultiStatusSelector} example.
class Item
extend Yast::I18n
textdomain "base"
# Map to icons used in GUI to represent all the known statuses in both scenarios, during
# installation (`inst` mode) and in a running system (`normal` mode).
#
# Available statuses are
#
# - `[ ]` not selected
# - `[x]` selected
# - `[a]` auto-selected
IMAGES = {
"inst:[a]:enabled" => "auto-selected.svg",
"inst:[x]:enabled" => "inst_checkbox-on.svg",
"inst:[x]:disabled" => "inst_checkbox-on-disabled.svg",
"inst:[ ]:enabled" => "inst_checkbox-off.svg",
"inst:[ ]:disabled" => "inst_checkbox-off-disabled.svg",
"normal:[a]:enabled" => "auto-selected.svg",
"normal:[x]:enabled" => "checkbox-on.svg",
"normal:[ ]:enabled" => "checkbox-off.svg",
# NOTE: Normal theme has no special images for disabled check boxes
"normal:[x]:disabled" => "checkbox-on.svg",
"normal:[ ]:disabled" => "checkbox-off.svg"
}.freeze
private_constant :IMAGES
# Path to the icons in the system
IMAGES_DIR = "/usr/share/YaST2/theme/current/wizard".freeze
private_constant :IMAGES_DIR
# Selected status
SELECTED = :selected
private_constant :SELECTED
# Not selected status
UNSELECTED = :unselected
private_constant :UNSELECTED
# Auto selected status
AUTO_SELECTED = :auto_selected
private_constant :AUTO_SELECTED
# Id to identify an event fired by the check box
EVENT_ID = "#checkbox#".freeze
private_constant :EVENT_ID
# Id to identify an event fired by the check box input
INPUT_EVENT_ID = "#{EVENT_ID}input".freeze
private_constant :INPUT_EVENT_ID
# Id to identify an event fired by the check box label
LABEL_EVENT_ID = "#{EVENT_ID}label".freeze
private_constant :LABEL_EVENT_ID
# @!method id
# The item id
# @return [#to_s]
abstract_method :id
# @!method label
# The item label
# @return [#to_s]
abstract_method :label
# @return [Symbol] the current item status
attr_reader :status
# Returns the common identifier of fired events
#
# @return [String] event identifier
def self.event_id
EVENT_ID
end
# Help text
#
# @return [String]
def self.help
help_text = "<p>"
# TRANSLATORS: help text for a not selected check box
help_text << "#{icon_for(UNSELECTED, mode: mode)} = #{_("Not selected")}<br />"
# TRANSLATORS: help text for a selected check box
help_text << "#{icon_for(SELECTED, mode: mode)} = #{_("Selected")}<br />"
# TRANSLATORS: help text for an automatically selected check box
# (it has a different look that a user selected check box)
help_text << "#{icon_for(AUTO_SELECTED, mode: mode)} = #{_("Auto selected")}"
help_text << "</p>"
help_text
end
# Returns the icon to be used for an item with given status and state
#
# @see .value_for
#
# @param status [Symbol] the item status (e.g., :selected, :registered, :auto_selected)
# @param mode [String] the running mode, "normal" or "inst"
# @param state [String] the item state, "enabled" or "disabled"
#
# @return [String] an <img> tag when running in GUI mode; plain text otherwise
def self.icon_for(status, mode: "normal", state: "enabled")
value = value_for(status)
if Yast::UI.TextMode
value
else
# an image key looks like "inst:[a]:enabled"
image_key = [mode, value, state].join(":")
"<img src=\"#{IMAGES_DIR}/#{IMAGES[image_key]}\">"
end
end
# Returns the status string representation
#
# @param status [Symbol] the status identifier
#
# @return [String] the status text representation
def self.value_for(status)
case status
when SELECTED
"[x]"
when AUTO_SELECTED
"[a]"
else
"[ ]"
end
end
# Toggles the current status
def toggle
@status = selected? ? UNSELECTED : SELECTED
end
# Determines if the item is enabled or not
#
# @return [Boolean] true when item is enabled; false otherwise
def enabled?
true
end
# Whether item is selected
#
# @return [Boolean] true if the status is selected; false otherwise
def selected?
status == SELECTED
end
# Whether item is not selected
#
# @return [Boolean] true if the status is not selected; false otherwise
def unselected?
[SELECTED, AUTO_SELECTED].none?(status)
end
# Whether item is auto selected
#
# @return [Boolean] true if the status is auto selected; false otherwise
def auto_selected?
status == AUTO_SELECTED
end
# Sets the item as selected
def select!
@status = SELECTED
end
# Sets the item as not selected
def unselect!
@status = UNSELECTED
end
# Sets the item as auto-selected
def auto_select!
@status = AUTO_SELECTED
end
# Returns richtext representation for the item
#
# Basically, an string containing two <a> or <span> tags, depending on the #enabled? method.
# One for the check box input and another for the label.
#
# @return [String] the item richtext representation
def to_richtext
"#{checkbox_input} #{checkbox_label}"
end
private
# @see .icon_for
def icon
self.class.icon_for(status, mode: self.class.mode, state: state)
end
# Builds the check box input representation
#
# @return [String]
def checkbox_input
if enabled?
"<a href=\"#{id}#{INPUT_EVENT_ID}\" style=\"#{text_style}\">#{icon}</a>"
else
"<span style\"#{text_style}\">#{icon}</a>"
end
end
# Builds the check box label representation
#
# @return [String]
def checkbox_label
if enabled?
"<a href=\"#{id}#{LABEL_EVENT_ID}\" style=\"#{text_style}\">#{label}</a>"
else
"<span style\"#{text_style}\">#{label}</a>"
end
end
# Returns the current mode
#
# @return [String] "normal" in a running system; "inst" during the installation
def self.mode
installation? ? "inst" : "normal"
end
private_class_method :mode
# Returns the current input state
#
# @return [String] "enabled" when item must be enabled; "disabled" otherwise
def state
enabled? ? "enabled" : "disabled"
end
# Returns style rules for the text
#
# @return [String] the status text representation
def text_style
"text-decoration: none; color: #{color}"
end
# Determines the color for the text
#
# @return [String] "grey" for a disabled item;
# "white" when enabled and running in installation mode;
# "black" otherwise
def color
return "grey" unless enabled?
return "white" if self.class.installation?
"black"
end
# Determines whether running in installation mode
#
# We do not use Stage.initial because of firstboot, which runs in 'installation' mode
# but in 'firstboot' stage.
#
# @return [Boolean] Boolean if running in installation or update mode
def self.installation?
Yast::Mode.installation || Yast::Mode.update
end
private_class_method :installation?
end
end
end