├── .gitignore ├── data ├── screenshot.png ├── icon-extensions.gnome.org.png ├── icon-i-color.svg ├── icon-i-white.svg ├── icon-i-white-base.svg └── icon-extensions.gnome.org.svg ├── src ├── schemas │ ├── gschemas.compiled │ └── org.gnome.shell.extensions.imgur.gschema.xml ├── clipboard.js ├── stylesheet.css ├── metadata.json ├── config.js ├── version.js ├── notifications.js ├── icons │ ├── imgur-uploader-symbolic.svg │ └── imgur-uploader-color.svg ├── indicator.js ├── uploader.js ├── convenience.js ├── extension.js ├── selection.js └── prefs.js ├── README.md ├── tools └── settings_test.js ├── Makefile ├── LICENSE └── README_DEPRECATED.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | -------------------------------------------------------------------------------- /data/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OttoAllmendinger/gnome-shell-imgur/HEAD/data/screenshot.png -------------------------------------------------------------------------------- /src/schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OttoAllmendinger/gnome-shell-imgur/HEAD/src/schemas/gschemas.compiled -------------------------------------------------------------------------------- /data/icon-extensions.gnome.org.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OttoAllmendinger/gnome-shell-imgur/HEAD/data/icon-extensions.gnome.org.png -------------------------------------------------------------------------------- /src/clipboard.js: -------------------------------------------------------------------------------- 1 | /*jshint moz:true */ 2 | const St = imports.gi.St; 3 | 4 | function set (string) { 5 | St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, string); 6 | St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, string); 7 | } 8 | -------------------------------------------------------------------------------- /src/stylesheet.css: -------------------------------------------------------------------------------- 1 | .area-selection { 2 | background-color: rgba(74, 144, 217, 0.5); 3 | border: 1px solid rgba(74, 144, 217, 1); 4 | 5 | /* 6 | imgur colors 7 | 8 | background-color: rgba(133, 191, 37, 0.5); 9 | border: 1px solid rgb(133, 191, 37); 10 | */ 11 | } 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This Project is Deprecated 2 | 3 | Development of this extension has stopped in favor of a more general extension. 4 | 5 | Check out the new extension at 6 | https://github.com/OttoAllmendinger/gnome-shell-screenshot 7 | 8 | # Old Readme 9 | 10 | [README_DEPRECATED.md](./README_DEPRECATED.md) 11 | -------------------------------------------------------------------------------- /src/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "shell-version": ["3.20", "3.22"], 3 | "uuid": "gnome-shell-imgur@ttll.de", 4 | "name": "Imgur Screenshot Uploader", 5 | "url": "https://github.com/OttoAllmendinger/gnome-shell-imgur/", 6 | "description": "Upload screenshots directly to imgur.com (Deprecated, now gnome-shell-screenshot)", 7 | "settings-schema": "org.gnome.shell.extensions.imgur", 8 | "gettext-domain": "imgur-uploader" 9 | } 10 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /*jshint moz:true */ 2 | // vi: sts=2 sw=2 et 3 | 4 | const IndicatorName = 'de.ttll.ImgurUploader'; 5 | 6 | const SettingsSchema = 'org.gnome.shell.extensions.imgur'; 7 | 8 | const KeyEnableIndicator = 'enable-indicator'; 9 | const KeyClickAction = 'click-action'; 10 | const KeyCopyClipboard = 'copy-clipboard'; 11 | const KeyKeepFile = 'keep-file'; 12 | const KeyShortcuts = [ 13 | 'shortcut-select-area', 14 | 'shortcut-select-window', 15 | 'shortcut-select-desktop' 16 | ]; 17 | 18 | const ClickActions = { 19 | SHOW_MENU: 0, 20 | SELECT_AREA: 1, 21 | SELECT_WINDOW: 2, 22 | SELECT_DESKTOP: 3 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /tools/settings_test.js: -------------------------------------------------------------------------------- 1 | const Gio = imports.gi.Gio; 2 | 3 | function getSettings(schema, path) { 4 | const GioSSS = Gio.SettingsSchemaSource; 5 | let schemaSource = GioSSS.new_from_directory(path, 6 | GioSSS.get_default(), 7 | false); 8 | 9 | let schemaObj = schemaSource.lookup(schema, true); 10 | 11 | if (!schemaObj) 12 | throw new Error('Schema ' + schema + ' could not be found for extension '); 13 | 14 | return new Gio.Settings({ settings_schema: schemaObj }); 15 | } 16 | 17 | let schema = "org.gnome.shell.extensions.imgur"; 18 | let path = 'src/schemas/'; 19 | let settings = getSettings(schema, path); 20 | 21 | log(settings.get_boolean('enable-indicator')); 22 | settings.set_boolean('enable-indicator', true); 23 | settings.set_strv('shortcut', ['F12']); 24 | 25 | settings.sync(); 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all schemas zipfile 2 | 3 | SCHEMA = org.gnome.shell.extensions.imgur.gschema.xml 4 | 5 | SOURCE = src/*.js \ 6 | src/stylesheet.css \ 7 | src/metadata.json \ 8 | src/icons \ 9 | src/schemas/* 10 | 11 | ZIPFILE = gnome-shell-imgur.zip 12 | 13 | UUID = gnome-shell-imgur@ttll.de 14 | EXTENSION_PATH = $(HOME)/.local/share/gnome-shell/extensions/$(UUID) 15 | 16 | all: archive 17 | 18 | schemas: src/schemas/gschemas.compiled 19 | 20 | archive: $(ZIPFILE) 21 | 22 | install: archive 23 | -rm -r $(EXTENSION_PATH) 24 | mkdir -p $(EXTENSION_PATH) 25 | unzip $(ZIPFILE) -d $(EXTENSION_PATH) 26 | 27 | 28 | src/schemas/gschemas.compiled: src/schemas/$(SCHEMA) 29 | glib-compile-schemas src/schemas/ 30 | 31 | $(ZIPFILE): $(SOURCE) schemas 32 | -rm $(ZIPFILE) 33 | cd src && zip -r ../$(ZIPFILE) $(patsubst src/%,%,$(SOURCE)) 34 | 35 | prefs: install 36 | gnome-shell-extension-prefs $(UUID) 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 otto.allmendinger@gmail.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | let versionArray = function (v) v.split(".").map(Number); 2 | 3 | let zip = function (a, b) { 4 | let headA = a.shift(), headB = b.shift(); 5 | if ((headA !== undefined) || (headB !== undefined)) { 6 | return [[headA, headB]].concat(zip(a, b)); 7 | } else { 8 | return []; 9 | } 10 | }; 11 | 12 | function versionEqual(a, b) { 13 | return zip(versionArray(a), versionArray(b)).reduce( 14 | function (prev, [a, b], index) prev && (a === b) 15 | , true); 16 | }; 17 | 18 | function versionGreater(a, b) { 19 | return (!versionEqual(a, b)) 20 | && zip(versionArray(a), versionArray(b)).reduce( 21 | function (prev, [a, b], index) prev && (a >= (b || 0)) 22 | , true); 23 | }; 24 | 25 | function versionSmaller(a, b) { 26 | return (!versionEqual(a, b)) && (!versionGreater(a, b)); 27 | }; 28 | 29 | if (this['ARGV'] !== undefined) { 30 | // ghetto tests 31 | // 32 | log('zip("1.2.3", "1.2")=' + JSON.stringify(zip( 33 | versionArray("1.2.3"), 34 | versionArray("1.2")))); 35 | log('versionEqual("1.2.3", "1.2")=' + versionEqual("1.2.3", "1.2")); 36 | log('versionGreater("3.10.1", "3.10")=' + versionGreater("3.10.1", "3.10")); 37 | } 38 | -------------------------------------------------------------------------------- /src/schemas/org.gnome.shell.extensions.imgur.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | false 16 | 17 | 18 | 19 | F11']]]> 20 | 21 | 22 | 23 | [] 24 | 25 | 26 | 27 | [] 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 37 | 'show-menu' 38 | 39 | 40 | 41 | 42 | false 43 | 44 | 45 | 46 | '/tmp/' 47 | File Save location 48 | Location where screenshots are stored 49 | 50 | 51 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /README_DEPRECATED.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](https://raw.github.com/OttoAllmendinger/gnome-shell-imgur/master/data/screenshot.png) 2 | 3 | Uploads screenshots directly to imgur.com and displays URL. 4 | 5 | ## Installation 6 | 7 | ### Via extensions.gnome.org 8 | 9 | The latest reviewed version can be found at [GNOME Shell Extensions](https://extensions.gnome.org/extension/683/imgur-screenshot-uploader/). 10 | 11 | An extension that allows copying screenshots to the local clipboard and saving 12 | them locally can be found at https://github.com/OttoAllmendinger/gnome-shell-screenshot 13 | 14 | ### Via github.com 15 | 16 | The latest development version can be installed manually with these commands: 17 | 18 | ```sh 19 | git clone https://github.com/OttoAllmendinger/gnome-shell-imgur.git 20 | cd gnome-shell-imgur 21 | make install 22 | ``` 23 | 24 | Then go to https://extensions.gnome.org/local/ to turn on the extension or use 25 | gnome-tweak-tool. 26 | 27 | ## Disclaimer 28 | 29 | The Imgur logo and "IMGUR" are trademarks of Imgur LLC and are used here for 30 | informational purposes only. My application, gnome-shell-imgur, is not 31 | affiliated with Imgur and has not been reviewed or approved by Imgur. 32 | 33 | ## Contributors 34 | 35 | * Andrey Sitnik - https://github.com/ai 36 | * Geoff Saulmon - https://github.com/gsaulmon 37 | * Jacob Mischka - https://github.com/jacobmischka 38 | * Kate Adams - https://github.com/KateAdams 39 | * Maxim Kraev - https://github.com/MaximKraev 40 | * papertoilet - https://github.com/papertoilet 41 | * Sigmund Vestergaard - https://github.com/sigmundv 42 | 43 | 44 | ## Tip Address 45 | 46 | Bitcoin tip address 17t3qB7fXnYpgfqvrh6gcwJBWWM6reYfoS 47 | 48 | -------------------------------------------------------------------------------- /src/notifications.js: -------------------------------------------------------------------------------- 1 | /*jshint moz:true */ 2 | const Lang = imports.lang; 3 | 4 | const Gio = imports.gi.Gio; 5 | 6 | const Main = imports.ui.main; 7 | const MessageTray = imports.ui.messageTray; 8 | 9 | const Gettext = imports.gettext.domain('gnome-shell-extensions'); 10 | const _ = Gettext.gettext; 11 | 12 | const ExtensionUtils = imports.misc.extensionUtils; 13 | const Local = ExtensionUtils.getCurrentExtension(); 14 | 15 | const NotificationIcon = 'imgur-uploader-color'; 16 | const NotificationSourceName = 'ImgurUploader'; 17 | const Convenience = Local.imports.convenience; 18 | const Clipboard = Local.imports.clipboard; 19 | 20 | 21 | const NotificationService = new Lang.Class({ 22 | Name: "ImgurUploader.NotificationService", 23 | 24 | _init: function () { 25 | this._notificationSource = new MessageTray.Source( 26 | NotificationSourceName, NotificationIcon 27 | ); 28 | this._notifications = []; 29 | }, 30 | 31 | make: function () { 32 | let n = new MessageTray.Notification( 33 | this._notificationSource, _("Upload") 34 | ); 35 | 36 | Main.messageTray.add(this._notificationSource); 37 | this._notificationSource.notify(n); 38 | return n; 39 | }, 40 | 41 | setProgress: function (notification, bytes, total) { 42 | notification.update( 43 | _("Upload"), 44 | '' + Math.floor(100 * (bytes / total)) + '%' 45 | ); 46 | }, 47 | 48 | setFinished: function (notification, url) { 49 | notification.setResident(true); 50 | 51 | notification.update(_("Upload Complete"), url); 52 | 53 | notification.addAction(_("Copy Link"), function () { 54 | Clipboard.set(url); 55 | }); 56 | 57 | this._notificationSource.notify(notification); 58 | }, 59 | 60 | setError: function (notification, error) { 61 | notification.setResident(true); 62 | 63 | notification.update( 64 | _("Error"), 65 | error, 66 | { secondaryGIcon: new Gio.ThemedIcon({name: 'dialog-error'}) } 67 | ); 68 | 69 | this._notificationSource.notify(notification); 70 | } 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /data/icon-i-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /data/icon-i-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/icons/imgur-uploader-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /data/icon-i-white-base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/icons/imgur-uploader-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/indicator.js: -------------------------------------------------------------------------------- 1 | // vi: sts=2 sw=2 et 2 | 3 | const Lang = imports.lang; 4 | const Signals = imports.signals; 5 | 6 | const St = imports.gi.St; 7 | 8 | const PanelMenu = imports.ui.panelMenu; 9 | const PopupMenu = imports.ui.popupMenu; 10 | 11 | const Gettext = imports.gettext.domain('gnome-shell-extensions'); 12 | const _ = Gettext.gettext; 13 | 14 | const ExtensionUtils = imports.misc.extensionUtils; 15 | const Local = ExtensionUtils.getCurrentExtension(); 16 | 17 | const Config = Local.imports.config; 18 | 19 | 20 | const DefaultIcon = 'imgur-uploader-symbolic'; 21 | const HoverIcon = 'imgur-uploader-color'; 22 | 23 | 24 | 25 | 26 | const Indicator = new Lang.Class({ 27 | Name: "ImgurUploader.Indicator", 28 | Extends: PanelMenu.Button, 29 | 30 | _init: function (extension) { 31 | this.parent(null, Config.IndicatorName); 32 | 33 | this._extension = extension; 34 | 35 | this._signalSettings = []; 36 | 37 | this._icon = new St.Icon({ 38 | icon_name: DefaultIcon, 39 | style_class: 'system-status-icon' 40 | }); 41 | 42 | this.actor.add_actor(this._icon); 43 | this.actor.connect('enter-event', this._hoverIcon.bind(this)); 44 | this.actor.connect('leave-event', this._resetIcon.bind(this)); 45 | 46 | this._signalSettings.push(this._extension.settings.connect( 47 | 'changed::' + Config.KeyClickAction, 48 | this._updateButton.bind(this) 49 | )); 50 | 51 | this._signalButtonPressEvent = this.actor.connect( 52 | 'button-press-event', 53 | this._onClick.bind(this) 54 | ); 55 | 56 | this._updateButton(); 57 | }, 58 | 59 | _updateButton: function () { 60 | const action = this._clickAction = 61 | this._extension.settings.get_string(Config.KeyClickAction); 62 | 63 | if (action === 'show-menu') { 64 | this._enableMenu() 65 | } else { 66 | this._disableMenu(); 67 | } 68 | }, 69 | 70 | _onClick: function () { 71 | if (this._clickAction && (this._clickAction !== 'show-menu')) { 72 | this._extension.onAction(this._clickAction); 73 | } 74 | }, 75 | 76 | _enableMenu: function () { 77 | const items = [ 78 | ["select-area", _("Select Area")], 79 | ["select-window", _("Select Window")], 80 | ["select-desktop", _("Select Desktop")] 81 | ]; 82 | 83 | for each (let [action, title] in items) { 84 | let item = new PopupMenu.PopupMenuItem(title); 85 | item.connect( 86 | 'activate', function (action) { 87 | this.menu.close(); 88 | this._extension.onAction(action); 89 | }.bind(this, action) 90 | ); 91 | this.menu.addMenuItem(item); 92 | } 93 | }, 94 | 95 | _disableMenu: function () { 96 | this.menu.removeAll(); 97 | }, 98 | 99 | startSelection: function () { 100 | this._selection = true; 101 | this._hoverIcon(); 102 | }, 103 | 104 | stopSelection: function () { 105 | this._selection = false; 106 | this._resetIcon(); 107 | }, 108 | 109 | _hoverIcon: function () { 110 | this._icon.icon_name = HoverIcon; 111 | }, 112 | 113 | _resetIcon: function () { 114 | if (!this._selection) { 115 | this._icon.icon_name = DefaultIcon; 116 | } 117 | }, 118 | 119 | destroy: function () { 120 | this.parent(); 121 | this._signalSettings.forEach(function (signal) { 122 | this._extension.settings.disconnect(signal); 123 | }.bind(this)); 124 | } 125 | }); 126 | -------------------------------------------------------------------------------- /data/icon-extensions.gnome.org.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 58 | 66 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/uploader.js: -------------------------------------------------------------------------------- 1 | // vi: sw=2 sts=2 2 | const Lang = imports.lang; 3 | const Signals = imports.signals; 4 | const Mainloop = imports.mainloop; 5 | 6 | const Gio = imports.gi.Gio; 7 | const Soup = imports.gi.Soup; 8 | 9 | const ClientId = "c5c1369fb46f29e"; 10 | 11 | const _httpSession = new Soup.SessionAsync(); 12 | 13 | 14 | const Uploader = new Lang.Class({ 15 | Name: "Uploader", 16 | _init: function () true 17 | }); 18 | 19 | 20 | Signals.addSignalMethods(Uploader.prototype); 21 | 22 | 23 | 24 | 25 | const ImgurUploader = new Lang.Class({ 26 | Name: "ImgurUploader", 27 | Extends: Uploader, 28 | 29 | baseUrl: "https://api.imgur.com/3/", 30 | 31 | _init: function (clientId) { 32 | this._clientId = clientId || ClientId; 33 | }, 34 | 35 | _getMimetype: function (filename) { 36 | return 'image/png'; // FIXME 37 | }, 38 | 39 | _getPostMessage: function (filename, callback) { 40 | let url = this.baseUrl + "image"; 41 | let file = Gio.File.new_for_path(filename); 42 | 43 | file.load_contents_async(null, Lang.bind(this, function (f, res) { 44 | let contents; 45 | 46 | try { 47 | [, contents] = f.load_contents_finish(res); 48 | } catch (e) { 49 | log("error loading file: " + e.message); 50 | callback(e, null); 51 | return; 52 | } 53 | 54 | let buffer = new Soup.Buffer(contents, contents.length); 55 | let mimetype = this._getMimetype(filename); 56 | let multipart = new Soup.Multipart(Soup.FORM_MIME_TYPE_MULTIPART); 57 | multipart.append_form_file('image', filename, mimetype, buffer); 58 | 59 | let message = Soup.form_request_new_from_multipart(url, multipart); 60 | 61 | message.request_headers.append( 62 | "Authorization", "Client-ID " + this._clientId 63 | ); 64 | 65 | callback(null, message); 66 | }), null); 67 | }, 68 | 69 | 70 | upload: function (filename) { 71 | this._getPostMessage(filename, Lang.bind(this, function (error, message) { 72 | let total = message.request_body.length; 73 | let uploaded = 0; 74 | 75 | if (error) { 76 | this.emit("error", error); 77 | return; 78 | } 79 | 80 | let signalProgress = message.connect( 81 | "wrote-body-data", 82 | Lang.bind(this, function (message, buffer) { 83 | uploaded += buffer.length; 84 | this.emit("progress", uploaded, total); 85 | }) 86 | ); 87 | 88 | Soup.Session.prototype.add_feature.call( 89 | _httpSession, new Soup.ProxyResolverDefault() 90 | ); 91 | _httpSession.queue_message(message, 92 | Lang.bind(this, function (session, {status_code, response_body}) { 93 | if (status_code == 200) { 94 | this.emit('done', JSON.parse(response_body.data).data); 95 | } else { 96 | log('getJSON error status code: ' + status_code); 97 | log('getJSON error response: ' + response_body.data); 98 | 99 | let errorMessage; 100 | 101 | try { 102 | errorMessage = JSON.parse(response_body.data).data.error; 103 | } catch (e) { 104 | log("failed to parse error message " + e); 105 | errorMessage = response_body.data 106 | } 107 | 108 | this.emit( 109 | 'error', 110 | "HTTP " + status_code + " - " + errorMessage 111 | ); 112 | } 113 | 114 | message.disconnect(signalProgress); 115 | })); 116 | })); 117 | } 118 | }); 119 | 120 | const DummyUploader = new Lang.Class({ 121 | Name: "DummyUploader", 122 | Extends: Uploader, 123 | 124 | _init: function () {}, 125 | 126 | upload: function (filename) { 127 | const testImage = 'http://i.imgur.com/Vkapy8W.png'; 128 | const size = 200000; 129 | const chunk = 10000; 130 | let progress = 0; 131 | 132 | log("DummyUploader.upload() filename=" + filename); 133 | 134 | let update = Lang.bind(this, function () { 135 | if (progress < size) { 136 | this.emit("progress", (progress += chunk), size); 137 | Mainloop.timeout_add(100, update); 138 | } else { 139 | this.emit("done", {link: testImage}); 140 | } 141 | }); 142 | 143 | Mainloop.idle_add(update); 144 | } 145 | }); 146 | 147 | 148 | if (this['ARGV'] !== undefined) { 149 | 150 | // run by gjs 151 | log("command line"); 152 | 153 | let uploader = new ImgurUploader(); 154 | 155 | uploader.connect("data", function (obj, data) { 156 | log(JSON.stringify(data)); 157 | }); 158 | 159 | uploader.upload("data/test.png"); 160 | 161 | Mainloop.run("main"); 162 | } 163 | -------------------------------------------------------------------------------- /src/convenience.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: js -*- */ 2 | /* 3 | Copyright (c) 2011-2012, Giovanni Campagna 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the GNOME nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | const Gettext = imports.gettext; 29 | const Gio = imports.gi.Gio; 30 | 31 | const Config = imports.misc.config; 32 | const ExtensionUtils = imports.misc.extensionUtils; 33 | const Local = ExtensionUtils.getCurrentExtension(); 34 | const Version = Local.imports.version; 35 | 36 | /** 37 | * initTranslations: 38 | * @domain: (optional): the gettext domain to use 39 | * 40 | * Initialize Gettext to load translations from extensionsdir/locale. 41 | * If @domain is not provided, it will be taken from metadata['gettext-domain'] 42 | */ 43 | function initTranslations(domain) { 44 | let extension = ExtensionUtils.getCurrentExtension(); 45 | 46 | domain = domain || extension.metadata['gettext-domain']; 47 | 48 | // check if this extension was built with "make zip-file", and thus 49 | // has the locale files in a subfolder 50 | // otherwise assume that extension has been installed in the 51 | // same prefix as gnome-shell 52 | let localeDir = extension.dir.get_child('locale'); 53 | if (localeDir.query_exists(null)) 54 | Gettext.bindtextdomain(domain, localeDir.get_path()); 55 | else 56 | Gettext.bindtextdomain(domain, Config.LOCALEDIR); 57 | } 58 | 59 | /** 60 | * getSettings: 61 | * @schema: (optional): the GSettings schema id 62 | * 63 | * Builds and return a GSettings schema for @schema, using schema files 64 | * in extensionsdir/schemas. If @schema is not provided, it is taken from 65 | * metadata['settings-schema']. 66 | */ 67 | function getSettings(schema) { 68 | let extension = ExtensionUtils.getCurrentExtension(); 69 | 70 | schema = schema || extension.metadata['settings-schema']; 71 | 72 | const GioSSS = Gio.SettingsSchemaSource; 73 | 74 | // check if this extension was built with "make zip-file", and thus 75 | // has the schema files in a subfolder 76 | // otherwise assume that extension has been installed in the 77 | // same prefix as gnome-shell (and therefore schemas are available 78 | // in the standard folders) 79 | let schemaDir = extension.dir.get_child('schemas'); 80 | let schemaSource; 81 | if (schemaDir.query_exists(null)) 82 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), 83 | GioSSS.get_default(), 84 | false); 85 | else 86 | schemaSource = GioSSS.get_default(); 87 | 88 | let schemaObj = schemaSource.lookup(schema, true); 89 | if (!schemaObj) 90 | throw new Error('Schema ' + schema + ' could not be found for extension ' 91 | + extension.metadata.uuid + '. Please check your installation.'); 92 | 93 | return new Gio.Settings({ settings_schema: schemaObj }); 94 | } 95 | 96 | 97 | function currentVersion () { 98 | return Config.PACKAGE_VERSION; 99 | } 100 | 101 | function currentVersionEqual(v) { 102 | return Version.versionEqual(currentVersion(), v); 103 | } 104 | 105 | function currentVersionGreaterEqual(v) { 106 | return Version.versionEqual(currentVersion(), v) 107 | || Version.versionGreater(currentVersion(), v); 108 | } 109 | 110 | function currentVersionSmallerEqual(v) { 111 | return Version.versionEqual(currentVersion(), v) 112 | && (!Version.versionGreater(currentVersion(), v)); 113 | } 114 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | /*jshint moz:true */ 2 | // vi: sts=2 sw=2 et 3 | // 4 | // props to 5 | // https://github.com/rjanja/desktop-capture 6 | // https://github.com/DASPRiD/gnome-shell-extension-area-screenshot 7 | 8 | const Lang = imports.lang; 9 | const Signals = imports.signals; 10 | const Mainloop = imports.mainloop; 11 | 12 | const Gio = imports.gi.Gio; 13 | const Meta = imports.gi.Meta; 14 | const Shell = imports.gi.Shell; 15 | 16 | const Main = imports.ui.main; 17 | 18 | const Gettext = imports.gettext.domain('gnome-shell-extensions'); 19 | const _ = Gettext.gettext; 20 | 21 | const ExtensionUtils = imports.misc.extensionUtils; 22 | const Local = ExtensionUtils.getCurrentExtension(); 23 | 24 | const Config = Local.imports.config; 25 | const Uploader = Local.imports.uploader; 26 | const Indicator = Local.imports.indicator; 27 | const Selection = Local.imports.selection; 28 | const Clipboard = Local.imports.clipboard; 29 | const Notifications = Local.imports.notifications; 30 | 31 | const Convenience = Local.imports.convenience; 32 | 33 | const Version316 = Convenience.currentVersionGreaterEqual("3.16"); 34 | 35 | const Extension = new Lang.Class({ 36 | Name: "ImgurUploader", 37 | 38 | _init: function () { 39 | this.settings = Convenience.getSettings(); 40 | 41 | this._notificationService = new Notifications.NotificationService(); 42 | 43 | this._signalSettings = []; 44 | 45 | this._signalSettings.push(this.settings.connect( 46 | 'changed::' + Config.KeyEnableIndicator, 47 | this._updateIndicator.bind(this) 48 | )); 49 | 50 | this._updateIndicator(); 51 | 52 | this._setKeybindings(); 53 | }, 54 | 55 | _setKeybindings: function () { 56 | let bindingMode; 57 | 58 | if (Version316) { 59 | bindingMode = Shell.ActionMode.NORMAL; 60 | } else { 61 | bindingMode = Shell.KeyBindingMode.NORMAL; 62 | } 63 | 64 | for each (let shortcut in Config.KeyShortcuts) { 65 | Main.wm.addKeybinding( 66 | shortcut, 67 | this.settings, 68 | Meta.KeyBindingFlags.NONE, 69 | bindingMode, 70 | this.onAction.bind(this, shortcut.replace('shortcut-', '')) 71 | ); 72 | } 73 | }, 74 | 75 | _unsetKeybindings: function () { 76 | for each (let shortcut in Config.KeyShortcuts) { 77 | Main.wm.removeKeybinding(shortcut); 78 | } 79 | }, 80 | 81 | _createIndicator: function () { 82 | if (!this._indicator) { 83 | this._indicator = new Indicator.Indicator(this); 84 | Main.panel.addToStatusArea(Config.IndicatorName, this._indicator); 85 | } 86 | }, 87 | 88 | _destroyIndicator: function () { 89 | if (this._indicator) { 90 | this._indicator.destroy(); 91 | this._indicator = null; 92 | } 93 | }, 94 | 95 | _updateIndicator: function () { 96 | if (this.settings.get_boolean(Config.KeyEnableIndicator)) { 97 | this._createIndicator(); 98 | } else { 99 | this._destroyIndicator(); 100 | } 101 | }, 102 | 103 | _startSelection: function (selection) { 104 | if (this._selection) { 105 | // prevent reentry 106 | log("_startSelection() error: selection already in progress"); 107 | return; 108 | } 109 | 110 | this._selection = selection; 111 | 112 | if (this._indicator) { 113 | this._indicator.startSelection(); 114 | } 115 | 116 | this._selection.connect("screenshot", function (selection, fileName) { 117 | this._uploadScreenshot(fileName, /* deleteAfterUpload */ !this.settings.get_boolean(Config.KeyKeepFile)); 118 | }.bind(this)); 119 | 120 | this._selection.connect("error", function (selection, message) { 121 | var n = _extension._notificationService.make(); 122 | this._notificationService.setError(n, message); 123 | }.bind(this)); 124 | 125 | this._selection.connect("stop", function () { 126 | this._selection = null; 127 | 128 | if (this._indicator) { 129 | this._indicator.stopSelection(); 130 | } 131 | }.bind(this)); 132 | }, 133 | 134 | _selectArea: function () { 135 | this._startSelection(new Selection.SelectionArea()); 136 | }, 137 | 138 | _selectWindow: function() { 139 | this._startSelection(new Selection.SelectionWindow()); 140 | }, 141 | 142 | _selectDesktop: function () { 143 | this._startSelection(new Selection.SelectionDesktop()); 144 | }, 145 | 146 | _uploadScreenshot: function (fileName, deleteAfterUpload) { 147 | let uploader = new Uploader.ImgurUploader(); 148 | // let uploader = new Uploader.DummyUploader(); 149 | 150 | let notification = this._notificationService.make(); 151 | let currentFile = Gio.File.new_for_path(fileName); 152 | let savepath = this.settings.get_string('save-location') + "/" + currentFile.get_basename(); 153 | 154 | let cleanup = function () { 155 | if (deleteAfterUpload) { 156 | currentFile.delete(/* cancellable */ null); 157 | } else { 158 | currentFile.move(Gio.File.new_for_path(savepath), Gio.FileCopyFlags.NONE, null, null); 159 | } 160 | uploader.disconnectAll(); 161 | }; 162 | 163 | uploader.connect('progress', 164 | function (obj, bytes, total) { 165 | this._notificationService.setProgress(notification, bytes, total); 166 | }.bind(this) 167 | ); 168 | 169 | uploader.connect('done', 170 | function (obj, data) { 171 | this._notificationService.setFinished(notification, data.link); 172 | 173 | if (this.settings.get_boolean(Config.KeyCopyClipboard)) { 174 | Clipboard.set(data.link); 175 | } 176 | 177 | cleanup(); 178 | }.bind(this) 179 | ); 180 | 181 | uploader.connect('error', 182 | function (obj, error) { 183 | this._notificationService.setError(notification, error); 184 | cleanup(); 185 | }.bind(this) 186 | ); 187 | 188 | uploader.upload(fileName); 189 | }, 190 | 191 | 192 | onAction: function (action) { 193 | let dispatch = { 194 | 'select-area': this._selectArea.bind(this), 195 | 'select-window': this._selectWindow.bind(this), 196 | 'select-desktop': this._selectDesktop.bind(this) 197 | }; 198 | 199 | let f = dispatch[action] || function () { 200 | throw new Error('unknown action: ' + action); 201 | }; 202 | 203 | try { 204 | f(); 205 | } catch (ex) { 206 | let notification = this._notificationService.make(); 207 | this._notificationService.setError(notification, ex.toString()); 208 | } 209 | }, 210 | 211 | destroy: function () { 212 | this._destroyIndicator(); 213 | this._unsetKeybindings(); 214 | 215 | this._signalSettings.forEach(function (signal) { 216 | this.settings.disconnect(signal); 217 | }.bind(this)); 218 | 219 | this.disconnectAll(); 220 | } 221 | }); 222 | 223 | Signals.addSignalMethods(Extension.prototype); 224 | 225 | 226 | 227 | let _extension; 228 | 229 | function init() { 230 | let theme = imports.gi.Gtk.IconTheme.get_default(); 231 | theme.append_search_path(Local.path + '/icons'); 232 | } 233 | 234 | function enable() { 235 | _extension = new Extension(); 236 | } 237 | 238 | function disable() { 239 | _extension.destroy(); 240 | _extension = null; 241 | } 242 | -------------------------------------------------------------------------------- /src/selection.js: -------------------------------------------------------------------------------- 1 | // vi: sts=2 sw=2 et 2 | 3 | const Lang = imports.lang; 4 | const Signals = imports.signals; 5 | const Mainloop = imports.mainloop; 6 | 7 | const GLib = imports.gi.GLib; 8 | const Shell = imports.gi.Shell; 9 | const Meta = imports.gi.Meta; 10 | const Clutter = imports.gi.Clutter; 11 | 12 | const Main = imports.ui.main; 13 | 14 | 15 | 16 | const Gettext = imports.gettext.domain('gnome-shell-extensions'); 17 | const _ = Gettext.gettext; 18 | 19 | const FileTemplate = 'gnome-shell-imgur-XXXXXX.png'; 20 | 21 | 22 | const ScreenshotWindowIncludeCursor = false; 23 | const ScreenshotWindowIncludeFrame = true; 24 | const ScreenshotDesktopIncludeCursor = false; 25 | 26 | const ExtensionUtils = imports.misc.extensionUtils; 27 | const Local = ExtensionUtils.getCurrentExtension(); 28 | const Convenience = Local.imports.convenience; 29 | 30 | const getTempFile = function () { 31 | let [fileHandle, fileName] = GLib.file_open_tmp(FileTemplate); 32 | return fileName; 33 | }; 34 | 35 | 36 | const getRectangle = function (x1, y1, x2, y2) { 37 | return { 38 | x: Math.min(x1, x2), 39 | y: Math.min(y1, y2), 40 | w: Math.abs(x1 - x2), 41 | h: Math.abs(y1 - y2) 42 | }; 43 | }; 44 | 45 | 46 | const getWindowRectangle = function (win) { 47 | let [wx, wy] = win.get_position(); 48 | let [width, height] = win.get_size(); 49 | 50 | return { 51 | x: wx, 52 | y: wy, 53 | w: width, 54 | h: height 55 | }; 56 | }; 57 | 58 | 59 | const selectWindow = function (windows, x, y) { 60 | let filtered = windows.filter(function (win) { 61 | if ((win !== undefined) 62 | && win.visible 63 | && (typeof win.get_meta_window === 'function')) { 64 | 65 | let [w, h] = win.get_size(); 66 | let [wx, wy] = win.get_position(); 67 | 68 | return ( 69 | (wx <= x) && (wy <= y) && ((wx + w) >= x) && ((wy + h) >= y) 70 | ); 71 | } else { 72 | return false; 73 | } 74 | }); 75 | 76 | filtered.sort(function (a, b) 77 | (a.get_meta_window().get_layer() <= b.get_meta_window().get_layer()) 78 | ); 79 | 80 | return filtered[0]; 81 | }; 82 | 83 | 84 | const makeAreaScreenshot = function ({x, y, w, h}, callback) { 85 | let fileName = getTempFile(); 86 | let screenshot = new Shell.Screenshot(); 87 | screenshot.screenshot_area( 88 | x, y, w, h, fileName, 89 | callback.bind(callback, fileName) 90 | ); 91 | }; 92 | 93 | const makeWindowScreenshot = function (win, callback) { 94 | let fileName = getTempFile(); 95 | let screenshot = new Shell.Screenshot(); 96 | 97 | screenshot.screenshot_window( 98 | ScreenshotWindowIncludeFrame, 99 | ScreenshotWindowIncludeCursor, 100 | fileName, 101 | callback.bind(callback, fileName) 102 | ); 103 | }; 104 | 105 | const makeDesktopScreenshot = function(callback) { 106 | let fileName = getTempFile(); 107 | let screenshot = new Shell.Screenshot(); 108 | screenshot.screenshot( 109 | ScreenshotDesktopIncludeCursor, 110 | fileName, 111 | callback.bind(callback, fileName) 112 | ); 113 | }; 114 | 115 | 116 | 117 | 118 | const Capture = new Lang.Class({ 119 | Name: "ImgurUploader.Capture", 120 | 121 | _init: function () { 122 | this._mouseDown = false; 123 | 124 | this._container = new Shell.GenericContainer({ 125 | name: 'area-selection', 126 | style_class: 'area-selection', 127 | visible: 'true', 128 | reactive: 'true', 129 | x: -10, 130 | y: -10 131 | }); 132 | 133 | Main.uiGroup.add_actor(this._container); 134 | 135 | if (Main.pushModal(this._container)) { 136 | this._signalCapturedEvent = global.stage.connect( 137 | 'captured-event', this._onCaptureEvent.bind(this) 138 | ); 139 | 140 | this._setCaptureCursor(); 141 | } else { 142 | log("Main.pushModal() === false"); 143 | } 144 | }, 145 | 146 | _setDefaultCursor: function () { 147 | global.screen.set_cursor(Meta.Cursor.DEFAULT); 148 | }, 149 | 150 | _setCaptureCursor: function () { 151 | global.screen.set_cursor(Meta.Cursor.CROSSHAIR); 152 | }, 153 | 154 | _onCaptureEvent: function (actor, event) { 155 | if (event.type() === Clutter.EventType.KEY_PRESS) { 156 | if (event.get_key_symbol() === Clutter.Escape) { 157 | this._stop(); 158 | } 159 | } 160 | 161 | this.emit("captured-event", event); 162 | }, 163 | 164 | drawContainer: function ({x, y, w, h}) { 165 | this._container.set_position(x, y); 166 | this._container.set_size(w, h); 167 | }, 168 | 169 | clearContainer: function () { 170 | this.drawContainer({x: -10, y: -10, w: 0, h: 0}); 171 | }, 172 | 173 | _stop: function () { 174 | global.stage.disconnect(this._signalCapturedEvent); 175 | this._setDefaultCursor(); 176 | Main.uiGroup.remove_actor(this._container); 177 | Main.popModal(this._container); 178 | this._container.destroy(); 179 | this.emit("stop"); 180 | this.disconnectAll(); 181 | } 182 | }); 183 | 184 | Signals.addSignalMethods(Capture.prototype); 185 | 186 | 187 | 188 | 189 | 190 | const SelectionArea = new Lang.Class({ 191 | Name: "ImgurUploader.SelectionArea", 192 | 193 | _init: function () { 194 | this._mouseDown = false; 195 | this._capture = new Capture(); 196 | this._capture.connect('captured-event', this._onEvent.bind(this)); 197 | this._capture.connect('stop', this.emit.bind(this, 'stop')); 198 | }, 199 | 200 | _onEvent: function (capture, event) { 201 | let type = event.type(); 202 | let [x, y, mask] = global.get_pointer(); 203 | 204 | if (type === Clutter.EventType.BUTTON_PRESS) { 205 | [this._startX, this._startY] = [x, y]; 206 | this._mouseDown = true; 207 | } else if (this._mouseDown) { 208 | let rect = getRectangle(this._startX, this._startY, x, y); 209 | if (type === Clutter.EventType.MOTION) { 210 | this._capture.drawContainer(rect); 211 | } else if (type === Clutter.EventType.BUTTON_RELEASE) { 212 | this._capture._stop(); 213 | this._screenshot(rect); 214 | } 215 | } 216 | }, 217 | 218 | _screenshot: function (region) { 219 | let fileName = getTempFile(); 220 | 221 | if ((region.w > 8) && (region.h > 8)) { 222 | makeAreaScreenshot( 223 | region, 224 | this.emit.bind(this, 'screenshot') 225 | ); 226 | } else { 227 | this.emit( 228 | "error", 229 | _("selected region was too small - please select a larger area") 230 | ); 231 | 232 | this.emit("stop"); 233 | } 234 | } 235 | }); 236 | 237 | Signals.addSignalMethods(SelectionArea.prototype); 238 | 239 | 240 | 241 | 242 | 243 | const SelectionWindow = new Lang.Class({ 244 | Name: "ImgurUploader.SelectionWindow", 245 | 246 | _init: function () { 247 | this._windows = global.get_window_actors(); 248 | this._capture = new Capture(); 249 | this._capture.connect('captured-event', this._onEvent.bind(this)); 250 | this._capture.connect('stop', this.emit.bind(this, 'stop')); 251 | }, 252 | 253 | _onEvent: function (capture, event) { 254 | let type = event.type(); 255 | let [x, y, mask] = global.get_pointer(); 256 | 257 | this._selectedWindow = selectWindow(this._windows, x, y) 258 | 259 | if (this._selectedWindow) { 260 | this._highlightWindow(this._selectedWindow); 261 | } else { 262 | this._clearHighlight(); 263 | } 264 | 265 | if (type === Clutter.EventType.BUTTON_PRESS) { 266 | if (this._selectedWindow) { 267 | this._screenshot(this._selectedWindow); 268 | } 269 | } 270 | }, 271 | 272 | _highlightWindow: function (win) { 273 | this._capture.drawContainer(getWindowRectangle(win)); 274 | }, 275 | 276 | _clearHighlight: function () { 277 | this._capture.clearContainer(); 278 | }, 279 | 280 | _screenshot: function (win) { 281 | Mainloop.idle_add(function () { 282 | Main.activateWindow(win.get_meta_window()); 283 | 284 | Mainloop.idle_add(function () { 285 | makeWindowScreenshot(win, this.emit.bind(this, 'screenshot')); 286 | this._capture._stop(); 287 | }.bind(this)); 288 | }.bind(this)); 289 | } 290 | }); 291 | 292 | Signals.addSignalMethods(SelectionWindow.prototype); 293 | 294 | 295 | 296 | 297 | 298 | 299 | const SelectionDesktop = new Lang.Class({ 300 | Name: "ImgurUploader.SelectionDesktop", 301 | 302 | _init: function () { 303 | this._windows = global.get_window_actors(); 304 | Mainloop.idle_add(function () { 305 | makeDesktopScreenshot(this.emit.bind(this, 'screenshot')); 306 | this.emit('stop'); 307 | }.bind(this)); 308 | } 309 | }); 310 | 311 | Signals.addSignalMethods(SelectionDesktop.prototype); 312 | -------------------------------------------------------------------------------- /src/prefs.js: -------------------------------------------------------------------------------- 1 | /*jshint moz:true */ 2 | // vi: sts=2 sw=2 et 3 | // 4 | // accelerator setting based on 5 | // https://github.com/ambrice/spatialnavigation-tastycactus.com/blob/master/prefs.js 6 | 7 | const Lang = imports.lang; 8 | const Signals = imports.signals; 9 | 10 | const Gtk = imports.gi.Gtk; 11 | // const Gio = imports.gi.Gio; 12 | const GObject = imports.gi.GObject; 13 | 14 | const Gettext = imports.gettext.domain('gnome-shell-extensions'); 15 | const _ = Gettext.gettext; 16 | 17 | const Local = imports.misc.extensionUtils.getCurrentExtension(); 18 | const Config = Local.imports.config; 19 | const Convenience = Local.imports.convenience; 20 | 21 | 22 | 23 | let _settings; 24 | 25 | 26 | const buildHbox = function () { 27 | return new Gtk.Box({ 28 | orientation: Gtk.Orientation.HORIZONTAL, 29 | margin_top: 5, 30 | expand: false 31 | }); 32 | }; 33 | 34 | const ImgurSettingsWidget = new GObject.Class({ 35 | Name: 'ImgurSettingsWidget', 36 | GTypeName: 'ImgurSettingsWidget', 37 | Extends: Gtk.Box, 38 | 39 | _init: function (params) { 40 | this.parent(params); 41 | this._initLayout(); 42 | }, 43 | 44 | _initLayout: function () { 45 | this._notebook = new Gtk.Notebook(); 46 | 47 | let label; 48 | 49 | this._prefsIndicator = this._makePrefsIndicator(); 50 | label = new Gtk.Label({label: _("Indicator")}); 51 | this._notebook.append_page(this._prefsIndicator, label); 52 | 53 | this._prefsDefaultActions = this._makePrefsDefaultActions(); 54 | label = new Gtk.Label({label: _("Default Actions")}); 55 | this._notebook.append_page(this._prefsDefaultActions, label); 56 | 57 | this._prefsKeybindings = this._makePrefsKeybindings(); 58 | label = new Gtk.Label({label: _("Keybindings")}); 59 | this._notebook.append_page(this._prefsKeybindings, label); 60 | 61 | this.add(this._notebook); 62 | }, 63 | 64 | _makePrefsIndicator: function () { 65 | let prefs = new Gtk.Box({ 66 | orientation: Gtk.Orientation.VERTICAL, 67 | margin: 20, 68 | margin_top: 10, 69 | expand: false 70 | }); 71 | 72 | let hbox; 73 | 74 | /* Show indicator [on|off] */ 75 | 76 | hbox = buildHbox(); 77 | 78 | const labelShowIndicator = new Gtk.Label({ 79 | label: _('Show indicator'), 80 | xalign: 0, 81 | expand: true 82 | }); 83 | 84 | const switchShowIndicator = new Gtk.Switch(); 85 | 86 | switchShowIndicator.connect('notify::active', function (button) { 87 | _settings.set_boolean(Config.KeyEnableIndicator, button.active); 88 | }.bind(this)); 89 | 90 | switchShowIndicator.active = _settings.get_boolean( 91 | Config.KeyEnableIndicator 92 | ); 93 | 94 | hbox.add(labelShowIndicator); 95 | hbox.add(switchShowIndicator); 96 | 97 | prefs.add(hbox, {fill: false}); 98 | 99 | 100 | /* Default click action [dropdown] */ 101 | 102 | hbox = buildHbox(); 103 | 104 | const labelDefaultClickAction = new Gtk.Label({ 105 | label: _('Default click action'), 106 | xalign: 0, 107 | expand: true 108 | }); 109 | 110 | const clickActionOptions = [ 111 | [_("Select Area") , Config.ClickActions.SELECT_AREA], 112 | [_("Select Window") , Config.ClickActions.SELECT_WINDOW], 113 | [_("Select Desktop") , Config.ClickActions.SELECT_DESKTOP], 114 | [_("Show Menu") , Config.ClickActions.SHOW_MENU] 115 | ]; 116 | 117 | const currentClickAction = _settings.get_enum(Config.KeyClickAction); 118 | 119 | const comboBoxDefaultClickAction = this._getComboBox( 120 | clickActionOptions, GObject.TYPE_INT, currentClickAction, 121 | function (value) _settings.set_enum(Config.KeyClickAction, value) 122 | ); 123 | 124 | hbox.add(labelDefaultClickAction); 125 | hbox.add(comboBoxDefaultClickAction); 126 | 127 | prefs.add(hbox, {fill: false}); 128 | 129 | return prefs; 130 | }, 131 | 132 | _makePrefsDefaultActions: function () { 133 | let prefs = new Gtk.Box({ 134 | orientation: Gtk.Orientation.VERTICAL, 135 | margin: 20, 136 | margin_top: 10, 137 | expand: false 138 | }); 139 | 140 | /* Copy link to clipboard [on|off] */ 141 | 142 | hbox = buildHbox(); 143 | 144 | const labelCopyClipboard = new Gtk.Label({ 145 | label: _('Copy URL to clipboard'), 146 | xalign: 0, 147 | expand: true 148 | }); 149 | 150 | const switchCopyClipboard = new Gtk.Switch(); 151 | 152 | switchCopyClipboard.connect('notify::active', function (button) { 153 | _settings.set_boolean(Config.KeyCopyClipboard, button.active); 154 | }.bind(this)); 155 | 156 | switchCopyClipboard.active = _settings.get_boolean( 157 | Config.KeyCopyClipboard 158 | ); 159 | 160 | hbox.add(labelCopyClipboard); 161 | hbox.add(switchCopyClipboard); 162 | 163 | prefs.add(hbox, {fill: false}); 164 | 165 | /* Keep file [on|off] */ 166 | 167 | hbox = buildHbox(); 168 | 169 | const labelKeepFile = new Gtk.Label({ 170 | label: _('Keep Saved File'), 171 | xalign: 0, 172 | expand: true 173 | }); 174 | 175 | const switchKeepFile = new Gtk.Switch(); 176 | 177 | switchKeepFile.connect('notify::active', function (button) { 178 | _settings.set_boolean(Config.KeyKeepFile, button.active); 179 | }.bind(this)); 180 | 181 | switchKeepFile.active = _settings.get_boolean( 182 | Config.KeyKeepFile 183 | ); 184 | 185 | hbox.add(labelKeepFile); 186 | hbox.add(switchKeepFile); 187 | 188 | prefs.add(hbox, {fill: false}); 189 | 190 | 191 | /* Save Location [filechooser] */ 192 | 193 | hbox = buildHbox(); 194 | 195 | const labelSaveLocation = new Gtk.Label({ 196 | label: _('Save location'), 197 | xalign: 0, 198 | expand: true 199 | }); 200 | 201 | 202 | const chooserSaveLocation = new Gtk.FileChooserButton({ 203 | title: _("Select") 204 | }); 205 | chooserSaveLocation.set_action(Gtk.FileChooserAction.SELECT_FOLDER); 206 | 207 | chooserSaveLocation.set_filename(_settings.get_string('save-location')); 208 | chooserSaveLocation.connect('file-set', function() { 209 | _settings.set_string( 210 | 'save-location', 211 | chooserSaveLocation.get_current_folder() 212 | ); 213 | }.bind(this)); 214 | 215 | const _setSensitivity = function () { 216 | var sensitive = _settings.get_boolean(Config.KeyKeepFile); 217 | labelSaveLocation.set_sensitive(sensitive); 218 | chooserSaveLocation.set_sensitive(sensitive); 219 | }; 220 | 221 | switchKeepFile.connect('notify::active', _setSensitivity); 222 | _setSensitivity(); 223 | 224 | hbox.add(labelSaveLocation); 225 | hbox.add(chooserSaveLocation); 226 | 227 | prefs.add(hbox, {fill: false}); 228 | 229 | return prefs; 230 | }, 231 | 232 | _makePrefsKeybindings: function () { 233 | let model = new Gtk.ListStore(); 234 | 235 | model.set_column_types([ 236 | GObject.TYPE_STRING, 237 | GObject.TYPE_STRING, 238 | GObject.TYPE_INT, 239 | GObject.TYPE_INT 240 | ]); 241 | 242 | let bindings = [ 243 | ["shortcut-select-area", "Select area"], 244 | ["shortcut-select-window", "Select window"], 245 | ["shortcut-select-desktop", "Select whole desktop"] 246 | ]; 247 | 248 | for each (let [name, description] in bindings) { 249 | log("binding: " + name + " description: " + description); 250 | let binding = _settings.get_strv(name)[0]; 251 | 252 | let key, mods; 253 | 254 | if (binding) { 255 | [key, mods] = Gtk.accelerator_parse(binding); 256 | } else { 257 | [key, mods] = [0, 0]; 258 | } 259 | 260 | let row = model.append(); 261 | 262 | model.set(row, [0, 1, 2, 3], [name, description, mods, key]); 263 | } 264 | 265 | let treeview = new Gtk.TreeView({ 266 | 'expand': true, 267 | 'model': model 268 | }); 269 | 270 | let cellrend = new Gtk.CellRendererText(); 271 | let col = new Gtk.TreeViewColumn({ 272 | 'title': 'Keyboard Shortcut', 273 | 'expand': true 274 | }); 275 | 276 | col.pack_start(cellrend, true); 277 | col.add_attribute(cellrend, 'text', 1); 278 | treeview.append_column(col); 279 | 280 | cellrend = new Gtk.CellRendererAccel({ 281 | 'editable': true, 282 | 'accel-mode': Gtk.CellRendererAccelMode.GTK 283 | }); 284 | 285 | cellrend.connect('accel-edited', function(rend, iter, key, mods) { 286 | let value = Gtk.accelerator_name(key, mods); 287 | let [succ, iterator] = model.get_iter_from_string(iter); 288 | 289 | if (!succ) { 290 | throw new Error("Error updating keybinding"); 291 | } 292 | 293 | let name = model.get_value(iterator, 0); 294 | 295 | model.set(iterator, [2, 3], [mods, key]); 296 | _settings.set_strv(name, [value]); 297 | }); 298 | 299 | cellrend.connect('accel-cleared', function(rend, iter, key, mods) { 300 | let [succ, iterator] = model.get_iter_from_string(iter); 301 | 302 | if (!succ) { 303 | throw new Error("Error clearing keybinding"); 304 | } 305 | 306 | let name = model.get_value(iterator, 0); 307 | 308 | model.set(iterator, [2, 3], [0, 0]); 309 | _settings.set_strv(name, []); 310 | }); 311 | 312 | col = new Gtk.TreeViewColumn({'title': 'Modify', min_width: 200}); 313 | 314 | col.pack_end(cellrend, false); 315 | col.add_attribute(cellrend, 'accel-mods', 2); 316 | col.add_attribute(cellrend, 'accel-key', 3); 317 | treeview.append_column(col); 318 | 319 | return treeview; 320 | }, 321 | 322 | _getComboBox: function (options, valueType, defaultValue, callback) { 323 | let model = new Gtk.ListStore(); 324 | 325 | let Columns = { LABEL: 0, VALUE: 1 }; 326 | 327 | model.set_column_types([GObject.TYPE_STRING, valueType]); 328 | 329 | let comboBox = new Gtk.ComboBox({model: model}); 330 | let renderer = new Gtk.CellRendererText(); 331 | 332 | comboBox.pack_start(renderer, true); 333 | comboBox.add_attribute(renderer, 'text', 0); 334 | 335 | for each (let [label, value] in options) { 336 | let iter; 337 | 338 | model.set( 339 | iter = model.append(), 340 | [Columns.LABEL, Columns.VALUE], 341 | [label, value] 342 | ); 343 | 344 | if (value === defaultValue) { 345 | comboBox.set_active_iter(iter); 346 | } 347 | } 348 | 349 | comboBox.connect('changed', function (entry) { 350 | let [success, iter] = comboBox.get_active_iter(); 351 | 352 | if (!success) { 353 | return; 354 | } 355 | 356 | let value = model.get_value(iter, Columns.VALUE); 357 | 358 | callback(value); 359 | }); 360 | 361 | return comboBox; 362 | } 363 | }); 364 | 365 | function init() { 366 | _settings = Convenience.getSettings(); 367 | } 368 | 369 | function buildPrefsWidget() { 370 | let widget = new ImgurSettingsWidget(); 371 | widget.show_all(); 372 | 373 | return widget; 374 | } 375 | --------------------------------------------------------------------------------