diff options
author | Toshiaki Asai <toshi.alternative@gmail.com> | 2016-10-20 21:49:19 +0900 |
---|---|---|
committer | Toshiaki Asai <toshi.alternative@gmail.com> | 2016-10-20 21:49:19 +0900 |
commit | c610da913865e25d4f789014747fd315760a6f9d (patch) | |
tree | 7a57925a76fc7a762f56437703bbea77f444f072 | |
parent | 1bcbace4f5ec391b780a05c9a01aaf2d0565d2f1 (diff) | |
parent | 8e403b697522adeb981d7c1a5ebe73348542fbbd (diff) | |
download | mikutter-c610da913865e25d4f789014747fd315760a6f9d.tar.gz |
Merge branch 'topic/905-auto-intent-selector' into develop
-rw-r--r-- | core/plugin/intent/intent.rb | 8 | ||||
-rw-r--r-- | core/plugin/intent_selector/intent_selector.rb | 117 | ||||
-rw-r--r-- | core/plugin/intent_selector/listview.rb | 108 | ||||
-rw-r--r-- | core/userconfig.rb | 11 |
4 files changed, 230 insertions, 14 deletions
diff --git a/core/plugin/intent/intent.rb b/core/plugin/intent/intent.rb index 862a1a84..12cc69fc 100644 --- a/core/plugin/intent/intent.rb +++ b/core/plugin/intent/intent.rb @@ -3,6 +3,10 @@ require_relative 'model/intent' require_relative 'model/intent_token' Plugin.create(:intent) do + # 全てのIntentを列挙するためのフィルタ + defevent :intent_catalog, + prototype: [:<<] + # _uri_ を開くことができる Model を列挙するためのフィルタ defevent :model_of_uri, prototype: [URI, :<<] @@ -40,6 +44,10 @@ Plugin.create(:intent) do end [target_model_slug, intents] end + filter_intent_all do |intents| + intents << my_intent + [intents] + end add_event(:"intent_open_#{slug}", &proc) self end diff --git a/core/plugin/intent_selector/intent_selector.rb b/core/plugin/intent_selector/intent_selector.rb index 07bafcf8..3cc0e45f 100644 --- a/core/plugin/intent_selector/intent_selector.rb +++ b/core/plugin/intent_selector/intent_selector.rb @@ -1,14 +1,46 @@ # -*- coding: utf-8 -*- +require_relative 'listview' Plugin.create(:intent_selector) do + UserConfig[:intent_selector_rules] ||= [] + on_intent_select do |intents, model| case model when Retriever::Model - intent_choose_dialog(intents, model: model) + intent_open(intents, model: model) when URI - intent_choose_dialog(intents, uri: model) + intent_open(intents, uri: model) when String - intent_choose_dialog(intents, model: URI.parse(model)) + intent_open(intents, uri: URI.parse(model)) + end + end + + settings(_('関連付け')) do + listview = Plugin::IntentSelector::IntentSelectorListView.new + pack_start(Gtk::VBox.new(false, 4). + closeup(listview.filter_entry). + add(Gtk::HBox.new(false, 4). + add(listview). + closeup(listview.buttons(Gtk::VBox)))) + end + + # _model:_ または _uri:_ を開くintentを _intents_ の中から選び出し、その方法で開く。 + # このメソッドは、まず設定されたルールでintentを選出し、一つにintentが定まれば直ちにそれで開く。 + # 候補が一つに絞れなかった場合は、intent選択ダイアログを表示して、ユーザに決定を仰ぐ。 + # ==== Args + # [intents] Intent modelの配列 + # [model:] 開くModel。 _uri:_ しかわからない場合は、省略してもよい + # [uri:] 開くURI。 _model:_ を渡している場合は、省略してもよい + def intent_open(intents, model: nil, uri: model.uri) + recommended, suggested = divide_intents(intents, uri, specified_model_slug(model)) + if recommended.size == 1 + Plugin::Intent::IntentToken.open( + uri: uri, + model: model, + intent: recommended.first, + parent: nil) + else + intent_choose_dialog(recommended + suggested, model: model, uri: uri) end end @@ -17,36 +49,93 @@ Plugin.create(:intent_selector) do dialog.window_position = Gtk::Window::POS_CENTER dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK) dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL) - dialog.vbox.closeup(Gtk::Label.new("%{uri} \nを開こうとしています。どの方法で開きますか?" % {uri: uri}, false)) - selected_intent = nil + dialog.vbox.closeup(Gtk::Label.new("%{uri}\nを開こうとしています。どの方法で開きますか?" % {uri: uri}, false)) + intent_token_builder = { + uri: uri, + model: model, + intent: nil, + parent: nil } intents.inject(nil) do |group, intent| if group radio = Gtk::RadioButton.new(group, intent.label) else - selected_intent = intent + intent_token_builder[:intent] = intent radio = Gtk::RadioButton.new(intent.label) end radio.ssc(:toggled) do |w| - selected_intent = intent + intent_token_builder[:intent] = intent false end radio.ssc(:activate) do |w| - selected_intent = intent + intent_token_builder[:intent] = intent dialog.signal_emit(:response, Gtk::Dialog::RESPONSE_OK) false end dialog.vbox.closeup(radio) group || radio end + saving_rule_checkbox(dialog, intent_token_builder, specified_model_slug(model)) dialog.ssc(:response) do |w, response_id| - if response_id == Gtk::Dialog::RESPONSE_OK and selected_intent - Plugin::Intent::IntentToken.open( - uri: uri, - model: model, - intent: selected_intent, - parent: nil) + if response_id == Gtk::Dialog::RESPONSE_OK and intent_token_builder[:intent] + Plugin::Intent::IntentToken.open(**intent_token_builder) end w.destroy + false end dialog.show_all end + + def saving_rule_checkbox(dialog, intent_token_builder, model_slug) + save_check = Gtk::CheckButton.new(_('次回から、次の内容から始まるURLはこの方法で開く')) + rule = Gtk::Entry.new.set_text(intent_token_builder[:uri].to_s) + rule.sensitive = false + save_check.ssc(:toggled) do |widget| + rule.sensitive = widget.active? + false + end + dialog.ssc(:response) do |w, response_id| + if response_id == Gtk::Dialog::RESPONSE_OK and intent_token_builder[:intent] and save_check.active? + add_intent_rule(intent: intent_token_builder[:intent], + str: rule.text, + rule: 'start', + model_slug: model_slug) + end + false + end + dialog.vbox. + closeup(save_check). + closeup(rule) + end + + def add_intent_rule(intent:, str:, rule:, model_slug:) + unless UserConfig[:intent_selector_rules].any?{|r| r[:intent].to_sym == intent.slug && r[:str] == str && r[:rule] == rule } + UserConfig[:intent_selector_rules] += [{uuid: SecureRandom.uuid, intent: intent.slug, model: model_slug, str: str, rule: rule}] + end + end + + # intent の配列を受け取り、ユーザが過去に入力したルールに基づき、 + # recommendedとsuggestedに分ける + # ==== Args + # [intents] スキャン対象のintent + # [uri] リソースのURI + # [model_slug] 絞り込みに使うModelのslug。 + # ==== Return + # 条件に対して推奨されるintentの配列と、intentsに指定されたそれ以外の値の配列 + def divide_intents(intents, uri, model_slug) + intent_slugs = UserConfig[:intent_selector_rules].select{|record| + model_slug == record[:model].to_s && uri.to_s.start_with?(record[:str]) + }.map{|record| + record[:intent].to_sym + } + intents.partition{|intent| intent_slugs.include?(intent.slug) } + end + + # _model_ のmodel slugを文字列で得る。 + # ==== Args + # [model] Retriever::Modelのインスタンス又はnil + # ==== Return + # [String] Modelのslug。 _model_ がnilだった場合は空文字列 + def specified_model_slug(model) + model ? model.class.slug.to_s : '' + end + end diff --git a/core/plugin/intent_selector/listview.rb b/core/plugin/intent_selector/listview.rb new file mode 100644 index 00000000..ae37ba52 --- /dev/null +++ b/core/plugin/intent_selector/listview.rb @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +module Plugin::IntentSelector + class IntentSelectorListView < ::Gtk::CRUD + COLUMN_INTENT_LABEL = 0 + COLUMN_MODEL_LABEL = 1 + COLUMN_STRING = 2 + COLUMN_INTENT = 3 + COLUMN_MODEL = 4 + COLUMN_UUID = 5 + + def initialize + super + intents = intent_catalog + models = model_catalog + UserConfig[:intent_selector_rules].each do |record| + iter = model.model.append + iter[COLUMN_INTENT] = record[:intent] + iter[COLUMN_INTENT_LABEL] = intents[record[:intent].to_sym] + iter[COLUMN_MODEL] = record[:model] + iter[COLUMN_MODEL_LABEL] = models[record[:model].to_s] + iter[COLUMN_STRING] = record[:str] + iter[COLUMN_UUID] = record[:uuid] + end + end + + def initialize_model + set_model(Gtk::TreeModelFilter.new(Gtk::ListStore.new(*column_schemer.flatten.map{|x| x[:type]}))) + model.set_visible_func{ |model, iter| + if defined?(@filter_entry) and @filter_entry + [COLUMN_INTENT, COLUMN_INTENT_LABEL, COLUMN_MODEL, COLUMN_MODEL_LABEL, COLUMN_STRING].any?{ |column| iter[column].to_s.include?(@filter_entry.text) } + else + true end } + end + + def column_schemer + [{kind: :text, type: Symbol, expand: true, label: _('開く方法')}, + {kind: :text, type: String, expand: true, label: _('対象')}, + {kind: :text, type: String, widget: :input, expand: true, label: _('条件')}, + {type: Symbol, widget: :chooseone, args: [intent_catalog], label: _('開く方法')}, + {type: String, widget: :chooseone, args: [model_catalog], label: _('対象')}, + {type: String}, + ].freeze + end + + def on_created(iter) + iter[COLUMN_UUID] = SecureRandom.uuid + iter[COLUMN_INTENT_LABEL] = intent_catalog[iter[COLUMN_INTENT].to_sym] + iter[COLUMN_MODEL_LABEL] = model_catalog[iter[COLUMN_MODEL].to_s] + UserConfig[:intent_selector_rules] += [{ + intent: iter[COLUMN_INTENT].to_sym, + model: iter[COLUMN_MODEL], + str: iter[COLUMN_STRING], + rule: 'start', + uuid: iter[COLUMN_UUID] + }] + end + + def on_updated(iter) + iter[COLUMN_INTENT_LABEL] = intent_catalog[iter[COLUMN_INTENT].to_sym] + iter[COLUMN_MODEL_LABEL] = model_catalog[iter[COLUMN_MODEL].to_s] + UserConfig[:intent_selector_rules] = UserConfig[:intent_selector_rules].map do |record| + if record[:uuid] == iter[COLUMN_UUID] + record.merge( + intent: iter[COLUMN_INTENT].to_sym, + model: iter[COLUMN_MODEL], + str: iter[COLUMN_STRING]) + else + record + end + end + end + + def on_deleted(iter) + UserConfig[:intent_selector_rules] = UserConfig[:intent_selector_rules].reject do |record| + record[:uuid] == iter[COLUMN_UUID] + end + end + + private + + def filter_entry + @filter_entry ||= Gtk::Entry.new.tap do |entry| + entry.primary_icon_pixbuf = Gdk::WebImageLoader.pixbuf(MUI::Skin.get("search.png"), 24, 24) + entry.ssc(:changed, self, &gen_refilter) + end + end + + def gen_refilter + proc do + model.refilter + end + end + + def _(str) + Plugin[:intent_selector]._(str) + end + + def intent_catalog + Hash[Plugin.filtering(:intent_all, []).first.map{|i|[i.slug, i.label]}] + end + + def model_catalog + Hash[Plugin.filtering(:retrievers, []).first.map{|s|[s[:slug].to_s,s[:name]]}].merge('': _('(未定義)')) + end + + end +end diff --git a/core/userconfig.rb b/core/userconfig.rb index bbb82ca9..051dbf3c 100644 --- a/core/userconfig.rb +++ b/core/userconfig.rb @@ -139,6 +139,17 @@ class UserConfig :quote_text_max_line_count => 10, :reply_clicked_action => :open, :quote_clicked_action => :open, + + :intent_selector_rules => [{:uuid=>"8ab31d89-6d5f-4765-bb99-7a93ce5b1139", + :intent=>:user_detail_view_twitter_user, + :model=>"", + :str=>"https://twitter.com/", + :rule=>"start"}, + {:uuid=>"c0095e59-75da-4177-98e0-d4955ece1d20", + :intent=>:message_detail_view_twitter_tweet, + :model=>"", + :str=>"https://twitter.com/", + :rule=>"start"}] } @@watcher = Hash.new{ [] } |