17 Commits

Author SHA1 Message Date
e291b79f2c Update metadata.json with Gnome Shell 48.5 2025-11-17 22:58:39 +01:00
b4af852a0f Fix formatting in README 2025-07-05 18:10:38 +02:00
6aa9f15d70 Update extension for GNOME 48 2025-03-31 15:14:59 +02:00
ca11b0e175 Version 15 2024-08-31 16:36:07 +02:00
d0e8f0cde5 Code review from JustPerfection 2024-08-31 13:58:43 +02:00
66d79cc804 Update for Gnome Shell 45+ 2024-08-29 20:46:21 +02:00
3ed3394a33 Update metadata.json with Gnome Shell 44 2023-09-25 19:14:35 +02:00
7cd668af14 Count number of threads instead of unread messages in gmail example 2022-11-20 21:01:41 +01:00
5690307c63 Update Gmail access with OAuth 2 2022-11-20 20:00:34 +01:00
b1feb004e8 Update metadata.json 2022-10-28 17:20:14 +02:00
73021a25a1 Update README 2022-10-28 08:09:34 +02:00
4c73986596 Updates for GNOME 43 2022-10-27 17:54:37 +02:00
d3665f2ff0 Quick update for GNOME 42. There is still some issues with menu. 2022-04-10 14:21:53 +02:00
00fb6120c9 Update README.md and version to be aligned on Gnome store version 2022-01-07 08:35:13 +01:00
02d3e8b72a Remove timer resource during SignalMgt destructor 2022-01-06 09:52:11 +01:00
78254a550e Update support for version 41 in metadata 2022-01-06 09:14:55 +01:00
7a5f506c23 Remove use of deprecated "lang" package 2021-10-01 09:02:34 +02:00
7 changed files with 276 additions and 159 deletions

View File

@@ -7,10 +7,17 @@ This GNOME Shell Extension aims to display information to center box. Using DBUS
Installation Installation
------------ ------------
Install it from https://extensions.gnome.org/extension/2826/generic-monitor/
OR
Create a symbolic link from your _.local_ directory and enable extension Create a symbolic link from your _.local_ directory and enable extension
```bash
ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/ ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/
gnome-extensions enable generic-monitor@gnome-shell-extensions gnome-extensions enable generic-monitor@gnome-shell-extensions
```
Restart GNOME Restart GNOME
@@ -20,6 +27,7 @@ DBUS protocol
All functions read JSON formatted parameters All functions read JSON formatted parameters
```js
notify(): notify():
{ {
"group": "groupname", "group": "groupname",
@@ -60,26 +68,32 @@ All functions read JSON formatted parameters
{ {
"item": "<itemName>@<groupName>" "item": "<itemName>@<groupName>"
} }
```
DBUS object DBUS object
=========== ===========
<text_object> is defined as: <text_object> is defined as:
```js
"text" : { "text" : {
"name" : "" // Optional, used with popup nested element "name" : "" // Optional, used with popup nested element
<signals_description>, // Optional, used with popup nested element <signals_description>, // Optional, used with popup nested element
"text" : "Text to be displayed", "text" : "Text to be displayed",
"style" : "CSS style to be applied", // Optional "style" : "CSS style to be applied", // Optional
} }
```
<icon_object> is defined as: <icon_object> is defined as:
```js
"icon" : { "icon" : {
"path" : "Icon path", "path" : "Icon path",
"style" : "CSS style to be applied", // Optional "style" : "CSS style to be applied", // Optional
} }
```
<picture_object> is defined as: <picture_object> is defined as:
```js
"picture" : { "picture" : {
"name" : "" // Optional, used with popup nested element "name" : "" // Optional, used with popup nested element
<signals_description>, // Optional, used with popup nested element <signals_description>, // Optional, used with popup nested element
@@ -87,18 +101,21 @@ DBUS object
"width" : XXX, // Optional : Force width in pixels, can be -1 for defaut value "width" : XXX, // Optional : Force width in pixels, can be -1 for defaut value
"height" : XXX, // Optional : Force height in pixels, can be -1 for defaut value "height" : XXX, // Optional : Force height in pixels, can be -1 for defaut value
} }
```
<popup_object is defuned as : <popup_object> is defined as:
```js
"popup" : { "popup" : {
"items": [<text_objects> and/or <picture_objects>] "items": [<text_objects> and/or <picture_objects>]
} }
```
Signals description Signals description
=================== ===================
Signals can be : Signals can be :
```js
"on-click" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-click" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
"on-dblclick" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-dblclick" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
"on-rightclick" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-rightclick" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
@@ -106,10 +123,11 @@ Signals can be :
"on-enter" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-enter" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
"on-leave" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-leave" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
"on-scroll" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"] "on-scroll" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
```
Targets : Targets :
* signal : emit a signal to desktop application * signal : Emit a signal to desktop application
* delete : Delete item * delete : Delete item
* open-popup : Open the popup if there is one * open-popup : Open the popup if there is one
* close-popup : Close the popup if there is one * close-popup : Close the popup if there is one
@@ -142,11 +160,15 @@ Example
You can test it with command line: You can test it with command line:
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.notify '{"group":"new","items":[{"name":"first","on-click":"toggle-popup","text":{"text":"Hello","style":"color:green"},"popup":{"items":[{"picture":{"path":"/tmp/cat2.jpg"}}]}}]}' ```bash
# Create new group and add items
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.notify '{"group":"new","items":[{"name":"first","on-click":"toggle-popup","text":{"text":"Hello","style":"color:green"},"popup":{"items":[{"picture":{"path":"/tmp/cat.jpg"}}]}}]}'
# Delete group
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}' gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}'
```
Python examples is available Python examples are available @ https://indefero.soutade.fr/p/genericmonitor/source/tree/master/examples
Development Development
@@ -154,12 +176,15 @@ Development
After doing code update, you can test it within a nested window. In other cases you have to restart GNOME. After doing code update, you can test it within a nested window. In other cases you have to restart GNOME.
```bash
dbus-run-session -- gnome-shell --nested [--wayland] dbus-run-session -- gnome-shell --nested [--wayland]
```
To see log & errors: To see log & errors:
```bash
journalctl /usr/bin/gnome-shell journalctl /usr/bin/gnome-shell
```
Licence Licence
------- -------

