├── .gitignore ├── icon.png ├── schemas ├── gschemas.compiled └── org.gnome.shell.extensions.web-search-dialog.gschema.xml ├── metadata.json ├── README.md ├── stylesheet.css ├── history_manager.js ├── helper.js ├── suggestions_box.js ├── utils.js ├── prefs.js └── extension.js /.gitignore: -------------------------------------------------------------------------------- 1 | #Translations 2 | *.mo 3 | .*~ 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awamper/web-search-dialog/HEAD/icon.png -------------------------------------------------------------------------------- /schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awamper/web-search-dialog/HEAD/schemas/gschemas.compiled -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Search the web directly from Gnome Shell.\n\n+space triggers the dialog. From there, type your search query, click the ENTER key and your default browser will open with your search result.\n\nList of features include:\n\n*Instant result/definition (DuckDuckGo helper) with pictures within the dialog\n*Search Suggestions\n*History with configurable limit\n*Choose your default search engine\n*Add multiple seach engines\n*Tab key to view & choose from search engine list\n*Ctrl+Shift+V to Paste & Search\n*Ctrl+Shift+G to Paste & Go (Open URL)\n*Ctrl+(1-9) to trigger search suggestion or history item in list\n*Add keyword for each search engine\n*Add keyword to go to URL directly", 3 | "name": "Web Search Dialog", 4 | "shell-version": [ 5 | "3.26" 6 | ], 7 | "url": "http://github.com/awamper/web-search-dialog", 8 | "uuid": "web_search_dialog@awamper.gmail.com", 9 | "settings-schema": "org.gnome.shell.extensions.web-search-dialog", 10 | "version": 1 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](https://extensions.gnome.org/extension-data/screenshots/screenshot_549_1.png) 2 | 3 | #### Search the web directly from Gnome Shell. 4 | < Ctrl >+space triggers the dialog. From there, type your search query, click the ENTER key and your default browser will open with your search result. 5 | 6 | List of features include: 7 | - Instant result/definition (DuckDuckGo helper) with pictures within the dialog 8 | - Search Suggestions 9 | - History with configurable limit 10 | - Choose your default search engine 11 | - Add multiple seach engines 12 | - Tab key to view & choose from search engine list 13 | - Ctrl+Shift+V to Paste & Search 14 | - Ctrl+Shift+G to Paste & Go (Open URL) 15 | - Ctrl+(1-9) to trigger search suggestion or history item in list 16 | - Add keyword for each search engine 17 | - Add keyword to go to URL directly 18 | 19 | ---- 20 | 21 | # Installation 22 | 23 | After the installation, restart GNOME Shell (`Alt`+`F2`, `r`, `Enter`) and enable the extension through *gnome-tweak-tool*. 24 | 25 | ### Install older version 26 | Navigate into the extension directory (e.g. `~/.local/share/gnome-shell/extensions/web_search_dialog@awamper.gmail.com`) and run ` git co X` Replace X with your GNOME Shell version (e.g. 3.20). 27 | 28 | ## Through extensions.gnome.org (Local installation) 29 | 30 | Go on the [Web Search Dialog ](https://extensions.gnome.org/extension/549/web-search-dialog/) extension page on extensions.gnome.org, click on the switch ("OFF" => "ON"), click on the install button. 31 | 32 | ## Generic (Local installation) 33 | 34 | Run the following command: 35 | 36 | `git clone https://github.com/awamper/web-search-dialog.git ~/.local/share/gnome-shell/extensions/web_search_dialog@awamper.gmail.com` 37 | 38 | 39 | ## Generic (Global installation, requires root) 40 | 41 | Run the following command: 42 | 43 | `sudo git clone https://github.com/awamper/web-search-dialog.git /usr/share/gnome-shell/extensions/web_search_dialog@awamper.gmail.com/` 44 | -------------------------------------------------------------------------------- /stylesheet.css: -------------------------------------------------------------------------------- 1 | .web-search-dialog { 2 | border-radius: 5px; 3 | background-color: rgba(0.0, 0.0, 0.0, 0.8); 4 | 5 | padding-right: 5px; 6 | padding-left: 5px; 7 | padding-bottom: 5px; 8 | padding-top: 5px; 9 | } 10 | 11 | .web-search-entry { 12 | width: 800px; 13 | height: 40pt; 14 | font-size: 30pt; 15 | color: rgb(128, 128, 128); 16 | border: 1px solid rgba(245,245,245,0.2); 17 | selection-background-color: white; 18 | selected-color: black; 19 | padding: 1px; 20 | background-color: rgba(0, 0, 0, 0); 21 | } 22 | 23 | .search-engine-label { 24 | border: 1px solid rgba(245,245,245,0.2); 25 | background-color: rgba(0.0, 0.0, 0.0, 0.0); 26 | font-size: 30pt; 27 | } 28 | 29 | .search-hint { 30 | padding-left: 5px; 31 | padding-top: 2px; 32 | font-weight: bold; 33 | font-size: 7pt; 34 | color: rgba(255, 255, 255, 1); 35 | } 36 | .hint-icon { 37 | padding-left: 5px; 38 | padding-top: 2px; 39 | icon-size: 20px; 40 | color: rgba(255, 255, 255, 1); 41 | } 42 | 43 | .menu-item-icon { 44 | icon-size: 15pt; 45 | color: #8d8f8a; 46 | } 47 | 48 | .helper-box { 49 | border: 1px solid rgba(245,245,245,0.2); 50 | } 51 | 52 | .helper-title { 53 | font-style: bold; 54 | padding-top: 1px; 55 | padding-right: 3px; 56 | padding-left: 3px; 57 | padding-bottom: 2px; 58 | font-size: 17pt; 59 | color: black; 60 | background-color: rgba(245,245,245,0.2); 61 | } 62 | 63 | .helper-abstract { 64 | padding-top: 5px; 65 | padding-right: 5px; 66 | padding-left: 5px; 67 | padding-bottom: 5px; 68 | font-size: 15pt; 69 | } 70 | 71 | .helper-definition { 72 | font-style: italic; 73 | padding-top: 5px; 74 | padding-right: 5px; 75 | padding-left: 5px; 76 | padding-bottom: 5px; 77 | font-size: 15pt; 78 | } 79 | 80 | .helper-icon-box { 81 | padding-left: 5px; 82 | padding-right: 5px; 83 | padding-top: 5px; 84 | padding-bottom: 5px; 85 | } 86 | 87 | .item-id-label { 88 | padding-top: 2px; 89 | padding-right: 2px; 90 | font-size: 10pt; 91 | color: rgba(255, 255, 255, 1); 92 | } 93 | 94 | .settings-icon { 95 | padding-right: 3px; 96 | icon-size: 25pt; 97 | color: #8d8f8a; 98 | } 99 | -------------------------------------------------------------------------------- /schemas/org.gnome.shell.extensions.web-search-dialog.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | space']]]> 8 | Open web search. 9 | 10 | 11 | 12 | "[]" 13 | 14 | 15 | 16 | 17 | [ 18 | '{"name": "Google", "keyword": "g", "url": "https://google.com/search?q={term}"}', 19 | '{"name": "DuckDuckGo", "keyword": "d", "url": "https://duckduckgo.com/?q={term}"}', 20 | '{"name": "Yandex", "keyword": "y", "url": "https://yandex.com/yandsearch?text={term}"}', 21 | '{"name": "Wikipedia", "keyword": "w", "url": "https://en.wikipedia.org/wiki/Special:Search?search={term}"}', 22 | '{"name": "Bing", "keyword": "b", "url": "https://www.bing.com/search?q={term}"}' 23 | ] 24 | 25 | Search engines. 26 | 27 | 28 | 29 | true 30 | Enable suggetsions 31 | 32 | 33 | 34 | true 35 | Always select first suggestion 36 | 37 | 38 | 39 | 150 40 | 41 | 42 | 43 | true 44 | 45 | 46 | 47 | 500 48 | 49 | 50 | 51 | "bottom" 52 | 53 | 54 | 55 | true 56 | Enable history suggestions. 57 | 58 | 59 | 60 | 500 61 | 62 | 63 | 64 | 0 65 | 66 | 67 | 68 | "Open URL" 69 | 70 | 71 | 72 | "url" 73 | 74 | 75 | 76 | 'en' 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /history_manager.js: -------------------------------------------------------------------------------- 1 | const Lang = imports.lang; 2 | const Params = imports.misc.params; 3 | 4 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 5 | const Prefs = Me.imports.prefs; 6 | const Utils = Me.imports.utils; 7 | 8 | var SearchHistoryManager = new Lang.Class({ 9 | Name: "SearchHistoryManager", 10 | 11 | _init: function(params) { 12 | this._settings = Utils.getSettings(); 13 | 14 | params = Params.parse(params, { 15 | gsettings_key: Prefs.HISTORY_KEY, 16 | limit: this._settings.get_int(Prefs.HISTORY_LIMIT_KEY) 17 | }); 18 | 19 | this._key = params.gsettings_key; 20 | this._limit = params.limit; 21 | 22 | if(this._key) { 23 | this._history = JSON.parse(this._settings.get_string(this._key)); 24 | this._settings.connect( 25 | 'changed::'+this._key, 26 | Lang.bind(this, this._history_changed) 27 | ); 28 | } 29 | else { 30 | this._history = []; 31 | } 32 | 33 | this._history_index = this._history.length; 34 | }, 35 | 36 | _history_changed: function() { 37 | this._history = JSON.parse(this._settings.get_string(this._key)); 38 | this._history_index = this._history.length; 39 | }, 40 | 41 | prev_item: function(text) { 42 | if(this._history_index <= 0) { 43 | return {query: text}; 44 | } 45 | 46 | this._history_index--; 47 | 48 | return this._index_changed(); 49 | }, 50 | 51 | next_item: function(text) { 52 | if(this._history_index >= this._history.length) { 53 | return {query: text}; 54 | } 55 | 56 | this._history_index++; 57 | 58 | return this._index_changed(); 59 | }, 60 | 61 | last_item: function() { 62 | if(this._history_index != this._history.length) { 63 | this._history_index = this._history.length; 64 | this._index_changed(); 65 | } 66 | 67 | return this._history_index[this._history.length]; 68 | }, 69 | 70 | current_index: function() { 71 | return this._history_index; 72 | }, 73 | 74 | total_items: function() { 75 | return this._history.length; 76 | }, 77 | 78 | add_item: function(input) { 79 | if(this._history.length == 0 || 80 | this._history[this._history.length - 1].query != input.query || 81 | this._history[this._history.length - 1].type != input.type) { 82 | 83 | this._history.push(input); 84 | this._save(); 85 | } 86 | this._history_index = this._history.length; 87 | }, 88 | 89 | get_best_matches: function(params) { 90 | params = Params.parse(params, { 91 | text: false, 92 | types: ['QUERY', 'NAVIGATION'], 93 | min_score: 0.5, 94 | limit: 5, 95 | fuzziness: 0.5 96 | }); 97 | 98 | if(params.text == false) { 99 | return false; 100 | } 101 | 102 | let result = []; 103 | let history = this._history; 104 | 105 | for(let i = 0; i < history.length; i++) { 106 | if(params.types.indexOf(history[i].type) == -1) { 107 | continue; 108 | } 109 | 110 | let score = Utils.string_score( 111 | history[i].query, 112 | params.text, 113 | params.fuzziness 114 | ); 115 | 116 | if(score >= params.min_score) { 117 | result.push([score, history[i]]); 118 | } 119 | } 120 | 121 | result.sort(function(a, b){return a[0] < b[0]}); 122 | 123 | return result.slice(0, params.limit); 124 | }, 125 | 126 | reset_index: function() { 127 | this._history_index = this._history.length; 128 | }, 129 | 130 | _index_changed: function() { 131 | let current = this._history[this._history_index] || { 132 | query: '' 133 | }; 134 | 135 | return current; 136 | }, 137 | 138 | _save: function() { 139 | if(this._history.length > this._limit) { 140 | this._history.splice(0, this._history.length - this._limit); 141 | } 142 | 143 | if(this._key) { 144 | this._settings.set_string(this._key, JSON.stringify(this._history)); 145 | } 146 | } 147 | }); -------------------------------------------------------------------------------- /helper.js: -------------------------------------------------------------------------------- 1 | const St = imports.gi.St; 2 | const Lang = imports.lang; 3 | const PopupMenu = imports.ui.popupMenu; 4 | const Params = imports.misc.params; 5 | const Tweener = imports.ui.tweener; 6 | const Soup = imports.gi.Soup; 7 | const Gio = imports.gi.Gio; 8 | const Clutter = imports.gi.Clutter; 9 | 10 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 11 | const Utils = Me.imports.utils; 12 | const Prefs = Me.imports.prefs; 13 | 14 | 15 | const DUCKDUCKGO_API_URL = 16 | "https://api.duckduckgo.com/?format=json&no_redirect=1"+ 17 | "&skip_disambig=1&q="; 18 | 19 | var HelperSpinnerMenuItem = Lang.Class({ 20 | Name: 'HelperSpinnerMenuItem', 21 | Extends: PopupMenu.PopupBaseMenuItem, 22 | 23 | _init: function(text) { 24 | this.parent({ 25 | reactive: false, 26 | activate: false, 27 | hover: false, 28 | can_focus: false 29 | }); 30 | this._type = 'HELPER'; 31 | 32 | let label = new St.Label({ 33 | text: Utils.is_blank(text) ? 'Checking helper...' : text 34 | }); 35 | 36 | let box = new St.BoxLayout({ 37 | style_class: 'helper-title' 38 | }); 39 | box.add(label); 40 | 41 | this.actor.add_child(box); 42 | } 43 | }); 44 | 45 | var DuckDuckGoHelperMenuItem = new Lang.Class({ 46 | Name: 'DuckDuckGoHelperMenuItem', 47 | Extends: PopupMenu.PopupBaseMenuItem, 48 | 49 | _init: function(data) { 50 | this.parent({ 51 | reactive: false, 52 | activate: false, 53 | hover: false, 54 | can_focus: false 55 | }); 56 | this._type = 'HELPER'; 57 | 58 | data = Params.parse(data, { 59 | heading: '', 60 | definition: '', 61 | abstract: '', 62 | icon: '' 63 | }); 64 | 65 | if(Utils.is_blank(data.abstract) && Utils.is_blank(data.definition)) { 66 | return false; 67 | } 68 | 69 | let icon = this._get_icon(data.icon); 70 | 71 | let grid_layout = new Clutter.GridLayout(); 72 | let grid = new St.Widget({ 73 | name: 'helper_table', 74 | style_class: 'helper-box', 75 | layout_manager: grid_layout, 76 | visible: true 77 | }); 78 | 79 | let max_length = 80; 80 | 81 | if(icon) { 82 | grid_layout.attach(icon, 0, 0, 1, 1); 83 | } 84 | else { 85 | max_length = 110; 86 | } 87 | 88 | let text = ''; 89 | if(data.definition) {text += ''+data.definition.trim()+'\n';} 90 | if(data.abstract) {text += data.abstract.trim();} 91 | let label = this._get_label(text, 'helper-abstract', max_length); 92 | 93 | grid_layout.attach(label, 1, 0, 1, 1); 94 | this.actor.add_child(grid); 95 | 96 | return true; 97 | }, 98 | 99 | _get_icon: function(icon_info) { 100 | let info = Params.parse(icon_info, { 101 | url: false, 102 | width: 120, 103 | height: 100 104 | }); 105 | 106 | if(!info.url) { 107 | return false; 108 | } 109 | 110 | let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; 111 | let textureCache = St.TextureCache.get_default(); 112 | let image_file = Gio.file_new_for_uri(info.url); 113 | let icon = textureCache.load_file_async( 114 | image_file, 115 | info.width, 116 | info.height, 117 | scale_factor 118 | ); 119 | 120 | this.icon_box = new St.BoxLayout({ 121 | style_class: 'helper-icon-box', 122 | opacity: 0 123 | }); 124 | 125 | this.icon_box.add(icon); 126 | this.icon_box.connect('notify::allocation', Lang.bind(this, function() { 127 | let natural_width = this.icon_box.get_preferred_width(-1)[1]; 128 | 129 | if(natural_width > 10) { 130 | Tweener.addTween(this.icon_box, { 131 | transition: 'easeOutQuad', 132 | time: 1, 133 | opacity: 255 134 | }); 135 | } 136 | })); 137 | 138 | return this.icon_box; 139 | }, 140 | 141 | _get_label: function(text, class_name, max_length) { 142 | if(Utils.is_blank(text)) { 143 | return false; 144 | } 145 | 146 | text = Utils.wordwrap(text.trim(), max_length); 147 | 148 | let label = new St.Label({ 149 | text: text, 150 | style_class: class_name 151 | }); 152 | label.clutter_text.use_markup = true; 153 | label.clutter_text.line_wrap = true; 154 | 155 | return label; 156 | } 157 | }); 158 | 159 | var DuckDuckGoHelper = new Lang.Class({ 160 | Name: 'DuckDuckGoHelper', 161 | 162 | _init: function() { 163 | this._settings = Utils.getSettings(); 164 | this._http_session = this._create_session(); 165 | }, 166 | 167 | _create_session: function() { 168 | let http_session = new Soup.Session({ 169 | user_agent: Utils.DEFAULT_USER_AGENT, 170 | timeout: 5, 171 | accept_language: 'en' 172 | }); 173 | Soup.Session.prototype.add_feature.call( 174 | http_session, 175 | new Soup.ProxyResolverDefault() 176 | ); 177 | 178 | return http_session; 179 | }, 180 | 181 | _get_data_async: function(url, callback) { 182 | let request = Soup.Message.new('GET', url); 183 | 184 | this._http_session.accept_language = this._settings.get_string(Prefs.LANGUAGE_CODE); 185 | this._http_session.queue_message(request, 186 | Lang.bind(this, function(http_session, message) { 187 | if(message.status_code === 200) { 188 | callback.call(this, request.response_body.data); 189 | } 190 | else { 191 | callback.call(this, false); 192 | } 193 | }) 194 | ); 195 | }, 196 | 197 | _parse_response: function(response) { 198 | response = JSON.parse(response); 199 | 200 | let result = { 201 | heading: Utils.is_blank(response.Heading) 202 | ? false 203 | : response.Heading.trim().replace(/<[^>]+>/g, ""), 204 | abstract: Utils.is_blank(response.Abstract) 205 | ? false 206 | : response.AbstractText.trim().replace(/<[^>]+>/g, ""), 207 | definition: 208 | Utils.is_blank(response.Definition) || 209 | response.Definition == response.Abstract 210 | ? false 211 | : response.Definition.trim().replace(/<[^>]+>/g, ""), 212 | image: Utils.is_blank(response.Image) 213 | ? false 214 | : response.Image.trim() 215 | }; 216 | 217 | return result; 218 | }, 219 | 220 | get_info: function(query, callback) { 221 | query = query.trim(); 222 | 223 | if(Utils.is_blank(query)) { 224 | return false; 225 | } 226 | 227 | let url = DUCKDUCKGO_API_URL+encodeURIComponent(query); 228 | this._get_data_async(url, Lang.bind(this, function(result) { 229 | if(!result) { 230 | callback.call(this, false); 231 | } 232 | 233 | let info = this._parse_response(result); 234 | callback.call(this, info); 235 | })); 236 | 237 | return true; 238 | }, 239 | 240 | get_menu_item: function(data) { 241 | data = Params.parse(data, { 242 | heading: '', 243 | definition: '', 244 | abstract: '', 245 | icon: false 246 | }); 247 | 248 | if(Utils.is_blank(data.abstract) && Utils.is_blank(data.definition)) { 249 | return false; 250 | } 251 | else { 252 | let menu_item = new DuckDuckGoHelperMenuItem(data); 253 | 254 | return menu_item; 255 | } 256 | } 257 | }); 258 | -------------------------------------------------------------------------------- /suggestions_box.js: -------------------------------------------------------------------------------- 1 | const St = imports.gi.St; 2 | const Lang = imports.lang; 3 | const Main = imports.ui.main; 4 | const Clutter = imports.gi.Clutter; 5 | const PopupMenu = imports.ui.popupMenu; 6 | const Params = imports.misc.params; 7 | const Soup = imports.gi.Soup; 8 | 9 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 10 | const Utils = Me.imports.utils; 11 | 12 | const ICONS = Utils.ICONS; 13 | 14 | const SuggestionMenuItem = new Lang.Class({ 15 | Name: 'SuggestionMenuItem', 16 | Extends: PopupMenu.PopupBaseMenuItem, 17 | 18 | _init: function(text, type, relevance, term, item_id, params) { 19 | this.parent(params); 20 | 21 | this._text = text.trim(); 22 | this._type = type; 23 | this._relevance = relevance; 24 | this._term = term.trim(); 25 | this._item_id = item_id; 26 | 27 | let highlight_text = Utils.escape_html(this._text).replace( 28 | new RegExp( 29 | '(.*?)('+Utils.escape_html(this._term)+')(.*?)', 30 | "i" 31 | ), 32 | "$1$2$3" 33 | ); 34 | 35 | let id_label = false; 36 | 37 | if(this._item_id > 0 && this._item_id <= 9) { 38 | id_label = new St.Label({ 39 | text: '^'+this._item_id.toString(), 40 | style_class: 'item-id-label' 41 | }); 42 | } 43 | 44 | let label = new St.Label({ 45 | text: highlight_text 46 | }); 47 | label.clutter_text.use_markup = true; 48 | 49 | let icon = new St.Icon({ 50 | style_class: 'menu-item-icon' 51 | }); 52 | 53 | if(this._type == 'NAVIGATION') { 54 | icon.icon_name = ICONS.web; 55 | } 56 | else { 57 | icon.icon_name = ICONS.find; 58 | } 59 | 60 | this._box = new St.BoxLayout(); 61 | 62 | if(id_label) { 63 | this._box.add(id_label); 64 | } 65 | 66 | this._box.add(icon); 67 | this._box.add(label); 68 | 69 | this.actor.add_child(this._box); 70 | this.actor.label_actor = label; 71 | }, 72 | 73 | _onKeyPressEvent: function(actor, event) { 74 | let symbol = event.get_key_symbol(); 75 | 76 | if(symbol == Clutter.Return || symbol == Clutter.KP_Enter) { 77 | this.activate(event); 78 | return true; 79 | } 80 | else { 81 | return false; 82 | } 83 | } 84 | }); 85 | 86 | var SuggestionsBox = new Lang.Class({ 87 | Name: 'SuggestionsBox', 88 | Extends: PopupMenu.PopupMenu, 89 | 90 | _init: function(search_dialog) { 91 | this._search_dialog = search_dialog; 92 | this._entry = this._search_dialog.search_entry; 93 | 94 | this.parent(this._entry, 0, St.Side.TOP); 95 | 96 | Main.uiGroup.add_actor(this.actor); 97 | this.actor.hide(); 98 | this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent)); 99 | }, 100 | 101 | _onKeyPressEvent: function(actor, event) { 102 | let symbol = event.get_key_symbol(); 103 | 104 | if(symbol == Clutter.Escape) { 105 | this.close(true); 106 | } 107 | else if(symbol == Clutter.BackSpace) { 108 | this._entry.grab_key_focus(); 109 | this._search_dialog.show_suggestions = false; 110 | this._entry.set_text(this._entry.get_text().slice(0, -1)); 111 | } 112 | else { 113 | let skip_keys = ( 114 | symbol == Clutter.Up || 115 | symbol == Clutter.Down || 116 | symbol == Clutter.Tab 117 | ); 118 | 119 | if(!skip_keys) { 120 | let ch = this._get_unichar(symbol); 121 | this._entry.grab_key_focus(); 122 | 123 | if(ch) { 124 | this._entry.set_text(this._entry.get_text() + ch); 125 | } 126 | } 127 | } 128 | }, 129 | 130 | _get_unichar: function(keyval) { 131 | let ch = Clutter.keysym_to_unicode(keyval); 132 | 133 | if(ch) { 134 | return String.fromCharCode(ch); 135 | } 136 | else { 137 | return false; 138 | } 139 | }, 140 | 141 | _on_activated: function(menu_item) { 142 | this._search_dialog._remove_delay_id(); 143 | this._search_dialog.suggestions_box.close(true); 144 | 145 | if(menu_item._type == "ENGINE") { 146 | let engine_keyword = menu_item._term.trim(); 147 | this._search_dialog._set_engine(engine_keyword); 148 | } 149 | else { 150 | let text = menu_item._text.trim(); 151 | 152 | if(menu_item._type == 'NAVIGATION') { 153 | this._search_dialog._open_url(text, true); 154 | } 155 | else { 156 | this._search_dialog._activate_search(text); 157 | } 158 | } 159 | 160 | return true; 161 | }, 162 | 163 | _on_active_changed: function(menu_item) { 164 | if(menu_item._type != 'ENGINE') { 165 | this._search_dialog.show_suggestions = false; 166 | this._entry.set_text(menu_item._text); 167 | } 168 | 169 | return true; 170 | }, 171 | 172 | _get_next_id: function() { 173 | let items = this._getMenuItems(); 174 | let types = ['NAVIGATION', 'QUERY', 'ENGINE']; 175 | let count = 1; 176 | 177 | for(let i = 0; i < items.length; i++) { 178 | if(types.indexOf(items[i]._type) != -1) { 179 | count++; 180 | 181 | if(count >= 9) { 182 | return false; 183 | } 184 | } 185 | } 186 | 187 | return count; 188 | }, 189 | 190 | activate_by_id: function(item_id) { 191 | if(item_id < 1 || item_id > 9) return; 192 | 193 | let items = this._getMenuItems(); 194 | 195 | for(let i = 0; i < items.length; i++) { 196 | if(items[i]._item_id === item_id) { 197 | items[i].activate(); 198 | break; 199 | } 200 | } 201 | 202 | }, 203 | 204 | add_suggestion: function(params) { 205 | params = Params.parse(params, { 206 | text: false, 207 | type: 'QUERY', 208 | relevance: 0, 209 | term: '' 210 | }); 211 | 212 | if(!params.text) { 213 | return false; 214 | } 215 | 216 | let item = new SuggestionMenuItem( 217 | params.text, 218 | params.type, 219 | params.relevance, 220 | params.term, 221 | this._get_next_id() 222 | ); 223 | item.connect( 224 | 'activate', 225 | Lang.bind(this, this._on_activated) 226 | ); 227 | item.connect( 228 | 'active-changed', 229 | Lang.bind(this, this._on_active_changed) 230 | ); 231 | this.addMenuItem(item) 232 | 233 | return true; 234 | }, 235 | 236 | add_label: function(text) { 237 | let item = new PopupMenu.PopupMenuItem(text, { 238 | reactive: false, 239 | activate: false, 240 | hover: false, 241 | can_focus: false 242 | }); 243 | item._type = 'LABEL'; 244 | this.addMenuItem(item); 245 | }, 246 | 247 | remove_all_by_types: function(types_array) { 248 | let children = this._getMenuItems(); 249 | 250 | for(let i = 0; i < children.length; i++) { 251 | let item = children[i]; 252 | 253 | if(types_array === 'ALL') { 254 | item.destroy(); 255 | } 256 | else if(types_array.indexOf(item._type) > -1) { 257 | item.destroy(); 258 | } 259 | else { 260 | continue; 261 | } 262 | } 263 | }, 264 | 265 | close: function() { 266 | this._search_dialog._remove_delay_id(); 267 | this._entry.grab_key_focus(); 268 | this.parent(); 269 | } 270 | }); 271 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | 3 | /* 4 | * Part of this file comes from gnome-shell-extensions: 5 | * http://git.gnome.org/browse/gnome-shell-extensions/ 6 | * 7 | */ 8 | 9 | 10 | const Gettext = imports.gettext; 11 | const Gio = imports.gi.Gio; 12 | const Lang = imports.lang; 13 | const Params = imports.misc.params; 14 | const Config = imports.misc.config; 15 | const ExtensionUtils = imports.misc.extensionUtils; 16 | const Soup = imports.gi.Soup; 17 | const Clutter = imports.gi.Clutter; 18 | 19 | var DEFAULT_USER_AGENT = 'WebSearchDialog extension (GNOME Shell)'; 20 | var _httpSession = new Soup.SessionAsync(); 21 | Soup.Session.prototype.add_feature.call( 22 | _httpSession, 23 | new Soup.ProxyResolverDefault() 24 | ); 25 | _httpSession.user_agent = DEFAULT_USER_AGENT; 26 | _httpSession.timeout = 1; 27 | 28 | var ICONS = { 29 | information: 'dialog-information-symbolic', 30 | error: 'dialog-error-symbolic', 31 | find: 'edit-find-symbolic', 32 | web: 'web-browser-symbolic' 33 | }; 34 | 35 | 36 | var KEYBOARD_NUMBERS = [ 37 | Clutter.KEY_0, 38 | Clutter.KEY_1, 39 | Clutter.KEY_2, 40 | Clutter.KEY_3, 41 | Clutter.KEY_4, 42 | Clutter.KEY_5, 43 | Clutter.KEY_6, 44 | Clutter.KEY_7, 45 | Clutter.KEY_8, 46 | Clutter.KEY_9, 47 | ]; 48 | 49 | /** 50 | * getSettings: 51 | * @schema: (optional): the GSettings schema id 52 | * 53 | * Builds and return a GSettings schema for @schema, using schema files 54 | * in extensionsdir/schemas. If @schema is not provided, it is taken from 55 | * metadata['settings-schema']. 56 | */ 57 | function getSettings(schema) { 58 | let extension = ExtensionUtils.getCurrentExtension(); 59 | 60 | schema = schema || extension.metadata['settings-schema']; 61 | 62 | const GioSSS = Gio.SettingsSchemaSource; 63 | 64 | // check if this extension was built with "make zip-file", and thus 65 | // has the schema files in a subfolder 66 | // otherwise assume that extension has been installed in the 67 | // same prefix as gnome-shell (and therefore schemas are available 68 | // in the standard folders) 69 | let schemaDir = extension.dir.get_child('schemas'); 70 | let schemaSource; 71 | if (schemaDir.query_exists(null)) 72 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), 73 | GioSSS.get_default(), 74 | false); 75 | else 76 | schemaSource = GioSSS.get_default(); 77 | 78 | let schemaObj = schemaSource.lookup(schema, true); 79 | if (!schemaObj) 80 | throw new Error('Schema ' + schema + ' could not be found for extension ' 81 | + extension.metadata.uuid + '. Please check your installation.'); 82 | 83 | return new Gio.Settings({ settings_schema: schemaObj }); 84 | } 85 | 86 | function is_blank(str) { 87 | return (!str || /^\s*$/.test(str)); 88 | } 89 | 90 | function starts_with(str1, str2) { 91 | return str1.slice(0, str2.length) == str2; 92 | } 93 | 94 | // Helper function to translate launch parameters into a GAppLaunchContext 95 | function _makeLaunchContext(params) { 96 | params = Params.parse(params, { 97 | workspace: -1, 98 | timestamp: global.display.get_current_time_roundtrip() 99 | }); 100 | 101 | let launchContext = global.create_app_launch_context(params.timestamp, params.workspace); 102 | 103 | return launchContext; 104 | } 105 | 106 | function escape_html(unsafe) { 107 | return unsafe 108 | .replace(/&/g, "&") 109 | .replace(//g, ">") 111 | .replace(/"/g, """) 112 | .replace(/'/g, "'"); 113 | } 114 | 115 | function is_matches_protocol(text) { 116 | text = text.trim(); 117 | let http = starts_with(text, 'http://'.slice(0, text.length)); 118 | let https = starts_with(text, 'https://'.slice(0, text.length)); 119 | 120 | if(http || https) { 121 | return true; 122 | } 123 | else { 124 | return false; 125 | } 126 | } 127 | 128 | function get_url(text) { 129 | let url_regexp = imports.misc.util._urlRegexp; 130 | let url = parseUri(text); 131 | let test_url = ''; 132 | 133 | if(is_blank(url.protocol)) { 134 | test_url = 'http://'+url.source; 135 | } 136 | else { 137 | test_url = url.source; 138 | } 139 | 140 | if(!test_url.match(url_regexp)) { 141 | return false; 142 | } 143 | else { 144 | return test_url; 145 | } 146 | } 147 | 148 | // parseUri 1.2.2 149 | // (c) Steven Levithan 150 | // MIT License 151 | 152 | function parseUri (str) { 153 | var o = parseUri.options, 154 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), 155 | uri = {}, 156 | i = 14; 157 | 158 | while (i--) uri[o.key[i]] = m[i] || ""; 159 | 160 | uri[o.q.name] = {}; 161 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { 162 | if ($1) uri[o.q.name][$1] = $2; 163 | }); 164 | 165 | return uri; 166 | }; 167 | 168 | parseUri.options = { 169 | strictMode: false, 170 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 171 | q: { 172 | name: "queryKey", 173 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 174 | }, 175 | parser: { 176 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 177 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 178 | } 179 | }; 180 | 181 | /*! 182 | * string_score.js: String Scoring Algorithm 0.1.10 183 | * 184 | * http://joshaven.com/string_score 185 | * https://github.com/joshaven/string_score 186 | * 187 | * Copyright (C) 2009-2011 Joshaven Potter 188 | * Special thanks to all of the contributors listed here https://github.com/joshaven/string_score 189 | * MIT license: http://www.opensource.org/licenses/mit-license.php 190 | * 191 | * Date: Tue Mar 1 2011 192 | */ 193 | 194 | /** 195 | * Scores a string against another string. 196 | * 'Hello World'.score('he'); //=> 0.5931818181818181 197 | * 'Hello World'.score('Hello'); //=> 0.7318181818181818 198 | */ 199 | function string_score(string, abbreviation, fuzziness) { 200 | // If the string is equal to the abbreviation, perfect match. 201 | if (string == abbreviation) {return 1;} 202 | //if it's not a perfect match and is empty return 0 203 | if(abbreviation == "" || string == "") {return 0;} 204 | 205 | var total_character_score = 0, 206 | abbreviation_length = abbreviation.length, 207 | string_length = string.length, 208 | start_of_string_bonus, 209 | abbreviation_score, 210 | fuzzies=1, 211 | final_score; 212 | 213 | // Walk through abbreviation and add up scores. 214 | for (var i = 0, 215 | character_score/* = 0*/, 216 | index_in_string/* = 0*/, 217 | c/* = ''*/, 218 | index_c_lowercase/* = 0*/, 219 | index_c_uppercase/* = 0*/, 220 | min_index/* = 0*/; 221 | i < abbreviation_length; 222 | ++i) { 223 | 224 | // Find the first case-insensitive match of a character. 225 | c = abbreviation.charAt(i); 226 | 227 | index_c_lowercase = string.indexOf(c.toLowerCase()); 228 | index_c_uppercase = string.indexOf(c.toUpperCase()); 229 | min_index = Math.min(index_c_lowercase, index_c_uppercase); 230 | index_in_string = (min_index > -1) ? min_index : Math.max(index_c_lowercase, index_c_uppercase); 231 | 232 | if (index_in_string === -1) { 233 | if (fuzziness) { 234 | fuzzies += 1-fuzziness; 235 | continue; 236 | } else { 237 | return 0; 238 | } 239 | } else { 240 | character_score = 0.1; 241 | } 242 | 243 | // Set base score for matching 'c'. 244 | 245 | // Same case bonus. 246 | if (string[index_in_string] === c) { 247 | character_score += 0.1; 248 | } 249 | 250 | // Consecutive letter & start-of-string Bonus 251 | if (index_in_string === 0) { 252 | // Increase the score when matching first character of the remainder of the string 253 | character_score += 0.6; 254 | if (i === 0) { 255 | // If match is the first character of the string 256 | // & the first character of abbreviation, add a 257 | // start-of-string match bonus. 258 | start_of_string_bonus = 1 //true; 259 | } 260 | } 261 | else { 262 | // Acronym Bonus 263 | // Weighing Logic: Typing the first character of an acronym is as if you 264 | // preceded it with two perfect character matches. 265 | if (string.charAt(index_in_string - 1) === ' ') { 266 | character_score += 0.8; // * Math.min(index_in_string, 5); // Cap bonus at 0.4 * 5 267 | } 268 | } 269 | 270 | // Left trim the already matched part of the string 271 | // (forces sequential matching). 272 | string = string.substring(index_in_string + 1, string_length); 273 | 274 | total_character_score += character_score; 275 | } // end of for loop 276 | 277 | // Uncomment to weigh smaller words higher. 278 | // return total_character_score / string_length; 279 | 280 | abbreviation_score = total_character_score / abbreviation_length; 281 | //percentage_of_matched_string = abbreviation_length / string_length; 282 | //word_score = abbreviation_score * percentage_of_matched_string; 283 | 284 | // Reduce penalty for longer strings. 285 | //final_score = (word_score + abbreviation_score) / 2; 286 | final_score = ((abbreviation_score * (abbreviation_length / string_length)) + abbreviation_score) / 2; 287 | 288 | final_score = final_score / fuzzies; 289 | 290 | if (start_of_string_bonus && (final_score + 0.15 < 1)) { 291 | final_score += 0.15; 292 | } 293 | 294 | return final_score; 295 | }; 296 | 297 | function wordwrap(str, width, brk, cut) { 298 | 299 | brk = brk || '\n'; 300 | width = width || 75; 301 | cut = cut || false; 302 | 303 | if (!str) { return str; } 304 | 305 | var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)'); 306 | 307 | return str.match( RegExp(regex, 'g') ).join( brk ); 308 | 309 | } 310 | -------------------------------------------------------------------------------- /prefs.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Ivan awamper@gmail.com 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License as 6 | published by the Free Software Foundation; either version 2 of 7 | the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | const GObject = imports.gi.GObject; 19 | const Gio = imports.gi.Gio; 20 | const Gtk = imports.gi.Gtk; 21 | const Lang = imports.lang; 22 | const Params = imports.misc.params; 23 | const ExtensionUtils = imports.misc.extensionUtils; 24 | 25 | const Me = ExtensionUtils.getCurrentExtension(); 26 | const Utils = Me.imports.utils; 27 | 28 | var ENGINES_KEY = 'search-engines'; 29 | var SUGGESTIONS_KEY = 'enable-suggestions'; 30 | var SUGGESTIONS_DELAY_KEY = 'suggestions-delay'; 31 | var HELPER_KEY = 'enable-duckduckgo-helper'; 32 | var HELPER_DELAY_KEY = 'helper-delay'; 33 | var HELPER_POSITION_KEY = 'helper-position'; 34 | var OPEN_URL_KEY = 'open-url-keyword'; 35 | var OPEN_URL_LABEL = 'open-url-label'; 36 | var HISTORY_KEY = 'search-history-data'; 37 | var HISTORY_SUGGESTIONS_KEY = 'enable-history-suggestions' 38 | var HISTORY_LIMIT_KEY = 'history-limit'; 39 | var DEFAULT_ENGINE_KEY = 'default-search-engine'; 40 | var OPEN_SEARCH_DIALOG_KEY = 'open-web-search-dialog'; 41 | var SELECT_FIRST_SUGGESTION = 'select-first-suggestion'; 42 | var LANGUAGE_CODE = 'language-code'; 43 | 44 | 45 | const KeybindingsWidget = new GObject.Class({ 46 | Name: 'Keybindings.Widget', 47 | GTypeName: 'KeybindingsWidget', 48 | Extends: Gtk.Box, 49 | 50 | _init: function(keybindings, settings) { 51 | this.parent(); 52 | this.set_orientation(Gtk.Orientation.VERTICAL); 53 | 54 | this._settings = settings; 55 | this._keybindings = keybindings; 56 | 57 | let scrolled_window = new Gtk.ScrolledWindow(); 58 | scrolled_window.set_policy( 59 | Gtk.PolicyType.AUTOMATIC, 60 | Gtk.PolicyType.AUTOMATIC 61 | ); 62 | 63 | this._columns = { 64 | NAME: 0, 65 | ACCEL_NAME: 1, 66 | MODS: 2, 67 | KEY: 3 68 | }; 69 | 70 | this._store = new Gtk.ListStore(); 71 | this._store.set_column_types([ 72 | GObject.TYPE_STRING, 73 | GObject.TYPE_STRING, 74 | GObject.TYPE_INT, 75 | GObject.TYPE_INT 76 | ]); 77 | 78 | this._tree_view = new Gtk.TreeView({ 79 | model: this._store, 80 | hexpand: true, 81 | vexpand: true 82 | }); 83 | this._tree_view.get_selection().set_mode(Gtk.SelectionMode.SINGLE); 84 | 85 | let action_renderer = new Gtk.CellRendererText(); 86 | let action_column = new Gtk.TreeViewColumn({ 87 | 'title': 'Action', 88 | 'expand': true 89 | }); 90 | action_column.pack_start(action_renderer, true); 91 | action_column.add_attribute(action_renderer, 'text', 1); 92 | this._tree_view.append_column(action_column); 93 | 94 | let keybinding_renderer = new Gtk.CellRendererAccel({ 95 | 'editable': true, 96 | 'accel-mode': Gtk.CellRendererAccelMode.GTK 97 | }); 98 | keybinding_renderer.connect('accel-edited', 99 | Lang.bind(this, function(renderer, iter, key, mods) { 100 | let value = Gtk.accelerator_name(key, mods); 101 | let [success, iterator ] = 102 | this._store.get_iter_from_string(iter); 103 | 104 | if(!success) { 105 | printerr("Can't change keybinding"); 106 | } 107 | 108 | let name = this._store.get_value(iterator, 0); 109 | 110 | this._store.set( 111 | iterator, 112 | [this._columns.MODS, this._columns.KEY], 113 | [mods, key] 114 | ); 115 | this._settings.set_strv(name, [value]); 116 | }) 117 | ); 118 | 119 | let keybinding_column = new Gtk.TreeViewColumn({ 120 | 'title': 'Modify' 121 | }); 122 | keybinding_column.pack_end(keybinding_renderer, false); 123 | keybinding_column.add_attribute( 124 | keybinding_renderer, 125 | 'accel-mods', 126 | this._columns.MODS 127 | ); 128 | keybinding_column.add_attribute( 129 | keybinding_renderer, 130 | 'accel-key', 131 | this._columns.KEY 132 | ); 133 | this._tree_view.append_column(keybinding_column); 134 | 135 | scrolled_window.add(this._tree_view); 136 | this.add(scrolled_window); 137 | 138 | this._refresh(); 139 | }, 140 | 141 | _refresh: function() { 142 | this._store.clear(); 143 | 144 | for(let settings_key in this._keybindings) { 145 | let [key, mods] = Gtk.accelerator_parse( 146 | this._settings.get_strv(settings_key)[0] 147 | ); 148 | 149 | let iter = this._store.append(); 150 | this._store.set(iter, 151 | [ 152 | this._columns.NAME, 153 | this._columns.ACCEL_NAME, 154 | this._columns.MODS, 155 | this._columns.KEY 156 | ], 157 | [ 158 | settings_key, 159 | this._keybindings[settings_key], 160 | mods, 161 | key 162 | ] 163 | ); 164 | } 165 | } 166 | }); 167 | 168 | 169 | const PrefsGrid = new GObject.Class({ 170 | Name: 'Prefs.Grid', 171 | GTypeName: 'PrefsGrid', 172 | Extends: Gtk.Grid, 173 | 174 | _init: function(settings, params) { 175 | this.parent(params); 176 | this._settings = settings; 177 | this.margin = this.row_spacing = this.column_spacing = 10; 178 | this._rownum = 0; 179 | }, 180 | 181 | add_entry: function(text, key) { 182 | let item = new Gtk.Entry({ 183 | hexpand: false 184 | }); 185 | item.text = this._settings.get_string(key); 186 | this._settings.bind(key, item, 'text', Gio.SettingsBindFlags.DEFAULT); 187 | 188 | return this.add_row(text, item); 189 | }, 190 | 191 | add_shortcut: function(text, settings_key) { 192 | let item = new Gtk.Entry({ 193 | hexpand: false 194 | }); 195 | item.set_text(this._settings.get_strv(settings_key)[0]); 196 | item.connect('changed', Lang.bind(this, function(entry) { 197 | let [key, mods] = Gtk.accelerator_parse(entry.get_text()); 198 | 199 | if(Gtk.accelerator_valid(key, mods)) { 200 | let shortcut = Gtk.accelerator_name(key, mods); 201 | this._settings.set_strv(settings_key, [shortcut]); 202 | } 203 | })); 204 | 205 | return this.add_row(text, item); 206 | }, 207 | 208 | add_boolean: function(text, key) { 209 | let item = new Gtk.Switch({ 210 | active: this._settings.get_boolean(key) 211 | }); 212 | this._settings.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); 213 | 214 | return this.add_row(text, item); 215 | }, 216 | 217 | add_combo: function(text, key, list, type) { 218 | let item = new Gtk.ComboBoxText(); 219 | 220 | for(let i = 0; i < list.length; i++) { 221 | let title = list[i].title.trim(); 222 | let id = list[i].value.toString(); 223 | item.insert(-1, id, title); 224 | } 225 | 226 | if(type === 'string') { 227 | item.set_active_id(this._settings.get_string(key)); 228 | } 229 | else { 230 | item.set_active_id(this._settings.get_int(key).toString()); 231 | } 232 | 233 | item.connect('changed', Lang.bind(this, function(combo) { 234 | let value = combo.get_active_id(); 235 | 236 | if(type === 'string') { 237 | if(this._settings.get_string(key) !== value) { 238 | this._settings.set_string(key, value); 239 | } 240 | } 241 | else { 242 | value = parseInt(value, 10); 243 | 244 | if(this._settings.get_int(key) !== value) { 245 | this._settings.set_int(key, value); 246 | } 247 | } 248 | })); 249 | 250 | return this.add_row(text, item); 251 | }, 252 | 253 | add_spin: function(label, key, adjustment_properties, type, spin_properties) { 254 | adjustment_properties = Params.parse(adjustment_properties, { 255 | lower: 0, 256 | upper: 100, 257 | step_increment: 100 258 | }); 259 | let adjustment = new Gtk.Adjustment(adjustment_properties); 260 | 261 | spin_properties = Params.parse(spin_properties, { 262 | adjustment: adjustment, 263 | numeric: true, 264 | snap_to_ticks: true 265 | }, true); 266 | let spin_button = new Gtk.SpinButton(spin_properties); 267 | 268 | if(type !== 'int') spin_button.set_digits(2); 269 | 270 | let get_method = type === 'int' ? 'get_int' : 'get_double'; 271 | let set_method = type === 'int' ? 'set_int' : 'set_double'; 272 | 273 | spin_button.set_value(this._settings[get_method](key)); 274 | spin_button.connect('value-changed', Lang.bind(this, function(spin) { 275 | let value 276 | 277 | if(type === 'int') value = spin.get_value_as_int(); 278 | else value = spin.get_value(); 279 | 280 | if(this._settings[get_method](key) !== value) { 281 | this._settings[set_method](key, value); 282 | } 283 | })); 284 | 285 | return this.add_row(label, spin_button, true); 286 | }, 287 | 288 | add_button: function(label, callback) { 289 | let item = new Gtk.Button({ 290 | label: label 291 | }); 292 | item.connect('clicked', callback); 293 | 294 | return this.add_item(item); 295 | }, 296 | 297 | add_row: function(text, widget, wrap) { 298 | let label = new Gtk.Label({ 299 | label: text, 300 | hexpand: true, 301 | halign: Gtk.Align.START 302 | }); 303 | label.set_line_wrap(wrap || false); 304 | 305 | this.attach(label, 0, this._rownum, 1, 1); // col, row, colspan, rowspan 306 | this.attach(widget, 1, this._rownum, 1, 1); 307 | this._rownum++; 308 | 309 | return widget; 310 | }, 311 | 312 | add_item: function(widget, col, colspan, rowspan) { 313 | this.attach( 314 | widget, 315 | col || 0, 316 | this._rownum, 317 | colspan || 2, 318 | rowspan || 1 319 | ); 320 | this._rownum++; 321 | 322 | return widget; 323 | }, 324 | 325 | add_range: function(label, key, range_properties) { 326 | range_properties = Params.parse(range_properties, { 327 | min: 0, 328 | max: 100, 329 | step: 10, 330 | mark_position: 0, 331 | add_mark: false, 332 | size: 200, 333 | draw_value: true 334 | }); 335 | 336 | let range = Gtk.Scale.new_with_range( 337 | Gtk.Orientation.HORIZONTAL, 338 | range_properties.min, 339 | range_properties.max, 340 | range_properties.step 341 | ); 342 | range.set_value(this._settings.get_int(key)); 343 | range.set_draw_value(range_properties.draw_value); 344 | 345 | if(range_properties.add_mark) { 346 | range.add_mark( 347 | range_properties.mark_position, 348 | Gtk.PositionType.BOTTOM, 349 | null 350 | ); 351 | } 352 | 353 | range.set_size_request(range_properties.size, -1); 354 | 355 | range.connect('value-changed', Lang.bind(this, function(slider) { 356 | this._settings.set_int(key, slider.get_value()); 357 | })); 358 | 359 | label = new Gtk.Label({ 360 | label: label, 361 | hexpand: true, 362 | halign: Gtk.Align.START 363 | }); 364 | label.set_line_wrap(false); 365 | 366 | let box = new Gtk.Box({ 367 | orientation: Gtk.Orientation.VERTICAL 368 | }); 369 | box.pack_start(label, true, false, 0); 370 | box.pack_start(range, true, false, 0); 371 | 372 | return this.add_item(box); 373 | }, 374 | 375 | add_separator: function() { 376 | let separator = new Gtk.Separator({ 377 | orientation: Gtk.Orientation.HORIZONTAL 378 | }); 379 | 380 | this.add_item(separator, 0, 2, 1); 381 | }, 382 | 383 | add_levelbar: function(params) { 384 | params = Params.parse(params, { 385 | min_value: 0, 386 | max_value: 100, 387 | value: 0, 388 | mode: Gtk.LevelBarMode.CONTINUOUS, 389 | inverted: false 390 | }); 391 | let item = new Gtk.LevelBar(params); 392 | return this.add_item(item); 393 | }, 394 | 395 | add_label: function(text, markup=null) { 396 | let label = new Gtk.Label({ 397 | hexpand: true, 398 | halign: Gtk.Align.START 399 | }); 400 | label.set_line_wrap(true); 401 | 402 | if(markup) label.set_markup(markup); 403 | else label.set_text(text); 404 | 405 | return this.add_item(label); 406 | } 407 | }); 408 | 409 | 410 | const WebSearchDialogPrefsEnginesList = new GObject.Class({ 411 | Name: 'WebSearchDialog.Prefs.EnginesList', 412 | GTypeName: 'WebSearchDialogPrefsEnginesList', 413 | Extends: Gtk.Box, 414 | 415 | _init: function(settings, params) { 416 | this.parent(params); 417 | this._settings = settings; 418 | this._settings.connect('changed', Lang.bind(this, this._refresh)); 419 | this.set_orientation(Gtk.Orientation.VERTICAL); 420 | 421 | this.columns = { 422 | DISPLAY_NAME: 0, 423 | KEYWORD: 1, 424 | URL: 2 425 | }; 426 | 427 | let engines_list_box = new Gtk.Box({ 428 | spacing: 30, 429 | margin_left: 10, 430 | margin_top: 10, 431 | margin_right: 10 432 | }); 433 | 434 | this._store = new Gtk.ListStore(); 435 | this._store.set_column_types([ 436 | GObject.TYPE_STRING, 437 | GObject.TYPE_STRING, 438 | GObject.TYPE_STRING 439 | ]); 440 | 441 | this._tree_view = new Gtk.TreeView({ 442 | model: this._store, 443 | hexpand: true, 444 | vexpand: true 445 | }); 446 | this._tree_view.get_selection().set_mode(Gtk.SelectionMode.SINGLE); 447 | 448 | //engine name 449 | let name_column = new Gtk.TreeViewColumn({ 450 | expand: true, 451 | sort_column_id: this.columns.DISPLAY_NAME, 452 | title: 'Name' 453 | }); 454 | let name_renderer = new Gtk.CellRendererText({ editable: true }); 455 | name_renderer.connect('edited', this._make_on_item_edited('name')); 456 | name_column.pack_start(name_renderer, true); 457 | name_column.add_attribute( 458 | name_renderer, 459 | "text", 460 | this.columns.DISPLAY_NAME 461 | ); 462 | this._tree_view.append_column(name_column); 463 | 464 | //engine keyword 465 | let keyword_column = new Gtk.TreeViewColumn({ 466 | title: 'Keyword', 467 | sort_column_id: this.columns.KEYWORD 468 | }); 469 | let keyword_renderer = new Gtk.CellRendererText({ editable: true }); 470 | keyword_renderer.connect('edited', this._make_on_item_edited('keyword')); 471 | keyword_column.pack_start(keyword_renderer, true); 472 | keyword_column.add_attribute( 473 | keyword_renderer, 474 | "text", 475 | this.columns.KEYWORD 476 | ); 477 | this._tree_view.append_column(keyword_column); 478 | 479 | //engine url 480 | let url_column = new Gtk.TreeViewColumn({ 481 | title: 'Url', 482 | sort_column_id: this.columns.URL 483 | }); 484 | let url_renderer = new Gtk.CellRendererText({ editable: true }); 485 | url_renderer.connect('edited', this._make_on_item_edited('url')); 486 | url_column.pack_start(url_renderer, true); 487 | url_column.add_attribute( 488 | url_renderer, 489 | "text", 490 | this.columns.URL 491 | ); 492 | this._tree_view.append_column(url_column); 493 | 494 | engines_list_box.add(this._tree_view); 495 | 496 | // buttons 497 | let toolbar = new Gtk.Toolbar({ 498 | hexpand: true, 499 | margin_left: 10, 500 | margin_top: 10, 501 | margin_right: 10 502 | }); 503 | toolbar.get_style_context().add_class( 504 | Gtk.STYLE_CLASS_INLINE_TOOLBAR 505 | ); 506 | 507 | let new_button = new Gtk.ToolButton({ 508 | stock_id: Gtk.STOCK_NEW, 509 | label: 'Add search engine', 510 | is_important: true 511 | }); 512 | new_button.connect('clicked', 513 | Lang.bind(this, this._create_new) 514 | ); 515 | toolbar.add(new_button); 516 | 517 | let delete_button = new Gtk.ToolButton({ 518 | stock_id: Gtk.STOCK_DELETE 519 | }); 520 | delete_button.connect('clicked', 521 | Lang.bind(this, this._delete_selected) 522 | ); 523 | toolbar.add(delete_button); 524 | 525 | let scrolled_window = new Gtk.ScrolledWindow(); 526 | scrolled_window.add(engines_list_box); 527 | 528 | this.add(scrolled_window); 529 | this.add(toolbar); 530 | 531 | this._changed_permitted = true; 532 | this._refresh(); 533 | }, 534 | 535 | _make_on_item_edited: function (column) { 536 | return Lang.bind(this, function (renderer, rowIndex, newVal) { 537 | let [any, model, iter] = this._tree_view.get_selection().get_selected(); 538 | let name = this._store.get_value(iter, this.columns.DISPLAY_NAME); 539 | let update = {}; 540 | update[column] = newVal; 541 | return this._update_item(name, update); 542 | }); 543 | }, 544 | 545 | _create_new: function() { 546 | let dialog = new Gtk.Dialog({ 547 | title: 'Add new search engine', 548 | transient_for: this.get_toplevel(), 549 | modal: true 550 | }); 551 | dialog.add_button( 552 | Gtk.STOCK_CANCEL, 553 | Gtk.ResponseType.CANCEL 554 | ); 555 | dialog.add_button( 556 | Gtk.STOCK_ADD, 557 | Gtk.ResponseType.OK 558 | ); 559 | dialog.set_default_response( 560 | Gtk.ResponseType.OK 561 | ); 562 | 563 | let grid = new Gtk.Grid({ 564 | column_spacing: 10, 565 | row_spacing: 15, 566 | margin: 10 567 | }); 568 | 569 | // Name 570 | grid.attach(new Gtk.Label({ label: 'Name:' }), 0, 0, 1, 1); 571 | dialog._engine_name = new Gtk.Entry({ 572 | hexpand: true 573 | }); 574 | grid.attach(dialog._engine_name, 1, 0, 1, 1); 575 | 576 | // Keyword 577 | grid.attach(new Gtk.Label({ label: 'Keyword:' }), 0, 1, 1, 1); 578 | dialog._engine_keyword = new Gtk.Entry({ 579 | hexpand: true 580 | }); 581 | grid.attach(dialog._engine_keyword, 1, 1, 1, 1); 582 | 583 | // Url 584 | grid.attach(new Gtk.Label({ label: 'Url:' }), 0, 2, 1, 1); 585 | dialog._engine_url = new Gtk.Entry({ 586 | hexpand: true 587 | }); 588 | grid.attach(dialog._engine_url, 1, 2, 1, 1); 589 | 590 | dialog.get_content_area().add(grid); 591 | 592 | dialog.connect('response', Lang.bind(this, function(dialog, id) { 593 | if(id != Gtk.ResponseType.OK) { 594 | dialog.destroy(); 595 | return; 596 | } 597 | 598 | let name = dialog._engine_name.get_text(); 599 | let keyword = dialog._engine_keyword.get_text(); 600 | let url = dialog._engine_url.get_text(); 601 | let new_item = { 602 | name: name, 603 | keyword: keyword, 604 | url: url 605 | }; 606 | 607 | if(!this._append_item(new_item)) { 608 | return; 609 | } 610 | 611 | dialog.destroy(); 612 | })); 613 | 614 | dialog.show_all(); 615 | }, 616 | 617 | _delete_selected: function() { 618 | let [any, model, iter] = 619 | this._tree_view.get_selection().get_selected(); 620 | 621 | if(any) { 622 | let name = this._store.get_value(iter, this.columns.DISPLAY_NAME); 623 | this._remove_item(name); 624 | this._store.remove(iter); 625 | } 626 | }, 627 | 628 | _refresh: function() { 629 | this._store.clear(); 630 | 631 | let current_items = this._settings.get_strv(ENGINES_KEY); 632 | let valid_items = []; 633 | 634 | for(let i = 0; i < current_items.length; i++) { 635 | let item = JSON.parse(current_items[i]); 636 | 637 | if(this._is_valid_item(item)) { 638 | valid_items.push(current_items[i]); 639 | let iter = this._store.append(); 640 | this._store.set(iter, 641 | [this.columns.DISPLAY_NAME, this.columns.KEYWORD, this.columns.URL], 642 | [item.name, item.keyword, item.url] 643 | ); 644 | } 645 | } 646 | 647 | if(valid_items.length != current_items.length) { 648 | // some items were filtered out 649 | this._settings.set_strv(ENGINES_KEY, valid_items); 650 | } 651 | }, 652 | 653 | _is_valid_item: function(item) { 654 | if(Utils.is_blank(item.name)) { 655 | return false; 656 | } 657 | else if(Utils.is_blank(item.keyword)) { 658 | return false; 659 | } 660 | else if(Utils.is_blank(item.url)) { 661 | return false; 662 | } 663 | else { 664 | return true; 665 | } 666 | }, 667 | 668 | _is_duplicate_item: function(item, ignoreIndex) { 669 | let current_items = this._settings.get_strv(ENGINES_KEY); 670 | 671 | for(let i = 0; i < current_items.length; i++) { 672 | if (i === ignoreIndex) continue; 673 | 674 | let info = JSON.parse(current_items[i]); 675 | 676 | if(info.name == item.name) { 677 | printerr("Already have an item for this name"); 678 | return false; 679 | } 680 | else if(info.keyword == item.keyword) { 681 | printerr("Already have an item for this keyword"); 682 | return false; 683 | } 684 | } 685 | 686 | return true; 687 | }, 688 | 689 | _append_item: function(new_item) { 690 | if(!this._is_valid_item(new_item) || !this._is_duplicate_item(new_item)) { 691 | return false; 692 | } 693 | 694 | let current_items = this._settings.get_strv(ENGINES_KEY); 695 | 696 | current_items.push(JSON.stringify(new_item)); 697 | this._settings.set_strv(ENGINES_KEY, current_items); 698 | return true; 699 | }, 700 | 701 | _update_item: function(name, update) { 702 | let current_items = this._settings.get_strv(ENGINES_KEY); 703 | 704 | for(let i = 0; i < current_items.length; i++) { 705 | let info = JSON.parse(current_items[i]); 706 | 707 | if(info.name == name) { 708 | for (let key of Object.keys(update)) { 709 | info[key] = update[key]; 710 | } 711 | 712 | if(!this._is_valid_item(info) || !this._is_duplicate_item(info, i)) { 713 | return false; 714 | } 715 | 716 | current_items[i] = JSON.stringify(info); 717 | this._settings.set_strv(ENGINES_KEY, current_items); 718 | return true; 719 | } 720 | } 721 | 722 | return false; 723 | }, 724 | 725 | _remove_item: function(name) { 726 | if(Utils.is_blank(name)) { 727 | return false; 728 | } 729 | 730 | let current_items = this._settings.get_strv(ENGINES_KEY); 731 | let result = null; 732 | 733 | for(let i = 0; i < current_items.length; i++) { 734 | let info = JSON.parse(current_items[i]); 735 | 736 | if(info.name == name) { 737 | current_items.splice(i, 1); 738 | result = true; 739 | break; 740 | } 741 | } 742 | 743 | this._settings.set_strv(ENGINES_KEY, current_items); 744 | return result; 745 | } 746 | }); 747 | 748 | 749 | const WebSearchDialogPrefsWidget = new GObject.Class({ 750 | Name: 'WebSearchDialog.Prefs.Widget', 751 | GTypeName: 'WebSearchDialogPrefsWidget', 752 | Extends: Gtk.Box, 753 | 754 | _init: function(params) { 755 | this.parent(params); 756 | this.set_orientation(Gtk.Orientation.VERTICAL); 757 | this._settings = Utils.getSettings(); 758 | 759 | let main = this._get_main_page(); 760 | let keybindings = this._get_keybindings_page(); 761 | 762 | let settings_grid = new PrefsGrid(this._settings); 763 | let settings_grid_label = new Gtk.Label({ 764 | label: "Settings" 765 | }); 766 | 767 | let engines = { 768 | page: new WebSearchDialogPrefsEnginesList(this._settings), 769 | name: 'Search engines' 770 | } 771 | 772 | let stack = new Gtk.Stack({ 773 | transition_type: Gtk.StackTransitionType.SLIDE_LEFT_RIGHT, 774 | transition_duration: 500 775 | }); 776 | let stack_switcher = new Gtk.StackSwitcher({ 777 | margin_left: 5, 778 | margin_top: 5, 779 | margin_bottom: 5, 780 | margin_right: 5, 781 | stack: stack 782 | }); 783 | stack.add_titled(main.page, main.name, main.name); 784 | stack.add_titled(engines.page, engines.name, engines.name); 785 | stack.add_titled(keybindings.page, keybindings.name, keybindings.name); 786 | 787 | this.add(stack); 788 | 789 | this.connect('realize', 790 | Lang.bind(this, function() { 791 | let headerbar = this.get_toplevel().get_titlebar(); 792 | headerbar.set_custom_title(stack_switcher); 793 | headerbar.show_all(); 794 | this.get_toplevel().resize(700, 250); 795 | }) 796 | ); 797 | }, 798 | 799 | _get_main_page: function() { 800 | let settings = Utils.getSettings(); 801 | let name = 'Main'; 802 | let page = new PrefsGrid(settings); 803 | 804 | let spin_properties = { 805 | lower: 0, 806 | upper: 0, 807 | step_increment: 0 808 | }; 809 | 810 | let engine_list = this._settings.get_strv(ENGINES_KEY); 811 | let result_list = []; 812 | 813 | for(let i = 0; i < engine_list.length; i++) { 814 | let info = JSON.parse(engine_list[i]); 815 | let result = { 816 | title: info.name, 817 | value: i 818 | }; 819 | result_list.push(result); 820 | } 821 | 822 | page.add_combo( 823 | 'Default search engine:', 824 | DEFAULT_ENGINE_KEY, 825 | result_list, 826 | 'int' 827 | ); 828 | 829 | page.add_separator(); 830 | 831 | page.add_boolean( 832 | 'Duckduckgo.com helper:', 833 | HELPER_KEY 834 | ); 835 | page.add_entry( 836 | 'Helper language code (e.g. ru, de, fr)', 837 | LANGUAGE_CODE 838 | ); 839 | page.add_boolean( 840 | 'Suggestions:', 841 | SUGGESTIONS_KEY 842 | ); 843 | page.add_boolean( 844 | 'Autocomplete with first suggestion:', 845 | SELECT_FIRST_SUGGESTION 846 | ); 847 | page.add_boolean( 848 | 'History suggestions:', 849 | HISTORY_SUGGESTIONS_KEY 850 | ); 851 | 852 | page.add_separator(); 853 | 854 | spin_properties.lower = 10; 855 | spin_properties.upper = 1000; 856 | spin_properties.step_increment = 5; 857 | page.add_spin( 858 | 'History limit:', 859 | HISTORY_LIMIT_KEY, 860 | spin_properties, 861 | 'int' 862 | ); 863 | 864 | spin_properties.lower = 200; 865 | spin_properties.upper = 1000; 866 | spin_properties.step_increment = 100; 867 | page.add_spin( 868 | 'Suggestions delay(ms):', 869 | SUGGESTIONS_DELAY_KEY, 870 | spin_properties, 871 | 'int' 872 | ); 873 | 874 | spin_properties.lower = 250; 875 | spin_properties.upper = 2000; 876 | spin_properties.step_increment = 50; 877 | page.add_spin( 878 | 'Helper delay(ms):', 879 | HELPER_DELAY_KEY, 880 | spin_properties, 881 | 'int' 882 | ); 883 | 884 | let options = [ 885 | {title: 'Top', value: 'top'}, 886 | {title: 'Bottom', value: 'bottom'} 887 | ]; 888 | page.add_combo( 889 | 'Helper position(top or bottom):', 890 | HELPER_POSITION_KEY, 891 | options, 892 | 'string' 893 | ); 894 | 895 | page.add_separator(); 896 | 897 | page.add_entry( 898 | 'Open url keyword(empty to disable):', 899 | OPEN_URL_KEY 900 | ); 901 | page.add_entry( 902 | 'Open url label:', 903 | OPEN_URL_LABEL 904 | ); 905 | 906 | return { 907 | page: page, 908 | name: name 909 | }; 910 | }, 911 | 912 | _get_keybindings_page: function() { 913 | let settings = Utils.getSettings(); 914 | let name = 'Keybindings'; 915 | let page = new PrefsGrid(settings); 916 | 917 | let keybindings = {}; 918 | keybindings[OPEN_SEARCH_DIALOG_KEY] = 'Open search dialog:'; 919 | 920 | let keybindings_widget = new KeybindingsWidget(keybindings, settings); 921 | page.add_item(keybindings_widget) 922 | 923 | return { 924 | page: page, 925 | name: name 926 | }; 927 | }, 928 | }); 929 | 930 | 931 | function init(){ 932 | // nothing 933 | } 934 | 935 | 936 | function buildPrefsWidget() { 937 | let widget = new WebSearchDialogPrefsWidget(); 938 | widget.show_all(); 939 | 940 | return widget; 941 | } 942 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const St = imports.gi.St; 2 | const Lang = imports.lang; 3 | const Meta = imports.gi.Meta; 4 | const Main = imports.ui.main; 5 | const Mainloop = imports.mainloop; 6 | const Clutter = imports.gi.Clutter; 7 | const Gio = imports.gi.Gio; 8 | const Soup = imports.gi.Soup; 9 | const Params = imports.misc.params; 10 | const ModalDialog = imports.ui.modalDialog; 11 | const Tweener = imports.ui.tweener; 12 | const Shell = imports.gi.Shell; 13 | 14 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 15 | const Suggestions = Me.imports.suggestions_box; 16 | const Helper = Me.imports.helper; 17 | const Utils = Me.imports.utils; 18 | const HistoryManager = Me.imports.history_manager; 19 | const Prefs = Me.imports.prefs; 20 | 21 | const _httpSession = Utils._httpSession; 22 | const ICONS = Utils.ICONS; 23 | 24 | const MAX_SUGGESTIONS = 3; 25 | const SUGGESTIONS_URL = 26 | "https://suggestqueries.google.com/complete/search?client=chrome&q="; 27 | 28 | const SETTINGS_ICON = 'emblem-system-symbolic'; 29 | 30 | function launch_extension_prefs(uuid) { 31 | let appSys = Shell.AppSystem.get_default(); 32 | let app = appSys.lookup_app('gnome-shell-extension-prefs.desktop'); 33 | let info = app.get_app_info(); 34 | let timestamp = global.display.get_current_time_roundtrip(); 35 | info.launch_uris( 36 | ['extension:///' + uuid], 37 | global.create_app_launch_context(timestamp, -1) 38 | ); 39 | } 40 | 41 | const WebSearchDialog = new Lang.Class({ 42 | Name: 'WebSearchDialog', 43 | Extends: ModalDialog.ModalDialog, 44 | 45 | _init: function() { 46 | this.parent({ 47 | destroyOnClose: false 48 | }); 49 | this._dialogLayout = 50 | typeof this.dialogLayout === "undefined" 51 | ? this._dialogLayout 52 | : this.dialogLayout; 53 | 54 | this._dialogLayout.set_style_class_name(''); 55 | this._dialogLayout.set_margin_bottom(300); 56 | this.contentLayout.set_style_class_name('web-search-dialog'); 57 | 58 | this._settings = Utils.getSettings(); 59 | this._clipboard = St.Clipboard.get_default(); 60 | this._suggestions_delay_id = 0; 61 | this._helper_delay_id = 0; 62 | this.show_suggestions = true; 63 | this.select_first_suggestion = true; 64 | this.search_engine = false; 65 | 66 | this._create_search_dialog(); 67 | 68 | this.activate_window = false; 69 | this._window_handler_id = global.display.connect( 70 | 'window-demands-attention', 71 | Lang.bind(this, this._on_window_demands_attention) 72 | ); 73 | }, 74 | 75 | _resize: function() { 76 | let monitor = Main.layoutManager.currentMonitor; 77 | let is_primary = monitor.index === Main.layoutManager.primaryIndex; 78 | 79 | let available_width = monitor.width; 80 | let available_height = monitor.height; 81 | if(is_primary) available_height -= Main.panel.actor.height; 82 | 83 | let width = Math.round(available_width / 100 * 85); 84 | 85 | this._dialogLayout.set_width(width); 86 | }, 87 | 88 | _reposition: function() { 89 | let monitor = Main.layoutManager.currentMonitor; 90 | this._dialogLayout.x = Math.round(monitor.width / 2 - this._dialogLayout.width / 2); 91 | this._dialogLayout.y = 100; 92 | }, 93 | 94 | _get_main_hint: function() { 95 | let default_engine = this._get_default_engine(); 96 | let hint = 97 | 'Type to search in '+default_engine.name+' or enter '+ 98 | 'a keyword and press "space".'; 99 | 100 | if(this._get_open_url_keyword()) { 101 | hint += 102 | '\nKeyword "'+this._get_open_url_keyword()+'" for open URL.'; 103 | } 104 | 105 | hint += '\nPress "Tab" for available search engines.'; 106 | 107 | return hint; 108 | }, 109 | 110 | _remove_delay_id: function() { 111 | if(this._suggestions_delay_id > 0) { 112 | Mainloop.source_remove(this._suggestions_delay_id); 113 | this._suggestions_delay_id = 0; 114 | } 115 | if(this._helper_delay_id > 0) { 116 | Mainloop.source_remove(this._helper_delay_id); 117 | this._helper_delay_id = 0; 118 | } 119 | }, 120 | 121 | _on_window_demands_attention: function(display, window) { 122 | if(this.activate_window) { 123 | this.activate_window = false; 124 | Main.activateWindow(window); 125 | } 126 | }, 127 | 128 | _create_search_dialog: function() { 129 | this.hint = new St.Label({ 130 | style_class: 'search-hint' 131 | }); 132 | this._hint_box = new St.BoxLayout({ 133 | visible: false 134 | }); 135 | this._hint_box.add(this.hint); 136 | 137 | this.search_engine_label = new St.Label({ 138 | style_class: 'search-engine-label', 139 | text: 'Web Search:' 140 | }); 141 | 142 | this.search_entry = new St.Entry({ 143 | style_class: 'web-search-entry' 144 | }); 145 | this.search_entry.connect( 146 | 'key-press-event', 147 | Lang.bind(this, this._on_key_press) 148 | ); 149 | this.search_entry.get_clutter_text().connect( 150 | 'activate', 151 | Lang.bind(this, this._on_text_activate) 152 | ); 153 | this.search_entry.get_clutter_text().connect( 154 | 'text-changed', 155 | Lang.bind(this, this._on_search_text_changed) 156 | ); 157 | this.search_entry.get_clutter_text().connect( 158 | 'key-press-event', 159 | Lang.bind(this, this._on_text_key_press) 160 | ); 161 | 162 | let secondary_icon = new St.Icon({ 163 | icon_name: SETTINGS_ICON, 164 | style_class: 'settings-icon' 165 | }); 166 | this.search_entry.set_secondary_icon(secondary_icon); 167 | this.search_entry.connect('secondary-icon-clicked', 168 | Lang.bind(this, function() { 169 | this.close(); 170 | launch_extension_prefs(Me.uuid); 171 | } 172 | )); 173 | 174 | this.duckduckgo_helper = new Helper.DuckDuckGoHelper(); 175 | this.suggestions_box = new Suggestions.SuggestionsBox(this); 176 | this.suggestions_box.setSourceAlignment(0.02); 177 | 178 | this.search_history = new HistoryManager.SearchHistoryManager(); 179 | 180 | this._search_table = new St.Widget({ 181 | name: 'web_search_table', 182 | layout_manager: new Clutter.TableLayout() 183 | }); 184 | let search_table_layout = this._search_table.layout_manager; 185 | search_table_layout.pack(this.search_engine_label, 0, 0); 186 | search_table_layout.pack(this.search_entry, 1, 0); 187 | this._search_table.show(); 188 | 189 | this.contentLayout.add(this._search_table); 190 | this.contentLayout.add(this._hint_box); 191 | }, 192 | 193 | _on_key_press: function(o, e) { 194 | let symbol = e.get_key_symbol(); 195 | let control_mask = (e.get_state() & Clutter.ModifierType.CONTROL_MASK); 196 | let shift_mask = (e.get_state() & Clutter.ModifierType.SHIFT_MASK); 197 | 198 | if(symbol == Clutter.Escape) { 199 | this.close(); 200 | } 201 | else if(symbol == Clutter.Tab) { 202 | if(this.suggestions_box.isOpen) { 203 | this.suggestions_box.firstMenuItem.setActive(true); 204 | } 205 | else { 206 | let text = this.search_entry.get_text(); 207 | 208 | if(Utils.is_blank(text)) { 209 | this._display_engines(); 210 | } 211 | } 212 | } 213 | else if(symbol == Clutter.Down) { 214 | if(this.suggestions_box.isOpen) { 215 | if(this._settings.get_boolean(Prefs.SELECT_FIRST_SUGGESTION)) { 216 | let items = this.suggestions_box._getMenuItems(); 217 | 218 | if(items.length > 1) { 219 | items[0].actor.remove_style_pseudo_class('active'); 220 | items[1].setActive(true); 221 | } 222 | } 223 | else { 224 | this.suggestions_box.firstMenuItem.setActive(true); 225 | } 226 | } 227 | else { 228 | this.show_suggestions = false; 229 | let text = this.search_entry.get_text(); 230 | let item = this.search_history.next_item(text); 231 | this.search_entry.set_text(item.query); 232 | 233 | // let hint_text = 'History.\nCurrent item '+ 234 | // this.search_history.current_index()+' of '+ 235 | // this.search_history.total_items(); 236 | // this._show_hint({ 237 | // text: hint_text, 238 | // icon_name: ICONS.information 239 | // }); 240 | } 241 | } 242 | else if(symbol == Clutter.Up) { 243 | if(!this.suggestions_box.isOpen) { 244 | this.show_suggestions = false; 245 | let text = this.search_entry.get_text(); 246 | let item = this.search_history.prev_item(text); 247 | this.search_entry.set_text(item.query); 248 | 249 | // let hint_text = 'History.\nCurrent item '+ 250 | // this.search_history.current_index()+' of '+ 251 | // this.search_history.total_items(); 252 | // this._show_hint({ 253 | // text: hint_text, 254 | // icon_name: ICONS.information 255 | // }); 256 | } 257 | } 258 | // Ctrl+V 259 | else if(control_mask && symbol == 118) { 260 | this._clipboard.get_text(Lang.bind(this, function(clipboard, text) { 261 | if (Utils.is_blank(text)) { 262 | return false; 263 | } 264 | 265 | let clutter_text = this.search_entry.get_clutter_text(); 266 | clutter_text.delete_selection(); 267 | let pos = clutter_text.get_cursor_position(); 268 | clutter_text.insert_text(text, pos); 269 | 270 | return true; 271 | })); 272 | } 273 | // Ctrl+C 274 | else if(control_mask && symbol == 99) { 275 | let clutter_text = this.search_entry.get_clutter_text(); 276 | let selection = clutter_text.get_selection(); 277 | this._clipboard.set_text(selection); 278 | } 279 | // Ctrl+Shift+V - paste and search 280 | else if(control_mask && shift_mask && symbol == 86) { 281 | if(!this.search_engine) { 282 | this._set_engine(); 283 | } 284 | 285 | this._clipboard.get_text(Lang.bind(this, function(clipboard, text) { 286 | if(Utils.is_blank(text)) { 287 | this._show_hint({ 288 | text: 'Clipboard is empty.', 289 | icon_name: ICONS.error 290 | }); 291 | 292 | return false; 293 | } 294 | else { 295 | this._activate_search(text); 296 | 297 | return true; 298 | } 299 | })); 300 | } 301 | // Ctrl+Shift+G - paste and go 302 | else if(control_mask && shift_mask && symbol == 71) { 303 | if(!this.search_engine) { 304 | this._set_engine(); 305 | } 306 | 307 | this._clipboard.get_text(Lang.bind(this, function(clipboard, url) { 308 | if(Utils.is_blank(url)) { 309 | this._show_hint({ 310 | text: 'Clipboard is empty.', 311 | icon_name: ICONS.error 312 | }); 313 | 314 | return false; 315 | } 316 | else { 317 | this._open_url(url, true); 318 | 319 | return true; 320 | } 321 | })); 322 | } 323 | else if(control_mask && Utils.KEYBOARD_NUMBERS.indexOf(symbol) != -1) { 324 | let item_id = Utils.KEYBOARD_NUMBERS.indexOf(symbol); 325 | this.suggestions_box.activate_by_id(item_id); 326 | } 327 | else { 328 | // nothing 329 | } 330 | 331 | return true; 332 | }, 333 | 334 | _on_text_activate: function(text) { 335 | text = text.get_text(); 336 | 337 | if(!Utils.is_blank(text)) { 338 | if(this.search_engine.open_url) { 339 | this._open_url(text, true); 340 | } 341 | else { 342 | this._activate_search(text); 343 | } 344 | } 345 | }, 346 | 347 | _on_text_key_press: function(o, e) { 348 | let symbol = e.get_key_symbol(); 349 | 350 | // reset the search engine on backspace with empty search text 351 | if( 352 | symbol == Clutter.BackSpace && 353 | !this.search_entry.text.length && 354 | !this.search_engine._default 355 | ) { 356 | this._set_engine(false); 357 | this._on_search_text_changed(); // trigger update of hint 358 | } 359 | else if(symbol == Clutter.BackSpace) { 360 | this.select_first_suggestion = false; 361 | } 362 | else if(symbol == Clutter.Right) { 363 | let sel = this.search_entry.clutter_text.get_selection_bound(); 364 | 365 | if(sel === -1) { 366 | this.search_entry.clutter_text.set_cursor_position( 367 | this.search_entry.text.length 368 | ); 369 | } 370 | } 371 | }, 372 | 373 | _on_search_text_changed: function() { 374 | this._remove_delay_id(); 375 | let text = this.search_entry.get_text(); 376 | 377 | if(Utils.is_blank(text)) { 378 | this.suggestions_box.close(); 379 | 380 | if(this.search_engine._default === true) { 381 | let hint_text = this._get_main_hint(); 382 | this._show_hint({ 383 | text: hint_text, 384 | icon_name: ICONS.information 385 | }); 386 | } 387 | } 388 | else { 389 | this._hide_hint(); 390 | } 391 | 392 | if(this.search_engine == false || this.search_engine._default) { 393 | let keyword = this._get_keyword(text); 394 | this._set_engine(keyword); 395 | } 396 | 397 | if(this.show_suggestions) { 398 | if(this.search_engine.open_url) { 399 | if(!Utils.is_matches_protocol(text)) { 400 | text = 'http://'+text; 401 | this.search_entry.set_text(text); 402 | } 403 | 404 | if(/^https?:\/\/.+?/.test(text)) { 405 | this._display_suggestions(text); 406 | } 407 | else { 408 | this.suggestions_box.close(); 409 | } 410 | } 411 | else { 412 | this._display_helper(text); 413 | this._display_suggestions(text); 414 | } 415 | } 416 | else { 417 | this.show_suggestions = true; 418 | } 419 | 420 | return true; 421 | }, 422 | 423 | _get_open_url_keyword: function() { 424 | let key = this._settings.get_string(Prefs.OPEN_URL_KEY); 425 | 426 | if(Utils.is_blank(key)) { 427 | return false; 428 | } 429 | else { 430 | return key.trim(); 431 | } 432 | }, 433 | 434 | _get_keyword: function(text) { 435 | let result = false; 436 | let web_search_query_regexp = /^(.+?)\s$/; 437 | 438 | if(web_search_query_regexp.test(text)) { 439 | let matches = web_search_query_regexp.exec(text); 440 | let keyword = matches[0].trim(); 441 | 442 | if(!Utils.is_blank(keyword)) { 443 | result = keyword; 444 | } 445 | } 446 | 447 | return result; 448 | }, 449 | 450 | _get_default_engine: function() { 451 | let engines = this._settings.get_strv(Prefs.ENGINES_KEY); 452 | let index = this._settings.get_int(Prefs.DEFAULT_ENGINE_KEY); 453 | let engine = JSON.parse(engines[index]); 454 | 455 | if(!Utils.is_blank(engine.url)) { 456 | return engine; 457 | } 458 | else { 459 | return false; 460 | } 461 | }, 462 | 463 | _get_engine: function(key) { 464 | if(Utils.is_blank(key)) { 465 | return false; 466 | } 467 | 468 | let info; 469 | key = key.trim(); 470 | 471 | if(key == this._get_open_url_keyword()) { 472 | info = { 473 | name: this._settings.get_string(Prefs.OPEN_URL_LABEL), 474 | keyword: this._settings.get_string(Prefs.OPEN_URL_KEY), 475 | open_url: true 476 | }; 477 | 478 | return info; 479 | } 480 | else { 481 | let engines_list = this._settings.get_strv(Prefs.ENGINES_KEY); 482 | 483 | for(let i = 0; i < engines_list.length; i++) { 484 | info = JSON.parse(engines_list[i]); 485 | 486 | if(info.keyword == key && info.url.length > 0) { 487 | info.open_url = false; 488 | return info; 489 | } 490 | } 491 | } 492 | 493 | return false; 494 | }, 495 | 496 | _set_engine: function(keyword) { 497 | this._remove_delay_id(); 498 | 499 | let engine_info = this._get_engine(keyword); 500 | let engine = {}; 501 | 502 | if(engine_info) { 503 | engine = engine_info; 504 | this.search_engine._default = false; 505 | } 506 | else { 507 | engine = this._get_default_engine(); 508 | engine._default = true; 509 | } 510 | 511 | if( 512 | engine.keyword === this.search_engine.keyword || 513 | this.search_engine._default && engine._default 514 | ) { 515 | return false; 516 | } 517 | 518 | this.search_engine = {}; 519 | this.search_engine._default = engine._default; 520 | this.search_engine.keyword = engine.keyword.trim(); 521 | this.search_engine.open_url = engine.open_url; 522 | this.search_engine.name = engine.name.trim(); 523 | this.search_engine.url = !engine.open_url 524 | ? engine.url.trim() 525 | : null 526 | 527 | // update the label any time we set an engine 528 | this._show_engine_label(this.search_engine.name+':'); 529 | 530 | if(!this.search_engine._default) { 531 | let hint_text; 532 | 533 | if(!engine.open_url) { 534 | hint_text = 'Type to search in '+this.search_engine.name+'.'; 535 | } 536 | else { 537 | hint_text = 'Please, enter a URL.'; 538 | } 539 | 540 | hint_text += '\nPress "Tab" to switch search engine.'; 541 | this.show_suggestions = false; 542 | this.search_entry.set_text(''); 543 | this._show_hint({ 544 | text: hint_text, 545 | icon_name: ICONS.information 546 | }); 547 | } 548 | 549 | return true; 550 | }, 551 | 552 | _show_hint: function(params) { 553 | params = Params.parse(params, { 554 | text: null, 555 | icon_name: ICONS.information 556 | }) 557 | 558 | if(Utils.is_blank(params.text)) { 559 | return false; 560 | } 561 | 562 | let icon = new St.Icon({ 563 | icon_name: params.icon_name, 564 | style_class: 'hint-icon' 565 | }); 566 | 567 | if(this._hint_box.get_children().length > 1) { 568 | this._hint_box.replace_child( 569 | this._hint_box.get_children()[0], 570 | icon 571 | ) 572 | } 573 | else { 574 | this._hint_box.insert_child_at_index(icon, 0); 575 | } 576 | 577 | this._hint_box.opacity = 30; 578 | this._hint_box.show(); 579 | 580 | if(params.text != this.hint.get_text()) { 581 | this.hint.set_text(params.text); 582 | } 583 | 584 | Tweener.addTween(this._hint_box, { 585 | time: 0.1, 586 | opacity: 255, 587 | transition: 'easeOutQuad', 588 | onComplete: Lang.bind(this, function() { 589 | Tweener.addTween(this._hint_box, { 590 | opacity: 120, 591 | time: 0.1, 592 | transition: 'easeOutQuad' 593 | }); 594 | }) 595 | }); 596 | 597 | return true; 598 | }, 599 | 600 | _hide_hint: function() { 601 | if(this._hint_box.visible) { 602 | Tweener.addTween(this._hint_box, { 603 | opacity: 0, 604 | height: 0, 605 | time: 0.1, 606 | transition: 'easeOutQuad', 607 | onComplete: Lang.bind(this, function() { 608 | this._hint_box.hide(); 609 | this._hint_box.set_height(-1); 610 | }) 611 | }) 612 | 613 | return true; 614 | } 615 | 616 | return false; 617 | }, 618 | 619 | _show_engine_label: function(text) { 620 | if(Utils.is_blank(text)) { 621 | return false; 622 | } 623 | 624 | let opacity = this.search_engine_label.opacity == 255; 625 | let visible = this.search_engine_label.visible; 626 | 627 | if(opacity && visible) { 628 | this._hide_engine_label(); 629 | } 630 | 631 | this.search_engine_label.opacity = 0; 632 | this.search_engine_label.set_text(text); 633 | this.search_engine_label.show() 634 | 635 | let natural_width = this.contentLayout.get_preferred_width(-1)[1]; 636 | 637 | Tweener.addTween(this.contentLayout, { 638 | width: natural_width+5, 639 | time: 0.2, 640 | transition: 'easeOutQuad', 641 | onStart: Lang.bind(this, function() { 642 | Tweener.addTween(this.search_engine_label, { 643 | opacity: 255, 644 | time: 0.1, 645 | transition: 'easeOutQuad' 646 | }) 647 | }), 648 | onComplete: Lang.bind(this, function() { 649 | this.contentLayout.set_width(-1); 650 | }) 651 | }); 652 | 653 | return true; 654 | }, 655 | 656 | _hide_engine_label: function() { 657 | if(!this.search_engine_label.visible) { 658 | return false; 659 | } 660 | 661 | Tweener.addTween(this.search_engine_label, { 662 | opacity: 0, 663 | time: 0.2, 664 | transition: 'easeOutQuad', 665 | onComplete: Lang.bind(this, function() { 666 | this.contentLayout.set_width(-1); 667 | this.search_engine_label.hide(); 668 | this.search_engine_label.set_text(''); 669 | }) 670 | }) 671 | 672 | return true; 673 | }, 674 | 675 | _parse_suggestions: function(suggestions_source) { 676 | if(suggestions_source[1].length < 1) { 677 | return false; 678 | } 679 | 680 | let result = new Array(); 681 | 682 | for(let i = 0; i < suggestions_source[1].length; i++) { 683 | let text = suggestions_source[1][i].trim(); 684 | let type = suggestions_source[4]['google:suggesttype'][i].trim(); 685 | let relevance = parseInt( 686 | suggestions_source[4]['google:suggestrelevance'][i] 687 | ); 688 | 689 | if(Utils.is_blank(text)) {continue;} 690 | if(Utils.is_blank(type)) {continue;} 691 | if(relevance < 1) {continue;} 692 | 693 | let suggestion = { 694 | text: text, 695 | type: type, 696 | relevance: relevance 697 | } 698 | result.push(suggestion); 699 | } 700 | 701 | return result.length > 0 ? result : false; 702 | }, 703 | 704 | _get_suggestions: function(text, callback) { 705 | let url = SUGGESTIONS_URL+encodeURIComponent(text); 706 | let here = this; 707 | 708 | let request = Soup.Message.new('GET', url); 709 | 710 | _httpSession.queue_message(request, function(_httpSession, message) { 711 | if(message.status_code === 200) { 712 | let result = JSON.parse(request.response_body.data); 713 | 714 | if(result[1].length < 1) { 715 | callback.call(here, text, false); 716 | } 717 | else { 718 | callback.call(here, text, result); 719 | } 720 | } 721 | else { 722 | callback.call(here, text, false); 723 | } 724 | }); 725 | }, 726 | 727 | _display_helper: function(text) { 728 | if(!this._settings.get_boolean(Prefs.HELPER_KEY)) { 729 | return false; 730 | } 731 | 732 | this.suggestions_box.remove_all_by_types(['HELPER']); 733 | this._helper_delay_id = Mainloop.timeout_add( 734 | this._settings.get_int(Prefs.HELPER_DELAY_KEY), 735 | Lang.bind(this, function() { 736 | this.suggestions_box.addMenuItem( 737 | new Helper.HelperSpinnerMenuItem() 738 | ); 739 | this.duckduckgo_helper.get_info(text, 740 | Lang.bind(this, function(result) { 741 | this.suggestions_box.remove_all_by_types(['HELPER']); 742 | let image = { 743 | url: result.image 744 | }; 745 | let menu_item = 746 | this.duckduckgo_helper.get_menu_item({ 747 | heading: result.heading, 748 | definition: result.definition, 749 | abstract: result.abstract, 750 | icon: image 751 | }); 752 | 753 | if(menu_item) { 754 | let position = 0; 755 | let set_position = this._settings.get_string( 756 | Prefs.HELPER_POSITION_KEY 757 | ); 758 | 759 | if(set_position === 'bottom') { 760 | position = this.suggestions_box.numMenuItems; 761 | 762 | if(position > 0) { 763 | position += 1; 764 | } 765 | } 766 | this.suggestions_box.addMenuItem(menu_item, position); 767 | this.suggestions_box.open(); 768 | } 769 | }) 770 | ); 771 | }) 772 | ); 773 | 774 | return true; 775 | }, 776 | 777 | _display_suggestions: function(text) { 778 | if(!this._settings.get_boolean(Prefs.SUGGESTIONS_KEY)) { 779 | return false; 780 | } 781 | if(!this.show_suggestions) { 782 | return false; 783 | } 784 | 785 | if(Utils.is_blank(text)) { 786 | this.suggestions_box.close(); 787 | 788 | return false; 789 | } 790 | 791 | this.suggestions_box.open(); 792 | // text = text.trim(); 793 | 794 | this._suggestions_delay_id = Mainloop.timeout_add( 795 | this._settings.get_int(Prefs.SUGGESTIONS_DELAY_KEY), 796 | Lang.bind(this, function() { 797 | this._get_suggestions(text, function(term, suggestions) { 798 | this.suggestions_box.remove_all_by_types('ALL'); 799 | 800 | if(!suggestions) { 801 | this.suggestions_box.close(); 802 | return false; 803 | } 804 | if(this.search_entry.text != term) return false; 805 | 806 | if(this.search_entry.text != term) return false; 807 | 808 | suggestions = this._parse_suggestions(suggestions); 809 | 810 | if(!suggestions){return false;} 811 | 812 | for(let i = 0; i < MAX_SUGGESTIONS; i++) { 813 | let suggestion = suggestions[i]; 814 | 815 | if(this.search_engine.open_url && 816 | suggestion.type != 'NAVIGATION') { 817 | 818 | continue; 819 | } 820 | if(suggestion.text == text) { 821 | continue; 822 | } 823 | 824 | this.suggestions_box.add_suggestion({ 825 | text: suggestion.text, 826 | type: suggestion.type, 827 | relevance: suggestion.relevance, 828 | term: text 829 | }); 830 | } 831 | 832 | this._display_history_suggestions(text); 833 | 834 | if(this.suggestions_box.isEmpty()) { 835 | this.suggestions_box.close(); 836 | } 837 | else { 838 | if(this.select_first_suggestion) { 839 | this._select_first_suggestion(text); 840 | } 841 | else { 842 | this.select_first_suggestion = true; 843 | } 844 | } 845 | 846 | return true; 847 | }); 848 | }) 849 | ); 850 | 851 | return true; 852 | }, 853 | 854 | _select_first_suggestion: function(text) { 855 | if(!this._settings.get_boolean(Prefs.SELECT_FIRST_SUGGESTION)) return false; 856 | if(text.slice(-1) == ' ') return false; 857 | 858 | let item = this.suggestions_box.firstMenuItem; 859 | 860 | let suggestion_t = item._text.slice(0, text.length).toUpperCase(); 861 | let source_t = text.toUpperCase(); 862 | 863 | if(suggestion_t != source_t) { 864 | return false; 865 | } 866 | 867 | this.show_suggestions = false; 868 | this.search_entry.set_text(item._text); 869 | this.search_entry.clutter_text.set_selection( 870 | text.length, 871 | item._text.length 872 | ); 873 | item.actor.add_style_pseudo_class('active'); 874 | 875 | this._display_helper(text); 876 | 877 | return true; 878 | }, 879 | 880 | _display_history_suggestions: function(text) { 881 | if(!this._settings.get_boolean(Prefs.HISTORY_SUGGESTIONS_KEY)) { 882 | return false; 883 | } 884 | 885 | let types = ['QUERY', 'NAVIGATION']; 886 | 887 | if(this.search_engine.open_url) { 888 | types = ['NAVIGATION']; 889 | } 890 | 891 | let history_suggestions = this.search_history.get_best_matches({ 892 | text: text, 893 | types: types, 894 | min_score: 0.35, 895 | limit: 3, 896 | fuzziness: 0.5 897 | }); 898 | 899 | if(history_suggestions.length > 0) { 900 | this.suggestions_box.add_label('History:'); 901 | 902 | for(let i = 0; i < history_suggestions.length; i++) { 903 | this.suggestions_box.add_suggestion({ 904 | text: history_suggestions[i][1].query, 905 | type: history_suggestions[i][1].type, 906 | relevance: history_suggestions[i][0], 907 | term: text 908 | }); 909 | } 910 | } 911 | 912 | return true; 913 | }, 914 | 915 | _display_engines: function() { 916 | this._remove_delay_id(); 917 | 918 | this.suggestions_box.removeAll(); 919 | let engines = this._settings.get_strv(Prefs.ENGINES_KEY); 920 | 921 | for(let i = 0; i < engines.length; i++) { 922 | let engine = JSON.parse(engines[i]); 923 | let default_engine = this._get_default_engine(); 924 | 925 | if(this.search_engine.keyword == engine.keyword) { 926 | continue; 927 | } 928 | else if(default_engine.name == engine.name) { 929 | continue; 930 | } 931 | else { 932 | this.suggestions_box.add_suggestion({ 933 | text: engine.name, 934 | type: 'ENGINE', 935 | term: engine.keyword 936 | }); 937 | } 938 | } 939 | 940 | if(this._get_open_url_keyword()) { 941 | this.suggestions_box.add_suggestion({ 942 | text: this._settings.get_string(Prefs.OPEN_URL_LABEL), 943 | type: 'ENGINE', 944 | term: this._get_open_url_keyword() 945 | }); 946 | } 947 | 948 | if(!this.suggestions_box.isEmpty()) { 949 | this.suggestions_box.open(); 950 | } 951 | }, 952 | 953 | _activate_search: function(text) { 954 | this.suggestions_box.close(); 955 | 956 | if(Utils.is_blank(text)) { 957 | this._show_hint({ 958 | text: 'Error.\nPlease, enter a query.', 959 | icon_name: ICONS.error 960 | }); 961 | 962 | return false; 963 | } 964 | 965 | this.search_history.add_item({ 966 | query: text, 967 | type: "QUERY" 968 | }); 969 | 970 | text = encodeURIComponent(text); 971 | let url = this.search_engine.url.replace('{term}', text); 972 | this._open_url(url); 973 | 974 | return true; 975 | }, 976 | 977 | _open_url: function(url, to_history) { 978 | url = Utils.get_url(url); 979 | 980 | if(!url) { 981 | this._show_hint({ 982 | text: 'Please, enter a valid url.', 983 | icon_name: ICONS.error 984 | }); 985 | 986 | return false; 987 | } 988 | else { 989 | this.close(); 990 | } 991 | 992 | if(to_history === true) { 993 | this.search_history.add_item({ 994 | query: url, 995 | type: "NAVIGATION" 996 | }); 997 | } 998 | 999 | this.activate_window = true; 1000 | 1001 | Gio.app_info_launch_default_for_uri( 1002 | url, 1003 | Utils._makeLaunchContext({}) 1004 | ); 1005 | 1006 | return true; 1007 | }, 1008 | 1009 | open: function() { 1010 | this.parent(); 1011 | this.search_entry.grab_key_focus(); 1012 | this._set_engine(); 1013 | this._show_hint({ 1014 | text: this._get_main_hint(), 1015 | icon_name: ICONS.information 1016 | }); 1017 | 1018 | this._resize(); 1019 | this._reposition(); 1020 | this._is_open = true; 1021 | }, 1022 | 1023 | close: function() { 1024 | this._remove_delay_id(); 1025 | this._hide_engine_label(); 1026 | this.search_entry.set_text(''); 1027 | this.search_engine = false; 1028 | this.suggestions_box.close(); 1029 | this.search_history.reset_index(); 1030 | this._is_open = false; 1031 | 1032 | this.parent(); 1033 | }, 1034 | 1035 | toggleOpen: function() { 1036 | this._is_open ? this.close() : this.open(); 1037 | }, 1038 | 1039 | enable: function() { 1040 | Main.wm.addKeybinding( 1041 | Prefs.OPEN_SEARCH_DIALOG_KEY, 1042 | this._settings, 1043 | Meta.KeyBindingFlags.NONE, 1044 | Shell.ActionMode.NORMAL | 1045 | Shell.ActionMode.OVERVIEW | 1046 | Shell.ActionMode.SYSTEM_MODAL, 1047 | Lang.bind(this, this.toggleOpen) 1048 | ); 1049 | }, 1050 | 1051 | disable: function() { 1052 | this._remove_delay_id(); 1053 | Main.wm.removeKeybinding(Prefs.OPEN_SEARCH_DIALOG_KEY); 1054 | global.display.disconnect(this._window_handler_id); 1055 | this.destroy(); 1056 | } 1057 | }); 1058 | 1059 | let search_dialog = null; 1060 | 1061 | function init() { 1062 | // nothing 1063 | } 1064 | 1065 | function enable() { 1066 | if(search_dialog === null) { 1067 | search_dialog = new WebSearchDialog(); 1068 | search_dialog.enable(); 1069 | } 1070 | } 1071 | 1072 | function disable() { 1073 | search_dialog.disable(); 1074 | search_dialog = null; 1075 | } 1076 | --------------------------------------------------------------------------------