Class: MetaRuby::GUI::ModelSelector

Inherits:
Qt::Widget
  • Object
show all
Defined in:
lib/metaruby/gui/model_selector.rb

Overview

A Qt widget based on RubyConstantsItemModel to browse a set of models, and display them when the user selects one

Defined Under Namespace

Classes: ModelPathCompleter

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent = nil) ⇒ ModelSelector

Create a new widget with an optional parent

Parameters:

  • parent (Qt::Widget, nil) (defaults to: nil)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/metaruby/gui/model_selector.rb', line 43

def initialize(parent = nil)
    super

    @type_info = Hash.new
    @browser_model = ModelHierarchy.new
    @type_filters = Hash.new

    layout = Qt::VBoxLayout.new(self)
    filter_button = Qt::PushButton.new('Filters', self)
    layout.add_widget(filter_button)
    @btn_type_filter_menu = Qt::Menu.new
    filter_button.menu = btn_type_filter_menu

    setup_tree_view(layout)
    setTabOrder(filter_box, filter_button)
    setTabOrder(filter_button, model_list)
end

Instance Attribute Details

#browser_modelModelHierarchy (readonly)

The Qt item model that represents the object hierarchy

Returns:



29
30
31
# File 'lib/metaruby/gui/model_selector.rb', line 29

def browser_model
  @browser_model
end

#btn_type_filter_menuQt::PushButton (readonly)

Returns a button allowing to filter models by type.

Returns:

  • (Qt::PushButton)

    a button allowing to filter models by type



33
34
35
# File 'lib/metaruby/gui/model_selector.rb', line 33

def btn_type_filter_menu
  @btn_type_filter_menu
end

#filter_boxQt::LineEdit (readonly)

Returns the line edit widget that allows to modify the tree view filter.

Returns:

  • (Qt::LineEdit)

    the line edit widget that allows to modify the tree view filter



36
37
38
# File 'lib/metaruby/gui/model_selector.rb', line 36

def filter_box
  @filter_box
end

#filter_completerQt::Completer (readonly)

Returns auto-completion for #filter_box.

Returns:



38
39
40
# File 'lib/metaruby/gui/model_selector.rb', line 38

def filter_completer
  @filter_completer
end

#model_filterQt::SortFilterProxyModel (readonly)

Qt model filter

Returns:

  • (Qt::SortFilterProxyModel)


19
20
21
# File 'lib/metaruby/gui/model_selector.rb', line 19

def model_filter
  @model_filter
end

#model_listQt::TreeView (readonly)

The view that shows the object hierarchy

Returns:

  • (Qt::TreeView)


15
16
17
# File 'lib/metaruby/gui/model_selector.rb', line 15

def model_list
  @model_list
end

#type_filtersHash<Module,Qt::Action> (readonly)

A per-type matching of the type to the actio that allows to filter/unfilter on this type

Returns:



10
11
12
# File 'lib/metaruby/gui/model_selector.rb', line 10

def type_filters
  @type_filters
end

#type_infoHash<Object,String> (readonly)

A mapping from a root model and the user-visible name for this root

Returns:

  • (Hash<Object,String>)


25
26
27
# File 'lib/metaruby/gui/model_selector.rb', line 25

def type_info
  @type_info
end

Instance Method Details

#all_leaves(model, limit = nil, item = Qt::ModelIndex.new, result = []) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Helper method for #select_first_item



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/metaruby/gui/model_selector.rb', line 185

def all_leaves(model, limit = nil, item = Qt::ModelIndex.new, result = [])
    if !model.hasChildren(item)
        result << item
        return result
    end

    row, child_item = 0, model.index(0, 0, item)
    while child_item.valid?
        all_leaves(model, limit, child_item, result)
        if limit && result.size == limit
            return result
        end
        row += 1
        child_item = model.index(row, 0, item)
    end
    return result
end

#auto_open(threshold = 5) ⇒ Object

Auto-open in the current state

Parameters:

  • threshold (Integer) (defaults to: 5)

    the method opens items whose number of children is lower than this threshold



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/metaruby/gui/model_selector.rb', line 150

def auto_open(threshold = 5)
    current_level = [Qt::ModelIndex.new]
    while !current_level.empty?
        count = current_level.inject(0) do |total, index|
            total + model_filter.rowCount(index)
        end
        close_this_level = (count > threshold)
        current_level.each do |index|
            model_filter.rowCount(index).times.each do |row|
                model_list.setExpanded(model_filter.index(row, 0, index), !close_this_level)
            end
        end
        return if close_this_level

        last_level, current_level = current_level, []
        last_level.each do |index|
            model_filter.rowCount(index).times.each do |row|
                current_level << model_filter.index(row, 0, index)
            end
        end
    end
