Import upstream version 53
Debian Janitor
11 months ago
8 | 8 | |
9 | 9 | # Conform to https://gitlab.gnome.org/GNOME/gjs/-/raw/gnome-3-34/.eslintrc.yml |
10 | 10 | parserOptions: |
11 | ecmaVersion: 2017 | |
11 | ecmaVersion: 2019 |
47 | 47 | else |
48 | 48 | PromiseUtils._promisify(Gtk.IconInfo.prototype, 'load_symbolic_async', 'load_symbolic_finish'); |
49 | 49 | |
50 | const MAX_UPDATE_FREQUENCY = 100; // In ms | |
50 | const MAX_UPDATE_FREQUENCY = 30; // In ms | |
51 | const FALLBACK_ICON_NAME = 'image-loading-symbolic'; | |
52 | const PIXMAPS_FORMAT = imports.gi.Cogl.PixelFormat.ARGB_8888; | |
51 | 53 | |
52 | 54 | // eslint-disable-next-line no-unused-vars |
53 | 55 | const SNICategory = Object.freeze({ |
80 | 82 | }, |
81 | 83 | }); |
82 | 84 | |
83 | var AppIndicatorProxy = GObject.registerClass({ | |
84 | Signals: { 'destroy': {} }, | |
85 | }, class AppIndicatorProxy extends Gio.DBusProxy { | |
85 | var AppIndicatorProxy = GObject.registerClass( | |
86 | class AppIndicatorProxy extends Util.DBusProxy { | |
86 | 87 | static get interfaceInfo() { |
87 | 88 | if (!this._interfaceInfo) { |
88 | 89 | this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml( |
108 | 109 | return this._tupleType; |
109 | 110 | } |
110 | 111 | |
111 | static get TUPLE_VARIANT_TYPE() { | |
112 | if (!this._tupleVariantType) | |
113 | this._tupleVariantType = new GLib.VariantType('(v)'); | |
114 | ||
115 | return this._tupleVariantType; | |
116 | } | |
117 | ||
118 | 112 | static destroy() { |
119 | 113 | delete this._interfaceInfo; |
120 | delete this._tupleVariantType; | |
121 | 114 | delete this._tupleType; |
122 | 115 | } |
123 | 116 | |
124 | 117 | _init(busName, objectPath) { |
125 | 118 | const { interfaceInfo } = AppIndicatorProxy; |
126 | 119 | |
127 | super._init({ | |
128 | g_connection: Gio.DBus.session, | |
129 | g_interface_name: interfaceInfo.name, | |
130 | g_interface_info: interfaceInfo, | |
131 | g_name: busName, | |
132 | g_object_path: objectPath, | |
133 | g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES, | |
134 | }); | |
120 | super._init(busName, objectPath, interfaceInfo, | |
121 | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES); | |
135 | 122 | |
136 | 123 | this.set_cached_property('Status', |
137 | 124 | new GLib.Variant('s', SNIStatus.PASSIVE)); |
138 | 125 | |
139 | this._signalIds = []; | |
126 | ||
140 | 127 | this._accumulatedProperties = new Set(); |
141 | 128 | this._cancellables = new Map(); |
142 | 129 | this._changedProperties = Object.create(null); |
143 | ||
144 | this._signalIds.push(this.connect('g-signal', | |
145 | (_proxy, ...args) => this._onSignal(...args).catch(logError))); | |
146 | ||
147 | this._signalIds.push(this.connect('notify::g-name-owner', () => { | |
148 | this._resetNeededProperties(); | |
149 | if (!this.gNameOwner) | |
150 | this._cancelRefreshProperties(); | |
151 | else | |
152 | this._setupProxyPropertyList(); | |
153 | })); | |
154 | 130 | } |
155 | 131 | |
156 | 132 | async initAsync(cancellable) { |
157 | cancellable = new Util.CancellableChild(cancellable); | |
158 | await this.init_async(GLib.PRIORITY_DEFAULT, cancellable); | |
159 | this._cancellable = cancellable; | |
160 | ||
161 | this.gInterfaceInfo.methods.map(m => m.name).forEach(method => | |
162 | this._ensureAsyncMethod(method)); | |
133 | await super.initAsync(cancellable); | |
163 | 134 | |
164 | 135 | this._setupProxyPropertyList(); |
165 | 136 | } |
166 | 137 | |
167 | 138 | destroy() { |
168 | this.emit('destroy'); | |
169 | this._signalIds.forEach(id => this.disconnect(id)); | |
170 | ||
171 | 139 | const cachedProperties = this.get_cached_property_names(); |
172 | 140 | if (cachedProperties) { |
173 | 141 | cachedProperties.forEach(propertyName => |
174 | 142 | this.set_cached_property(propertyName, null)); |
175 | 143 | } |
176 | 144 | |
177 | if (this._cancellable) | |
178 | this._cancellable.cancel(); | |
179 | ||
180 | this._cancellables.clear(); | |
145 | super.destroy(); | |
146 | } | |
147 | ||
148 | _onNameOwnerChanged() { | |
149 | this._resetNeededProperties(); | |
150 | ||
151 | if (!this.gNameOwner) | |
152 | this._cancelRefreshProperties(); | |
153 | else | |
154 | this._setupProxyPropertyList(); | |
181 | 155 | } |
182 | 156 | |
183 | 157 | _setupProxyPropertyList() { |
235 | 209 | })); |
236 | 210 | } |
237 | 211 | |
238 | async _onSignal(_sender, signal, params) { | |
212 | _onSignal(...args) { | |
213 | this._onSignalAsync(...args).catch(e => { | |
214 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
215 | logError(e); | |
216 | }); | |
217 | } | |
218 | ||
219 | async _onSignalAsync(_sender, signal, params) { | |
239 | 220 | const property = this._signalToPropertyName(signal); |
240 | 221 | if (!property) |
241 | 222 | return; |
266 | 247 | return; |
267 | 248 | |
268 | 249 | this._signalsAccumulator = new PromiseUtils.TimeoutPromise( |
269 | GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, this._cancellable); | |
250 | MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, this._cancellable); | |
270 | 251 | try { |
271 | 252 | await this._signalsAccumulator; |
272 | 253 | const refreshPropertiesPromises = |
285 | 266 | _resetNeededProperties() { |
286 | 267 | AppIndicator.NEEDED_PROPERTIES.forEach(p => |
287 | 268 | this.set_cached_property(p, null)); |
288 | } | |
289 | ||
290 | // This can be removed when we will have GNOME 43 as minimum version | |
291 | _ensureAsyncMethod(method) { | |
292 | if (this[`${method}Async`]) | |
293 | return; | |
294 | ||
295 | if (!this[`${method}Remote`]) | |
296 | throw new Error(`Missing remote method '${method}'`); | |
297 | ||
298 | this[`${method}Async`] = function (...args) { | |
299 | return new Promise((resolve, reject) => { | |
300 | this[`${method}Remote`](...args, (ret, e) => { | |
301 | if (e) | |
302 | reject(e); | |
303 | else | |
304 | resolve(ret); | |
305 | }); | |
306 | }); | |
307 | }; | |
308 | } | |
309 | ||
310 | getProperty(propertyName, cancellable) { | |
311 | return this.g_connection.call(this.g_name, | |
312 | this.g_object_path, 'org.freedesktop.DBus.Properties', 'Get', | |
313 | GLib.Variant.new('(ss)', [this.g_interface_name, propertyName]), | |
314 | AppIndicatorProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1, | |
315 | cancellable); | |
316 | } | |
317 | ||
318 | getProperties(cancellable) { | |
319 | return this.g_connection.call(this.g_name, | |
320 | this.g_object_path, 'org.freedesktop.DBus.Properties', 'GetAll', | |
321 | GLib.Variant.new('(s)', [this.g_interface_name]), | |
322 | GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1, | |
323 | cancellable); | |
324 | 269 | } |
325 | 270 | |
326 | 271 | async refreshAllProperties() { |
411 | 356 | addNew: true, |
412 | 357 | }); |
413 | 358 | } |
414 | this._propertiesEmitTimeout = new PromiseUtils.TimeoutPromise(16, | |
415 | GLib.PRIORITY_DEFAULT_IDLE, params.cancellable); | |
359 | this._propertiesEmitTimeout = new PromiseUtils.TimeoutPromise( | |
360 | MAX_UPDATE_FREQUENCY * 2, GLib.PRIORITY_DEFAULT_IDLE, params.cancellable); | |
416 | 361 | await this._propertiesEmitTimeout; |
417 | 362 | |
418 | 363 | if (Object.keys(this._changedProperties).length) { |
643 | 588 | }; |
644 | 589 | } |
645 | 590 | |
591 | get hasOverlayIcon() { | |
592 | const { name, pixmap } = this.overlayIcon; | |
593 | ||
594 | return name || (pixmap && pixmap.n_children()); | |
595 | } | |
596 | ||
646 | 597 | get hasNameOwner() { |
647 | 598 | if (this._nameWatcher && !this._nameWatcher.nameOnBus) |
648 | 599 | return false; |
897 | 848 | }; |
898 | 849 | Signals.addSignalMethods(AppIndicator.prototype); |
899 | 850 | |
900 | let StTextureCacheSkippingGIcon; | |
851 | let StTextureCacheSkippingFileIcon; | |
901 | 852 | |
902 | 853 | if (imports.system.version >= 17501) { |
903 | 854 | try { |
904 | StTextureCacheSkippingGIcon = GObject.registerClass({ | |
855 | StTextureCacheSkippingFileIcon = GObject.registerClass({ | |
905 | 856 | Implements: [Gio.Icon], |
906 | }, class StTextureCacheSkippingGIconClass extends Gio.EmblemedIcon { | |
857 | }, class StTextureCacheSkippingFileIconImpl extends Gio.EmblemedIcon { | |
858 | _init(params) { | |
859 | // FIXME: We can't just inherit from Gio.FileIcon for some reason | |
860 | super._init({ gicon: new Gio.FileIcon(params) }); | |
861 | } | |
862 | ||
907 | 863 | vfunc_to_tokens() { |
908 | 864 | // Disables the to_tokens() vfunc so that the icon to_string() |
909 | 865 | // method won't work and thus can't be kept forever around by |
917 | 873 | } catch (e) {} |
918 | 874 | } |
919 | 875 | |
920 | var IconActor = GObject.registerClass({ | |
921 | Signals: { | |
922 | 'requires-custom-image': {}, | |
923 | }, | |
924 | }, | |
876 | var IconActor = GObject.registerClass( | |
925 | 877 | class AppIndicatorsIconActor extends St.Icon { |
878 | ||
879 | static get DEFAULT_STYLE() { | |
880 | return 'padding: 0'; | |
881 | } | |
882 | ||
883 | static get USER_WRITABLE_PATHS() { | |
884 | if (!this._userWritablePaths) { | |
885 | this._userWritablePaths = [ | |
886 | GLib.get_user_cache_dir(), | |
887 | GLib.get_user_data_dir(), | |
888 | GLib.get_user_config_dir(), | |
889 | GLib.get_user_runtime_dir(), | |
890 | GLib.get_home_dir(), | |
891 | GLib.get_tmp_dir(), | |
892 | ]; | |
893 | ||
894 | this._userWritablePaths.push(Object.values(GLib.UserDirectory).slice( | |
895 | 0, -1).map(dirId => GLib.get_user_special_dir(dirId))); | |
896 | } | |
897 | ||
898 | return this._userWritablePaths; | |
899 | } | |
926 | 900 | |
927 | 901 | _init(indicator, iconSize) { |
928 | 902 | super._init({ |
929 | 903 | reactive: true, |
930 | 904 | style_class: 'system-status-icon', |
931 | fallback_icon_name: 'image-loading-symbolic', | |
905 | fallbackIconName: FALLBACK_ICON_NAME, | |
932 | 906 | }); |
933 | 907 | |
934 | 908 | this.name = this.constructor.name; |
935 | 909 | this.add_style_class_name('appindicator-icon'); |
936 | 910 | this.add_style_class_name('status-notifier-icon'); |
937 | this.set_style('padding:0'); | |
911 | this.set_style(AppIndicatorsIconActor.DEFAULT_STYLE); | |
938 | 912 | |
939 | 913 | let themeContext = St.ThemeContext.get_for_stage(global.stage); |
940 | 914 | this.height = iconSize * themeContext.scale_factor; |
948 | 922 | |
949 | 923 | Object.values(SNIconType).forEach(t => (this._loadingIcons[t] = new Map())); |
950 | 924 | |
951 | Util.connectSmart(this._indicator, 'icon', this, () => this._updateIcon().catch(logError)); | |
952 | Util.connectSmart(this._indicator, 'overlay-icon', this, this._updateOverlayIcon); | |
953 | Util.connectSmart(this._indicator, 'reset', this, () => this._invalidateIcon()); | |
925 | Util.connectSmart(this._indicator, 'icon', this, () => { | |
926 | if (this.is_mapped()) | |
927 | this._updateIcon(); | |
928 | }); | |
929 | Util.connectSmart(this._indicator, 'overlay-icon', this, () => { | |
930 | if (this.is_mapped()) | |
931 | this._updateIcon(); | |
932 | }); | |
933 | Util.connectSmart(this._indicator, 'reset', this, | |
934 | () => this._invalidateIconWhenFullyReady()); | |
954 | 935 | |
955 | 936 | const settings = SettingsManager.getDefaultGSettings(); |
956 | Util.connectSmart(settings, 'changed::icon-size', this, () => this._invalidateIcon()); | |
937 | Util.connectSmart(settings, 'changed::icon-size', this, () => | |
938 | this._updateWhenFullyReady()); | |
957 | 939 | Util.connectSmart(settings, 'changed::custom-icons', this, () => { |
958 | 940 | this._updateCustomIcons(); |
959 | this._invalidateIcon(); | |
941 | this._invalidateIconWhenFullyReady(); | |
960 | 942 | }); |
961 | 943 | |
962 | 944 | if (GObject.signal_lookup('resource-scale-changed', this)) |
965 | 947 | this.connect('notify::resource-scale', () => this._invalidateIcon()); |
966 | 948 | |
967 | 949 | Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => { |
968 | this.height = iconSize * tc.scale_factor; | |
950 | this._updateIconSize(); | |
951 | this.height = this._iconSize * tc.scale_factor; | |
952 | this.width = -1; | |
969 | 953 | this._invalidateIcon(); |
970 | 954 | }); |
971 | 955 | |
972 | Util.connectSmart(this._indicator, 'ready', this, () => { | |
973 | this._updateIconClass(); | |
974 | this._updateCustomIcons(); | |
975 | this._invalidateIcon(); | |
976 | }); | |
977 | ||
978 | Util.connectSmart(Util.getDefaultTheme(), 'changed', this, this._invalidateIcon); | |
979 | ||
980 | if (indicator.isReady) { | |
981 | this._updateCustomIcons(); | |
982 | ||
983 | if (this.get_stage()) { | |
984 | this._invalidateIcon(); | |
985 | } else { | |
986 | const id = this.connect('parent-set', () => { | |
987 | if (this.get_stage()) { | |
988 | this.disconnect(id); | |
989 | this._invalidateIcon(); | |
990 | } | |
991 | }); | |
992 | } | |
993 | } | |
956 | Util.connectSmart(Util.getDefaultTheme(), 'changed', this, | |
957 | () => this._invalidateIconWhenFullyReady()); | |
958 | ||
959 | this.connect('notify::mapped', () => { | |
960 | if (!this.is_mapped()) | |
961 | this._updateWhenFullyReady(); | |
962 | }); | |
963 | ||
964 | this._updateWhenFullyReady(); | |
994 | 965 | |
995 | 966 | this.connect('destroy', () => { |
996 | 967 | this._iconCache.destroy(); |
998 | 969 | this._cancellable = null; |
999 | 970 | this._indicator = null; |
1000 | 971 | this._loadingIcons = null; |
972 | this._iconTheme = null; | |
1001 | 973 | }); |
1002 | 974 | } |
1003 | 975 | |
1004 | 976 | get debugId() { |
1005 | 977 | return this._indicator ? this._indicator.id : this.toString(); |
978 | } | |
979 | ||
980 | async _waitForFullyReady() { | |
981 | const waitConditions = []; | |
982 | ||
983 | if (!this.is_mapped()) { | |
984 | waitConditions.push(new PromiseUtils.SignalConnectionPromise( | |
985 | this, 'notify::mapped', this._cancellable)); | |
986 | } | |
987 | ||
988 | if (!this._indicator.isReady) { | |
989 | waitConditions.push(new PromiseUtils.SignalConnectionPromise( | |
990 | this._indicator, 'ready', this._cancellable)); | |
991 | } | |
992 | ||
993 | if (!waitConditions.length) | |
994 | return true; | |
995 | ||
996 | await Promise.all(waitConditions); | |
997 | return this._waitForFullyReady(); | |
998 | } | |
999 | ||
1000 | async _updateWhenFullyReady() { | |
1001 | if (this._waitingReady) | |
1002 | return; | |
1003 | ||
1004 | try { | |
1005 | this._waitingReady = true; | |
1006 | await this._waitForFullyReady(); | |
1007 | ||
1008 | this._updateIconSize(); | |
1009 | this._updateIconClass(); | |
1010 | this._updateCustomIcons(); | |
1011 | this._invalidateIcon(); | |
1012 | } catch (e) { | |
1013 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
1014 | logError(e); | |
1015 | } finally { | |
1016 | delete this._waitingReady; | |
1017 | } | |
1006 | 1018 | } |
1007 | 1019 | |
1008 | 1020 | _updateIconClass() { |
1067 | 1079 | if (gicon) |
1068 | 1080 | return gicon; |
1069 | 1081 | |
1070 | const iconInfo = this._getIconInfo(iconName, themePath, iconSize, iconScaling); | |
1071 | const loadingId = iconInfo.path || id; | |
1082 | const iconData = this._getIconData(iconName, themePath, iconSize, iconScaling); | |
1083 | const loadingId = iconData.file ? iconData.file.get_path() : id; | |
1072 | 1084 | |
1073 | 1085 | const cancellable = await this._getIconLoadingCancellable(iconType, id); |
1074 | 1086 | try { |
1075 | gicon = await this._createIconByIconData(iconInfo, iconSize, | |
1087 | gicon = await this._createIconByIconData(iconData, iconSize, | |
1076 | 1088 | iconScaling, cancellable); |
1077 | 1089 | } finally { |
1078 | 1090 | this._cleanupIconLoadingCancellable(iconType, loadingId); |
1083 | 1095 | } |
1084 | 1096 | |
1085 | 1097 | _getIconLookupFlags(themeNode) { |
1086 | // FIXME: Use St version if available (>= 44) | |
1087 | 1098 | let lookupFlags = 0; |
1088 | 1099 | |
1089 | 1100 | if (!themeNode) |
1104 | 1115 | return lookupFlags; |
1105 | 1116 | } |
1106 | 1117 | |
1107 | _getIconLoadingColors(themeNode) { | |
1118 | _getIconLoadingColors() { | |
1119 | const themeNode = this.get_theme_node(); | |
1120 | ||
1108 | 1121 | if (!themeNode) |
1109 | 1122 | return null; |
1110 | 1123 | |
1144 | 1157 | } |
1145 | 1158 | } |
1146 | 1159 | |
1147 | async _createIconByIconInfo(iconInfo, iconSize, iconScaling, cancellable) { | |
1148 | const themeNode = this.get_theme_node(); | |
1149 | const iconColors = this._getIconLoadingColors(themeNode); | |
1150 | ||
1160 | async _createIconByIconInfo(iconInfo, iconSize, iconScaling, iconColors, cancellable) { | |
1151 | 1161 | if (iconColors) { |
1152 | 1162 | const args = St.IconInfo && iconInfo instanceof St.IconInfo |
1153 | 1163 | ? [iconColors] : [iconColors.foreground, iconColors.success, |
1161 | 1171 | iconSize, iconScaling, cancellable); |
1162 | 1172 | } |
1163 | 1173 | |
1164 | async _createIconByIconData({ iconInfo, path }, iconSize, iconScaling, cancellable) { | |
1165 | if (!path && !iconInfo) { | |
1174 | async _createIconByIconData(iconData, iconSize, iconScaling, cancellable) { | |
1175 | const { file, iconInfo, name } = iconData; | |
1176 | ||
1177 | if (!file && !name) { | |
1166 | 1178 | if (this._createIconIdle) { |
1167 | 1179 | throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, |
1168 | 1180 | 'Already in progress'); |
1179 | 1191 | } finally { |
1180 | 1192 | delete this._createIconIdle; |
1181 | 1193 | } |
1182 | return null; | |
1194 | return this.gicon; | |
1183 | 1195 | } else if (this._createIconIdle) { |
1184 | 1196 | this._createIconIdle.cancel(); |
1185 | 1197 | delete this._createIconIdle; |
1186 | 1198 | } |
1187 | 1199 | |
1200 | if (name) | |
1201 | return new Gio.ThemedIcon({ name }); | |
1202 | ||
1203 | if (!file) | |
1204 | throw new Error('Neither file or name are set'); | |
1205 | ||
1206 | if (!this._isFileInWritableArea(file)) | |
1207 | return new Gio.FileIcon({ file }); | |
1208 | ||
1188 | 1209 | try { |
1189 | 1210 | const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async( |
1190 | path, cancellable); | |
1211 | file.get_path(), cancellable); | |
1191 | 1212 | |
1192 | 1213 | if (!format) { |
1193 | Util.Logger.critical(`${this.debugId}, Invalid image format: ${path}`); | |
1214 | Util.Logger.critical(`${this.debugId}, Invalid image format: ${file.get_path()}`); | |
1194 | 1215 | return null; |
1195 | 1216 | } |
1196 | 1217 | |
1197 | 1218 | if (width >= height * 1.5) { |
1198 | 1219 | /* Hello indicator-multiload! */ |
1199 | await this._loadCustomImage(Gio.File.new_for_path(path), | |
1200 | width, height, cancellable); | |
1220 | await this._loadCustomImage(file, | |
1221 | width, height, iconSize, iconScaling, cancellable); | |
1201 | 1222 | return null; |
1202 | } else if (StTextureCacheSkippingGIcon) { | |
1223 | } else if (StTextureCacheSkippingFileIcon) { | |
1203 | 1224 | /* We'll wrap the icon so that it won't be cached forever by the shell */ |
1204 | return new Gio.FileIcon({ file: Gio.File.new_for_path(path) }); | |
1225 | return new StTextureCacheSkippingFileIcon({ file }); | |
1205 | 1226 | } else if (iconInfo) { |
1206 | 1227 | return this._createIconByIconInfo(iconInfo, iconSize, |
1207 | iconScaling, cancellable); | |
1208 | } else { | |
1209 | return this._createIconByFile(Gio.File.new_for_path(path), | |
1210 | iconSize, iconScaling, cancellable); | |
1211 | } | |
1228 | iconScaling, this._getIconLoadingColors(), cancellable); | |
1229 | } else if (format.name === 'svg') { | |
1230 | const iconColors = this._getIconLoadingColors(); | |
1231 | ||
1232 | if (iconColors) { | |
1233 | const fileIcon = new Gio.FileIcon({ file }); | |
1234 | const iconTheme = this._iconTheme || this._createIconTheme(); | |
1235 | const fileIconInfo = iconTheme.lookup_by_gicon_for_scale( | |
1236 | fileIcon, iconSize, iconScaling, | |
1237 | this._getIconLookupFlags(this.get_theme_node())); | |
1238 | ||
1239 | if (fileIconInfo) { | |
1240 | return this._createIconByIconInfo(fileIconInfo, | |
1241 | iconSize, iconScaling, iconColors, cancellable); | |
1242 | } | |
1243 | } | |
1244 | } | |
1245 | ||
1246 | return this._createIconByFile(file, | |
1247 | iconSize, iconScaling, cancellable); | |
1212 | 1248 | } catch (e) { |
1213 | 1249 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { |
1214 | 1250 | Util.Logger.warn( |
1215 | `${this.debugId}, Impossible to read image info from path '${path}': ${e}`); | |
1251 | `${this.debugId}, Impossible to read image info from ` + | |
1252 | `path '${file ? file.get_path() : null}' or name '${name}': ${e}`); | |
1216 | 1253 | } |
1217 | 1254 | throw e; |
1218 | 1255 | } |
1219 | 1256 | } |
1220 | 1257 | |
1221 | async _loadCustomImage(file, width, height, cancellable) { | |
1222 | if (!(this instanceof CustomImageIconActor)) { | |
1223 | this.emit('requires-custom-image'); | |
1224 | throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, | |
1225 | 'Loading cancelled, need specific class'); | |
1226 | } | |
1227 | ||
1228 | const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); | |
1258 | async _loadCustomImage(file, width, height, iconSize, iconScaling, cancellable) { | |
1229 | 1259 | const textureCache = St.TextureCache.get_default(); |
1230 | const resourceScale = this._getResourceScale(); | |
1231 | ||
1232 | 1260 | const customImage = textureCache.load_file_async(file, -1, |
1233 | height, scaleFactor, resourceScale); | |
1234 | ||
1235 | customImage.set({ | |
1236 | xAlign: imports.gi.Clutter.ActorAlign.CENTER, | |
1237 | yAlign: imports.gi.Clutter.ActorAlign.CENTER, | |
1238 | }); | |
1261 | height, 1, iconScaling); | |
1262 | ||
1263 | const setCustomImageActor = imageActor => { | |
1264 | const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); | |
1265 | const { content } = imageActor; | |
1266 | imageActor.content = null; | |
1267 | imageActor.destroy(); | |
1268 | ||
1269 | this._setImageContent(content, | |
1270 | width * scaleFactor, height * scaleFactor); | |
1271 | }; | |
1239 | 1272 | |
1240 | 1273 | if (customImage.content) { |
1241 | this._setCustomImage(customImage, width, height); | |
1274 | setCustomImageActor(customImage); | |
1242 | 1275 | return; |
1243 | 1276 | } |
1244 | 1277 | |
1252 | 1285 | try { |
1253 | 1286 | await Promise.race(racingPromises); |
1254 | 1287 | if (!waitPromise.resolved()) |
1255 | this._setCustomImage(customImage, width, height); | |
1288 | setCustomImageActor(customImage); | |
1256 | 1289 | } catch (e) { |
1257 | 1290 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) |
1258 | 1291 | throw e; |
1259 | 1292 | } finally { |
1260 | 1293 | racingPromises.forEach(p => p.cancel()); |
1261 | ||
1262 | if (this._customImage !== customImage) | |
1263 | customImage.destroy(); | |
1264 | } | |
1265 | } | |
1266 | ||
1267 | _setCustomImage(imageActor, width, height) { | |
1268 | if (this._customImage) | |
1269 | this._customImage.destroy(); | |
1270 | ||
1271 | this._customImage = imageActor; | |
1272 | this.add_child(this._customImage); | |
1273 | this.width = width; | |
1274 | this.height = height; | |
1275 | } | |
1276 | ||
1277 | _getIconInfo(name, themePath, size, scale) { | |
1278 | if (name && name[0] === '/') { | |
1279 | // HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it. | |
1280 | return { iconInfo: null, path: name }; | |
1281 | } else if (name) { | |
1282 | // we manually look up the icon instead of letting st.icon do it for us | |
1283 | // this allows us to sneak in an indicator provided search path and to avoid ugly upscaled icons | |
1284 | ||
1285 | // indicator-application looks up a special "panel" variant, we just replicate that here | |
1286 | name += '-panel'; | |
1287 | ||
1288 | // icon info as returned by the lookup | |
1289 | let iconInfo = null; | |
1290 | ||
1291 | // we try to avoid messing with the default icon theme, so we'll create a new one if needed | |
1292 | let iconTheme = null; | |
1293 | const defaultTheme = Util.getDefaultTheme(); | |
1294 | if (themePath) { | |
1295 | iconTheme = St.IconTheme ? new St.IconTheme() : new Gtk.IconTheme(); | |
1296 | defaultTheme.get_search_path().forEach(p => | |
1297 | iconTheme.append_search_path(p)); | |
1298 | iconTheme.append_search_path(themePath); | |
1299 | ||
1300 | if (!Meta.is_wayland_compositor() && !St.IconTheme) { | |
1301 | const defaultScreen = imports.gi.Gdk.Screen.get_default(); | |
1302 | if (defaultScreen) | |
1303 | iconTheme.set_screen(defaultScreen); | |
1304 | } | |
1294 | } | |
1295 | } | |
1296 | ||
1297 | _isFileInWritableArea(file) { | |
1298 | // No need to use IO here, we can just do some assumptions | |
1299 | // print('Writable paths', IconActor.USER_WRITABLE_PATHS) | |
1300 | const path = file.get_path(); | |
1301 | return IconActor.USER_WRITABLE_PATHS.some(writablePath => | |
1302 | path.startsWith(writablePath)); | |
1303 | } | |
1304 | ||
1305 | _createIconTheme(searchPath = []) { | |
1306 | if (St.IconTheme) { | |
1307 | const iconTheme = new St.IconTheme(); | |
1308 | iconTheme.set_search_path(searchPath); | |
1309 | ||
1310 | return iconTheme; | |
1311 | } | |
1312 | ||
1313 | const iconTheme = new Gtk.IconTheme(); | |
1314 | iconTheme.set_search_path(searchPath); | |
1315 | ||
1316 | if (!Meta.is_wayland_compositor()) { | |
1317 | const defaultScreen = Gdk.Screen.get_default(); | |
1318 | if (defaultScreen) | |
1319 | iconTheme.set_screen(defaultScreen); | |
1320 | } | |
1321 | ||
1322 | return iconTheme; | |
1323 | } | |
1324 | ||
1325 | _getIconData(name, themePath, size, scale) { | |
1326 | const emptyIconData = { iconInfo: null, file: null, name: null }; | |
1327 | ||
1328 | if (!name) { | |
1329 | delete this._iconTheme; | |
1330 | return emptyIconData; | |
1331 | } | |
1332 | ||
1333 | // HACK: icon is a path name. This is not specified by the API, | |
1334 | // but at least indicator-sensors uses it. | |
1335 | if (name[0] === '/') { | |
1336 | delete this._iconTheme; | |
1337 | ||
1338 | const file = Gio.File.new_for_path(name); | |
1339 | return { file, iconInfo: null, name: null }; | |
1340 | } | |
1341 | ||
1342 | if (name.includes('.')) { | |
1343 | const splits = name.split('.'); | |
1344 | ||
1345 | if (['svg', 'png'].includes(splits[splits.length - 1])) | |
1346 | name = splits.slice(0, -1).join(''); | |
1347 | } | |
1348 | ||
1349 | if (themePath && Util.getDefaultTheme().get_search_path().includes(themePath)) | |
1350 | themePath = null; | |
1351 | ||
1352 | if (themePath) { | |
1353 | // If a theme path is provided, we need to lookup the icon ourself | |
1354 | // as St won't be able to do it unless we mess with default theme | |
1355 | // that is something we prefer not to do, as it would imply lots of | |
1356 | // St.TextureCache cleanups. | |
1357 | ||
1358 | const newSearchPath = [themePath]; | |
1359 | if (!this._iconTheme) { | |
1360 | this._iconTheme = this._createIconTheme(newSearchPath); | |
1305 | 1361 | } else { |
1306 | iconTheme = defaultTheme; | |
1307 | } | |
1308 | if (iconTheme) { | |
1309 | // try to look up the icon in the icon theme | |
1310 | iconInfo = iconTheme.lookup_icon_for_scale(name, size, scale, | |
1311 | this._getIconLookupFlags(this.get_theme_node()) | | |
1312 | (St.IconLookupFlags | |
1313 | ? St.IconLookupFlags.GENERIC_FALLBACK | |
1314 | : Gtk.IconLookupFlags.GENERIC_FALLBACK)); | |
1315 | // no icon? that's bad! | |
1316 | if (iconInfo === null) { | |
1317 | const msg = `${this.debugId}, Impossible to lookup icon for '${name}' in`; | |
1318 | Util.Logger.warn(`${msg} ${themePath ? `path ${themePath}` : 'default theme'}`); | |
1319 | } else { // we have an icon | |
1320 | // get the icon path | |
1321 | return { iconInfo, path: iconInfo.get_filename() }; | |
1322 | } | |
1323 | } | |
1324 | } | |
1325 | return { iconInfo: null, path: null }; | |
1326 | } | |
1327 | ||
1328 | async _argbToRgba(src, cancellable) { | |
1329 | await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable); | |
1330 | ||
1331 | return PixmapsUtils.argbToRgba(src); | |
1332 | } | |
1333 | ||
1334 | async _createIconFromPixmap(iconType, iconSize, iconScaling, pixmapsVariant) { | |
1335 | iconSize *= iconScaling; | |
1336 | ||
1362 | const currentSearchPath = this._iconTheme.get_search_path(); | |
1363 | ||
1364 | if (!currentSearchPath.includes(newSearchPath)) | |
1365 | this._iconTheme.set_search_path(newSearchPath); | |
1366 | } | |
1367 | ||
1368 | // try to look up the icon in the icon theme | |
1369 | const iconInfo = this._iconTheme.lookup_icon_for_scale(`${name}`, | |
1370 | size, scale, this._getIconLookupFlags(this.get_theme_node()) | | |
1371 | (St.IconLookupFlags | |
1372 | ? St.IconLookupFlags.GENERIC_FALLBACK | |
1373 | : Gtk.IconLookupFlags.GENERIC_FALLBACK)); | |
1374 | ||
1375 | if (iconInfo) { | |
1376 | return { | |
1377 | iconInfo, | |
1378 | file: Gio.File.new_for_path(iconInfo.get_filename()), | |
1379 | name: null, | |
1380 | }; | |
1381 | } | |
1382 | ||
1383 | const logger = this.gicon ? Util.Logger.debug : Util.Logger.warn; | |
1384 | logger(`${this.debugId}, Impossible to lookup icon ` + | |
1385 | `for '${name}' in ${themePath}`); | |
1386 | ||
1387 | return emptyIconData; | |
1388 | } | |
1389 | ||
1390 | delete this._iconTheme; | |
1391 | return { name, iconInfo: null, file: null }; | |
1392 | } | |
1393 | ||
1394 | _setImageContent(content, width, height) { | |
1395 | this.set({ | |
1396 | content, | |
1397 | width, | |
1398 | height, | |
1399 | contentGravity: Clutter.ContentGravity.RESIZE_ASPECT, | |
1400 | fallbackIconName: null, | |
1401 | }); | |
1402 | } | |
1403 | ||
1404 | async _createIconFromPixmap(iconType, iconSize, iconScaling, scaleFactor, pixmapsVariant) { | |
1337 | 1405 | const { pixmapVariant, width, height, rowStride } = |
1338 | PixmapsUtils.getBestPixmap(pixmapsVariant, iconSize); | |
1406 | PixmapsUtils.getBestPixmap(pixmapsVariant, iconSize * iconScaling); | |
1339 | 1407 | |
1340 | 1408 | const id = `__PIXMAP_ICON_${width}x${height}`; |
1341 | 1409 | |
1410 | const imageContent = new St.ImageContent({ | |
1411 | preferredWidth: width, | |
1412 | preferredHeight: height, | |
1413 | }); | |
1414 | ||
1415 | imageContent.set_bytes(pixmapVariant.get_data_as_bytes(), PIXMAPS_FORMAT, | |
1416 | width, height, rowStride); | |
1417 | ||
1418 | if (iconType !== SNIconType.OVERLAY && !this._indicator.hasOverlayIcon) { | |
1419 | const scaledSize = iconSize * scaleFactor; | |
1420 | this._setImageContent(imageContent, scaledSize, scaledSize); | |
1421 | return null; | |
1422 | } | |
1423 | ||
1342 | 1424 | const cancellable = this._getIconLoadingCancellable(iconType, id); |
1343 | 1425 | try { |
1344 | return GdkPixbuf.Pixbuf.new_from_bytes( | |
1345 | await this._argbToRgba(pixmapVariant.deep_unpack(), cancellable), | |
1346 | GdkPixbuf.Colorspace.RGB, true, | |
1347 | 8, width, height, rowStride); | |
1426 | // FIXME: async API results in a gray icon for some reason | |
1427 | const [inputStream] = imageContent.load(iconSize, cancellable); | |
1428 | return await GdkPixbuf.Pixbuf.new_from_stream_at_scale_async( | |
1429 | inputStream, -1, iconSize * iconScaling, true, cancellable); | |
1348 | 1430 | } catch (e) { |
1349 | 1431 | // the image data was probably bogus. We don't really know why, but it _does_ happen. |
1350 | 1432 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) |
1359 | 1441 | // the cached one (as in some cases it may be equal, but not the same object). |
1360 | 1442 | // So when it's not need anymore we make sure to check the active state |
1361 | 1443 | // and set it to false so that it can be picked up by the garbage collector. |
1362 | _setGicon(iconType, gicon, iconSize) { | |
1444 | _setGicon(iconType, gicon) { | |
1363 | 1445 | if (iconType !== SNIconType.OVERLAY) { |
1364 | 1446 | if (gicon) { |
1365 | const isPixbuf = gicon instanceof GdkPixbuf.Pixbuf; | |
1366 | this.gicon = StTextureCacheSkippingGIcon && !isPixbuf | |
1367 | ? new StTextureCacheSkippingGIcon({ gicon }) | |
1368 | : new Gio.EmblemedIcon({ gicon }); | |
1447 | if (this.gicon === gicon || | |
1448 | (this.gicon && this.gicon.get_icon() === gicon)) | |
1449 | return; | |
1450 | ||
1451 | if (gicon instanceof Gio.EmblemedIcon) | |
1452 | this.gicon = gicon; | |
1453 | else | |
1454 | this.gicon = new Gio.EmblemedIcon({ gicon }); | |
1369 | 1455 | |
1370 | 1456 | this._iconCache.updateActive(SNIconType.NORMAL, gicon, |
1371 | 1457 | this.gicon.get_icon() === gicon); |
1372 | ||
1373 | this.set_icon_size(iconSize); | |
1374 | 1458 | } else { |
1375 | 1459 | this.gicon = null; |
1376 | 1460 | } |
1430 | 1514 | e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) |
1431 | 1515 | return null; |
1432 | 1516 | |
1433 | ||
1434 | if (iconType === SNIconType.OVERLAY) | |
1517 | if (iconType === SNIconType.OVERLAY) { | |
1435 | 1518 | logError(e, `${this.debugId} unable to update icon emblem`); |
1436 | else | |
1519 | } else { | |
1520 | this.fallbackIconName = FALLBACK_ICON_NAME; | |
1437 | 1521 | logError(e, `${this.debugId} unable to update icon`); |
1438 | } | |
1439 | ||
1440 | try { | |
1441 | this._setGicon(iconType, gicon, iconSize); | |
1522 | } | |
1523 | } | |
1524 | ||
1525 | try { | |
1526 | this._setGicon(iconType, gicon); | |
1442 | 1527 | |
1443 | 1528 | if (pixmap && this.gicon) { |
1444 | 1529 | // The pixmap has been saved, we can free the variants memory |
1469 | 1554 | return gicon; |
1470 | 1555 | } |
1471 | 1556 | |
1472 | if (pixmap && pixmap.n_children()) | |
1473 | return this._createIconFromPixmap(iconType, iconSize, iconScaling, pixmap); | |
1557 | if (pixmap && pixmap.n_children()) { | |
1558 | return this._createIconFromPixmap(iconType, | |
1559 | iconSize, iconScaling, scaleFactor, pixmap); | |
1560 | } | |
1474 | 1561 | |
1475 | 1562 | return null; |
1476 | 1563 | } |
1489 | 1576 | let iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION |
1490 | 1577 | ? SNIconType.ATTENTION : SNIconType.NORMAL; |
1491 | 1578 | |
1492 | this._updateIconSize(); | |
1493 | ||
1494 | 1579 | try { |
1495 | 1580 | await this._updateIconByType(iconType, this._iconSize); |
1496 | 1581 | } catch (e) { |
1520 | 1605 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) && |
1521 | 1606 | !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) |
1522 | 1607 | logError(e, `${this.debugId}: Updating overlay icon failed`); |
1608 | } | |
1609 | } | |
1610 | ||
1611 | async _invalidateIconWhenFullyReady() { | |
1612 | if (this._waitingInvalidation) | |
1613 | return; | |
1614 | ||
1615 | try { | |
1616 | this._waitingInvalidation = true; | |
1617 | await this._waitForFullyReady(); | |
1618 | this._invalidateIcon(); | |
1619 | } catch (e) { | |
1620 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
1621 | logError(e); | |
1622 | } finally { | |
1623 | delete this._waitingInvalidation; | |
1523 | 1624 | } |
1524 | 1625 | } |
1525 | 1626 | |
1538 | 1639 | _updateIconSize() { |
1539 | 1640 | const settings = SettingsManager.getDefaultGSettings(); |
1540 | 1641 | const sizeValue = settings.get_int('icon-size'); |
1642 | ||
1541 | 1643 | if (sizeValue > 0) { |
1542 | 1644 | if (!this._defaultIconSize) |
1543 | 1645 | this._defaultIconSize = this._iconSize; |
1547 | 1649 | this._iconSize = this._defaultIconSize; |
1548 | 1650 | delete this._defaultIconSize; |
1549 | 1651 | } |
1652 | ||
1653 | const themeIconSize = Math.round( | |
1654 | this.get_theme_node().get_length('icon-size')); | |
1655 | let iconStyle = AppIndicatorsIconActor.DEFAULT_STYLE; | |
1656 | ||
1657 | if (themeIconSize > 0) { | |
1658 | const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage); | |
1659 | ||
1660 | if (themeIconSize / scaleFactor !== this._iconSize) { | |
1661 | iconStyle = `${AppIndicatorsIconActor.DEFAULT_STYLE};` + | |
1662 | 'icon-size: 0'; | |
1663 | } | |
1664 | } | |
1665 | ||
1666 | this.set_style(iconStyle); | |
1667 | this.set_icon_size(this._iconSize); | |
1550 | 1668 | } |
1551 | 1669 | |
1552 | 1670 | _updateCustomIcons() { |
1562 | 1680 | }); |
1563 | 1681 | } |
1564 | 1682 | }); |
1565 | ||
1566 | var CustomImageIconActor = GObject.registerClass( | |
1567 | class CustomImageIconActor extends IconActor { | |
1568 | vfunc_paint(paintContext) { | |
1569 | if (this._customImage) { | |
1570 | this.paint_background(paintContext); | |
1571 | this._customImage.paint(paintContext); | |
1572 | return; | |
1573 | } | |
1574 | ||
1575 | super.vfunc_paint(paintContext); | |
1576 | } | |
1577 | }); |
13 | 13 | // along with this program; if not, write to the Free Software |
14 | 14 | // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
15 | 15 | const Gio = imports.gi.Gio; |
16 | const GObject = imports.gi.GObject; | |
16 | 17 | const GLib = imports.gi.GLib; |
17 | 18 | const GdkPixbuf = imports.gi.GdkPixbuf; |
18 | 19 | const PopupMenu = imports.ui.popupMenu; |
198 | 199 | Signals.addSignalMethods(DbusMenuItem.prototype); |
199 | 200 | |
200 | 201 | |
201 | const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu); | |
202 | ||
203 | 202 | /** |
204 | 203 | * The client does the heavy lifting of actually reading layouts and distributing events |
205 | 204 | */ |
206 | var DBusClient = class AppIndicatorsDBusClient { | |
207 | ||
208 | constructor(busName, busPath) { | |
209 | this._cancellable = new Gio.Cancellable(); | |
210 | this._proxy = new BusClientProxy(Gio.DBus.session, | |
211 | busName, | |
212 | busPath, | |
213 | this._clientReady.bind(this), | |
214 | this._cancellable); | |
215 | this._items = new Map([ | |
216 | [ | |
217 | 0, | |
218 | new DbusMenuItem(this, 0, { | |
219 | 'children-display': GLib.Variant.new_string('submenu'), | |
220 | }, []), | |
221 | ], | |
222 | ]); | |
223 | ||
224 | // will be set to true if a layout update is requested while one is already in progress | |
225 | // then the handler that completes the layout update will request another update | |
205 | ||
206 | var DBusClient = GObject.registerClass({ | |
207 | Signals: { 'ready-changed': {} }, | |
208 | }, class AppIndicatorsDBusClient extends Util.DBusProxy { | |
209 | static get interfaceInfo() { | |
210 | if (!this._interfaceInfo) { | |
211 | this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml( | |
212 | DBusInterfaces.DBusMenu); | |
213 | } | |
214 | return this._interfaceInfo; | |
215 | } | |
216 | ||
217 | static get baseItems() { | |
218 | if (!this._baseItems) { | |
219 | this._baseItems = { | |
220 | 'children-display': GLib.Variant.new_string('submenu'), | |
221 | }; | |
222 | } | |
223 | return this._baseItems; | |
224 | } | |
225 | ||
226 | static destroy() { | |
227 | delete this._interfaceInfo; | |
228 | } | |
229 | ||
230 | _init(busName, objectPath) { | |
231 | const { interfaceInfo } = AppIndicatorsDBusClient; | |
232 | ||
233 | super._init(busName, objectPath, interfaceInfo, | |
234 | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); | |
235 | ||
236 | this._items = new Map(); | |
237 | this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, [])); | |
238 | this._flagItemsUpdateRequired = false; | |
239 | ||
240 | // will be set to true if a layout update is needed once active | |
226 | 241 | this._flagLayoutUpdateRequired = false; |
227 | this._flagLayoutUpdateInProgress = false; | |
228 | 242 | |
229 | 243 | // property requests are queued |
230 | 244 | this._propertiesRequestedFor = new Set(/* ids */); |
231 | 245 | |
232 | 246 | this._layoutUpdated = false; |
233 | Util.connectSmart(this._proxy, 'notify::g-name-owner', this, () => { | |
234 | if (this.isReady) | |
235 | this._requestLayoutUpdate(); | |
236 | }); | |
247 | this._active = false; | |
248 | } | |
249 | ||
250 | async initAsync(cancellable) { | |
251 | await super.initAsync(cancellable); | |
252 | ||
253 | this._requestLayoutUpdate(); | |
254 | } | |
255 | ||
256 | _onNameOwnerChanged() { | |
257 | if (this.isReady) | |
258 | this._requestLayoutUpdate(); | |
237 | 259 | } |
238 | 260 | |
239 | 261 | get isReady() { |
240 | return this._layoutUpdated && !!this._proxy.g_name_owner; | |
262 | return this._layoutUpdated && !!this.gNameOwner; | |
241 | 263 | } |
242 | 264 | |
243 | 265 | get cancellable() { |
249 | 271 | } |
250 | 272 | |
251 | 273 | _requestLayoutUpdate() { |
252 | if (this._flagLayoutUpdateInProgress) | |
253 | this._flagLayoutUpdateRequired = true; | |
254 | else | |
255 | this._beginLayoutUpdate(); | |
256 | } | |
257 | ||
258 | async _requestProperties(id) { | |
259 | this._propertiesRequestedFor.add(id); | |
274 | const cancellable = new Util.CancellableChild(this._cancellable); | |
275 | this._beginLayoutUpdate(cancellable); | |
276 | } | |
277 | ||
278 | async _requestProperties(propertyId, cancellable) { | |
279 | this._propertiesRequestedFor.add(propertyId); | |
280 | ||
281 | if (this._propertiesRequest && this._propertiesRequest.pending()) | |
282 | return; | |
260 | 283 | |
261 | 284 | // if we don't have any requests queued, we'll need to add one |
262 | if (!this._propertiesRequest || !this._propertiesRequest.pending()) { | |
263 | this._propertiesRequest = new PromiseUtils.IdlePromise( | |
264 | GLib.PRIORITY_DEFAULT_IDLE, this._cancellable); | |
265 | await this._propertiesRequest; | |
266 | this._beginRequestProperties(); | |
267 | } | |
268 | } | |
269 | ||
270 | _beginRequestProperties() { | |
271 | this._proxy.GetGroupPropertiesRemote( | |
272 | Array.from(this._propertiesRequestedFor), | |
273 | [], | |
274 | this._cancellable, | |
275 | this._endRequestProperties.bind(this)); | |
276 | ||
285 | this._propertiesRequest = new PromiseUtils.IdlePromise( | |
286 | GLib.PRIORITY_DEFAULT_IDLE, cancellable); | |
287 | await this._propertiesRequest; | |
288 | ||
289 | const requestedProperties = Array.from(this._propertiesRequestedFor); | |
277 | 290 | this._propertiesRequestedFor.clear(); |
278 | return false; | |
279 | } | |
280 | ||
281 | _endRequestProperties(result, error) { | |
282 | if (error) { | |
283 | if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
284 | Util.Logger.warn(`Could not retrieve properties: ${error}`); | |
285 | return; | |
286 | } | |
287 | ||
288 | // for some funny reason, the result array is hidden in an array | |
289 | result[0].forEach(([id, properties]) => { | |
291 | const [result] = await this.GetGroupPropertiesAsync(requestedProperties, | |
292 | [], cancellable); | |
293 | ||
294 | result.forEach(([id, properties]) => { | |
290 | 295 | let item = this._items.get(id); |
291 | 296 | if (!item) |
292 | 297 | return; |
316 | 321 | |
317 | 322 | // the original implementation will only request partial layouts if somehow possible |
318 | 323 | // we try to save us from multiple kinds of race conditions by always requesting a full layout |
319 | _beginLayoutUpdate() { | |
324 | _beginLayoutUpdate(cancellable) { | |
325 | this._layoutUpdateUpdateAsync(cancellable).catch(e => { | |
326 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
327 | logError(e); | |
328 | }); | |
329 | } | |
330 | ||
331 | // the original implementation will only request partial layouts if somehow possible | |
332 | // we try to save us from multiple kinds of race conditions by always requesting a full layout | |
333 | async _layoutUpdateUpdateAsync(cancellable) { | |
320 | 334 | // we only read the type property, because if the type changes after reading all properties, |
321 | 335 | // the view would have to replace the item completely which we try to avoid |
322 | this._proxy.GetLayoutRemote(0, -1, | |
323 | ['type', 'children-display'], | |
324 | this._cancellable, | |
325 | this._endLayoutUpdate.bind(this)); | |
326 | ||
327 | this._flagLayoutUpdateRequired = false; | |
328 | this._flagLayoutUpdateInProgress = true; | |
336 | if (this._layoutUpdateCancellable) | |
337 | this._layoutUpdateCancellable.cancel(); | |
338 | ||
339 | this._layoutUpdateCancellable = cancellable; | |
340 | ||
341 | try { | |
342 | const [revision_, root] = await this.GetLayoutAsync(0, -1, | |
343 | ['type', 'children-display'], cancellable); | |
344 | ||
345 | this._updateLayoutState(true); | |
346 | this._doLayoutUpdate(root, cancellable); | |
347 | this._gcItems(); | |
348 | this._flagLayoutUpdateRequired = false; | |
349 | this._flagItemsUpdateRequired = false; | |
350 | } catch (e) { | |
351 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
352 | this._updateLayoutState(false); | |
353 | throw e; | |
354 | } finally { | |
355 | if (this._layoutUpdateCancellable === cancellable) | |
356 | this._layoutUpdateCancellable = null; | |
357 | } | |
329 | 358 | } |
330 | 359 | |
331 | 360 | _updateLayoutState(state) { |
335 | 364 | this.emit('ready-changed'); |
336 | 365 | } |
337 | 366 | |
338 | _endLayoutUpdate(result, error) { | |
339 | if (error) { | |
340 | if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { | |
341 | Util.Logger.warn(`While reading menu layout on proxy ${this._proxy.g_name_owner}: ${error}`); | |
342 | this._updateLayoutState(false); | |
343 | } | |
344 | return; | |
345 | } | |
346 | ||
347 | let [revision_, root] = result; | |
348 | this._updateLayoutState(true); | |
349 | this._doLayoutUpdate(root); | |
350 | this._gcItems(); | |
351 | ||
352 | if (this._flagLayoutUpdateRequired) | |
353 | this._beginLayoutUpdate(); | |
354 | else | |
355 | this._flagLayoutUpdateInProgress = false; | |
356 | } | |
357 | ||
358 | _doLayoutUpdate(item) { | |
359 | let [id, properties, children] = item; | |
360 | ||
361 | let childrenUnpacked = children.map(c => c.deep_unpack()); | |
362 | let childrenIds = childrenUnpacked.map(c => c[0]); | |
367 | _doLayoutUpdate(item, cancellable) { | |
368 | const [id, properties, children] = item; | |
369 | ||
370 | const childrenUnpacked = children.map(c => c.deep_unpack()); | |
371 | const childrenIds = childrenUnpacked.map(([c]) => c); | |
363 | 372 | |
364 | 373 | // make sure all our children exist |
365 | childrenUnpacked.forEach(c => this._doLayoutUpdate(c)); | |
374 | childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable)); | |
366 | 375 | |
367 | 376 | // make sure we exist |
368 | 377 | const menuItem = this._items.get(id); |
378 | ||
369 | 379 | if (menuItem) { |
370 | 380 | // we do, update our properties if necessary |
371 | 381 | for (let prop in properties) |
372 | 382 | menuItem.propertySet(prop, properties[prop]); |
373 | ||
374 | 383 | |
375 | 384 | // make sure our children are all at the right place, and exist |
376 | 385 | let oldChildrenIds = menuItem.getChildrenIds(); |
395 | 404 | |
396 | 405 | // remove any old children that weren't reused |
397 | 406 | oldChildrenIds.forEach(c => menuItem.removeChild(c)); |
398 | } else { | |
399 | // we don't, so let's create us | |
400 | this._items.set(id, new DbusMenuItem(this, id, properties, childrenIds)); | |
401 | this._requestProperties(id).catch(e => { | |
402 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
403 | Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); | |
404 | }); | |
405 | } | |
407 | ||
408 | if (!this._flagItemsUpdateRequired) | |
409 | return id; | |
410 | } | |
411 | ||
412 | // we don't, so let's create us | |
413 | let newMenuItem = menuItem; | |
414 | ||
415 | if (!newMenuItem) { | |
416 | newMenuItem = new DbusMenuItem(this, id, properties, childrenIds); | |
417 | this._items.set(id, newMenuItem); | |
418 | } | |
419 | ||
420 | this._requestProperties(id, cancellable).catch(e => { | |
421 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
422 | Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); | |
423 | }); | |
406 | 424 | |
407 | 425 | return id; |
408 | 426 | } |
409 | 427 | |
410 | _clientReady(result, error) { | |
411 | if (error) { | |
412 | if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
413 | Util.Logger.warn(`Could not initialize menu proxy: ${error}`); | |
414 | return; | |
415 | } | |
416 | ||
417 | this._requestLayoutUpdate(); | |
418 | ||
419 | // listen for updated layouts and properties | |
420 | this._proxy.connectSignal('LayoutUpdated', this._onLayoutUpdated.bind(this)); | |
421 | this._proxy.connectSignal('ItemsPropertiesUpdated', this._onPropertiesUpdated.bind(this)); | |
428 | async _doPropertiesUpdateAsync(cancellable) { | |
429 | if (this._propertiesUpdateCancellable) | |
430 | this._propertiesUpdateCancellable.cancel(); | |
431 | ||
432 | this._propertiesUpdateCancellable = cancellable; | |
433 | ||
434 | try { | |
435 | const requests = []; | |
436 | ||
437 | this._items.forEach((_, id) => | |
438 | requests.push(this._requestProperties(id, cancellable))); | |
439 | ||
440 | await Promise.all(requests); | |
441 | } finally { | |
442 | if (this._propertiesUpdateCancellable === cancellable) | |
443 | this._propertiesUpdateCancellable = null; | |
444 | } | |
445 | } | |
446 | ||
447 | _doPropertiesUpdate() { | |
448 | const cancellable = new Util.CancellableChild(this._cancellable); | |
449 | this._doPropertiesUpdateAsync(cancellable).catch(e => { | |
450 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
451 | Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); | |
452 | }); | |
453 | } | |
454 | ||
455 | ||
456 | set active(active) { | |
457 | const wasActive = this._active; | |
458 | this._active = active; | |
459 | ||
460 | if (active && wasActive !== active) { | |
461 | if (this._flagLayoutUpdateRequired) { | |
462 | this._requestLayoutUpdate(); | |
463 | } else if (this._flagItemsUpdateRequired) { | |
464 | this._doPropertiesUpdate(); | |
465 | this._flagItemsUpdateRequired = false; | |
466 | } | |
467 | } | |
468 | } | |
469 | ||
470 | _onSignal(_sender, signal, params) { | |
471 | if (signal === 'LayoutUpdated') { | |
472 | if (!this._active) { | |
473 | this._flagLayoutUpdateRequired = true; | |
474 | return; | |
475 | } | |
476 | ||
477 | this._requestLayoutUpdate(); | |
478 | } else if (signal === 'ItemsPropertiesUpdated') { | |
479 | if (!this._active) { | |
480 | this._flagItemsUpdateRequired = true; | |
481 | return; | |
482 | } | |
483 | ||
484 | this._onPropertiesUpdated(params.deep_unpack()); | |
485 | } | |
422 | 486 | } |
423 | 487 | |
424 | 488 | getItem(id) { |
429 | 493 | } |
430 | 494 | |
431 | 495 | // we don't need to cache and burst-send that since it will not happen that frequently |
432 | sendAboutToShow(id) { | |
496 | async sendAboutToShow(id) { | |
433 | 497 | /* Some indicators (you, dropbox!) don't use the right signature |
434 | 498 | * and don't return a boolean, so we need to support both cases */ |
435 | let connection = this._proxy.get_connection(); | |
436 | connection.call(this._proxy.get_name(), this._proxy.get_object_path(), | |
437 | this._proxy.get_interface_name(), 'AboutToShow', | |
438 | new GLib.Variant('(i)', [id]), null, | |
439 | Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { | |
440 | try { | |
441 | let ret = proxy.call_finish(res); | |
442 | if ((ret.is_of_type(new GLib.VariantType('(b)')) && | |
443 | ret.get_child_value(0).get_boolean()) || | |
444 | ret.is_of_type(new GLib.VariantType('()'))) | |
445 | this._requestLayoutUpdate(); | |
446 | ||
447 | } catch (e) { | |
448 | Util.Logger.warn(`Impossible to send about-to-show to menu: ${e}`); | |
449 | } | |
450 | }); | |
499 | try { | |
500 | const ret = await this.gConnection.call(this.gName, this.gObjectPath, | |
501 | this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]), | |
502 | null, Gio.DBusCallFlags.NONE, -1, this._cancellable); | |
503 | ||
504 | if ((ret.is_of_type(new GLib.VariantType('(b)')) && | |
505 | ret.get_child_value(0).get_boolean()) || | |
506 | ret.is_of_type(new GLib.VariantType('()'))) | |
507 | this._requestLayoutUpdate(); | |
508 | } catch (e) { | |
509 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
510 | logError(e); | |
511 | } | |
451 | 512 | } |
452 | 513 | |
453 | 514 | sendEvent(id, event, params, timestamp) { |
454 | if (!this._proxy) | |
515 | if (!this.gNameOwner) | |
455 | 516 | return; |
456 | 517 | |
457 | this._proxy.EventRemote(id, event, params, timestamp, this._cancellable, | |
458 | () => { /* we don't care */ }); | |
459 | } | |
460 | ||
461 | _onLayoutUpdated() { | |
462 | this._requestLayoutUpdate(); | |
463 | } | |
464 | ||
465 | _onPropertiesUpdated(proxy, name, [changed, removed]) { | |
518 | this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => { | |
519 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
520 | logError(e); | |
521 | }); | |
522 | } | |
523 | ||
524 | _onPropertiesUpdated([changed, removed]) { | |
466 | 525 | changed.forEach(([id, props]) => { |
467 | 526 | let item = this._items.get(id); |
468 | 527 | if (!item) |
479 | 538 | propNames.forEach(propName => item.propertySet(propName, null)); |
480 | 539 | }); |
481 | 540 | } |
482 | ||
483 | destroy() { | |
484 | this.emit('destroy'); | |
485 | ||
486 | this._cancellable.cancel(); | |
487 | Signals._disconnectAll.apply(this._proxy); | |
488 | ||
489 | this._proxy = null; | |
490 | } | |
491 | }; | |
492 | Signals.addSignalMethods(DBusClient.prototype); | |
541 | }); | |
542 | ||
543 | if (imports.system.version < 17101) { | |
544 | /* In old versions wrappers are not applied to sub-classes, so let's do it */ | |
545 | DBusClient.prototype.init_async = Gio.DBusProxy.prototype.init_async; | |
546 | } | |
493 | 547 | |
494 | 548 | // //////////////////////////////////////////////////////////////////////// |
495 | 549 | // PART TWO: "View" frontend implementation. |
769 | 823 | this._rootMenu = null; // the shell menu |
770 | 824 | this._rootItem = null; // the DbusMenuItem for the root |
771 | 825 | this.indicator = indicator; |
826 | this.cancellable = new Util.CancellableChild(this.indicator.cancellable); | |
827 | ||
828 | this._client.initAsync(this.cancellable).catch(e => { | |
829 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
830 | logError(e); | |
831 | }); | |
772 | 832 | |
773 | 833 | Util.connectSmart(this._client, 'ready-changed', this, |
774 | 834 | () => this.emit('ready-changed')); |
775 | } | |
776 | ||
777 | get cancellable() { | |
778 | return this._client.cancellable; | |
779 | 835 | } |
780 | 836 | |
781 | 837 | get isReady() { |
787 | 843 | attachToMenu(menu) { |
788 | 844 | this._rootMenu = menu; |
789 | 845 | this._rootItem = this._client.getRoot(); |
846 | this._itemsBeingAdded = new Set(); | |
790 | 847 | |
791 | 848 | // cleanup: remove existing children (just in case) |
792 | 849 | this._rootMenu.removeAll(); |
806 | 863 | this._rootItem.sendAboutToShow(); |
807 | 864 | |
808 | 865 | // fill the menu for the first time |
809 | this._rootItem.getChildren().forEach(child => | |
810 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child))); | |
866 | const children = this._rootItem.getChildren(); | |
867 | children.forEach(child => | |
868 | this._onRootChildAdded(this._rootItem, child)); | |
811 | 869 | } |
812 | 870 | |
813 | 871 | _setOpenedSubmenu(submenu) { |
827 | 885 | } |
828 | 886 | |
829 | 887 | _onRootChildAdded(dbusItem, child, position) { |
830 | this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position); | |
888 | // Menu additions can be expensive, so let's do it in different chunks | |
889 | const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW; | |
890 | const idlePromise = new PromiseUtils.IdlePromise( | |
891 | basePriority + this._itemsBeingAdded.size, this.cancellable); | |
892 | this._itemsBeingAdded.add(child); | |
893 | ||
894 | idlePromise.then(() => { | |
895 | if (!this._itemsBeingAdded.has(child)) | |
896 | return; | |
897 | ||
898 | this._rootMenu.addMenuItem( | |
899 | MenuItemFactory.createItem(this, child), position); | |
900 | }).catch(e => { | |
901 | if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
902 | logError(e); | |
903 | }).finally(() => this._itemsBeingAdded.delete(child)); | |
831 | 904 | } |
832 | 905 | |
833 | 906 | _onRootChildRemoved(dbusItem, child) { |
834 | 907 | // children like to play hide and seek |
835 | 908 | // but we know how to find it for sure! |
836 | this._rootMenu._getMenuItems().forEach(item => { | |
837 | if (item._dbusItem === child) | |
838 | item.destroy(); | |
839 | }); | |
909 | const item = this._rootMenu._getMenuItems().find(it => | |
910 | it._dbusItem === child); | |
911 | ||
912 | if (item) | |
913 | item.destroy(); | |
914 | else | |
915 | this._itemsBeingAdded.delete(child); | |
916 | ||
840 | 917 | } |
841 | 918 | |
842 | 919 | _onRootChildMoved(dbusItem, child, oldpos, newpos) { |
846 | 923 | _onMenuOpened(menu, state) { |
847 | 924 | if (!this._rootItem) |
848 | 925 | return; |
926 | ||
927 | this._client.active = state; | |
849 | 928 | |
850 | 929 | if (state) { |
851 | 930 | if (this._openedSubMenu && this._openedSubMenu.isOpen) |
868 | 947 | this._rootItem = null; |
869 | 948 | this._rootMenu = null; |
870 | 949 | this.indicator = null; |
950 | this._itemsBeingAdded = null; | |
871 | 951 | } |
872 | 952 | }; |
873 | 953 | Signals.addSignalMethods(Client.prototype); |
60 | 60 | 16, Gtk.IconLookupFlags.GENERIC_FALLBACK); |
61 | 61 | let iconFile = Gio.File.new_for_path(iconInfo.get_filename()); |
62 | 62 | let [, extension] = iconFile.get_basename().split('.'); |
63 | let newName = `${iconName}-${Math.floor(Math.random() * 100)}.${extension}`; | |
63 | let newName = `${Math.floor(Math.random() * 100)}${iconName}.${extension}`; | |
64 | 64 | let newFile = Gio.File.new_for_path( |
65 | 65 | `${GLib.dir_make_tmp('indicator-test-XXXXXX')}/${newName}`); |
66 | 66 | temporaryFiles.push(newFile, newFile.get_parent()); |
67 | 67 | iconFile.copy(newFile, Gio.FileCopyFlags.OVERWRITE, null, null); |
68 | 68 | |
69 | 69 | indicator.set_icon_theme_path(newFile.get_parent().get_path()); |
70 | indicator.set_icon(newFile.get_basename()); | |
70 | indicator.set_icon(newFile.get_basename().split('.').slice(0, -1).join('')); | |
71 | 71 | }; |
72 | 72 | |
73 | 73 | var menu = new Gtk.Menu(); |
76 | 76 | menu.append(item); |
77 | 77 | |
78 | 78 | item = Gtk.MenuItem.new_with_label('Foo'); |
79 | const fooItem = item; | |
80 | let fooId = item.connect('activate', () => { | |
81 | GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { | |
82 | print('Changing item label', fooItem.get_label()); | |
83 | fooItem.set_label('Destroy me now...'); | |
84 | fooItem.connect('activate', () => { | |
85 | print('Removed item labeled', fooItem.get_label()); | |
86 | fooItem.destroy(); | |
87 | }); | |
88 | fooItem.disconnect(fooId); | |
89 | ||
90 | const barItem = Gtk.MenuItem.new_with_label('Bar'); | |
91 | menu.insert(barItem, 2); | |
92 | barItem.show(); | |
93 | return GLib.SOURCE_REMOVE; | |
94 | }); | |
95 | }); | |
79 | 96 | menu.append(item); |
80 | 97 | |
81 | 98 | item = Gtk.ImageMenuItem.new_with_label('Calculator'); |
83 | 100 | menu.append(item); |
84 | 101 | |
85 | 102 | item = Gtk.CheckMenuItem.new_with_label('Check me!'); |
103 | const checkItem = item; | |
104 | item.connect('activate', () => { | |
105 | GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { | |
106 | print('changed item label', checkItem.get_label()); | |
107 | checkItem.set_label(`Checked at ${new Date().getTime()}`); | |
108 | return GLib.SOURCE_REMOVE; | |
109 | }); | |
110 | }); | |
86 | 111 | menu.append(item); |
87 | 112 | |
88 | 113 | item = Gtk.MenuItem.new_with_label('Blub'); |
245 | 245 | Util.connectSmart(this._indicator, 'reset', this, () => { |
246 | 246 | this._updateStatus(); |
247 | 247 | this._updateLabel(); |
248 | }); | |
249 | Util.connectSmart(this.icon, 'requires-custom-image', this, () => { | |
250 | this._setIconActor(new AppIndicator.CustomImageIconActor( | |
251 | indicator, Panel.PANEL_ICON_SIZE)); | |
252 | 248 | }); |
253 | 249 | Util.connectSmart(this._indicator, 'accessible-name', this, () => |
254 | 250 | this.set_accessible_name(this._indicator.accessibleName)); |
594 | 590 | iconSize = Panel.PANEL_ICON_SIZE; |
595 | 591 | |
596 | 592 | this.height = -1; |
597 | this._icon.set_width(iconSize * scaleFactor); | |
598 | this._icon.set_height(iconSize * scaleFactor); | |
599 | this._icon.set_y_align(Clutter.ActorAlign.CENTER); | |
600 | this._icon.set_x_align(Clutter.ActorAlign.CENTER); | |
593 | this._icon.set({ | |
594 | width: iconSize * scaleFactor, | |
595 | height: iconSize * scaleFactor, | |
596 | xAlign: Clutter.ActorAlign.CENTER, | |
597 | yAlign: Clutter.ActorAlign.CENTER, | |
598 | }); | |
601 | 599 | } |
602 | 600 | }); |
48 | 48 | <arg type="ai" name="idErrors" direction="out" /> |
49 | 49 | </method> |
50 | 50 | |
51 | <!-- Signals --> | |
51 | <!-- Signals | |
52 | 52 | <signal name="ItemsPropertiesUpdated"> |
53 | 53 | <arg type="a(ia{sv})" name="updatedProps" direction="out" /> |
54 | 54 | <arg type="a(ias)" name="removedProps" direction="out" /> |
61 | 61 | <arg type="i" name="id" direction="out" /> |
62 | 62 | <arg type="u" name="timestamp" direction="out" /> |
63 | 63 | </signal> |
64 | --> | |
64 | 65 | </interface> |
82 | 82 | <arg name="orientation" type="s" direction="in"/> |
83 | 83 | </method> |
84 | 84 | |
85 | <!-- Signals: the client wants to change something in the status--> | |
85 | <!-- Signals: the client wants to change something in the status | |
86 | 86 | <signal name="NewTitle"> |
87 | 87 | </signal> |
88 | 88 | |
94 | 94 | |
95 | 95 | <signal name="NewOverlayIcon"> |
96 | 96 | </signal> |
97 | ||
97 | --> | |
98 | 98 | <!-- We disable this as we don't support tooltip, so no need to go through it |
99 | 99 | <signal name="NewToolTip"> |
100 | 100 | </signal> |
101 | 101 | --> |
102 | 102 | |
103 | <!-- | |
103 | 104 | <signal name="NewStatus"> |
104 | 105 | <arg name="status" type="s"/> |
105 | 106 | </signal> |
107 | --> | |
106 | 108 | |
107 | 109 | |
108 | <!-- The following items are not supported by specs, but widely used --> | |
110 | <!-- The following items are not supported by specs, but widely used | |
109 | 111 | <signal name="NewIconThemePath"> |
110 | 112 | <arg type="s" name="icon_theme_path" direction="out" /> |
111 | 113 | </signal> |
112 | 114 | |
113 | 115 | <signal name="NewMenu"></signal> |
116 | --> | |
114 | 117 | |
115 | 118 | <!-- ayatana labels --> |
116 | 119 | <!-- These are commented out because GDBusProxy would otherwise require them, |
21 | 21 | |
22 | 22 | |
23 | 23 | <!-- signals --> |
24 | ||
25 | 24 | <signal name="StatusNotifierItemRegistered"> |
26 | 25 | <arg type="s"/> |
27 | 26 | </signal> |
7 | 7 | msgstr "" |
8 | 8 | "Project-Id-Version: AppIndicatorExtension\n" |
9 | 9 | "Report-Msgid-Bugs-To: \n" |
10 | "POT-Creation-Date: 2021-09-27 20:43+0200\n" | |
10 | "POT-Creation-Date: 2023-03-07 02:36+0100\n" | |
11 | 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" |
12 | 12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" |
13 | 13 | "Language-Team: LANGUAGE <LL@li.org>\n" |
16 | 16 | "Content-Type: text/plain; charset=CHARSET\n" |
17 | 17 | "Content-Transfer-Encoding: 8bit\n" |
18 | 18 | |
19 | #: prefs.js:67 | |
19 | #: prefs.js:59 | |
20 | msgid "Enable Legacy Tray Icons support" | |
21 | msgstr "" | |
22 | ||
23 | #: prefs.js:91 | |
20 | 24 | msgid "Opacity (min: 0, max: 255)" |
21 | 25 | msgstr "" |
22 | 26 | |
23 | #: prefs.js:95 | |
27 | #: prefs.js:120 | |
24 | 28 | msgid "Desaturation (min: 0.0, max: 1.0)" |
25 | 29 | msgstr "" |
26 | 30 | |
27 | #: prefs.js:123 | |
31 | #: prefs.js:148 | |
28 | 32 | msgid "Brightness (min: -1.0, max: 1.0)" |
29 | 33 | msgstr "" |
30 | 34 | |
31 | #: prefs.js:151 | |
35 | #: prefs.js:176 | |
32 | 36 | msgid "Contrast (min: -1.0, max: 1.0)" |
33 | 37 | msgstr "" |
34 | 38 | |
35 | #: prefs.js:179 | |
39 | #: prefs.js:204 | |
36 | 40 | msgid "Icon size (min: 0, max: 96)" |
37 | 41 | msgstr "" |
38 | 42 | |
39 | #: prefs.js:207 | |
43 | #: prefs.js:232 | |
40 | 44 | msgid "Tray horizontal alignment" |
41 | 45 | msgstr "" |
42 | 46 | |
43 | #: prefs.js:212 | |
47 | #: prefs.js:237 | |
44 | 48 | msgid "Center" |
45 | 49 | msgstr "" |
46 | 50 | |
47 | #: prefs.js:213 | |
51 | #: prefs.js:238 | |
48 | 52 | msgid "Left" |
49 | 53 | msgstr "" |
50 | 54 | |
51 | #: prefs.js:214 | |
55 | #: prefs.js:239 | |
52 | 56 | msgid "Right" |
53 | 57 | msgstr "" |
54 | 58 | |
55 | #: prefs.js:259 | |
59 | #: prefs.js:286 | |
56 | 60 | msgid "Indicator ID" |
57 | 61 | msgstr "" |
58 | 62 | |
59 | #: prefs.js:260 | |
63 | #: prefs.js:287 | |
60 | 64 | msgid "Icon Name" |
61 | 65 | msgstr "" |
62 | 66 | |
63 | #: prefs.js:261 | |
67 | #: prefs.js:288 | |
64 | 68 | msgid "Attention Icon Name" |
65 | 69 | msgstr "" |
66 | 70 | |
67 | #: prefs.js:336 | |
71 | #: prefs.js:363 | |
68 | 72 | msgid "Preferences" |
69 | 73 | msgstr "" |
70 | 74 | |
71 | #: prefs.js:338 | |
75 | #: prefs.js:365 | |
72 | 76 | msgid "Custom Icons" |
73 | 77 | msgstr "" |
74 | 78 | |
75 | 79 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:5 |
80 | msgid "Enable legacy tray icons support" | |
81 | msgstr "" | |
82 | ||
83 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:9 | |
76 | 84 | msgid "Saturation" |
77 | 85 | msgstr "" |
78 | 86 | |
79 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:9 | |
87 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:13 | |
80 | 88 | msgid "Brightness" |
81 | 89 | msgstr "" |
82 | 90 | |
83 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:13 | |
91 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:17 | |
84 | 92 | msgid "Contrast" |
85 | 93 | msgstr "" |
86 | 94 | |
87 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:17 | |
95 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:21 | |
88 | 96 | msgid "Opacity" |
89 | 97 | msgstr "" |
90 | 98 | |
91 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:21 | |
99 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:25 | |
92 | 100 | msgid "Icon size" |
93 | 101 | msgstr "" |
94 | 102 | |
95 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:22 | |
103 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:26 | |
96 | 104 | msgid "Icon size in pixel" |
97 | 105 | msgstr "" |
98 | 106 | |
99 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:26 | |
107 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:30 | |
100 | 108 | msgid "Icon spacing" |
101 | 109 | msgstr "" |
102 | 110 | |
103 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:27 | |
111 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:31 | |
104 | 112 | msgid "Icon spacing within the tray" |
105 | 113 | msgstr "" |
106 | 114 | |
107 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:31 | |
115 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:35 | |
108 | 116 | msgid "Position in tray" |
109 | 117 | msgstr "" |
110 | 118 | |
111 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:32 | |
119 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:36 | |
112 | 120 | msgid "Set where the Icon tray should appear in Gnome tray" |
113 | 121 | msgstr "" |
114 | 122 | |
115 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:36 | |
123 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:40 | |
116 | 124 | msgid "Order in tray" |
117 | 125 | msgstr "" |
118 | 126 | |
119 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:37 | |
127 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:41 | |
120 | 128 | msgid "Set where the Icon tray should appear among other trays" |
121 | 129 | msgstr "" |
122 | 130 | |
123 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:41 | |
131 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:45 | |
124 | 132 | msgid "Custom icons" |
125 | 133 | msgstr "" |
126 | 134 | |
127 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:42 | |
135 | #: schemas/org.gnome.shell.extensions.appindicator.gschema.xml:46 | |
128 | 136 | msgid "Replace any icons with custom icons from themes" |
129 | 137 | msgstr "" |
0 | 0 | project('gnome-shell-ubuntu-appindicators', |
1 | version : '50', | |
1 | version : '53', | |
2 | 2 | meson_version : '>= 0.53', |
3 | 3 | license: 'GPL2', |
4 | 4 | ) |
21 | 21 | const Extension = imports.misc.extensionUtils.getCurrentExtension(); |
22 | 22 | |
23 | 23 | const AppIndicator = Extension.imports.appIndicator; |
24 | const DBusMenu = Extension.imports.dbusMenu; | |
24 | 25 | const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon; |
25 | 26 | const Interfaces = Extension.imports.interfaces; |
26 | 27 | const PromiseUtils = Extension.imports.promiseUtils; |
148 | 149 | // StatusNotifierItem interface... However let's do it after a low |
149 | 150 | // priority idle, so that it won't affect startup. |
150 | 151 | const cancellable = this._cancellable; |
151 | await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable); | |
152 | 152 | const bus = Gio.DBus.session; |
153 | 153 | const uniqueNames = await Util.getBusNames(bus, cancellable); |
154 | 154 | const introspectName = async name => { |
155 | const nodes = await Util.introspectBusObject(bus, name, cancellable); | |
155 | const nodes = Util.introspectBusObject(bus, name, cancellable, | |
156 | ['org.kde.StatusNotifierItem']); | |
156 | 157 | const services = [...uniqueNames.get(name)]; |
157 | nodes.forEach(({ nodeInfo, path }) => { | |
158 | if (Util.dbusNodeImplementsInterfaces(nodeInfo, ['org.kde.StatusNotifierItem'])) { | |
159 | const ids = services.map(s => Util.indicatorId(s, name, path)); | |
160 | if (ids.every(id => !this._items.has(id))) { | |
161 | const service = services.find(s => | |
162 | s.startsWith('org.kde.StatusNotifierItem')) || services[0]; | |
163 | const id = Util.indicatorId( | |
164 | path === DEFAULT_ITEM_OBJECT_PATH ? service : null, | |
165 | name, path); | |
166 | Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`); | |
167 | this._registerItem(service, name, path); | |
168 | } | |
158 | ||
159 | for await (const node of nodes) { | |
160 | const { path } = node; | |
161 | const ids = services.map(s => Util.indicatorId(s, name, path)); | |
162 | if (ids.every(id => !this._items.has(id))) { | |
163 | const service = services.find(s => | |
164 | s && s.startsWith('org.kde.StatusNotifierItem')) || services[0]; | |
165 | const id = Util.indicatorId( | |
166 | path === DEFAULT_ITEM_OBJECT_PATH ? service : null, | |
167 | name, path); | |
168 | Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`); | |
169 | this._registerItem(service, name, path); | |
169 | 170 | } |
170 | }); | |
171 | } | |
171 | 172 | }; |
172 | 173 | await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n))); |
173 | 174 | } |
274 | 275 | Util.Logger.warn(`Failed to unexport watcher object: ${e}`); |
275 | 276 | } |
276 | 277 | |
278 | DBusMenu.DBusClient.destroy(); | |
277 | 279 | AppIndicator.AppIndicatorProxy.destroy(); |
280 | Util.DBusProxy.destroy(); | |
281 | Util.destroyDefaultTheme(); | |
278 | 282 | |
279 | 283 | this._dbusImpl.run_dispose(); |
280 | 284 | delete this._dbusImpl; |
15 | 15 | |
16 | 16 | /* exported CancellableChild, getUniqueBusName, getBusNames, |
17 | 17 | introspectBusObject, dbusNodeImplementsInterfaces, waitForStartupCompletion, |
18 | connectSmart, disconnectSmart, versionCheck, getDefaultTheme, | |
19 | getProcessName, indicatorId, tryCleanupOldIndicators */ | |
18 | connectSmart, disconnectSmart, versionCheck, getDefaultTheme, destroyDefaultTheme, | |
19 | getProcessName, indicatorId, tryCleanupOldIndicators, DBusProxy */ | |
20 | 20 | |
21 | 21 | const ByteArray = imports.byteArray; |
22 | 22 | const Gio = imports.gi.Gio; |
31 | 31 | const Config = imports.misc.config; |
32 | 32 | const ExtensionUtils = imports.misc.extensionUtils; |
33 | 33 | const Extension = ExtensionUtils.getCurrentExtension(); |
34 | const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon; | |
35 | 34 | const PromiseUtils = Extension.imports.promiseUtils; |
36 | 35 | const Signals = imports.signals; |
37 | 36 | |
113 | 112 | return ByteArray.toString(bytes.toArray().map(v => !v ? 0x20 : v)); |
114 | 113 | } |
115 | 114 | |
116 | async function introspectBusObject(bus, name, cancellable, path = undefined) { | |
115 | async function* introspectBusObject(bus, name, cancellable, | |
116 | interfaces = undefined, path = undefined) { | |
117 | 117 | if (!path) |
118 | 118 | path = '/'; |
119 | 119 | |
120 | 120 | const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable', |
121 | 121 | 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE, |
122 | -1, cancellable)).deep_unpack(); | |
122 | 5000, cancellable)).deep_unpack(); | |
123 | 123 | |
124 | 124 | const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection); |
125 | const nodes = [{ nodeInfo, path }]; | |
125 | ||
126 | if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces)) | |
127 | yield { nodeInfo, path }; | |
126 | 128 | |
127 | 129 | if (path === '/') |
128 | 130 | path = ''; |
129 | 131 | |
130 | const requests = []; | |
131 | for (const subNodes of nodeInfo.nodes) { | |
132 | const subPath = `${path}/${subNodes.path}`; | |
133 | requests.push(introspectBusObject(bus, name, cancellable, subPath)); | |
134 | } | |
135 | ||
136 | for (const result of await Promise.allSettled(requests)) { | |
137 | if (result.status === 'fulfilled') | |
138 | result.value.forEach(n => nodes.push(n)); | |
139 | else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) | |
140 | Logger.debug(`Impossible to get node info: ${result.reason}`); | |
141 | } | |
142 | ||
143 | return nodes; | |
132 | for (const subNodeInfo of nodeInfo.nodes) { | |
133 | const subPath = `${path}/${subNodeInfo.path}`; | |
134 | yield* introspectBusObject(bus, name, cancellable, interfaces, subPath); | |
135 | } | |
144 | 136 | } |
145 | 137 | |
146 | 138 | function dbusNodeImplementsInterfaces(nodeInfo, interfaces) { |
260 | 252 | throw new TypeError('Unexpected number of arguments'); |
261 | 253 | } |
262 | 254 | |
255 | let _defaultTheme; | |
263 | 256 | function getDefaultTheme() { |
264 | if (St.IconTheme) | |
265 | return new St.IconTheme(); | |
257 | if (_defaultTheme) | |
258 | return _defaultTheme; | |
259 | ||
260 | if (St.IconTheme) { | |
261 | _defaultTheme = new St.IconTheme(); | |
262 | return _defaultTheme; | |
263 | } | |
266 | 264 | |
267 | 265 | if (Gdk.Screen && Gdk.Screen.get_default()) { |
268 | const defaultTheme = Gtk.IconTheme.get_default(); | |
269 | if (defaultTheme) | |
270 | return defaultTheme; | |
271 | } | |
272 | ||
273 | const defaultTheme = new Gtk.IconTheme(); | |
274 | defaultTheme.set_custom_theme(St.Settings.get().gtk_icon_theme); | |
275 | return defaultTheme; | |
266 | _defaultTheme = Gtk.IconTheme.get_default(); | |
267 | if (_defaultTheme) | |
268 | return _defaultTheme; | |
269 | } | |
270 | ||
271 | _defaultTheme = new Gtk.IconTheme(); | |
272 | _defaultTheme.set_custom_theme(St.Settings.get().gtk_icon_theme); | |
273 | return _defaultTheme; | |
274 | } | |
275 | ||
276 | function destroyDefaultTheme() { | |
277 | _defaultTheme = null; | |
276 | 278 | } |
277 | 279 | |
278 | 280 | // eslint-disable-next-line valid-jsdoc |
392 | 394 | } |
393 | 395 | |
394 | 396 | function tryCleanupOldIndicators() { |
397 | const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon; | |
395 | 398 | const indicatorType = IndicatorStatusIcon.BaseStatusIcon; |
396 | 399 | const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType); |
397 | 400 | |
465 | 468 | this._realCancel(); |
466 | 469 | } |
467 | 470 | }); |
471 | ||
472 | var DBusProxy = GObject.registerClass({ | |
473 | Signals: { 'destroy': {} }, | |
474 | }, class DBusProxy extends Gio.DBusProxy { | |
475 | static get TUPLE_VARIANT_TYPE() { | |
476 | if (!this._tupleVariantType) | |
477 | this._tupleVariantType = new GLib.VariantType('(v)'); | |
478 | ||
479 | return this._tupleVariantType; | |
480 | } | |
481 | ||
482 | static destroy() { | |
483 | delete this._tupleType; | |
484 | } | |
485 | ||
486 | _init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) { | |
487 | if (interfaceInfo.signals.length) | |
488 | Logger.warn('Avoid exposing signals to gjs!'); | |
489 | ||
490 | super._init({ | |
491 | gConnection: Gio.DBus.session, | |
492 | gInterfaceName: interfaceInfo.name, | |
493 | gInterfaceInfo: interfaceInfo, | |
494 | gName: busName, | |
495 | gObjectPath: objectPath, | |
496 | gFlags: flags, | |
497 | }); | |
498 | ||
499 | this._signalIds = []; | |
500 | ||
501 | if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) { | |
502 | this._signalIds.push(this.connect('g-signal', | |
503 | (_proxy, ...args) => this._onSignal(...args))); | |
504 | } | |
505 | ||
506 | this._signalIds.push(this.connect('notify::g-name-owner', () => | |
507 | this._onNameOwnerChanged())); | |
508 | } | |
509 | ||
510 | async initAsync(cancellable) { | |
511 | cancellable = new CancellableChild(cancellable); | |
512 | await this.init_async(GLib.PRIORITY_DEFAULT, cancellable); | |
513 | this._cancellable = cancellable; | |
514 | ||
515 | this.gInterfaceInfo.methods.map(m => m.name).forEach(method => | |
516 | this._ensureAsyncMethod(method)); | |
517 | } | |
518 | ||
519 | destroy() { | |
520 | this.emit('destroy'); | |
521 | ||
522 | this._signalIds.forEach(id => this.disconnect(id)); | |
523 | ||
524 | if (this._cancellable) | |
525 | this._cancellable.cancel(); | |
526 | } | |
527 | ||
528 | // This can be removed when we will have GNOME 43 as minimum version | |
529 | _ensureAsyncMethod(method) { | |
530 | if (this[`${method}Async`]) | |
531 | return; | |
532 | ||
533 | if (!this[`${method}Remote`]) | |
534 | throw new Error(`Missing remote method '${method}'`); | |
535 | ||
536 | this[`${method}Async`] = function (...args) { | |
537 | return new Promise((resolve, reject) => { | |
538 | this[`${method}Remote`](...args, (ret, e) => { | |
539 | if (e) | |
540 | reject(e); | |
541 | else | |
542 | resolve(ret); | |
543 | }); | |
544 | }); | |
545 | }; | |
546 | } | |
547 | ||
548 | _onSignal() { | |
549 | } | |
550 | ||
551 | getProperty(propertyName, cancellable) { | |
552 | return this.gConnection.call(this.gName, | |
553 | this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get', | |
554 | GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]), | |
555 | DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1, | |
556 | cancellable); | |
557 | } | |
558 | ||
559 | getProperties(cancellable) { | |
560 | return this.gConnection.call(this.gName, | |
561 | this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll', | |
562 | GLib.Variant.new('(s)', [this.gInterfaceName]), | |
563 | GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1, | |
564 | cancellable); | |
565 | } | |
566 | }); | |
567 | ||
568 | if (imports.system.version < 17101) { | |
569 | /* In old versions wrappers are not applied to sub-classes, so let's do it */ | |
570 | DBusProxy.prototype.init_async = Gio.DBusProxy.prototype.init_async; | |
571 | } |