Merge pull request '1047' into v0.6

- notifications moved into plugins
- improvements in setup.py
- remove obsolete imports
This commit is contained in:
Peter Šurda 2017-09-21 13:30:53 +02:00
commit 4f75dcb9be
Signed by untrusted user: PeterSurda
GPG Key ID: 0C5F50C0B5F37D87
9 changed files with 443 additions and 315 deletions

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
import shutil
try: try:
from setuptools import setup, Extension from setuptools import setup, Extension
from setuptools.command.install import install from setuptools.command.install import install
@ -167,6 +168,7 @@ def prereqToPackages():
print packageName[package].get('description') print packageName[package].get('description')
def compilerToPackages(): def compilerToPackages():
if not detectOS() in compiling: if not detectOS() in compiling:
return return
@ -202,6 +204,14 @@ class InstallCmd(install):
except (EOFError, NameError): except (EOFError, NameError):
pass pass
# prepare icons directories
os.makedirs('desktop/icons/scalable')
shutil.copyfile(
'desktop/can-icon.svg', 'desktop/icons/scalable/pybitmessage.svg')
os.makedirs('desktop/icons/24x24')
shutil.copyfile(
'desktop/icon24.png', 'desktop/icons/24x24/pybitmessage.png')
return install.run(self) return install.run(self)
@ -253,8 +263,11 @@ if __name__ == "__main__":
#keywords='', #keywords='',
install_requires=installRequires, install_requires=installRequires,
extras_require={ extras_require={
'gir': ['pygobject'],
'qrcode': ['qrcode'], 'qrcode': ['qrcode'],
'pyopencl': ['pyopencl'] 'pyopencl': ['pyopencl'],
'notify2': ['notify2'],
'sound:platform_system=="Windows"': ['winsound']
}, },
classifiers=[ classifiers=[
"License :: OSI Approved :: MIT License" "License :: OSI Approved :: MIT License"
@ -271,16 +284,39 @@ if __name__ == "__main__":
'translations/*.ts', 'translations/*.qm', 'translations/*.ts', 'translations/*.qm',
'images/*.png', 'images/*.ico', 'images/*.icns' 'images/*.png', 'images/*.ico', 'images/*.icns'
]}, ]},
data_files=[
('share/applications/',
['desktop/pybitmessage.desktop']),
('share/icons/hicolor/scalable/apps/',
['desktop/icons/scalable/pybitmessage.svg']),
('share/icons/hicolor/24x24/apps/',
['desktop/icons/24x24/pybitmessage.png'])
],
ext_modules=[bitmsghash], ext_modules=[bitmsghash],
zip_safe=False, zip_safe=False,
entry_points={ entry_points={
'gui.menu': [ 'bitmessage.gui.menu': [
'popMenuYourIdentities.qrcode = ' 'popMenuYourIdentities.qrcode = '
'pybitmessage.plugins.qrcodeui [qrcode]' 'pybitmessage.plugins.qrcodeui [qrcode]'
], ],
# 'console_scripts': [ 'bitmessage.notification.message': [
# 'pybitmessage = pybitmessage.bitmessagemain:main' 'notify2 = pybitmessage.plugins.notification_notify2'
# ] '[gir, notify2]'
],
'bitmessage.notification.sound': [
'theme.canberra = pybitmessage.plugins.sound_canberra',
'file.gstreamer = pybitmessage.plugins.sound_gstreamer'
'[gir]',
'file.fallback = pybitmessage.plugins.sound_playfile'
'[sound]'
],
'bitmessage.indicator': [
'libmessaging ='
'pybitmessage.plugins.indicator_libmessaging [gir]'
],
# 'console_scripts': [
# 'pybitmessage = pybitmessage.bitmessagemain:main'
# ]
}, },
scripts=['src/pybitmessage'], scripts=['src/pybitmessage'],
cmdclass={'install': InstallCmd} cmdclass={'install': InstallCmd}

View File