end

#current_selectionRubyModuleModel::ModuleInfo?

Returns the currently selected item

Returns:

  • (RubyModuleModel::ModuleInfo, nil)

    nil if there are no selections



322
323
324
325
326
327
328
# File 'lib/metaruby/gui/model_selector.rb', line 322

def current_selection
    index = model_list.selection_model.current_index
    if index.valid?
        index = model_filter.map_to_source(index)
        browser_model.find_model_from_index(index)
    end
end

#dump_filtered_item_model(parent = Qt::ModelIndex.new, indent = "") ⇒ Object



138
139
140
141
142
143
144
# File 'lib/metaruby/gui/model_selector.rb', line 138

def dump_filtered_item_model(parent = Qt::ModelIndex.new, indent = "")
    model_items_from_filter(parent).each_with_index do |(model_item, filter_index), i|
        data = model_item.data(Qt::UserRole).to_string
        puts "#{indent}[#{i}] #{model_item.text} #{data}"
        dump_filtered_item_model(filter_index, indent + "  ")
    end
end

#filter_row_count(parent = Qt::ModelIndex.new) ⇒ Object



122
123
124
# File 'lib/metaruby/gui/model_selector.rb', line 122

def filter_row_count(parent = Qt::ModelIndex.new)
    model_filter.row_count(parent)
end

#find_resolver_from_model(model) ⇒ Object?

Find the resolver object that has been responsible for a given object’s discovery

Returns:

  • (Object, nil)


89
90
91
# File 'lib/metaruby/gui/model_selector.rb', line 89

def find_resolver_from_model(model)
    @browser_model.find_resolver_from_model(model)
end

#map_index_from_source(source_index, reset_filter: true) ⇒ Qt::ModelIndex

Maps a model index from the source index to the filtered index, e.g. to select something in the view.

In addition to the normal map_from_source call, it allows to control whether the filter should be reset if the index given as parameter is currently filtered out

Parameters:

  • source_index (Qt::ModelIndex)

    an index valid in #browser_model

  • reset_filter (Boolean) (defaults to: true)

    if true, the filter is reset if the requested index is currently filtered out

Returns:



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/metaruby/gui/model_selector.rb', line 279

def map_index_from_source(source_index, reset_filter: true)
    index = model_filter.map_from_source(source_index)
    if !index
        return
    elsif !index.valid?
        if !reset_filter
            return index
        end
        self.reset_filter
        model_filter.map_from_source(source_index)
    else index
    end
end

#model_item_from_filter_row(row, parent = Qt::ModelIndex.new) ⇒ Object



132
133
134
135
136
# File 'lib/metaruby/gui/model_selector.rb', line 132

def model_item_from_filter_row(row, parent = Qt::ModelIndex.new)
    filter_index = model_filter.index(row, 0, parent)
    model_index  = model_filter.map_to_source(filter_index)
    return browser_model.item_from_index(model_index), filter_index
end

#model_items_from_filter(parent = Qt::ModelIndex.new) ⇒ Object



126
127
128
129
130
# File 'lib/metaruby/gui/model_selector.rb', line 126

def model_items_from_filter(parent = Qt::ModelIndex.new)
    (0...filter_row_count(parent)).map do |i|
        model_item_from_filter_row(i, parent)
    end
end

#object_pathsObject



330
331
332
# File 'lib/metaruby/gui/model_selector.rb', line 330

def object_paths
    browser_model.object_paths
end

#register_type(root_model, name, priority = 0, categories: [], resolver: ModelHierarchy::Resolver.new) ⇒ Object

Register a new object type

Parameters:

  • model_base (Module)

    a module or class whose all objects of this type have as superclass

  • name (String)

    the string that should be used to represent objects of this type

  • priority (Integer) (defaults to: 0)

    if an object’s ancestry matches multiple types, only the ones of the highest priority will be retained



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/metaruby/gui/model_selector.rb', line 69

def register_type(root_model, name, priority = 0, categories: [], resolver: ModelHierarchy::Resolver.new)
    @browser_model.add_root(root_model, priority, categories: categories, resolver: resolver)
    type_info[root_model] = name
    action = Qt::Action.new(name, self)
    action.checkable = true
    action.checked = true
    type_filters[root_model] = action
    btn_type_filter_menu.add_action(action)
    connect(action, SIGNAL('triggered()')) do
        update_model_filter
    end
end

#reloadObject

Reload the object model, keeping the current selection if possible



