Class: Ruber::SettingsDialogManager

Inherits:
Qt::Object
  • Object
show all
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
  1. This is the store method for the list option. It takes the text in the line
  2. edit and splits it on commas, returning the array def items @line_edit.text.split ‘,’ end
  1. This is the read method for the list option. It takes the array containing
  2. the value of the option (an array) as argument, then sets the text of the
  3. line edit to the string obtained by calling join on the array def items= array @line_edit.text = array.join ‘,’ end
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)

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, options, widgets
  super(dlg)
  @widgets = widgets
  @container = dlg.settings_container
  @associations = {}
  options.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.

Raises:

  • (ArgumentError)


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.meta_object
  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 call_widget_method *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, widget, ulw
  read, store, access = methods
  if access and (read or store)
    raise ArgumentError, "The widget #{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]] : [widget, read]
  data[:store] = store[0,1] == '$' ? [ulw, store[1..-1]] : [widget, 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 = call_widget_method data
    value = convert_value(value, old_value)
    call_widget_method 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 = call_widget_method data
    value = convert_value(value, old_value)
    call_widget_method 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.enable_button_apply 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, widget, ulw
  #Qt::Variant#to_string returns nil if the variant is invalid
  signals = Array(YAML.load(widget.property('signal').to_string || '[]'))
  #Qt::Variant#to_string returns nil if the variant is invalid
  methods = %w[read store access].map{|m| prop = widget.property(m).to_string}
  if signals.empty?
    data = DEFAULT_PROPERTIES.fetch(widget.class) do
      raise ArgumentError, "No default signal exists for class #{widget.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], widget
    connect widget, SIGNAL(signals[i]), self, SLOT(:settings_changed)
  end
  data = find_associated_methods signals, methods, widget, ulw
  @associations[widget.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 = widget_name opt.group, opt.name
  widgets = @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, *widgets if widgets
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 = call_widget_method(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 widget_name group, name
  "_#{group}__#{name}"
end