View File

@@ -1,47 +1,47 @@
<interface name="com.soutade.GenericMonitor"> <interface name="com.soutade.GenericMonitor">
<!-- Functions --> <!-- Functions -->
<method name="notify"> <method name="notify">
<arg type="s" direction="in" /> <arg name="parameters" type="s" direction="in" />
</method> </method>
<method name="deleteItems"> <method name="deleteItems">
<arg type="s" direction="in" /> <arg name="items" type="s" direction="in" />
</method> </method>
<method name="deleteGroups"> <method name="deleteGroups">
<arg type="s" direction="in" /> <arg name="groups" type="s" direction="in" />
</method> </method>
<method name="openPopup"> <method name="openPopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<method name="closePopup"> <method name="closePopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<method name="togglePopup"> <method name="togglePopup">
<arg type="s" direction="in" /> <arg name="popup" type="s" direction="in" />
</method> </method>
<!-- Events --> <!-- Events -->
<signal name="onClick"> <signal name="onClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onRightClick"> <signal name="onRightClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onDblClick"> <signal name="onDblClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onRightDblClick"> <signal name="onRightDblClick">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onScrollUp"> <signal name="onScrollUp">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onScrollDown"> <signal name="onScrollDown">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onEnter"> <signal name="onEnter">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<signal name="onLeave"> <signal name="onLeave">
<arg type="s" direction="out" /> <arg name="fullName" type="s" direction="out" />
</signal> </signal>
<!-- Activate/Deactivate signals --> <!-- Activate/Deactivate signals -->
<signal name="onActivate"> <signal name="onActivate">

85
examples/gmail.py Normal file
View File

