48 Commits
v2 ... v19

Author SHA1 Message Date
soutade 15ecfff82b Update metadata.json 2025-11-23 11:52:36 +01:00
soutade e291b79f2c Update metadata.json with Gnome Shell 48.5 2025-11-17 22:58:39 +01:00
Christian-Fritz b4af852a0f Fix formatting in README 2025-07-05 18:10:38 +02:00
soutade 6aa9f15d70 Update extension for GNOME 48 2025-03-31 15:14:59 +02:00
soutade ca11b0e175 Version 15 2024-08-31 16:36:07 +02:00
soutade d0e8f0cde5 Code review from JustPerfection 2024-08-31 13:58:43 +02:00
soutade 66d79cc804 Update for Gnome Shell 45+ 2024-08-29 20:46:21 +02:00
soutade 3ed3394a33 Update metadata.json with Gnome Shell 44 2023-09-25 19:14:35 +02:00
soutade 7cd668af14 Count number of threads instead of unread messages in gmail example 2022-11-20 21:01:41 +01:00
soutade 5690307c63 Update Gmail access with OAuth 2 2022-11-20 20:00:34 +01:00
soutade b1feb004e8 Update metadata.json 2022-10-28 17:20:14 +02:00
soutade 73021a25a1 Update README 2022-10-28 08:09:34 +02:00
soutade 4c73986596 Updates for GNOME 43 2022-10-27 17:54:37 +02:00
soutade d3665f2ff0 Quick update for GNOME 42. There is still some issues with menu. 2022-04-10 14:21:53 +02:00
soutade 00fb6120c9 Update README.md and version to be aligned on Gnome store version 2022-01-07 08:35:13 +01:00
soutade 02d3e8b72a Remove timer resource during SignalMgt destructor 2022-01-06 09:52:11 +01:00
soutade 78254a550e Update support for version 41 in metadata 2022-01-06 09:14:55 +01:00
soutade 7a5f506c23 Remove use of deprecated "lang" package 2021-10-01 09:02:34 +02:00
soutade 1d3328f71e Update support for version 40 in metadata 2021-09-30 16:33:06 +02:00
soutade 45d0cc9d11 Use WeakMap to store objects as key and rename MonitorWidget signals field into signalManager to avoid confusions 2021-09-30 16:32:26 +02:00
soutade 4d6fe82790 Update version 2021-02-02 07:04:45 +01:00
soutade 9943a9075f Popup text must be labels not buttons 2021-01-31 20:18:11 +01:00
soutade 83609b58ab After linter pass 2021-01-29 09:15:40 +01:00
soutade b85ff4a782 Update examples 2021-01-28 10:34:18 +01:00
soutade 30bfc796b9 Update README 2021-01-28 10:34:18 +01:00
soutade 35e34b52f2 Move signal management into external class. It allows to handle signals for popup nested elements. 2021-01-28 10:34:17 +01:00
soutade 1b4430c3ac Update README 2020-11-28 12:14:48 +01:00
soutade ca5426bbde Some spaces changes 2020-11-25 10:53:06 +01:00
soutade c8dc01fe62 Add signal management in _manageEventAction() 2020-11-25 10:53:06 +01:00
soutade fbf138faee Enter/Leave problem fixed with c5b9f25587 2020-11-25 10:53:06 +01:00
soutade b0977da704 Fix some errors 2020-11-25 10:53:06 +01:00
soutade bdf3bac8c4 Update metadata 2020-11-25 10:53:06 +01:00
soutade 5d64f4e8db Update code for Gnome Shell coding rules 2020-11-25 10:53:06 +01:00
soutade 11acefa076 Fix quit script action 2020-11-25 10:53:06 +01:00
soutade 4438709a99 Add Python picture.py example 2020-11-25 10:53:06 +01:00
soutade 389e6a108a Update Python example with new API 2020-11-25 10:53:01 +01:00
soutade d756ef2c1e Update README 2020-11-18 16:20:12 +01:00
soutade 4c52c9fa8e Add management for onDblClick, onRightClick and onRightDblClick events. Before they were put under Click event. 2020-11-18 16:17:58 +01:00
soutade 8a480254cd Add picture into an inner layout in order to avoid parent resize 2020-11-18 16:17:13 +01:00
soutade c5b9f25587 Propagate events. If not, other items doesn't get it 2020-11-18 16:16:06 +01:00
soutade b10b73135d Add onScroll[Up|Down] event 2020-11-12 14:16:17 +01:00
soutade 6b1fe1dd2d Add openPopup(), closePopup() and togglePopup() functions to DBUS interface 2020-11-12 10:42:11 +01:00
soutade d3eb13994a Work on events 2020-11-06 16:22:05 +01:00
soutade bd9e3722a2 Remove useless old code 2020-11-05 15:38:50 +01:00
soutade cf879a2c46 Manage events 2020-11-05 15:36:14 +01:00
soutade 8c709d38d5 Fix positioning and blank spaces 2020-11-04 14:52:43 +01:00
soutade 5458908603 New architecture : create & update works, signals and remove doesn't 2020-11-03 16:59:27 +01:00
soutade 8032ce0245 WIP 2020-11-02 17:03:34 +01:00
9 changed files with 1454 additions and 369 deletions
+122 -24
View File
@@ -7,10 +7,17 @@ This GNOME Shell Extension aims to display information to center box. Using DBUS
Installation
------------
Install it from https://extensions.gnome.org/extension/2826/generic-monitor/
OR
Create a symbolic link from your _.local_ directory and enable extension
ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/
gnome-extensions enable generic-monitor@gnome-shell-extensions
```bash
ln -s $PWD/generic-monitor@gnome-shell-extensions/ ~/.local/share/gnome-shell/extensions/
gnome-extensions enable generic-monitor@gnome-shell-extensions
```
Restart GNOME
@@ -20,21 +27,18 @@ DBUS protocol
All functions read JSON formatted parameters
```js
notify():
{
"group": "groupname",
// item can have text and/or icon, other are optional
// click action can be : "signal" or "delete"
// box property can be : "left", "center" or "right"
"items": [
{
"name":"",
"text":"",
"style":"",
"icon-style":"",
"icon":"",
"on-click":"",
"box":"",
"name" : "",
"box" : ["left"|"center"|"right"], // Optional : center by default
"text" : <text_object>, // Optional
"icon" : <icon_object>, // Optional
"popup" : <popup_object>, // Optional
<signals_description>, // Optional
}, ...
]
}
@@ -50,16 +54,102 @@ All functions read JSON formatted parameters
"groups": ["<groupName>", ...]
}
openPopup():
{
"item": "<itemName>@<groupName>"
}
When text/icon is clicked and on-click parameter is set to "signal",
extension emit one of the following signals :
closePopup():
{
"item": "<itemName>@<groupName>"
}
togglePopup():
{
"item": "<itemName>@<groupName>"
}
```
DBUS object
===========
<text_object> is defined as:
```js
"text" : {
"name" : "" // Optional, used with popup nested element
<signals_description>, // Optional, used with popup nested element
"text" : "Text to be displayed",
"style" : "CSS style to be applied", // Optional
}
```
<icon_object> is defined as:
```js
"icon" : {
"path" : "Icon path",
"style" : "CSS style to be applied", // Optional
}
```
<picture_object> is defined as:
```js
"picture" : {
"name" : "" // Optional, used with popup nested element
<signals_description>, // Optional, used with popup nested element
"path" : "Icon path",
"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
}
```
<popup_object> is defined as:
```js
"popup" : {
"items": [<text_objects> and/or <picture_objects>]
}
```
Signals description
===================
Signals can be :
```js
"on-click" : ["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-rightdblclick" : ["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-scroll" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
```
Targets :
* signal : Emit a signal to desktop application
* delete : Delete item
* open-popup : Open the popup if there is one
* close-popup : Close the popup if there is one
* toggle-popup : Toggle (open/close) the popup if there is one
Signal names emit when action "signal" is specified:
* onClick
* onRightClick
* onDblClick
* onRightClick
* onRightDblClick
* onEnter
* onLeave
* onScrollUp
* onScrollDown
Other signals are available when extension is activated/deactivated :
Each signal is sent with one parameter: "<itemName>@<groupName>"
For popup nested elements, parameter is "<nestedName>@<itemName>@<groupName>"
where nestedName can be empty if not defined by user
Other signals are available when extension is activated/deactivated:
* onActivate
* onDeactivate
@@ -68,12 +158,17 @@ Other signals are available when extension is activated/deactivated :
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","text":"Hello","style":"color:green"}]}'
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}'
```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"}}]}}]}'
Python example is available
# Delete group
gdbus call --session --dest org.gnome.Shell --object-path /com/soutade/GenericMonitor --method com.soutade.GenericMonitor.deleteGroups '{"groups":["new"]}'
```
Python examples are available @ https://indefero.soutade.fr/p/genericmonitor/source/tree/master/examples
Development
@@ -81,12 +176,15 @@ Development
After doing code update, you can test it within a nested window. In other cases you have to restart GNOME.
dbus-run-session -- gnome-shell --nested [--wayland]
```bash
dbus-run-session -- gnome-shell --nested [--wayland]
```
To see log & errors :
journalctl /usr/bin/gnome-shell
To see log & errors:
```bash
journalctl /usr/bin/gnome-shell
```
Licence
-------
+29 -8
View File
@@ -1,26 +1,47 @@
<interface name="com.soutade.GenericMonitor">
<!-- Functions -->
<method name="notify">
<arg type="s" direction="in" />
<arg name="parameters" type="s" direction="in" />
</method>
<method name="deleteItems">
<arg type="s" direction="in" />
<arg name="items" type="s" direction="in" />
</method>
<method name="deleteGroups">
<arg type="s" direction="in" />
<arg name="groups" type="s" direction="in" />
</method>
<!-- Click events -->
<method name="openPopup">
<arg name="popup" type="s" direction="in" />
</method>
<method name="closePopup">
<arg name="popup" type="s" direction="in" />
</method>
<method name="togglePopup">
<arg name="popup" type="s" direction="in" />
</method>
<!-- Events -->
<signal name="onClick">
<arg type="s" direction="out" />
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onRightClick">
<arg type="s" direction="out" />
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onDblClick">
<arg type="s" direction="out" />
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onRightDblClick">
<arg type="s" direction="out" />
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onScrollUp">
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onScrollDown">
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onEnter">
<arg name="fullName" type="s" direction="out" />
</signal>
<signal name="onLeave">
<arg name="fullName" type="s" direction="out" />
</signal>
<!-- Activate/Deactivate signals -->
<signal name="onActivate">
+413 -29
View File
@@ -20,10 +20,17 @@ import gi
gi.require_version('Gst', '1.0')
from gi.repository import GObject, GLib, Gst
#
# DBUS interface
#
class GenericMonitor:
"""
Class that manage DBUS communication with GNOME generic monitor addon.
You have to subclass it
"""
def setupMonitor(self):
""" Setup DBUS stuff (equivalent to constructor) """
self._activated = True
self._encoder = json.JSONEncoder()
self._dbus_interface = 'com.soutade.GenericMonitor'
@@ -33,108 +40,485 @@ class GenericMonitor:
self.systray_proxy = self._dbus.get_object('org.gnome.Shell', '/com/soutade/GenericMonitor')
self._dbus.add_signal_receiver(self.onClick, 'onClick', self._dbus_interface)
self._dbus.add_signal_receiver(self.onDblClick, 'onDblClick', self._dbus_interface)
self._dbus.add_signal_receiver(self.onRightClick, 'onRightClick', self._dbus_interface)
self._dbus.add_signal_receiver(self.onDblRightClick, 'onDblRightClick', self._dbus_interface)
self.add_signal_receiver(self.onClick, 'onClick')
self.add_signal_receiver(self.onDblClick, 'onDblClick')
self.add_signal_receiver(self.onRightClick, 'onRightClick')
self.add_signal_receiver(self.onRightDblClick, 'onRightDblClick')
self.add_signal_receiver(self.onScrollUp, 'onScrollUp')
self.add_signal_receiver(self.onScrollDown, 'onScrollDown')
self.add_signal_receiver(self.onEnter, 'onEnter')
self.add_signal_receiver(self.onLeave, 'onLeave')
self._dbus.add_signal_receiver(self.onActivate, 'onActivate', self._dbus_interface)
self._dbus.add_signal_receiver(self.onDeactivate, 'onDeactivate', self._dbus_interface)
self.add_signal_receiver(self.onActivate, 'onActivate')
self.add_signal_receiver(self.onDeactivate, 'onDeactivate')
def runMainLoop(self):
""" Start infinite loop that allows to send and receive events and functions """
self._mainLoop = GLib.MainLoop()
self._mainLoop.run()
def stopMainLoop(self):
""" Stop infinite main loop """
self._mainLoop.quit()
# Generic Monitor functions
def notify(self, group):
""" Send notify() function
Parameters
----------
group : GenericMonitorGroup
group to notify
"""
if self._activated:
if type(group) == GenericMonitorGroup:
group = group.getValues()
self.systray_proxy.notify(self._encoder.encode(group), dbus_interface=self._dbus_interface)
def deleteItems(self, items):
""" Send deleteItems() function
Parameters
----------
items : list of str (<itemName>@<groupName>)
items to delete
"""
if self._activated:
items = {'items':items}
self.systray_proxy.deleteItems(self._encoder.encode(items), dbus_interface=self._dbus_interface)
def deleteGroups(self, groups):
""" Send deleteGroups() function
Parameters
----------
groups : list of str (<groupName>)
groups to delete
"""
if self._activated:
groups = {'groups':groups}
self.systray_proxy.deleteGroups(self._encoder.encode(groups), dbus_interface=self._dbus_interface)
def openPopup(self, item):
""" Send openPopup() function
Parameters
----------
item : str (<itemName>@<groupName>)
Open popup (if there is one) of item
"""
if self._activated:
item = {'item':item}
self.systray_proxy.openPopup(self._encoder.encode(item), dbus_interface=self._dbus_interface)
def closePopup(self, item):
""" Send closePopup() function
Parameters
----------
item : str (<itemName>@<groupName>)
Close popup (if there is one) of item
"""
if self._activated:
item = {'item':item}
self.systray_proxy.closePopup(self._encoder.encode(item), dbus_interface=self._dbus_interface)
def togglePopup(self, item):
""" Send togglePopup() function
Parameters
----------
item : str (<itemName>@<groupName>)
Toggle popup (if there is one) of item
"""
if self._activated:
item = {'item':item}
self.systray_proxy.togglePopup(self._encoder.encode(item), dbus_interface=self._dbus_interface)
# Generic Monitor signals
def onClick(self, sender):
""" onClick event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onRightClick(self, sender):
""" onRightClick event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onDblClick(self, sender):
""" onDblClick event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onDblRightClick(self, sender):
def onRightDblClick(self, sender):
""" onRightDblClick event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onEnter(self, sender):
""" onEnter event (mouse enter in item)
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onLeave(self, sender):
""" onLeave event (mouse leave item)
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onScrollUp(self, sender):
""" onScrollUp event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onScrollDown(self, sender):
""" onScrollDown event
Parameters
----------
sender : str (<itemName>@<groupName>)
Sender which event has been raised
"""
pass
def onActivate(self):
""" onActivate event (addon activated)
"""
self._activated = True
def onDeactivate(self):
""" onDeactivate event (addon deactivated)
"""
self._activated = False
# DBUS method
def add_signal_receiver(self, callback, signalName, interface):
""" Add callback when DBUS signal is raised
Parameters
----------
callback : function
Callback raised on DBUS signal
signalName : str
Name of DBUS signal
interface : str
Name of DBUS interface
"""
def add_signal_receiver(self, callback, signalName, interface=None):
if not interface:
interface = self._dbus_interface
self._dbus.add_signal_receiver(callback, signalName, interface)
class GenericMonitorItem:
def __init__(self, name, text='', style='', icon='', iconStyle='', onClick='', box=''):
#
# Item stuff
#
class GenericMonitorGenericWidget:
""" Generic widget class, parent of all widgets
"""
def __init__(self, style='', name='', signals={}):
"""
Parameters
----------
name : str, optional
Widget name
signals : dictionary, optional
Dictionary of signals and their action
"""
self.valuesToMap = ['name', 'style']
self.mapValues = {}
self.mapName = ''
self.style = style
self.name = name
self.signals = signals
def setStyle(self, style):
self.style = style
def _toMap(self):
""" Return dictionary of class elements to send to addon
"""
self.mapValues = {}
for p in self.valuesToMap:
if self.__dict__[p]:
self.mapValues[p] = self.__dict__[p]
for (name, value) in self.signals.items():
self.mapValues[name] = value
return {self.mapName:self.mapValues}
class GenericMonitorTextWidget(GenericMonitorGenericWidget):
""" Text widget
"""
def __init__(self, text, style='', name='', signals={}):
"""
Parameters
----------
text : str
Text to display
style : str, optional
CSS style
name : str, optional
Widget name
signals : dictionary, optional
Dictionary of signals and their action
"""
super().__init__(style, name, signals)
self.valuesToMap += ['text']
self.mapName = 'text'
self.text = text
self.style= style
self.icon = icon
self.iconStyle = iconStyle
self.onClick = onClick
def setText(self, text):
self.text = text
class GenericMonitorIconWidget(GenericMonitorGenericWidget):
""" Icon widget
"""
def __init__(self, path, style=''):
"""
Parameters
----------
path : str
Icon path
style : str, optional
CSS style
"""
super().__init__(style=style)
self.valuesToMap += ['path']
self.mapName = 'icon'
self.path = path
self.style = style
def setPath(self, path):
self.path = path
class GenericMonitorPictureWidget(GenericMonitorGenericWidget):
""" Picture widget
"""
def __init__(self, path, style='', width=-1, height=-1, name='', signals={}):
"""
Parameters
----------
path : str
Picture path
style : str, optional
CSS style
width : int, optional
Width of displayed picture (-1 for default width)
height : int, optional
Width of displayed picture (-1 for default width)
name : str, optional
Widget name
signals : dictionary, optional
Dictionary of signals and their action
"""
super().__init__(style, name, signals)
self.valuesToMap += ['path', 'width', 'height']
self.mapName = 'picture'
self.path = path
self.width = width
self.height = height
def setPath(self, path):
self.path = path
def setWidth(self, width):
self.width = width
def setHeight(self, height):
self.height = height
class GenericMonitorPopup(GenericMonitorGenericWidget):
""" Popup of current item
"""
def __init__(self, items):
"""
Parameters
----------
items : list of GenericMonitorTextWidget and GenericMonitorPictureWidget
List of items (text or picture)
"""
self.valuesToMap = ('items',)
self.mapName = 'popup'
self.items = items
def _toMap(self):
self.mapValues = {}
self.mapValues['items'] = []
for item in self.items:
self.mapValues['items'] += [item._toMap()]
return {self.mapName:self.mapValues}
def clear(self):
""" Clear items list
"""
self.items = []
def setItems(self, items):
self.items = items
class GenericMonitorItem:
""" Addon item that will be displayed in status bar
"""
def __init__(self, name, items=[], signals={}, popup=None, box='center'):
"""
Parameters
----------
name : str
Item name
items : list of GenericMonitorTextWidget and GenericMonitorIconWidget, optional
List of items (text or icon)
signals : dictionary, optional
Dictionary of signals and their action
"on-click" : ["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-rightdblclick" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
"on-click" : ["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-scroll" : ["signal"|"delete"|"open-popup"|"close-popup"|"toggle-popup"]
popup : GenericMonitorPopup, optional
Popup to be displayed
box : str, optional
Box were to put items : left, center (default), or right
"""
self.name = name
self.items = items
self.signals = signals
self.popup = popup
self.box = box
self.group = ''
self._checkValues()
def _checkValues(self):
if self.onClick and not self.onClick in ('signal', 'delete'):
raise ValueError('Invalid onClick value')
if not self.name:
raise ValueError('Need a name')
if len(self.items) > 2:
raise ValueError('Maximum 2 items can be displayed')
for (name, value) in self.signals.items():
if not name in ('on-click', 'on-dblclick', 'on-rightclick', 'on-rightdblclick',
'on-enter', 'on-leave', 'on-scroll'):
raise ValueError('Invalid signal name ' + name)
if not value in ('signal', 'delete', 'open-popup', 'close-popup', 'toggle-popup'):
raise ValueError('Invalid signal value ' + value)
for item in self.items:
if not isinstance(item, GenericMonitorGenericWidget):
raise ValueError('Invalid item ' + item)
if self.popup and not isinstance(self.popup, GenericMonitorPopup):
raise ValueError('Invalid popup object')
if self.box and not self.box in ('left', 'center', 'right'):
raise ValueError('Invalid box value')
def toMap(self):
myMap = {"name":self.name}
for p in ('text', 'style', 'icon', 'box'):
def setGroup(self, group):
""" Set current group (automatically done when added in a group)
Parameters
----------
group : str
Group name
"""
self.group = group
def getName(self):
return self.name
def getFullName(self):
""" return full name used by addon
"""
return '%s@%s' % (self.name, self.group)
def _toMap(self):
myMap = {}
for p in ('name', 'box'):
if self.__dict__[p]:
myMap[p] = self.__dict__[p]
if self.iconStyle:
myMap['icon-style'] = self.iconStyle
if self.onClick:
myMap['on-click'] = self.onClick
for item in self.items:
item._toMap()
myMap[item.mapName] = item.mapValues
if self.popup:
self.popup._toMap()
myMap['popup'] = self.popup.mapValues
for (name, value) in self.signals.items():
myMap[name] = value
return [myMap]
class GenericMonitorGroup:
""" Group of items
"""
def __init__(self, name, items=[]):
"""
Parameters
----------
name : str
Group name
items : list of GenericMonitorItem, optional
List of items
"""
self.name = name
self.items = []
if type(items) != list:
self.items = [items]
self.addItem(items)
else:
self.items = items
self.addItems(items)
def addItem(self, item):
""" Add item into the groupw
Parameters
----------
item : GenericMonitorItem
Item to add
"""
item.setGroup(self.name)
self.items.append(item)
def addItems(self, items):
""" Add items into the group
Parameters
----------
items : list of GenericMonitorItem
Items to add
"""
for item in items:
self.addItem(item)
def clear(self):
""" Clear items list
"""
for item in items:
item.setGroup('')
self.items = []
def getValues(self):
""" Returns group and its items in addon format
"""
res = {'group': self.name, 'items':[]}
for item in self.items:
res['items'] += item.toMap()
res['items'] += item._toMap()
return res
def __str__(self):
+85
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)
+25 -28
View File
@@ -23,11 +23,11 @@ import time
import requests
from requests.auth import HTTPBasicAuth
import xml.dom.minidom
import getpass
from threading import Thread
from signal import signal, SIGINT
import sys
from genericmonitor import GenericMonitor, GenericMonitorGroup, GenericMonitorItem
from genericmonitor import *
from gmail import getUnreadMails
PURPLE_CONV_UPDATE_UNSEEN = 4
PURPLE_MESSAGE_SEND = 0
@@ -59,34 +59,27 @@ class PidginConversation:
class EventThread(Thread,GenericMonitor):
SLEEP_TIME = 30
MAIL_ADDRESS='XXX@gmail.com'
def stop(self):
self._stopLoop = True
self.stopMainLoop()
def _getMail(self):
mailItem = GenericMonitorItem('mail')
mailGroup = GenericMonitorGroup('Mail', [mailItem])
text = ''
style = ''
try:
nb_messages = getUnreadMails()
if nb_messages == 1:
text = '1 msg'
elif nb_messages > 1:
text = '%d msgs' % (nb_messages)
style = 'color:white'
except Exception as e:
text = str(e)
address = "https://mail.google.com/mail/feed/atom"
auth = HTTPBasicAuth(self.MAIL_ADDRESS, self._mail_password)
req = requests.get(address, auth=auth)
if req.status_code == requests.codes.ok:
dom = xml.dom.minidom.parseString(req.text)
try:
nb_messages = int(dom.getElementsByTagName('fullcount')[0].firstChild.nodeValue)
if nb_messages == 1:
mailItem.text = '1 msg'
elif nb_messages > 1:
mailItem.text = '%d msgs' % (nb_messages)
mailItem.style = 'color:white'
except Exception as e:
mailItem.text = str(e)
else:
mailItem.text = 'Mail error %d' % (req.status_code)
self.notify(mailGroup)
self.mailWidget.setText(text)
self.mailWidget.setStyle(style)
self.notify(self.mailGroup)
def getEvents(self):
self._getMail()
@@ -102,7 +95,13 @@ class EventThread(Thread,GenericMonitor):
self.add_signal_receiver(self.pidginMessageWrote, 'WroteChatMsg', '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('')
mailItem = GenericMonitorItem('mail', [self.mailWidget])
self.mailGroup = GenericMonitorGroup('Mail', [mailItem])
icon = GenericMonitorIconWidget('/usr/share/icons/hicolor/22x22/apps/pidgin.png', 'icon-size:22px')
pidginItem = GenericMonitorItem('pidgin', [icon])
self.pidginGroup = GenericMonitorGroup('Pidgin', pidginItem)
while not self._stopLoop:
self.getEvents()
@@ -129,9 +128,7 @@ class EventThread(Thread,GenericMonitor):
pidginConversation.nbMessages = 1
def displayIcon(self):
pidginItem = GenericMonitorItem('pidgin', icon='/usr/share/icons/hicolor/22x22/apps/pidgin.png', iconStyle='icon-size:22px')
pidginGroup = GenericMonitorGroup('Pidgin', pidginItem)
self.notify(pidginGroup)
self.notify(self.pidginGroup)
def pidginConversationUpdated(self, conversation, _type):
if _type != PURPLE_CONV_UPDATE_UNSEEN:
@@ -171,7 +168,7 @@ class EventThread(Thread,GenericMonitor):
def signalHandler(signal_received, frame):
eventThread.stop()
eventThread.join()
groups = {'groups':['Mail', 'Pidgin']}
groups = ['Mail', 'Pidgin']
eventThread.deleteGroups(groups)
sys.exit(0)
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
'''
Display random picture from unsplash.com in a popup
* Click : open/close popup
* Popup item click : display who clicked
* ScrollUp/ScrollDown/Double click : display next picture
* Right click : exit
'''
import urllib.request
import sys
import signal
from genericmonitor import *
class PicturePopup(GenericMonitor):
def __init__(self):
self.scrolling = False
self.item = None
self.imgs_idx = 0
self.setupMonitor()
def run(self):
self.display_next_img()
self.runMainLoop()
def display_next_img(self):
filedata = urllib.request.urlopen('https://picsum.photos/500/500')
datatowrite = filedata.read()
with open('/tmp/cat2.jpg', 'wb') as f:
f.write(datatowrite)
widget = GenericMonitorTextWidget('#%d' % self.imgs_idx, 'color:purple')
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'})
popup = GenericMonitorPopup([url_widget, picture_widget])
signals = {
'on-click':'toggle-popup',
# Could also use this behavior [bugged since GNOME 42]
# 'on-enter':'open-popup',
# 'on-leave':'close-popup',
'on-dblclick':'signal',
'on-rightclick':'signal',
'on-scroll':'signal',
}
self.item = GenericMonitorItem('picturepopup', [widget], signals, popup)
group = GenericMonitorGroup('PicturePopup', [self.item])
self.notify(group)
self.imgs_idx += 1
def _forMe(self, sender):
return str(sender).endswith(self.item.getFullName())
def onClick(self, sender):
if not self._forMe(sender): return
print('Click from {}'.format(sender))
def _onScroll(self, sender):
if not self._forMe(sender): return
if self.scrolling: return
self.scrolling = True
self.display_next_img()
self.scrolling = False
def onScrollUp(self, sender):
self._onScroll(sender)
def onScrollDown(self, sender):
self._onScroll(sender)
def onDblClick(self, sender):
self.onScrollUp(sender)
def onRightClick(self, sender):
if not self._forMe(sender): return
if self.item:
self.deleteItems([self.item.getFullName()])
self.stopMainLoop()
def signal_handler(sig, frame):
picture.deleteGroups(['PicturePopup'])
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
picture = PicturePopup()
picture.run()
+22 -12
View File
@@ -29,7 +29,7 @@ import time
from threading import Thread
from signal import signal, SIGINT
import sys
from genericmonitor import GenericMonitor, GenericMonitorGroup, GenericMonitorItem
from genericmonitor import *
class TimerThread(Thread,GenericMonitor):
@@ -38,20 +38,24 @@ class TimerThread(Thread,GenericMonitor):
self.stopMainLoop()
def _displayTimerValue(self):
item = GenericMonitorItem('timer', onClick='signal', box='right')
group = GenericMonitorGroup('Timer', item)
curValue = self.timers[self.curTimer]
item.text = '%02d:%02d' % (int(curValue/60)%60, curValue%60)
text = '%02d:%02d' % (int(curValue/60)%60, curValue%60)
if curValue >= (60*60):
item.text = '%02d:%s' % (int(curValue/(60*60)), item.text)
text = '%02d:%s' % (int(curValue/(60*60)), text)
if self.curTimer == 0:
style = 'color:white'
if curValue > (60*60):
style += ';background-color:red'
if self.timerPaused and curValue:
style = 'color:black;background-color:white'
else:
style = 'color:white'
if curValue >= (60*60):
style += ';background-color:red'
else:
style = 'color:#215D9C'
item.style = style
self.notify(group)
self.textWidget.setText(text)
self.textWidget.setStyle(style)
self.notify(self.monitorGroup)
def run(self):
self.setupMonitor()
@@ -59,6 +63,12 @@ class TimerThread(Thread,GenericMonitor):
self.curTimer = 0
self.timerPaused = False
self._stopLoop = False
self.textWidget = GenericMonitorTextWidget('')
signals = {'on-click':'signal','on-dblclick':'signal','on-rightclick':'signal'}
self.monitorItem = GenericMonitorItem('timer', [self.textWidget], signals, box='right')
self.monitorGroup = GenericMonitorGroup('Timer', self.monitorItem)
while not self._stopLoop:
time.sleep(1)
if not self.timerPaused:
@@ -66,7 +76,7 @@ class TimerThread(Thread,GenericMonitor):
self._displayTimerValue()
def _forMe(self, sender):
return sender == 'timer@Timer'
return sender == self.monitorItem.getFullName()
def onClick(self, sender):
if not self._forMe(sender): return
@@ -103,7 +113,7 @@ class TimerThread(Thread,GenericMonitor):
def signalHandler(signal_received, frame):
timerThread.stop()
timerThread.join()
groups = {'groups':['Timer']}
groups = ['Timer']
timerThread.deleteGroups(groups)
sys.exit(0)
+652 -264
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -2,10 +2,9 @@
"uuid": "generic-monitor@gnome-shell-extensions",
"name": "Generic Monitor",
"description": "Display text & icon on systray using DBUS",
"version": "2",
"version": "19",
"shell-version": [
"3.36",
"3.34"
"48",
],
"url": "http://indefero.soutade.fr/p/genericmonitor"
"url": "https://forge.soutade.fr/soutade/GnomeShellGenericMonitor"
}