@ -1,21 +1,10 @@
from debug import logger from debug import logger
withMessagingMenu = False
try:
import gi
gi.require_version('MessagingMenu', '1.0')
from gi.repository import MessagingMenu
gi.require_version('Notify', '0.7')
from gi.repository import Notify
withMessagingMenu = True
except (ImportError, ValueError):
MessagingMenu = None
try: try:
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import * from PyQt4.QtCore import *
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtNetwork import QLocalSocket, QLocalServer from PyQt4.QtNetwork import QLocalSocket, QLocalServer
except Exception as err: except Exception as err:
logmsg = 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download it from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\' (without quotes).' logmsg = 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download it from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\' (without quotes).'
logger.critical(logmsg, exc_info=True) logger.critical(logmsg, exc_info=True)
@ -32,7 +21,7 @@ import shared
from bitmessageui import * from bitmessageui import *
from bmconfigparser import BMConfigParser from bmconfigparser import BMConfigParser
import defaults import defaults
from namecoin import namecoinConnection, ensureNamecoinOptions from namecoin import namecoinConnection
from newaddressdialog import * from newaddressdialog import *
from newaddresswizard import * from newaddresswizard import *
from messageview import MessageView from messageview import MessageView
@ -53,31 +42,26 @@ from iconglossary import *
from connect import * from connect import *
import locale import locale
import sys import sys
from time import strftime, localtime, gmtime
import time import time
import os import os
import hashlib import hashlib
from pyelliptic.openssl import OpenSSL from pyelliptic.openssl import OpenSSL
import platform
import textwrap import textwrap
import debug import debug
import random import random
import subprocess
import string import string
import datetime from datetime import datetime, timedelta
from helper_sql import * from helper_sql import *
import helper_search import helper_search
import l10n import l10n
import openclpow import openclpow
import types from utils import str_broadcast_subscribers, avatarize
from utils import *
from collections import OrderedDict
from account import * from account import *
from class_objectHashHolder import objectHashHolder
from class_singleWorker import singleWorker
from dialogs import AddAddressDialog from dialogs import AddAddressDialog
from helper_generic import powQueueSize from helper_generic import powQueueSize
from inventory import Inventory, PendingDownloadQueue, PendingUpload, PendingUploadDeadlineException from inventory import (
Inventory, PendingDownloadQueue, PendingUpload,
PendingUploadDeadlineException)
import knownnodes import knownnodes
import paths import paths
from proofofwork import getPowType from proofofwork import getPowType
@ -87,9 +71,10 @@ import state
from statusbar import BMStatusBar from statusbar import BMStatusBar
import throttle import throttle
from version import softwareVersion from version import softwareVersion
import sound
try: try:
from plugins.plugin import get_plugins from plugins.plugin import get_plugin, get_plugins
except ImportError: except ImportError:
get_plugins = False get_plugins = False
@ -142,24 +127,15 @@ def change_translation(newlocale):
except: except:
logger.error("Failed to set locale to %s", lang, exc_info=True) logger.error("Failed to set locale to %s", lang, exc_info=True)
class MyForm(settingsmixin.SMainWindow): class MyForm(settingsmixin.SMainWindow):
# sound type constants
SOUND_NONE = 0
SOUND_KNOWN = 1
SOUND_UNKNOWN = 2
SOUND_CONNECTED = 3
SOUND_DISCONNECTED = 4
SOUND_CONNECTION_GREEN = 5
# the last time that a message arrival sound was played # the last time that a message arrival sound was played
lastSoundTime = datetime.datetime.now() - datetime.timedelta(days=1) lastSoundTime = datetime.now() - timedelta(days=1)
# the maximum frequency of message sounds in seconds # the maximum frequency of message sounds in seconds
maxSoundFrequencySec = 60 maxSoundFrequencySec = 60
str_chan = '[chan]'
REPLY_TYPE_SENDER = 0 REPLY_TYPE_SENDER = 0
REPLY_TYPE_CHAN = 1 REPLY_TYPE_CHAN = 1
@ -349,6 +325,10 @@ class MyForm(settingsmixin.SMainWindow):
_translate( _translate(
"MainWindow", "Set avatar..."), "MainWindow", "Set avatar..."),
self.on_action_AddressBookSetAvatar) self.on_action_AddressBookSetAvatar)
self.actionAddressBookSetSound = \
self.ui.addressBookContextMenuToolbar.addAction(
_translate("MainWindow", "Set notification sound..."),
self.on_action_AddressBookSetSound)
self.actionAddressBookNew = self.ui.addressBookContextMenuToolbar.addAction( self.actionAddressBookNew = self.ui.addressBookContextMenuToolbar.addAction(
_translate( _translate(
"MainWindow", "Add New Address"), self.on_action_AddressBookNew) "MainWindow", "Add New Address"), self.on_action_AddressBookNew)
@ -868,14 +848,6 @@ class MyForm(settingsmixin.SMainWindow):
self.raise_() self.raise_()
self.activateWindow() self.activateWindow()
# pointer to the application
# app = None
# The most recent message
newMessageItem = None
# The most recent broadcast
newBroadcastItem = None
# show the application window # show the application window
def appIndicatorShow(self): def appIndicatorShow(self):
if self.actionShow is None: if self.actionShow is None:
@ -905,32 +877,19 @@ class MyForm(settingsmixin.SMainWindow):
self.appIndicatorShowOrHideWindow()""" self.appIndicatorShowOrHideWindow()"""
# Show the program window and select inbox tab # Show the program window and select inbox tab
def appIndicatorInbox(self, mm_app, source_id): def appIndicatorInbox(self, item=None):
self.appIndicatorShow() self.appIndicatorShow()
# select inbox # select inbox
self.ui.tabWidget.setCurrentIndex(0) self.ui.tabWidget.setCurrentIndex(0)
selectedItem = None self.ui.treeWidgetYourIdentities.setCurrentItem(
if source_id == 'Subscriptions': self.ui.treeWidgetYourIdentities.topLevelItem(0).child(0)
# select unread broadcast )
if self.newBroadcastItem is not None:
selectedItem = self.newBroadcastItem if item:
self.newBroadcastItem = None self.ui.tableWidgetInbox.setCurrentItem(item)
else:
# select unread message
if self.newMessageItem is not None:
selectedItem = self.newMessageItem
self.newMessageItem = None
# make it the current item
if selectedItem is not None:
try:
self.ui.tableWidgetInbox.setCurrentItem(selectedItem)
except Exception:
self.ui.tableWidgetInbox.setCurrentCell(0, 0)
self.tableWidgetInboxItemClicked() self.tableWidgetInboxItemClicked()
else: else:
# just select the first item
self.ui.tableWidgetInbox.setCurrentCell(0, 0) self.ui.tableWidgetInbox.setCurrentCell(0, 0)
self.tableWidgetInboxItemClicked()
# Show the program window and select send tab # Show the program window and select send tab
def appIndicatorSend(self): def appIndicatorSend(self):
@ -1228,262 +1187,136 @@ class MyForm(settingsmixin.SMainWindow):
self.tray.setContextMenu(m) self.tray.setContextMenu(m)
self.tray.show() self.tray.show()
# Ubuntu Messaging menu object
mmapp = None
# is the operating system Ubuntu?
def isUbuntu(self):
for entry in platform.uname():
if "Ubuntu" in entry:
return True
return False
# When an unread inbox row is selected on then clear the messaging menu
def ubuntuMessagingMenuClear(self, inventoryHash):
# if this isn't ubuntu then don't do anything
if not self.isUbuntu():
return
# has messageing menu been installed
if not withMessagingMenu:
return
# if there are no items on the messaging menu then
# the subsequent query can be avoided
if not (self.mmapp.has_source("Subscriptions") or self.mmapp.has_source("Messages")):
return
queryreturn = sqlQuery(
'''SELECT toaddress, read FROM inbox WHERE msgid=?''', inventoryHash)
for row in queryreturn:
toAddress, read = row
if not read:
if toAddress == str_broadcast_subscribers:
if self.mmapp.has_source("Subscriptions"):
self.mmapp.remove_source("Subscriptions")
else:
if self.mmapp.has_source("Messages"):
self.mmapp.remove_source("Messages")
# returns the number of unread messages and subscriptions # returns the number of unread messages and subscriptions
def getUnread(self): def getUnread(self):
unreadMessages = 0 counters = [0, 0]
unreadSubscriptions = 0
queryreturn = sqlQuery( queryreturn = sqlQuery('''
'''SELECT msgid, toaddress, read FROM inbox where folder='inbox' ''') SELECT msgid, toaddress, read FROM inbox where folder='inbox'
for row in queryreturn: ''')
msgid, toAddress, read = row for msgid, toAddress, read in queryreturn:
try:
if toAddress == str_broadcast_subscribers:
toLabel = str_broadcast_subscribers
else:
toLabel = BMConfigParser().get(toAddress, 'label')
except:
toLabel = ''
if toLabel == '':
toLabel = toAddress
if not read: if not read:
if toLabel == str_broadcast_subscribers: # increment the unread subscriptions if True (1)
# increment the unread subscriptions # else messages (0)
unreadSubscriptions = unreadSubscriptions + 1 counters[toAddress == str_broadcast_subscribers] += 1
else:
# increment the unread messages
unreadMessages = unreadMessages + 1
return unreadMessages, unreadSubscriptions
# show the number of unread messages and subscriptions on the messaging return counters
# menu
def ubuntuMessagingMenuUnread(self, drawAttention):
unreadMessages, unreadSubscriptions = self.getUnread()
# unread messages
if unreadMessages > 0:
self.mmapp.append_source(
"Messages", None, "Messages (" + str(unreadMessages) + ")")
if drawAttention:
self.mmapp.draw_attention("Messages")
# unread subscriptions
if unreadSubscriptions > 0:
self.mmapp.append_source("Subscriptions", None, "Subscriptions (" + str(
unreadSubscriptions) + ")")
if drawAttention:
self.mmapp.draw_attention("Subscriptions")
# initialise the Ubuntu messaging menu
def ubuntuMessagingMenuInit(self):
global withMessagingMenu
# if this isn't ubuntu then don't do anything
if not self.isUbuntu():
return
# has messageing menu been installed
if not withMessagingMenu:
logger.warning('WARNING: MessagingMenu is not available. Is libmessaging-menu-dev installed?')
return
# create the menu server
if withMessagingMenu:
try:
self.mmapp = MessagingMenu.App(
desktop_id='pybitmessage.desktop')
self.mmapp.register()
self.mmapp.connect('activate-source', self.appIndicatorInbox)
self.ubuntuMessagingMenuUnread(True)
except Exception:
withMessagingMenu = False
logger.warning('WARNING: messaging menu disabled')
# update the Ubuntu messaging menu
def ubuntuMessagingMenuUpdate(self, drawAttention, newItem, toLabel):
# if this isn't ubuntu then don't do anything
if not self.isUbuntu():
return
# has messageing menu been installed
if not withMessagingMenu:
logger.warning('WARNING: messaging menu disabled or libmessaging-menu-dev not installed')
return
# remember this item to that the messaging menu can find it
if toLabel == str_broadcast_subscribers:
self.newBroadcastItem = newItem
else:
self.newMessageItem = newItem
# Remove previous messages and subscriptions entries, then recreate them
# There might be a better way to do it than this
if self.mmapp.has_source("Messages"):
self.mmapp.remove_source("Messages")
if self.mmapp.has_source("Subscriptions"):
self.mmapp.remove_source("Subscriptions")
# update the menu entries
self.ubuntuMessagingMenuUnread(drawAttention)
# returns true if the given sound category is a connection sound
# rather than a received message sound
def isConnectionSound(self, category):
if (category is self.SOUND_CONNECTED or
category is self.SOUND_DISCONNECTED or
category is self.SOUND_CONNECTION_GREEN):
return True
return False
# play a sound # play a sound
def playSound(self, category, label): def playSound(self, category, label):
# filename of the sound to be played # filename of the sound to be played
soundFilename = None soundFilename = None
# whether to play a sound or not def _choose_ext(basename):
play = True for ext in sound.extensions:
if os.path.isfile(os.extsep.join([basename, ext])):
return os.extsep + ext
# if the address had a known label in the address book # if the address had a known label in the address book
if label is not None: if label:
# Does a sound file exist for this particular contact? # Does a sound file exist for this particular contact?
if (os.path.isfile(state.appdata + 'sounds/' + label + '.wav') or soundFilename = state.appdata + 'sounds/' + label
os.path.isfile(state.appdata + 'sounds/' + label + '.mp3')): ext = _choose_ext(soundFilename)
soundFilename = state.appdata + 'sounds/' + label if not ext:
category = sound.SOUND_KNOWN
# Avoid making sounds more frequently than the threshold. soundFilename = None
# This suppresses playing sounds repeatedly when there
# are many new messages
if (soundFilename is None and
not self.isConnectionSound(category)):
# elapsed time since the last sound was played
dt = datetime.datetime.now() - self.lastSoundTime
# suppress sounds which are more frequent than the threshold
if dt.total_seconds() < self.maxSoundFrequencySec:
play = False
if soundFilename is None: if soundFilename is None:
# Avoid making sounds more frequently than the threshold.
# This suppresses playing sounds repeatedly when there
# are many new messages
if not sound.is_connection_sound(category):
# elapsed time since the last sound was played
dt = datetime.now() - self.lastSoundTime
# suppress sounds which are more frequent than the threshold
if dt.total_seconds() < self.maxSoundFrequencySec:
return
# the sound is for an address which exists in the address book # the sound is for an address which exists in the address book
if category is self.SOUND_KNOWN: if category is sound.SOUND_KNOWN:
soundFilename = state.appdata + 'sounds/known' soundFilename = state.appdata + 'sounds/known'
# the sound is for an unknown address # the sound is for an unknown address
elif category is self.SOUND_UNKNOWN: elif category is sound.SOUND_UNKNOWN:
soundFilename = state.appdata + 'sounds/unknown' soundFilename = state.appdata + 'sounds/unknown'
# initial connection sound # initial connection sound
elif category is self.SOUND_CONNECTED: elif category is sound.SOUND_CONNECTED:
soundFilename = state.appdata + 'sounds/connected' soundFilename = state.appdata + 'sounds/connected'
# disconnected sound # disconnected sound
elif category is self.SOUND_DISCONNECTED: elif category is sound.SOUND_DISCONNECTED:
soundFilename = state.appdata + 'sounds/disconnected' soundFilename = state.appdata + 'sounds/disconnected'
# sound when the connection status becomes green # sound when the connection status becomes green
elif category is self.SOUND_CONNECTION_GREEN: elif category is sound.SOUND_CONNECTION_GREEN:
soundFilename = state.appdata + 'sounds/green' soundFilename = state.appdata + 'sounds/green'
if soundFilename is not None and play is True: if soundFilename is None:
if not self.isConnectionSound(category): logger.warning("Probably wrong category number in playSound()")
# record the last time that a received message sound was played return
self.lastSoundTime = datetime.datetime.now()
# if not wav then try mp3 format if not sound.is_connection_sound(category):
if not os.path.isfile(soundFilename + '.wav'): # record the last time that a received message sound was played
soundFilename = soundFilename + '.mp3' self.lastSoundTime = datetime.now()
else:
soundFilename = soundFilename + '.wav'
if os.path.isfile(soundFilename): try: # try already known format
if 'linux' in sys.platform: soundFilename += ext
# Note: QSound was a nice idea but it didn't work except (TypeError, NameError):
if '.mp3' in soundFilename: ext = _choose_ext(soundFilename)
gst_available=False if not ext:
try: try: # if no user sound file found try to play from theme
subprocess.call(["gst123", soundFilename], return self._theme_player(category, label)
stdin=subprocess.PIPE, except TypeError:
stdout=subprocess.PIPE) return
gst_available=True
except: soundFilename += ext
logger.warning("WARNING: gst123 must be installed in order to play mp3 sounds")
if not gst_available: self._player(soundFilename)
try:
subprocess.call(["mpg123", soundFilename], # Try init the distro specific appindicator,
stdin=subprocess.PIPE, # for example the Ubuntu MessagingMenu
stdout=subprocess.PIPE) def indicatorInit(self):
gst_available=True def _noop_update(*args, **kwargs):
except: pass
logger.warning("WARNING: mpg123 must be installed in order to play mp3 sounds")
else: try:
try: self.indicatorUpdate = get_plugin('indicator')(self)
subprocess.call(["aplay", soundFilename], except (NameError, TypeError):
stdin=subprocess.PIPE, self.indicatorUpdate = _noop_update
stdout=subprocess.PIPE)
except:
logger.warning("WARNING: aplay must be installed in order to play WAV sounds")
elif sys.platform[0:3] == 'win':
# use winsound on Windows
import winsound
winsound.PlaySound(soundFilename, winsound.SND_FILENAME)
# initialise the message notifier # initialise the message notifier
def notifierInit(self): def notifierInit(self):
if withMessagingMenu: def _simple_notify(
Notify.init('pybitmessage') title, subtitle, category, label=None, icon=None):
# shows a notification
def notifierShow(self, title, subtitle, fromCategory, label):
self.playSound(fromCategory, label)
if withMessagingMenu:
n = Notify.Notification.new(
title, subtitle, 'notification-message-email')
try:
n.show()
except:
# n.show() has been known to throw this exception:
# gi._glib.GError: GDBus.Error:org.freedesktop.Notifications.
# MaxNotificationsExceeded: Exceeded maximum number of
# notifications
pass
return
else:
self.tray.showMessage(title, subtitle, 1, 2000) self.tray.showMessage(title, subtitle, 1, 2000)
self._notifier = _simple_notify
# does nothing if isAvailable returns false
self._player = QtGui.QSound.play
if not get_plugins:
return
_plugin = get_plugin('notification.message')
if _plugin:
self._notifier = _plugin
else:
logger.warning("No notification.message plugin found")
self._theme_player = get_plugin('notification.sound', 'theme')
if not QtGui.QSound.isAvailable():
_plugin = get_plugin(
'notification.sound', 'file', fallback='file.fallback')
if _plugin:
self._player = _plugin
else:
logger.warning("No notification.sound plugin found")
def notifierShow(
self, title, subtitle, category, label=None, icon=None):
self.playSound(category, label)
self._notifier(
unicode(title), unicode(subtitle), category, label, icon)
# tree # tree
def treeWidgetKeyPressEvent(self, event): def treeWidgetKeyPressEvent(self, event):
return self.handleKeyPress(event, self.getCurrentTreeWidget()) return self.handleKeyPress(event, self.getCurrentTreeWidget())
@ -1663,15 +1496,18 @@ class MyForm(settingsmixin.SMainWindow):
def setStatusIcon(self, color): def setStatusIcon(self, color):
# print 'setting status icon color' # print 'setting status icon color'
_notifications_enabled = not BMConfigParser().getboolean(
'bitmessagesettings', 'hidetrayconnectionnotifications')
if color == 'red': if color == 'red':
self.pushButtonStatusIcon.setIcon( self.pushButtonStatusIcon.setIcon(
QIcon(":/newPrefix/images/redicon.png")) QIcon(":/newPrefix/images/redicon.png"))
shared.statusIconColor = 'red' shared.statusIconColor = 'red'
# if the connection is lost then show a notification # if the connection is lost then show a notification
if self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): if self.connected and _notifications_enabled:
self.notifierShow('Bitmessage', unicode(_translate( self.notifierShow(
"MainWindow", "Connection lost").toUtf8(),'utf-8'), 'Bitmessage',
self.SOUND_DISCONNECTED, None) _translate("MainWindow", "Connection lost"),
sound.SOUND_DISCONNECTED)
if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and \ if not BMConfigParser().safeGetBoolean('bitmessagesettings', 'upnp') and \
BMConfigParser().get('bitmessagesettings', 'socksproxytype') == "none": BMConfigParser().get('bitmessagesettings', 'socksproxytype') == "none":
self.statusBar().showMessage(_translate( self.statusBar().showMessage(_translate(
@ -1689,10 +1525,11 @@ class MyForm(settingsmixin.SMainWindow):
":/newPrefix/images/yellowicon.png")) ":/newPrefix/images/yellowicon.png"))
shared.statusIconColor = 'yellow' shared.statusIconColor = 'yellow'
# if a new connection has been established then show a notification # if a new connection has been established then show a notification
if not self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): if not self.connected and _notifications_enabled:
self.notifierShow('Bitmessage', unicode(_translate( self.notifierShow(
"MainWindow", "Connected").toUtf8(),'utf-8'), 'Bitmessage',
self.SOUND_CONNECTED, None) _translate("MainWindow", "Connected"),
sound.SOUND_CONNECTED)
self.connected = True self.connected = True
if self.actionStatus is not None: if self.actionStatus is not None:
@ -1705,10 +1542,11 @@ class MyForm(settingsmixin.SMainWindow):
self.pushButtonStatusIcon.setIcon( self.pushButtonStatusIcon.setIcon(
QIcon(":/newPrefix/images/greenicon.png")) QIcon(":/newPrefix/images/greenicon.png"))
shared.statusIconColor = 'green' shared.statusIconColor = 'green'
if not self.connected and not BMConfigParser().getboolean('bitmessagesettings', 'hidetrayconnectionnotifications'): if not self.connected and _notifications_enabled:
self.notifierShow('Bitmessage', unicode(_translate( self.notifierShow(
"MainWindow", "Connected").toUtf8(),'utf-8'), 'Bitmessage',
self.SOUND_CONNECTION_GREEN, None) _translate("MainWindow", "Connected"),
sound.SOUND_CONNECTION_GREEN)
self.connected = True self.connected = True
if self.actionStatus is not None: if self.actionStatus is not None:
@ -2253,11 +2091,19 @@ class MyForm(settingsmixin.SMainWindow):
else: else:
acct = ret acct = ret
self.propagateUnreadCount(acct.address) self.propagateUnreadCount(acct.address)
if BMConfigParser().getboolean('bitmessagesettings', 'showtraynotifications'): if BMConfigParser().getboolean(
self.notifierShow(unicode(_translate("MainWindow",'New Message').toUtf8(),'utf-8'), unicode(_translate("MainWindow",'From ').toUtf8(),'utf-8') + unicode(acct.fromLabel, 'utf-8'), self.SOUND_UNKNOWN, None) 'bitmessagesettings', 'showtraynotifications'):
self.notifierShow(
_translate("MainWindow", "New Message"),
_translate("MainWindow", "From %1").arg(
unicode(acct.fromLabel, 'utf-8')),
sound.SOUND_UNKNOWN
)
if self.getCurrentAccount() is not None and ((self.getCurrentFolder(treeWidget) != "inbox" and self.getCurrentFolder(treeWidget) is not None) or self.getCurrentAccount(treeWidget) != acct.address): if self.getCurrentAccount() is not None and ((self.getCurrentFolder(treeWidget) != "inbox" and self.getCurrentFolder(treeWidget) is not None) or self.getCurrentAccount(treeWidget) != acct.address):
# Ubuntu should notify of new message irespective of whether it's in current message list or not # Ubuntu should notify of new message irespective of
self.ubuntuMessagingMenuUpdate(True, None, acct.toLabel) # whether it's in current message list or not
self.indicatorUpdate(True, to_label=acct.toLabel)
# cannot find item to pass here ):
if hasattr(acct, "feedback") and acct.feedback != GatewayAccount.ALL_OK: if hasattr(acct, "feedback") and acct.feedback != GatewayAccount.ALL_OK:
if acct.feedback == GatewayAccount.REGISTRATION_DENIED: if acct.feedback == GatewayAccount.REGISTRATION_DENIED:
self.dialog = EmailGatewayRegistrationDialog(self, _translate("EmailGatewayRegistrationDialog", "Registration failed:"), self.dialog = EmailGatewayRegistrationDialog(self, _translate("EmailGatewayRegistrationDialog", "Registration failed:"),
@ -2851,9 +2697,6 @@ class MyForm(settingsmixin.SMainWindow):
shutdown.doCleanShutdown() shutdown.doCleanShutdown()
self.statusBar().showMessage(_translate("MainWindow", "Stopping notifications... %1%").arg(str(90))) self.statusBar().showMessage(_translate("MainWindow", "Stopping notifications... %1%").arg(str(90)))
self.tray.hide() self.tray.hide()
# unregister the messaging system
if self.mmapp is not None:
self.mmapp.unregister()
self.statusBar().showMessage(_translate("MainWindow", "Shutdown imminent... %1%").arg(str(100))) self.statusBar().showMessage(_translate("MainWindow", "Shutdown imminent... %1%").arg(str(100)))
shared.thisapp.cleanup() shared.thisapp.cleanup()
@ -3336,6 +3179,7 @@ class MyForm(settingsmixin.SMainWindow):
self.popMenuAddressBook.addAction(self.actionAddressBookClipboard) self.popMenuAddressBook.addAction(self.actionAddressBookClipboard)
self.popMenuAddressBook.addAction(self.actionAddressBookSubscribe) self.popMenuAddressBook.addAction(self.actionAddressBookSubscribe)
self.popMenuAddressBook.addAction(self.actionAddressBookSetAvatar) self.popMenuAddressBook.addAction(self.actionAddressBookSetAvatar)
self.popMenuAddressBook.addAction(self.actionAddressBookSetSound)
self.popMenuAddressBook.addSeparator() self.popMenuAddressBook.addSeparator()
self.popMenuAddressBook.addAction(self.actionAddressBookNew) self.popMenuAddressBook.addAction(self.actionAddressBookNew)
normal = True normal = True
@ -3740,6 +3584,50 @@ class MyForm(settingsmixin.SMainWindow):
return True return True
def on_action_AddressBookSetSound(self):
widget = self.ui.tableWidgetAddressBook
self.setAddressSound(widget.item(widget.currentRow(), 0).text())
def setAddressSound(self, addr):
filters = [unicode(_translate(
"MainWindow", "Sound files (%s)" %
' '.join(['*%s%s' % (os.extsep, ext) for ext in sound.extensions])
))]
sourcefile = unicode(QtGui.QFileDialog.getOpenFileName(
self, _translate("MainWindow", "Set notification sound..."),
filter=';;'.join(filters)
))
if not sourcefile:
return
destdir = os.path.join(state.appdata, 'sounds')
destfile = unicode(addr) + os.path.splitext(sourcefile)[-1]
destination = os.path.join(destdir, destfile)
if sourcefile == destination:
return
pattern = destfile.lower()
for item in os.listdir(destdir):
if item.lower() == pattern:
overwrite = QtGui.QMessageBox.question(
self, _translate("MainWindow", "Message"),
_translate(
"MainWindow",
"You have already set a notification sound"
" for this address book entry."
" Do you really want to overwrite it?"),
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No
) == QtGui.QMessageBox.Yes
if overwrite:
QtCore.QFile.remove(os.path.join(destdir, item))
break
if not QtCore.QFile.copy(sourcefile, destination):
logger.error(
'couldn\'t copy %s to %s', sourcefile, destination)
def on_context_menuYourIdentities(self, point): def on_context_menuYourIdentities(self, point):
currentItem = self.getCurrentItem() currentItem = self.getCurrentItem()
self.popMenuYourIdentities = QtGui.QMenu(self) self.popMenuYourIdentities = QtGui.QMenu(self)
@ -4525,7 +4413,7 @@ def run():
myapp = MyForm() myapp = MyForm()
myapp.appIndicatorInit(app) myapp.appIndicatorInit(app)
myapp.ubuntuMessagingMenuInit() myapp.indicatorInit()
myapp.notifierInit() myapp.notifierInit()
myapp._firstrun = BMConfigParser().safeGetBoolean( myapp._firstrun = BMConfigParser().safeGetBoolean(
'bitmessagesettings', 'dontconnect') 'bitmessagesettings', 'dontconnect')

21
src/bitmessageqt/sound.py Normal file
View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# sound type constants
SOUND_NONE = 0
SOUND_KNOWN = 1
SOUND_UNKNOWN = 2
SOUND_CONNECTED = 3
SOUND_DISCONNECTED = 4
SOUND_CONNECTION_GREEN = 5
# returns true if the given sound category is a connection sound
# rather than a received message sound
def is_connection_sound(category):
return category in (
SOUND_CONNECTED,
SOUND_DISCONNECTED,
SOUND_CONNECTION_GREEN
)
extensions = ('wav', 'mp3', 'oga')

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
import gi
gi.require_version('MessagingMenu', '1.0')
from gi.repository import MessagingMenu
from pybitmessage.bitmessageqt.utils import str_broadcast_subscribers
from pybitmessage.tr import _translate
class IndicatorLibmessaging(object):
def __init__(self, form):
try:
self.app = MessagingMenu.App(desktop_id='pybitmessage.desktop')
self.app.register()
self.app.connect('activate-source', self.activate)
except:
self.app = None
return
self._menu = {
'send': str(_translate('MainWindow', 'Send')),
'messages': str(_translate('MainWindow', 'Messages')),
'subscriptions': str(_translate('MainWindow', 'Subscriptions'))
}
self.new_message_item = self.new_broadcast_item = None
self.form = form
self.show_unread()
def __del__(self):
if self.app:
self.app.unregister()
def activate(self, app, source):
self.form.appIndicatorInbox(
self.new_message_item if source == 'messages'
else self.new_broadcast_item
)
# show the number of unread messages and subscriptions
# on the messaging menu
def show_unread(self, draw_attention=False):
for source, count in zip(
('messages', 'subscriptions'),
self.form.getUnread()
):
if count > 0:
if self.app.has_source(source):
self.app.set_source_count(source, count)
else:
self.app.append_source_with_count(
source, None, self._menu[source], count)
if draw_attention:
self.app.draw_attention(source)
# update the Ubuntu messaging menu
def __call__(self, draw_attention, item=None, to_label=None):
if not self.app:
return
# remember this item to that the activate() can find it
if item:
if to_label == str_broadcast_subscribers:
self.new_broadcast_item = item
else:
self.new_message_item = item
self.show_unread(draw_attention)
connect_plugin = IndicatorLibmessaging

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify
Notify.init('pybitmessage')
def connect_plugin(title, subtitle, category, label, icon):
if not icon:
icon = 'mail-message-new' if category == 2 else 'pybitmessage'
Notify.Notification.new(title, subtitle, icon).show()

View File

@ -3,10 +3,33 @@
import pkg_resources import pkg_resources
def get_plugins(group, point, name=None): def get_plugins(group, point='', name=None, fallback=None):
for plugin in pkg_resources.iter_entry_points(group): """
if plugin.name.startswith(point): Iterate through plugins (`connect_plugin` attribute of entry point)
which name starts with `point` or equals to `name`.
If `fallback` kwarg specified, plugin with that name yield last.
"""
for ep in pkg_resources.iter_entry_points('bitmessage.' + group):
if name and ep.name == name or ep.name.startswith(point):
try: try:
yield plugin.load().connect_plugin plugin = ep.load().connect_plugin
except (AttributeError, pkg_resources.DistributionNotFound): if ep.name == fallback:
_fallback = plugin
else:
yield plugin
except (AttributeError,
ImportError,
ValueError,
pkg_resources.DistributionNotFound,
pkg_resources.UnknownExtra):
continue continue
try:
yield _fallback
except NameError:
pass
def get_plugin(*args, **kwargs):
"""Returns first available plugin `from get_plugins()` if any."""
for plugin in get_plugins(*args, **kwargs):
return plugin

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from pybitmessage.bitmessageqt import sound
import pycanberra
_canberra = pycanberra.Canberra()
_theme = {
sound.SOUND_UNKNOWN: 'message-new-email',
sound.SOUND_CONNECTED: 'network-connectivity-established',
sound.SOUND_DISCONNECTED: 'network-connectivity-lost',
sound.SOUND_CONNECTION_GREEN: 'network-connectivity-established'
}
def connect_plugin(category, label=None):
try:
_canberra.play(0, pycanberra.CA_PROP_EVENT_ID, _theme[category], None)
except (KeyError, pycanberra.CanberraException):
pass

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst # noqa: E402
Gst.init(None)
_player = Gst.ElementFactory.make("playbin", "player")
def connect_plugin(sound_file):
_player.set_state(Gst.State.NULL)
_player.set_property("uri", "file://" + sound_file)
_player.set_state(Gst.State.PLAYING)

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
try:
import winsound
def connect_plugin(sound_file):
winsound.PlaySound(sound_file, winsound.SND_FILENAME)
except ImportError:
import os
import subprocess
play_cmd = {}
def _subprocess(*args):
FNULL = open(os.devnull, 'wb')
subprocess.call(
args, stdout=FNULL, stderr=subprocess.STDOUT, close_fds=True)
def connect_plugin(sound_file):
global play_cmd
ext = os.path.splitext(sound_file)[-1]
try:
return _subprocess(play_cmd[ext], sound_file)
except (KeyError, AttributeError):
pass
programs = ['gst123', 'gst-play-1.0']
if ext == '.wav':
programs.append('aplay')
elif ext == '.mp3':
programs += ['mpg123', 'mpg321', 'mpg321-mpg123']
for cmd in programs:
try:
_subprocess(cmd, sound_file)
except OSError:
pass # log here!
else:
play_cmd[ext] = cmd
break