Class: Ruber::SettingsDialogManager
- Inherits:
-
Qt::Object
- Object
- Qt::Object
- Ruber::SettingsDialogManager
- Defined in:
- lib/ruber/settings_dialog_manager.rb
Overview
Class which takes care of automatically syncronize (some of) the options in an SettingsContainer with the widgets in the SettingsDialog.
In the following documentation, the widgets passed as argument to the constructor will be called upper level widgets, because they’re the topmost widgets this class is interested in. The term child widget will be used to refer to any widget which is child of an upper level widget, while the generic widget can refer to both. The upper level widget corresponding to a widget is the upper level widget the given widget is child of (or the widget itself if it already is an upper level widget)
To syncronize options and widgets contents, this class looks among the upper level widgets and their children for the widget which have an object_name of the form _group__option, that is: an underscore, a group name, two underscores, an option name (both the group and the option name can contain underscores. This is the reason for which two underscores are used to separate them). Such a widget is associated with the option with name name belonging to the group group.
Having a widget associated with an option means three things: 1. when the widget emits some signals (which mean that the settings have changed, the dialog’s Apply button is enabled) 2. when the dialog is displayed, the widget is updated so that it displays the value stored in the SettingsContainer 3. when the Apply or the OK button of the dialog is clicked, the option in the SettingsContainer is updated with the value of the widget.
For all this to happen, who writes the widget (or the upper level widget the widget is child of) must give it several custom properties (this can be done with the UI designer, if it is used to create the widget, or by hand). These properties are:
- signal: it’s a string containing the signal(s) on which the Apply button of the dialog will be enabled. If more than one signal should be used, you can specify them using a YAML array (that is, enclose them in square brackets and separate them with a comma followed by one space). If the widget has only a single signal with that name, you can omit the signal’s signature, otherwise you need to include it (for example, Qt::LineEdit has a single signall called textChanged, so you can simply use that instead of textChanged(QString). On the other hand, Qt::ComboBox has two signals called currentIndexChanged, so you must fully specify them: currentIndexChanged(QString) or currentIndexChanged(int)).
- read: is the method to use to sync the value in the widget with that in the SettingsContainer. The reason of the name “read” is that it is used to read the value from the container.
- store: is the method to use to sync the value in the SettingsContainer with that in the widget.
- access: a method name to derive the read and the store methods from. The store method has the same name as this property, while the read method has the same name with ending “?” and “!” removed and an ending “=” added.
In the documentation for this class, the term read method refers to the method specified in the read property or derived from the access property by adding the = to it. The term store method refers to the method specified in the store or access property.
If the read, store or access properties start with a $, they’ll be called on the upper level widget corresponding to the widget, instead than on the widget itself.
Not all of the above properties need to be specified. In particular, the access property can’t coexhist the read and the store properties. On the other hand, you can’t give only one of the read and store properties. If you omit the access, store and read property entierely, and the signal property only contains one signal, then an access property is automatically created using the name of the signal after having removed from its end the strings ‘Edited’, ‘Changed’, ‘Modified’, ‘_edited’, ‘_changed’, ‘_modified’ (for example, if the signal is ’textChanged(QString), then the access property will become text.
If the signal property hasn’t been specified, a default one will be used, depending on the class of the widgets. See the documentation for DEFAULT_PROPERTIES to see which signals will be used for which classes. If neither the access property nor the read and store properties have been given, a default access property will also be used. If the class of the widget is not included in DEFAULT_PROPERTIES, an exception will be raised.
A read method must accept one argument, which is the value of the option, and display it in the corresponding widget in whichever way is appropriate. A store method, instead, should take no arguments, retrieve the option value from the widget, again using the most appropriate way, and return it.
Often, the default access method is almost, but not completely, enough. For example, if the widget is a KDE::UrlRequester, but you want to store the option as a string, instead of using a KDE::Url, you’d need to create a method whose only task is to convert a KDE::Url to a string and vice versa. The same could happen with a symbol and a Qt::LineEdit. To avoid such a need, this class also performs automatic conversions, when reading or storing an option. It works this way: if the value to store in the SettingsContainer or in the widget are of a different class from the one previously contained there, the DEFAULT_CONVERSIONS hash is scanned for an entry corresponding to the two classes and, if found, the value returned by the corresponding Proc is stored instead of the original one.
===Example
Consider the following situation:
Options: OpenStruct.new({:name => :number, :group => :G1, :default => 4}):: this is an option which contains an integral value, with default 4 OpenStruct.new({:name => :path, :group => :G1, :default => ENV[[‘HOME’]]}):: this is an option which contains a string representing a path. The default value is the user’s home directory (contained in the environment variable HOME) OpenStruct.new({:name => :list, :group => :G2, :default => %w[a b c]}):: this is an option which contains an array of strings, with default value [‘a’, ‘b’, ‘c’].
Widgets:
There’s a single upper level widget, of class MyWidget, which contains a Qt::SpinBox, a KDE::UrlRequester and a Qt::LineEdit. The value displayed in the spin box should be associated to the :number option, while the url requester should be associated to the :path option. The line edit widget should be associated with the :list option, by splitting the text on commas.
We can make some observations:
- the spin box doesn’t need anything except having its name set to match the :number option: the default signal and access method provided by DEFAULT_PROPERTIES are perfectly adequate to this situation.
- the url requester doesn’t need any special settings, aside from the object name: the default signal and access method provided by DEFAULT_PROPERTIES are almost what we need and the only issue is that the methods take and return a KDE::Url instead of a string. But since the DEFAULT_CONVERSIONS contains conversion procs for this pair of classes, even this is handled automatically
- the line edit requires custom read and store methods (which can be specified with a signle access property), because there’s no default conversion from array to string and vice versa. The default signal, instead, is suitable for our needs, so we don’t need to specify one.
Here’s how the class MyWidget could be written (here, all widgets are created manually. In general, it’s more likely that you’d use the Qt Designer to create it. In that case, you can set the widgets’ properties using the designer itself). Note that in the constructor we make use of the block form of the widgets’ constructors, which evaluates the given block in the new widget’s context.
class MyWidget < Qt::Widget def initialize parent = nil super @spin_box = Qt::SpinBox.new{ self.object_name = ’_G1__number’} @url_req = KDE::UrlRequester.new= ’_G1__path’ @line_edit = Qt::LineEdit.new do self.object_name = ‘_G2__list’ set_property ‘access’, ‘$items’ end end- This is the store method for the list option. It takes the text in the line
- edit and splits it on commas, returning the array def items @line_edit.text.split ‘,’ end
- This is the read method for the list option. It takes the array containing
- the value of the option (an array) as argument, then sets the text of the
- line edit to the string obtained by calling join on the array def items= array @line_edit.text = array.join ‘,’ end
Constant Summary
- DEFAULT_PROPERTIES =
A hash containing the default signal and access methods to use for a number of classes, when the signal property isn’t given. The keys of the hash are the classes, while the values are arrays of two elements. The first element is the name of the signal, while the second is the name of the access method.
{ Qt::CheckBox => [ 'toggled(bool)', "checked?"], Qt::PushButton => [ 'toggled(bool)', "checked?"], KDE::PushButton => [ 'toggled(bool)', "checked?"], KDE::ColorButton => [ 'changed(QColor)', "color"], KDE::IconButton => [ 'iconChanged(QString)', "icon"], Qt::LineEdit => [ 'textChanged(QString)', "text"], KDE::LineEdit => [ 'textChanged(QString)', "text"], KDE::RestrictedLine => [ 'textChanged(QString)', "text"], Qt::ComboBox => [ 'currentIndexChanged(int)', "current_index"], KDE::ComboBox => [ 'currentIndexChanged(int)', "current_index"], KDE::ColorCombo => [ 'currentIndexChanged(int)', "color"], Qt::TextEdit => [ 'textChanged(QString)', "text"], KDE::TextEdit => [ 'textChanged(QString)', "text"], Qt::PlainTextEdit => [ 'textChanged(QString)', "text"], Qt::SpinBox => [ 'valueChanged(int)', "value"], KDE::IntSpinBox => [ 'valueChanged(int)', "value"], Qt::DoubleSpinBox => [ 'valueChanged(double)', "value"], KDE::IntNumInput => [ 'valueChanged(int)', "value"], KDE::DoubleNumInput => [ 'valueChanged(double)', "value"], Qt::TimeEdit => [ 'timeChanged(QTime)', "time"], Qt::DateEdit => [ 'dateChanged(QDate)', "date"], Qt::DateTimeEdit => [ 'dateTimeChanged(QDateTime)', "date_time"], Qt::Dial => [ 'valueChanged(int)', "value"], Qt::Slider => [ 'valueChanged(int)', "value"], KDE::DatePicker => [ 'dateChanged(QDate)', "date"], KDE::DateTimeWidget => [ 'valueChanged(QDateTime)', "date_time"], KDE::DateWidget => [ 'changed(QDate)', "date"], KDE::FontComboBox => [ 'currentFontChanged(QFont)', "current_font"], KDE::FontRequester => [ 'fontSelected(QFont)', "font"], KDE::UrlRequester => [ 'textChanged(QString)', "url"] }
- DEFAULT_CONVERSIONS =
Hash which contains the Procs used by convert_value to convert a value from its class to another. Each key must an array of two classes, corresponding respectively to the class to convert from and to the class to convert to. The values should be Procs which take one argument of class corresponding to the first entry of the key and return an object of class equal to the second argument of the key.
If you want to implement a new automatic conversion, all you need to do is to add the appropriate entries here. Note that usually you’ll want to add two entries: one for the conversion from A to B and one for the conversion in the opposite direction.
{ [Symbol, String] => proc{|sym| sym.to_s}, [String, Symbol] => proc{|str| str.to_sym}, [String, KDE::Url] => proc{|str| KDE::Url.from_path(str)}, # KDE::Url#path_or_url returns nil if the KDE::Url is not valid, so, we use # || '' to ensure the returned object is a string [KDE::Url, String] => proc{|url| url.path_or_url || ''}, [String, Fixnum] => proc{|str| str.to_i}, [Fixnum, String] => proc{|n| n.to_s}, [String, Float] => proc{|str| str.to_f}, [Float, String] => proc{|x| x.to_s}, }
Instance Method Summary (collapse)
-
- (Object) add_signal_signature(sig, obj)
private
Returns the full signature of the signal with name sig in the Qt::Object obj.
-
- (Object) call_widget_method(*args)
private
:call-seq: manager.call_widget_method data manager.call_widget_method data, value.
-
- (Object) convert_value(new, old)
private
Converts the value new so that it has the same class as old.
-
- (Object) find_associated_methods(signals, methods, widget, ulw)
private
Finds the read and store methods to associate to the widget widget.
-
- (SettingsDialogManager) initialize(dlg, options, widgets)
constructor
Creates a new SettingsDialogManager.
-
- (Object) read_default_settings
It works like read_settings except for the fact that it updates the widgets with the default values of the options.
-
- (Object) read_settings
Updates all the widgets corresponding to an automatically managed option by calling the associated reader methods, passing them the value read from the option container, converted as described in the the documentation for SettingsDialogManager, convert_value and DEFAULT_CONVERSIONS.
-
- (Object) settings_changed
slot
private
Enables the Apply button of the dialog.
-
- (Object) setup_automatic_option(opt, widget, ulw)
private
Associates the widget w with the option option.
-
- (Object) setup_option(opt)
private
Looks in the list of widgets passed to the constructor and their children for a widget whose name corresponds to that of the option (see widget_name for the meaning of corresponds).
-
- (Object) store_settings
Updates all the automatically managed options by calling setting them to the values returned by the associated ‘store’ methods, converted as described in the the documentation for SettingsDialogManager, convert_value and DEFAULT_CONVERSIONS.
-
- (Object) widget_name(group, name)
private
Returns the name of the widget corresponding to the option belonging to group group and with name name.
Constructor Details
- (SettingsDialogManager) initialize(dlg, options, widgets)
Creates a new SettingsDialogManager. The first argument is the SettingsDialog whose widgets will be managed by the new instance. The second argument is an array with the option objects corresponding to the options which are candidates for automatic management. The keys of the hash are the option objects, which contain all the information about the options, while the values are the values of the options. widgets is a list of widgets where to look for for widgets corresponding to the options.
When the SettingsDialogManager instance is created, associations between options and widgets are created, but the widgets themselves aren’t updated with the values of the options.
274 275 276 277 278 279 280 |
# File 'lib/ruber/settings_dialog_manager.rb', line 274 def initialize dlg, , super(dlg) @widgets = @container = dlg.settings_container @associations = {} .each{|o| setup_option o} end |
Instance Method Details
- (Object) add_signal_signature(sig, obj) (private)
Returns the full signature of the signal with name sig in the Qt::Object obj. If sig already has a signature, it is returned as it is. Otherwise, the list of signals of obj is searched for a signal whose name is equal to sig and the full name of that signal is returned. If no signal has that name, or if more than one signal have that name, ArgumentError is raised.
411 412 413 414 415 416 417 418 419 |
# File 'lib/ruber/settings_dialog_manager.rb', line 411 def add_signal_signature sig, obj return sig if sig.index '(' mo = obj. reg = /^#{Regexp.quote sig}/ signals = mo.each_signal.find_all{|s| s.signature =~ reg} raise ArgumentError, "Ambiguous signal name, '#{sig}'" if signals.size > 1 raise ArgumentError, "No signal with name '#{sig}' exist" if signals.empty? signals.first.signature end |
- (Object) call_widget_method(*args) (private)
:call-seq: manager.call_widget_method data manager.call_widget_method data, value
Helper method which calls the ‘read’ or ‘store’ method for an association. If called in the first form, calls the ‘store’ method, while in the first form it calls the ‘read’ method passing value as argument. data is one of the hashes contained in the @associations instance variable as values: it has the following form: {:read => [widget, read_method], :store => [widget, store_method]}. In both versions, it returns what the called method returned.
471 472 473 474 475 476 |
# File 'lib/ruber/settings_dialog_manager.rb', line 471 def *args data = args[0] if args.size == 1 then data[:store][0].send data[:store][1] else data[:read][0].send data[:read][1], args[1] end end |
- (Object) convert_value(new, old) (private)
Converts the value new so that it has the same class as old_. To do so, it looks in the +DEFAULTCONVERSIONS+ hash for an entry whose key is an array with the class of new as first entry and the class of old as second entry. If the key is found, the corresponding value (which is a Proc), is called, passing it new and the method returns its return value. If the key isn’t found (including the case when new and old have the same class), new is returned as it is.
486 487 488 489 490 491 492 |
# File 'lib/ruber/settings_dialog_manager.rb', line 486 def convert_value new, old prc = DEFAULT_CONVERSIONS[[new.class, old.class]] if prc then prc.call new else new end end |
- (Object) find_associated_methods(signals, methods, widget, ulw) (private)
Finds the read and store methods to associate to the widget widget. It returns an hash with keys :read and :store and for values pairs of the form [receiver, method], where method is the method to call to read or store the option value and receiver is the object to call the method on (it may be either widget, if the corresponding property doesn’t begin with $, or its upper level widget, ulw, if the property begins with $).
signals is an array with all the signals included in the signal property of the widget. methods is an array with the contents of the read, store and access properties, (in this order), converted to string (if one property is missing, the corresponding entry will be nil). widget is the widget for which the association should be done and ulw is the corresponding upper level widget.
See the documentation for setup_automatic_option for a description of the situation on which an ArgumentError exception is raised.
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 |
# File 'lib/ruber/settings_dialog_manager.rb', line 438 def find_associated_methods signals, methods, , ulw read, store, access = methods if access and (read or store) raise ArgumentError, "The widget #{.object_name} has both the access property and one or both of the store and read properties" elsif access read = access.sub(/[?!=]$/,'')+'=' store = access elsif !access and !(store and read) if signals.size > 1 raise ArgumentError, "When more signals are specified, you need to specify also the access property or both the read and store properties" end sig = signals.first store ||= sig[/[^(]+/].snakecase.sub(/_(?:changed|modified|edited)$/, '') read ||= sig[/[^(]+/].snakecase.sub(/_(?:changed|modified|edited)$/, '') + '=' end data = {} data[:read] = read[0,1] == '$' ? [ulw, read[1..-1]] : [, read] data[:store] = store[0,1] == '$' ? [ulw, store[1..-1]] : [, store] data end |
- (Object) read_default_settings
It works like read_settings except for the fact that it updates the widgets with the default values of the options.
320 321 322 323 324 325 326 327 328 |
# File 'lib/ruber/settings_dialog_manager.rb', line 320 def read_default_settings @associations.each_pair do |o, data| group, name = o[1..-1].split('__').map(&:to_sym) value = @container.default(group, name) old_value = data value = convert_value(value, old_value) data, value end end |
- (Object) read_settings
Updates all the widgets corresponding to an automatically managed option by calling the associated reader methods, passing them the value read from the option container, converted as described in the the documentation for SettingsDialogManager, convert_value and DEFAULT_CONVERSIONS. It emits the settings_changed signal with true as argument.
289 290 291 292 293 294 295 296 297 |
# File 'lib/ruber/settings_dialog_manager.rb', line 289 def read_settings @associations.each_pair do |o, data| group, name = o[1..-1].split('__').map(&:to_sym) value = @container.relative_path?(group, name) ? @container[group, name, :abs] : @container[group, name] old_value = data value = convert_value(value, old_value) data, value end end |
- (Object) settings_changed (private)
Enables the Apply button of the dialog
497 498 499 |
# File 'lib/ruber/settings_dialog_manager.rb', line 497 def settings_changed parent. true end |
Slot Signature:
settings_changed()
- (Object) setup_automatic_option(opt, widget, ulw) (private)
Associates the widget w with the option option. ulw is the upper level widget associated with widget. Associating a widget with an option means two things:
- connecting each signal specified in the signal property of the widget with the settings_changed slot of self
- creating an entry in the @associations instance variable with the name of the widget as key and a hash as value. Each hash has a read and a store entry, which are both arrays: the second element is the name of the read and store method respectively, while the first is the widget on which it should be called (which will be either widget or the corresponding upper level widget, ulw).
As explained in the documentation for this class, some of the properties may be missing and be automatically determined. If one of the following situations happen, ArgumentError will be raised:
- no signal property exist for widget and its class is not in DEFAULT_PROPERTIES
- widget doesn’t specify neither the access property nor the read and the store properties and more than one signal is specified in the signal property
- both the access property and the read and/or store properties are specified for widget
- the signature of one signal isn’t given and it couldn’t be determined automatically because either no signal or more than one signals of widget have that name
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/ruber/settings_dialog_manager.rb', line 374 def setup_automatic_option opt, , ulw #Qt::Variant#to_string returns nil if the variant is invalid signals = Array(YAML.load(.property('signal').to_string || '[]')) #Qt::Variant#to_string returns nil if the variant is invalid methods = %w[read store access].map{|m| prop = .property(m).to_string} if signals.empty? data = DEFAULT_PROPERTIES.fetch(.class) do raise ArgumentError, "No default signal exists for class #{.class}, you need to specify one" end signals = [data[0]] methods[2] = data[1] if methods == Array.new(3, nil) end signals.each_index do |i| signals[i] = add_signal_signature signals[i], connect , SIGNAL(signals[i]), self, SLOT(:settings_changed) end data = find_associated_methods signals, methods, , ulw @associations[.object_name] = data end |
- (Object) setup_option(opt) (private)
Looks in the list of widgets passed to the constructor and their children for a widget whose name corresponds to that of the option (see widget_name for the meaning of corresponds_). If one is found, the setup_automaticoption method is called for that option. If no widget is found, nothing is done.
338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/ruber/settings_dialog_manager.rb', line 338 def setup_option opt name = opt.group, opt.name = @widgets.find! do |w| if w.object_name == name then [w, w] else child = w.find_child(Qt::Widget, name) child ? [child, w] : nil end end setup_automatic_option opt, * if end |
- (Object) store_settings
Updates all the automatically managed options by calling setting them to the values returned by the associated ‘store’ methods, converted as described in the the documentation for SettingsDialogManager, convert_value and DEFAULT_CONVERSIONS. It emits the settings_changed signal with false as argument.
306 307 308 309 310 311 312 313 314 |
# File 'lib/ruber/settings_dialog_manager.rb', line 306 def store_settings @associations.each_pair do |o, data| group, name = o[1..-1].split('__').map(&:to_sym) value = (data) old_value = @container[group, name] value = convert_value value, old_value @container[group, name] = value end end |
- (Object) widget_name(group, name) (private)
Returns the name of the widget corresponding to the option belonging to group group and with name name. The widget name has the form: underscore, group, double underscore, name. For example if group is :general and name is path_, this method would return "_general_path"
400 401 402 |
# File 'lib/ruber/settings_dialog_manager.rb', line 400 def group, name "_#{group}__#{name}" end |