246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/metaruby/gui/model_selector.rb', line 246

def reload
    if current_model = current_selection
        current_path = @browser_model.find_path_from_model(current_model)
    end

    browser_model.reload
    if current_path
        select_by_path(*current_path)
    elsif current_model
        select_by_model(current_model)
    end
end

#reset_filterObject

Resets the current filter



260
261
262
263
264
265
266
# File 'lib/metaruby/gui/model_selector.rb', line 260

def reset_filter
    # If there is a filter, reset it and try again
    if !filter_box.text.empty?
        filter_box.text = ""
        true
    end
end

#select_by_model(model) ⇒ Boolean

Selects the given model if it registered in the model list This emits the model_selected signal

Returns:

  • (Boolean)

    true if the path resolved to something known, and false otherwise



311
312
313
314
315
316
317
# File 'lib/metaruby/gui/model_selector.rb', line 311

def select_by_model(model)
    if index = browser_model.find_index_by_model(model)
        index = map_index_from_source(index)
        model_list.current_index = index
        true
    end
end

#select_by_path(*path) ⇒ Boolean

Selects the current model given a path in the constant names This emits the model_selected signal

Returns:

  • (Boolean)

    true if the path resolved to something known, and false otherwise



298
299
300
301
302
303
304
# File 'lib/metaruby/gui/model_selector.rb', line 298

def select_by_path(*path)
    if index = browser_model.find_index_by_path(*path)
        index = map_index_from_source(index)
        model_list.current_index = index
        true
    end
end

#select_first_itemObject

Select the first displayed item



204
205
206
207
208
# File 'lib/metaruby/gui/model_selector.rb', line 204

def select_first_item
    if item = all_leaves(model_filter, 1).first
        model_list.setCurrentIndex(item)
    end
end

#setup_tree_view(layout) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Create and setup #model_list



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
# File 'lib/metaruby/gui/model_selector.rb', line 213

def setup_tree_view(layout)
    @model_list = Qt::TreeView.new(self)
    @model_filter = Qt::SortFilterProxyModel.new
    model_filter.filter_case_sensitivity = Qt::CaseInsensitive
    model_filter.filter_role = Qt::UserRole
    model_filter.dynamic_sort_filter = true
    model_filter.source_model = browser_model
    model_filter.sort(0)
    model_list.model = model_filter

    @filter_box = Qt::LineEdit.new(self)
    filter_box.connect(SIGNAL('textChanged(QString)')) do |text|
        update_model_filter
    end
    filter_box.connect(SIGNAL('returnPressed()')) do |text|
        select_first_item
    end
    @filter_completer = ModelPathCompleter.new(browser_model, self)
    filter_completer.case_sensitivity = Qt::CaseInsensitive
    filter_box.completer = filter_completer
    layout.add_widget(filter_box)
    layout.add_widget(model_list)

    model_list.selection_model.connect(SIGNAL('currentChanged(const QModelIndex&, const QModelIndex&)')) do |index, _|
        index = model_filter.map_to_source(index)
        if model = browser_model.find_model_from_index(index)
            emit model_selected(Qt::Variant.from_ruby(model, model))
        end
    end
end

#updateObject

Update the view, reloading the underlying model



83
84
85
86
# File 'lib/metaruby/gui/model_selector.rb', line 83

def update
    reload
    update_model_filter
end

#update_model_filterObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Update #model_filter to match the current filter setup



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
# File 'lib/metaruby/gui/model_selector.rb', line 96

def update_model_filter
    type_rx = type_filters.map do |model_base, act|
        if act.checked?
            type_info[model_base]
        end
    end
    type_rx = type_rx.compact.join(",|,")

    model_filter.filter_role = Qt::UserRole # this contains the keywords (ancestry and/or name)
    # This workaround a problem that I did not have time to
    # investigate. Adding new characters to the filter updates the
    # browser just fine, while removing some characters does not
    #
    # This successfully resets the filter
    model_filter.filter_reg_exp = Qt::RegExp.new("")
    # The pattern has to match every element in the hierarchy. We
    # achieve this by making the suffix part optional
    name_rx = filter_box.text.downcase.gsub(/:+/, "/")
    name_rx = '[^;]*,[^,]*' + name_rx.split('/').join("[^,]*,[^;]*;[^;]*,") + '[^,]*,[^;]*'
    regexp = Qt::RegExp.new("(,#{type_rx},)[^;]*;([^;]*;)*#{name_rx}")
    regexp.case_sensitivity = Qt::CaseInsensitive
    model_filter.filter_reg_exp = regexp
    model_filter.invalidate
    auto_open
end