@@ -0,0 +1,85 @@
#
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Setup : https://developers.google.com/gmail/api/quickstart/python
# Need to have credentials.json were you call main script
#
# File imported from https://github.com/googleworkspace/python-samples/blob/main/gmail/quickstart/quickstart.py
#
from __future__ import print_function
import os.path
import json
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
creds = None
def _initCreds():
global creds
if creds: return
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('./token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
data = {}
data['refresh_token'] = creds.refresh_token
data['client_id'] = creds.client_id
data['client_secret'] = creds.client_secret
data['token_uri'] = creds.token_uri
data['id_token'] = creds.id_token
token.write(json.dumps(data))
def getUnreadMails():
"""
Get number of unread threads (that may contain multiple messages)
"""
_initCreds()
service = build('gmail', 'v1', credentials=creds)
pageToken = ''
threads = set()
while True:
results = service.users().messages().list(userId='me', labelIds=['UNREAD'],\
includeSpamTrash=False, pageToken=pageToken)\
.execute()
if not 'messages' in results.keys(): continue
threads = threads.union(set([k['threadId'] for k in results['messages']]))
# Loop over all result pages (100 results per page by default)
pageToken = results.get('nextPageToken', '')
if not pageToken: break
return len(threads)

View File

@@ -23,11 +23,11 @@ import time
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
import xml.dom.minidom import xml.dom.minidom
import getpass
from threading import Thread from threading import Thread
from signal import signal, SIGINT from signal import signal, SIGINT
import sys import sys
from genericmonitor import * from genericmonitor import *
from gmail import getUnreadMails
PURPLE_CONV_UPDATE_UNSEEN = 4 PURPLE_CONV_UPDATE_UNSEEN = 4
PURPLE_MESSAGE_SEND = 0 PURPLE_MESSAGE_SEND = 0
@@ -59,22 +59,16 @@ class PidginConversation:
class EventThread(Thread,GenericMonitor): class EventThread(Thread,GenericMonitor):
SLEEP_TIME = 30 SLEEP_TIME = 30
MAIL_ADDRESS='XXX@gmail.com'
def stop(self): def stop(self):
self._stopLoop = True self._stopLoop = True
self.stopMainLoop() self.stopMainLoop()
def _getMail(self): def _getMail(self):
address = "https://mail.google.com/mail/feed/atom"
auth = HTTPBasicAuth(self.MAIL_ADDRESS, self._mail_password)
req = requests.get(address, auth=auth)
text = '' text = ''
style = '' style = ''
if req.status_code == requests.codes.ok:
dom = xml.dom.minidom.parseString(req.text)
try: try:
nb_messages = int(dom.getElementsByTagName('fullcount')[0].firstChild.nodeValue) nb_messages = getUnreadMails()
if nb_messages == 1: if nb_messages == 1:
text = '1 msg' text = '1 msg'
elif nb_messages > 1: elif nb_messages > 1:
@@ -82,8 +76,6 @@ class EventThread(Thread,GenericMonitor):
style = 'color:white' style = 'color:white'
except Exception as e: except Exception as e:
text = str(e) text = str(e)
else:
text = 'Mail error %d' % (req.status_code)
self.mailWidget.setText(text) self.mailWidget.setText(text)
self.mailWidget.setStyle(style) self.mailWidget.setStyle(style)
@@ -103,8 +95,6 @@ class EventThread(Thread,GenericMonitor):
self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', 'im.pidgin.purple.PurpleInterface')
self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface') self.add_signal_receiver(self.pidginConversationUpdated, 'ConversationUpdated', 'im.pidgin.purple.PurpleInterface')
self._mail_password = getpass.getpass('Enter password for address %s: ' % (self.MAIL_ADDRESS))
self.mailWidget = GenericMonitorTextWidget('') self.mailWidget = GenericMonitorTextWidget('')
mailItem = GenericMonitorItem('mail', [self.mailWidget]) mailItem = GenericMonitorItem('mail', [self.mailWidget])
self.mailGroup = GenericMonitorGroup('Mail', [mailItem]) self.mailGroup = GenericMonitorGroup('Mail', [mailItem])

View File

@@ -43,20 +43,17 @@ class PicturePopup(GenericMonitor):
self.runMainLoop() self.runMainLoop()
def display_next_img(self): def display_next_img(self):
filedata = urllib.request.urlopen('https://source.unsplash.com/random') filedata = urllib.request.urlopen('https://picsum.photos/500/500')
# Get redirected URL without parameters
url = filedata.url.split('?')[0]
filedata = urllib.request.urlopen(url + '?fit=max&width=500&height=500')
datatowrite = filedata.read() datatowrite = filedata.read()
with open('/tmp/cat2.jpg', 'wb') as f: with open('/tmp/cat2.jpg', 'wb') as f:
f.write(datatowrite) f.write(datatowrite)
widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple') widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple')
url_widget = GenericMonitorTextWidget(url, 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here url_widget = GenericMonitorTextWidget('random_pic', 'color:white;font-weight:bold', signals={'on-click':'signal'}) # No name here
picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'}) picture_widget = GenericMonitorPictureWidget('/tmp/cat2.jpg', name='NestedWidget', signals={'on-click':'signal'})
popup = GenericMonitorPopup([url_widget, picture_widget]) popup = GenericMonitorPopup([url_widget, picture_widget])
signals = { signals = {
'on-click':'toggle-popup', 'on-click':'toggle-popup',
# Could also use this behavior # Could also use this behavior [bugged since GNOME 42]
# 'on-enter':'open-popup', # 'on-enter':'open-popup',
# 'on-leave':'close-popup', # 'on-leave':'close-popup',
'on-dblclick':'signal', 'on-dblclick':'signal',

View File

@@ -24,23 +24,22 @@
https://github.com/bananenfisch/RecentItems/blob/master/extension.js https://github.com/bananenfisch/RecentItems/blob/master/extension.js
https://github.com/julio641742/gnome-shell-extension-reference/blob/master/tutorials/POPUPMENU-EXTENSION.md https://github.com/julio641742/gnome-shell-extension-reference/blob/master/tutorials/POPUPMENU-EXTENSION.md
https://gjs-docs.gnome.org/st10~1.0_api/st.widget https://gjs-docs.gnome.org/st10~1.0_api/st.widget
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panelMenu.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panelMenu.js
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/popupMenu.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/popupMenu.js
https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panel.js https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/panel.js
*/ */
const St = imports.gi.St; import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
const Gio = imports.gi.Gio; import St from 'gi://St';
const Lang = imports.lang; import Gio from 'gi://Gio';
const GLib = imports.gi.GLib import GLib from 'gi://GLib';
const Main = imports.ui.main; import Clutter from 'gi://Clutter';
const Mainloop = imports.mainloop; import GObject from 'gi://GObject';
const Clutter = imports.gi.Clutter; import Pixbuf from 'gi://GdkPixbuf';
const PanelMenu = imports.ui.panelMenu; import Cogl from 'gi://Cogl';
const PopupMenu = imports.ui.popupMenu; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
const GObject = imports.gi.GObject; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
const Pixbuf = imports.gi.GdkPixbuf; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
const Cogl = imports.gi.Cogl;
function hashGet(hash, key, defaultValue) { function hashGet(hash, key, defaultValue) {
@@ -50,22 +49,20 @@ function hashGet(hash, key, defaultValue) {
return defaultValue; return defaultValue;
} }
function log(message) {
global.log('[GenericMontior]', message);
}
class SignalMgt { class SignalMgt {
constructor(item, name, group, dbus, menu) { constructor(item, name, group, dbus, logger, buttonMenu) {
this.name = name; this.name = name;
this.group = group; this.group = group;
this.fullname = this.name + '@' + this.group; this.fullname = this.name + '@' + this.group;
this.dbus = dbus; this.dbus = dbus;
this.menu = menu; this.logger = logger;
this.buttonMenu = buttonMenu;
this.signals = new WeakMap(); this.signals = new WeakMap();
this.widgets = new Array(); this.widgets = new Array();
this.timeouts = new Array();
this.menuOpen = false;
this.nbClicks = 0; this.nbClicks = 0;
this.button = -1; this.button = -1;
@@ -78,9 +75,11 @@ class SignalMgt {
this.onScroll = hashGet(item, 'on-scroll', ''); this.onScroll = hashGet(item, 'on-scroll', '');
} }
destructor() { destroy() {
for(let widgetIdx in this.widgets) for(let widgetIdx in this.widgets)
this.disconnectWidgetSignals(this.widgets[widgetIdx]); this.disconnectWidgetSignals(this.widgets[widgetIdx]);
for(let timeoutIdx in this.timeouts)
GLib.Source.remove(this.timeouts[timeoutIdx]);
} }
updateSignals(item) { updateSignals(item) {
@@ -97,6 +96,7 @@ class SignalMgt {
this.widgets.push(widget); this.widgets.push(widget);
let array = new Array(); let array = new Array();
this.signals.set(widget, array); this.signals.set(widget, array);
widget.set_reactive(true);
let id; let id;
id = widget.connect('enter-event', this._onEnter.bind(this)); id = widget.connect('enter-event', this._onEnter.bind(this));
array.push(id); array.push(id);
@@ -104,8 +104,7 @@ class SignalMgt {
array.push(id); array.push(id);
id = widget.connect('scroll-event', this._onScroll.bind(this)); id = widget.connect('scroll-event', this._onScroll.bind(this));
array.push(id); array.push(id);
widget.set_reactive(true); id = widget.connect('button-release-event', this._clicked.bind(this));
id = widget.connect('button-release-event', Lang.bind(this, this._clicked));
array.push(id); array.push(id);
} }
@@ -117,22 +116,35 @@ class SignalMgt {
} }
_manageEventAction(action, signalName) { _manageEventAction(action, signalName) {
/*
GNOME 48 : onEnter && this.buttonMenu.menuOpen == close popup without onClick event
*/
if (signalName == "onEnter" && this.buttonMenu.menuOpen)
{
this.buttonMenu.menuOpen = false;
return Clutter.EVENT_STOP;
}
if (action === 'open-popup') if (action === 'open-popup')
this.menu.open(true); {
this.buttonMenu.openPopup();
}
else if (action === 'close-popup') else if (action === 'close-popup')
this.menu.close(); {
this.buttonMenu.closePopup();
}
else if (action === 'toggle-popup') else if (action === 'toggle-popup')
this.menu.toggle(); {
else if (action == 'delete') this.buttonMenu.togglePopup();
}
else if (action === 'delete')
this.dbus.deleteItem(this, this.group); this.dbus.deleteItem(this, this.group);
else if (action === 'signal') else if (action === 'signal')
this.dbus.emitSignal(signalName, this.fullname); this.dbus.emitSignal(signalName, this.fullname);
else
return Clutter.EVENT_PROPAGATE; return Clutter.EVENT_PROPAGATE;
}
_manageLeaveEvent() { return Clutter.EVENT_STOP;
this._manageEventAction(this.onLeave);
} }
_doClickCallback() { _doClickCallback() {
@@ -154,11 +166,11 @@ class SignalMgt {
case 'onRightDblClick': action = this.onRightDblClick; break; case 'onRightDblClick': action = this.onRightDblClick; break;
} }
this._manageEventAction(action, signalName);
this.nbClicks = 0; this.nbClicks = 0;
this.button = -1; this.button = -1;
this._manageEventAction(action, signalName);
return false; return false;
} }
@@ -169,11 +181,13 @@ class SignalMgt {
this.button = event.get_button(); this.button = event.get_button();
this.nbClicks = 1; this.nbClicks = 1;
Mainloop.timeout_add(this.dbus.ClutterSettings['double-click-time'], let sourceId = GLib.timeout_add(GLib.G_PRIORITY_DEFAULT,
Lang.bind(this, this._doClickCallback)); this.dbus.ClutterSettings['double-click-time'],
this._doClickCallback.bind(this));
this.timeouts.push(sourceId);
} }
return Clutter.EVENT_PROPAGATE; return Clutter.EVENT_STOP;
} }
_onEnter(/*actor, event*/) { _onEnter(/*actor, event*/) {
@@ -204,10 +218,10 @@ class MyPopupMenuItem extends PopupMenu.PopupBaseMenuItem {
super._init(params); super._init(params);
this.box = new St.BoxLayout({ style_class: 'popup-combobox-item' }); this.box = new St.BoxLayout({ style_class: 'popup-combobox-item' });
this.box.set_vertical(true); this.box.set_orientation(Clutter.Orientation.VERTICAL);
for (let widgetIndex in widgets) for (let widgetIndex in widgets)
this.box.add(widgets[widgetIndex]); this.box.add_child(widgets[widgetIndex]);
this.add_child(this.box); this.add_child(this.box);
} }
@@ -219,14 +233,16 @@ var MonitorWidget = GObject.registerClass({
}, },
class MonitorWidget extends PanelMenu.Button { class MonitorWidget extends PanelMenu.Button {
_init(item, group, dbus, position) { _init(item, group, dbus, logger, position) {
super._init(0.0); super._init(0.0);
this.name = item['name']; this.name = item['name'];
this.group = group; this.group = group;
this.fullname = this.name + '@' + this.group; this.fullname = this.name + '@' + this.group;
this.dbus = dbus; this.dbus = dbus;
this.signalManager = new SignalMgt(item, this.name, group, dbus, this.menu); this.logger = logger;
this.menuItem = null;
this.signalManager = new SignalMgt(item, this.name, group, dbus, logger, this);
this.popup_signals = null; this.popup_signals = null;
this.popup_widgets = null; this.popup_widgets = null;
@@ -359,18 +375,24 @@ class MonitorWidget extends PanelMenu.Button {
openPopup() { openPopup() {
this.menu.open(true); this.menu.open(true);
this.menuOpen = this.menu.isOpen = true;
} }
closePopup() { closePopup() {
this.menu.close(); this.menu.close();
this.menuOpen = this.menu.isOpen = false;
} }
togglePopup() { togglePopup() {
this.menu.toggle(); if (this.menuOpen)
this.closePopup();
else
this.openPopup();
} }
destroy() { destroy() {
this.menu.close(); this.menu.close();
this.signalManager.destroy();
super.destroy(); super.destroy();
} }
@@ -400,7 +422,7 @@ class MonitorWidget extends PanelMenu.Button {
nestedItem = widgetDict['picture']; nestedItem = widgetDict['picture'];
widget = this._createPicture(nestedItem); widget = this._createPicture(nestedItem);
} else { } else {
log('No known widget defined in popup'); this.logger.error('No known widget defined in popup');
} }
if (nestedItem === null) { if (nestedItem === null) {
@@ -411,7 +433,7 @@ class MonitorWidget extends PanelMenu.Button {
const name = hashGet(nestedItem, 'name', ''); const name = hashGet(nestedItem, 'name', '');
this.popup_signals[widget] = new SignalMgt(nestedItem, name, this.popup_signals[widget] = new SignalMgt(nestedItem, name,
this.fullname, this.dbus, this.fullname, this.dbus,
this.menu); this.logger, this);
this.popup_signals[widget].connectWidgetSignals(widget); this.popup_signals[widget].connectWidgetSignals(widget);
this.popup_widgets.push(widget); this.popup_widgets.push(widget);
} }
@@ -436,7 +458,7 @@ class MonitorWidget extends PanelMenu.Button {
__createText(item, isLabel) { __createText(item, isLabel) {
if (!item.hasOwnProperty('text')) { if (!item.hasOwnProperty('text')) {
log('Text must have a \'text\' value'); this.logger.error('Text must have a \'text\' value');
return null; return null;
} }
@@ -476,7 +498,7 @@ class MonitorWidget extends PanelMenu.Button {
_createIcon(item) { _createIcon(item) {
if (!item.hasOwnProperty('path')) { if (!item.hasOwnProperty('path')) {
log('Icon must have a \'path\' value'); this.logger.error('Icon must have a \'path\' value');
return null; return null;
} }
@@ -497,7 +519,7 @@ class MonitorWidget extends PanelMenu.Button {
_createPicture(item) { _createPicture(item) {
if (!item.hasOwnProperty('path')) { if (!item.hasOwnProperty('path')) {
log('Picture must have a \'path\' value'); this.logger.error('Picture must have a \'path\' value');
return null; return null;
} }
@@ -510,9 +532,11 @@ class MonitorWidget extends PanelMenu.Button {
if (typeof(height) === 'string') if (typeof(height) === 'string')
height = parseInt(height, 10); height = parseInt(height, 10);
const img = new Clutter.Image();
const initialPixbuf = Pixbuf.Pixbuf.new_from_file(item['path']); const initialPixbuf = Pixbuf.Pixbuf.new_from_file(item['path']);
img.set_data(initialPixbuf.get_pixels(), const img = St.ImageContent.new_with_preferred_size(initialPixbuf.get_width(),
initialPixbuf.get_height());
img.set_data(global.stage.context.get_backend().get_cogl_context(),
initialPixbuf.get_pixels(),
initialPixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 initialPixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888
: Cogl.PixelFormat.RGB_888, : Cogl.PixelFormat.RGB_888,
initialPixbuf.get_width(), initialPixbuf.get_width(),
@@ -533,14 +557,12 @@ class MonitorWidget extends PanelMenu.Button {
// From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js // From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js
// loads a xml file into an in-memory string // loads a xml file into an in-memory string
function loadInterfaceXml(filename) { function loadInterfaceXml(extension, filename) {
const extension = imports.misc.extensionUtils.getCurrentExtension();
const interfacesDir = extension.dir.get_child('.'); const interfacesDir = extension.dir.get_child('.');
const file = interfacesDir.get_child(filename); const file = interfacesDir.get_child(filename);
let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); let [result, contents] = GLib.file_get_contents(file.get_path());
if (result) { if (result) {
// HACK: The "" + trick is important as hell because file_get_contents returns // HACK: The "" + trick is important as hell because file_get_contents returns
@@ -549,7 +571,10 @@ function loadInterfaceXml(filename) {
// is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
// will spit out a TypeError soon). // will spit out a TypeError soon).
if (contents instanceof Uint8Array) if (contents instanceof Uint8Array)
contents = imports.byteArray.toString(contents); {
const decoder = new TextDecoder();
contents = decoder.decode(contents);
}
const res = `<node>${contents}</node>`; const res = `<node>${contents}</node>`;
return res; return res;
} else { } else {
@@ -558,11 +583,12 @@ function loadInterfaceXml(filename) {
} }
class GenericMonitorDBUS { class GenericMonitorDBUS {
constructor() { constructor(extension) {
this.logger = extension.getLogger();
this.monitor_groups = {}; this.monitor_groups = {};
this.actor_clicked = {}; this.actor_clicked = {};
this.ClutterSettings = Clutter.Settings.get_default(); this.ClutterSettings = Clutter.Settings.get_default();
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml('dbus.xml'), this); this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml(extension, 'dbus.xml'), this);
this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor'); this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor');
this._dbusImpl.emit_signal('onActivate', null); this._dbusImpl.emit_signal('onActivate', null);
} }
@@ -573,19 +599,19 @@ class GenericMonitorDBUS {
_checkParameters(parameters) { _checkParameters(parameters) {
if (!parameters.hasOwnProperty('group')) { if (!parameters.hasOwnProperty('group')) {
log('No group defined'); this.logger.error('No group defined');
return false; return false;
} }
if (!parameters.hasOwnProperty('items')) { if (!parameters.hasOwnProperty('items')) {
log('No items defined'); this.logger.error('No items defined');
return false; return false;
} }
for (let itemIndex in parameters['items']) { for (let itemIndex in parameters['items']) {
const item = parameters['items'][itemIndex]; const item = parameters['items'][itemIndex];
if (!item.hasOwnProperty('name')) { if (!item.hasOwnProperty('name')) {
log('No name defined for item'); this.logger.error('No name defined for item');
return false; return false;
} }
} }
@@ -606,7 +632,7 @@ class GenericMonitorDBUS {
_getItemFromFullName(fullname) { _getItemFromFullName(fullname) {
const splitName = fullname.split('@'); const splitName = fullname.split('@');
if (splitName.length !== 2) { if (splitName.length !== 2) {
log(`Invalid name ${fullname}`); this.logger.error(`Invalid name ${fullname}`);
return null; return null;
} }
if (!this.monitor_groups.hasOwnProperty(splitName[1])) if (!this.monitor_groups.hasOwnProperty(splitName[1]))
@@ -661,7 +687,7 @@ class GenericMonitorDBUS {
if (position >= childrens.length) if (position >= childrens.length)
position = -1; position = -1;
} }
monitorWidget = new MonitorWidget(item, groupName, this, position); monitorWidget = new MonitorWidget(item, groupName, this, this.logger, position);
group.push(monitorWidget); group.push(monitorWidget);
} else { } else {
monitorWidget.update(item); monitorWidget.update(item);
@@ -683,7 +709,7 @@ class GenericMonitorDBUS {
const parameters = JSON.parse(str); const parameters = JSON.parse(str);
if (!parameters.hasOwnProperty('items')) { if (!parameters.hasOwnProperty('items')) {
log('No items defined'); this.logger.error('No items defined');
return false; return false;
} }
@@ -691,7 +717,7 @@ class GenericMonitorDBUS {
let itemName = parameters['items'][itemIndex]; let itemName = parameters['items'][itemIndex];
const splitName = itemName.split('@'); const splitName = itemName.split('@');
if (splitName.length !== 2) { if (splitName.length !== 2) {
log(`Invalid name ${itemName}`); this.logger.error(`Invalid name ${itemName}`);
return false; return false;
} }
itemName = splitName[0]; itemName = splitName[0];
@@ -710,7 +736,7 @@ class GenericMonitorDBUS {
const parameters = JSON.parse(str); const parameters = JSON.parse(str);
if (!parameters.hasOwnProperty('groups')) { if (!parameters.hasOwnProperty('groups')) {
log('No groups defined'); this.logger.error('No groups defined');
return false; return false;
} }
@@ -734,7 +760,7 @@ class GenericMonitorDBUS {
const parameters = JSON.parse(str); const parameters = JSON.parse(str);
if (!parameters.hasOwnProperty('item')) { if (!parameters.hasOwnProperty('item')) {
log('No item defined'); this.logger.error('No item defined');
return false; return false;
} }
@@ -742,7 +768,7 @@ class GenericMonitorDBUS {
if (monitorWidget !== null) if (monitorWidget !== null)
return monitorWidget; return monitorWidget;
else else
log(`Item ${str} not found`); this.logger.error(`Item ${str} not found`);
return null; return null;
} }
@@ -765,7 +791,7 @@ class GenericMonitorDBUS {
monitorWidget.togglePopup(); monitorWidget.togglePopup();
} }
destructor() { destroy() {
this._dbusImpl.emit_signal('onDeactivate', null); this._dbusImpl.emit_signal('onDeactivate', null);
for (let groupIndex in this.monitor_groups) { for (let groupIndex in this.monitor_groups) {
const group = this.monitor_groups[groupIndex]; const group = this.monitor_groups[groupIndex];
@@ -777,26 +803,21 @@ class GenericMonitorDBUS {
} }
} }
class Extension { export default class GenericMonitorExtension extends Extension.Extension {
constructor(...args) {
super(...args);
this.textDBusService = null;
}
enable() { enable() {
this.textDBusService = new GenericMonitorDBUS(); this.textDBusService = new GenericMonitorDBUS(this);
} }
disable() { disable() {
this.textDBusService.destructor(); if (this.textDBusService !== null) {
this.textDBusService.destroy();
delete this.textDBusService; delete this.textDBusService;
this.textDBusService = null;
} }
} }
const extension = new Extension();
function init() {
}
function enable() {
extension.enable();
}
function disable() {
extension.disable();
} }

View File

@@ -2,11 +2,10 @@
"uuid": "generic-monitor@gnome-shell-extensions", "uuid": "generic-monitor@gnome-shell-extensions",
"name": "Generic Monitor", "name": "Generic Monitor",
"description": "Display text & icon on systray using DBUS", "description": "Display text & icon on systray using DBUS",
"version": "5", "version": "18",
"shell-version": [ "shell-version": [
"40", "48",
"3.38", "48.5"
"3.36"
], ],
"url": "http://indefero.soutade.fr/p/genericmonitor" "url": "https://forge.soutade.fr/soutade/GnomeShellGenericMonitor"
} }