diff -Nru blueberry-1.1.9/blueberry.pot blueberry-1.1.10/blueberry.pot --- blueberry-1.1.9/blueberry.pot 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/blueberry.pot 2017-02-17 11:52:21.000000000 +0000 @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-06-15 11:46+0100\n" +"POT-Creation-Date: 2017-02-17 11:24+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,44 +18,144 @@ "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: usr/lib/blueberry/blueberry.py:68 usr/lib/blueberry/blueberry-tray.py:35 -#: usr/lib/blueberry/blueberry-tray.py:52 +#: usr/lib/blueberry/blueberry-obex-agent.py:303 +msgid "Incoming file over Bluetooth" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:304 +#, python-format +msgid "Incoming file %(0)s from %(1)s" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:306 +msgid "Accept" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:306 +msgid "Reject" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:309 +msgid "Receiving file" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:310 +#, python-format +msgid "Receiving file %(0)s from %(1)s" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:402 +msgid "File received" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:403 +#, python-format +msgid "File %(0)s from %(1)s successfully received" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:408 +msgid "Transfer failed" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:409 +#, python-format +msgid "Transfer of file %(0)s failed" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:425 +#: usr/lib/blueberry/blueberry-obex-agent.py:431 +msgid "Files received" +msgstr "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:426 +#, python-format +msgid "Received %d file in the background" +msgid_plural "Received %d files in the background" +msgstr[0] "" +msgstr[1] "" + +#: usr/lib/blueberry/blueberry-obex-agent.py:432 +#, python-format +msgid "Received %d more file in the background" +msgid_plural "Received %d more files in the background" +msgstr[0] "" +msgstr[1] "" + +#: usr/lib/blueberry/blueberry.py:76 usr/lib/blueberry/blueberry-tray.py:37 #: usr/lib/blueberry/blueberry-tray.py:67 generate_desktop_files:25 msgid "Bluetooth" msgstr "" -#: usr/lib/blueberry/blueberry.py:82 +#: usr/lib/blueberry/blueberry.py:126 usr/lib/blueberry/blueberry-tray.py:54 msgid "Bluetooth is disabled" msgstr "" -#: usr/lib/blueberry/blueberry.py:83 +#: usr/lib/blueberry/blueberry.py:127 msgid "No Bluetooth adapters found" msgstr "" -#: usr/lib/blueberry/blueberry.py:84 +#: usr/lib/blueberry/blueberry.py:128 msgid "Bluetooth is disabled by hardware switch" msgstr "" -#: usr/lib/blueberry/blueberry.py:119 +#: usr/lib/blueberry/blueberry.py:146 +msgid "Devices" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:152 +msgid "Bluetooth settings" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:156 +msgid "Name" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:157 +msgid "This is the Bluetooth name of your computer" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:164 +msgid "Receive files from remote devices" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:165 +msgid "" +"This option allows your computer to receive files transferred over Bluetooth " +"(OBEX)" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:174 msgid "Show a tray icon" msgstr "" -#: usr/lib/blueberry/blueberry-tray.py:62 +#: usr/lib/blueberry/blueberry.py:178 +msgid "Settings" +msgstr "" + +#: usr/lib/blueberry/blueberry.py:253 #, python-format -msgid "Bluetooth: %d device connected" -msgid_plural "Bluetooth: %d devices connected" -msgstr[0] "" -msgstr[1] "" +msgid "Visible as %s and available for Bluetooth file transfers." +msgstr "" + +#: usr/lib/blueberry/blueberry.py:255 +#, python-format +msgid "Visible as %s." +msgstr "" + +#: usr/lib/blueberry/blueberry-tray.py:64 +#, python-format +msgid "Bluetooth: Connected to %s" +msgstr "" -#: usr/lib/blueberry/blueberry-tray.py:109 +#: usr/lib/blueberry/blueberry-tray.py:107 msgid "Send files to a device" msgstr "" -#: usr/lib/blueberry/blueberry-tray.py:113 +#: usr/lib/blueberry/blueberry-tray.py:111 msgid "Open Bluetooth device manager" msgstr "" -#: usr/lib/blueberry/blueberry-tray.py:119 +#: usr/lib/blueberry/blueberry-tray.py:117 msgid "Quit" msgstr "" diff -Nru blueberry-1.1.9/debian/changelog blueberry-1.1.10/debian/changelog --- blueberry-1.1.9/debian/changelog 2016-12-23 18:22:07.000000000 +0000 +++ blueberry-1.1.10/debian/changelog 2017-02-27 21:20:49.000000000 +0000 @@ -1,8 +1,27 @@ -blueberry (1.1.9-1~yakkety0) yakkety; urgency=medium +blueberry (1.1.10-1~yakkety0) yakkety; urgency=medium * Package version bump to avoid release clash on Launchpad. - -- embrosyn Fri, 23 Dec 2016 19:22:00 +0100 + -- embrosyn Mon, 27 Feb 2017 22:20:49 +0100 + +blueberry (1.1.10) yakkety; urgency=medium + + * Add OBEX support + * Packaging: Add dependency on bluez-obexd + * Packaging: Add dependency on python-dbus + * Packaging: Add dependency on python-gi and gir1.2-notify-0.7 + * Use pavucontrol for sound if present and DE isn't detected + * l10n: Update POT file + * Tray: Improve tooltip to show the name of the connected devices + * Provide a Cinnamon applet + * Overwrite the label in GnomeBluetooth SettingsWidget + * Fix cinnamon icons + * Remove blueberrySettings.py + * Add settings + * Add missing file + * Widen the default window size to 640px + + -- Clement Lefebvre Fri, 17 Feb 2017 11:51:52 +0000 blueberry (1.1.9) yakkety; urgency=medium diff -Nru blueberry-1.1.9/debian/control blueberry-1.1.10/debian/control --- blueberry-1.1.9/debian/control 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/debian/control 2017-02-17 11:52:21.000000000 +0000 @@ -7,7 +7,7 @@ Package: blueberry Architecture: all -Depends: python (>= 2.4), python (<< 3), gnome-bluetooth, gir1.2-gnomebluetooth-1.0, rfkill, wmctrl +Depends: python (>= 2.4), python (<< 3), gnome-bluetooth, gir1.2-gnomebluetooth-1.0, rfkill, wmctrl, python-setproctitle, bluez-obexd, bluez-tools, python-dbus, gir1.2-notify-0.7, python-gi Breaks: cinnamon-bluetooth Replaces: cinnamon-bluetooth Description: A configuration tool for Bluetooth diff -Nru blueberry-1.1.9/etc/xdg/autostart/blueberry-obex-agent.desktop blueberry-1.1.10/etc/xdg/autostart/blueberry-obex-agent.desktop --- blueberry-1.1.9/etc/xdg/autostart/blueberry-obex-agent.desktop 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/etc/xdg/autostart/blueberry-obex-agent.desktop 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=Bluetooth OBEX Agent +Comment=Allows to receive files via Bluetooth +Keywords=files;bluetooth;obex;receive; +AutostartCondition=GSETTINGS org.blueberry obex-enabled +Icon=blueberry +Exec=/usr/lib/blueberry/blueberry-obex-agent.py +Terminal=false +Type=Application +Categories=GTK;GNOME;Settings;X-GNOME-NetworkSettings; +StartupNotify=false +NoDisplay=true +NotShowIn=GNOME;KDE;Unity; diff -Nru blueberry-1.1.9/usr/lib/blueberry/blueberry-obex-agent.py blueberry-1.1.10/usr/lib/blueberry/blueberry-obex-agent.py --- blueberry-1.1.9/usr/lib/blueberry/blueberry-obex-agent.py 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/lib/blueberry/blueberry-obex-agent.py 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,729 @@ +#!/usr/bin/python2 + +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +# CREDITS +# -------- +# This OBEX agent was ported from the Blueman project +# https://github.com/blueman-project/blueman +# where it was implemented by Christopher Schramm and Sander Sweers. + +import dbus +import dbus.mainloop.glib +import dbus.service +import fcntl +import gettext +import gi +import os +import setproctitle +import shutil +import struct +import subprocess +import sys +import termios +import traceback + +from datetime import datetime +from gi.types import GObjectMeta +from inspect import isclass + +gi.require_version("Gtk", "3.0") +gi.require_version('Notify', '0.7') + +from gi.repository import GObject, GLib, Gtk, Gio, Notify + +BOLD = lambda x: "\033[1m" + x + "\033[0m" + +SHARED_PATH = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD) +if not os.path.exists(SHARED_PATH): + SHARED_PATH = os.path.expanduser("~") + +# i18n +gettext.install("blueberry", "/usr/share/locale") + +setproctitle.setproctitle("blueberry-obex-agent") + +Notify.init("Blueberry") + +try: + in_fg = os.getpgrp() == struct.unpack(str('h'), fcntl.ioctl(0, termios.TIOCGPGRP, " "))[0] +except IOError: + in_fg = 'DEBUG' in os.environ + +def dprint(*args): + #dont print if in the background + if in_fg: + + s = "" + for a in args: + s += ("%s " % a) + co = sys._getframe(1).f_code + + fname = BOLD(co.co_name) + + print("_________") + print("%s %s" % (fname, "(%s:%d)" % (co.co_filename, co.co_firstlineno))) + print(s) + try: + sys.stdout.flush() + except IOError: + pass + +class _GDbusObjectType(dbus.service.InterfaceType, GObjectMeta): + pass + +_GDBusObject = _GDbusObjectType(str('_GDBusObject'), (dbus.service.Object, GObject.GObject), {}) + +# noinspection PyPep8Naming +class Agent(_GDBusObject, dbus.service.Object, GObject.GObject): + __gsignals__ = { + str('release'): (GObject.SignalFlags.NO_HOOKS, None, ()), + str('authorize'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)), + str('cancel'): (GObject.SignalFlags.NO_HOOKS, None, ()), + } + + def __init__(self, agent_path): + self._agent_path = agent_path + dbus.service.Object.__init__(self, dbus.SessionBus(), agent_path) + GObject.GObject.__init__(self) + self._reply_handler = None + self._error_handler = None + + @dbus.service.method('org.bluez.obex.Agent1') + def Release(self): + dprint(self._agent_path) + self.emit('release') + + @dbus.service.method('org.bluez.obex.Agent1', async_callbacks=('reply_handler', 'error_handler')) + def AuthorizePush(self, transfer_path, reply_handler, error_handler): + dprint(self._agent_path, transfer_path) + self._reply_handler = reply_handler + self._error_handler = error_handler + self.emit('authorize', transfer_path, None, None, None) + + @dbus.service.method('org.bluez.obex.Agent1') + def Cancel(self): + dprint(self._agent_path) + self.emit('cancel') + + @dbus.service.method('org.bluez.obex.Agent', async_callbacks=('reply_handler', 'error_handler')) + def Authorize(self, transfer_path, bt_address, name, _type, length, _time, reply_handler, error_handler): + dprint(self._agent_path, transfer_path, bt_address, name, length) + self._reply_handler = reply_handler + self._error_handler = error_handler + self.emit('authorize', transfer_path, bt_address, name, length) + + @dbus.service.method('org.bluez.obex.Agent') + def Cancel(self): + dprint(self._agent_path) + self.emit('cancel') + + def reply(self, reply): + dprint(self._agent_path, reply) + self._reply_handler(reply) + self._reply_handler = None + self._error_handler = None + + def reply_cancelled(self, reply): + dprint(self._agent_path, reply) + self._error_handler(dbus.DBusException(name=('org.bluez.obex.Error.Canceled'))) + self._reply_handler = None + self._error_handler = None + + def reply_rejected(self, reply): + dprint(self._agent_path, reply) + self._error_handler(dbus.DBusException(name=('org.bluez.obex.Error.Rejected'))) + self._reply_handler = None + self._error_handler = None + +class SignalTracker: + def __init__(self): + self._signals = [] + + def Handle(self, *args, **kwargs): + if "sigid" in kwargs: + sigid = kwargs["sigid"] + del kwargs["sigid"] + else: + sigid = None + + objtype = args[0] + obj = args[1] + args = args[2:] + + if objtype == "bluez": + obj.handle_signal(*args, **kwargs) + elif objtype == "gobject": + args = obj.connect(*args) + elif objtype == "dbus": + if isinstance(obj, dbus.Bus): + obj.add_signal_receiver(*args, **kwargs) + else: + print("Deprecated use of dbus signaltracker") + traceback.print_stack() + obj.bus.add_signal_receiver(*args, **kwargs) + + self._signals.append((sigid, objtype, obj, args, kwargs)) + + def Disconnect(self, sigid): + for sig in self._signals: + (_sigid, objtype, obj, args, kwargs) = sig + if sigid != None and _sigid == sigid: + if objtype == "bluez": + obj.unhandle_signal(*args) + elif objtype == "gobject": + obj.disconnect(args) + elif objtype == "dbus": + if isinstance(obj, dbus.Bus): + if "path" in kwargs: + obj.remove_signal_receiver(*args, **kwargs) + else: + obj.remove_signal_receiver(*args) + else: + obj.bus.remove_signal_receiver(*args) + + self._signals.remove(sig) + + + def DisconnectAll(self): + for sig in self._signals: + + (sigid, objtype, obj, args, kwargs) = sig + if objtype == "bluez": + obj.unhandle_signal(*args) + elif objtype == "gobject": + obj.disconnect(args) + elif objtype == "dbus": + if isinstance(obj, dbus.Bus): + if "path" in kwargs: + obj.remove_signal_receiver(*args, **kwargs) + else: + obj.remove_signal_receiver(*args) + else: + obj.bus.remove_signal_receiver(*args) + + self._signals = [] + +class NotificationBubble(Notify.Notification): + + @staticmethod + def actions_supported(): + return "actions" in Notify.get_server_caps() + + def __new__(cls, summary, message, timeout=-1, actions=None, actions_cb=None): + self = Notify.Notification.new(summary, message, None) + + def on_notification_closed(n, *args): + self.disconnect(closed_sig) + if actions_cb: + actions_cb(n, "closed") + + def on_action(n, action, *args): + self.disconnect(closed_sig) + actions_cb(n, action) + + self.set_icon_from_pixbuf(Gtk.IconTheme.get_default().load_icon("blueberry", 48, 0)) + + if actions: + for action in actions: + self.add_action(action[0], action[1], on_action, None) + self.add_action("default", "Default Action", on_action, None) + + closed_sig = self.connect("closed", on_notification_closed) + if timeout != -1: + self.set_timeout(timeout) + + self.show() + + return self + +class _Agent: + def __init__(self): + self._agent_path = '/org/blueberry/obex_agent' + + self._agent = Agent(self._agent_path) + self._agent.connect('release', self._on_release) + self._agent.connect('authorize', self._on_authorize) + self._agent.connect('cancel', self._on_cancel) + + self._allowed_devices = [] + self._notification = None + self._pending_transfer = None + self.transfers = {} + + AgentManager().register_agent(self._agent_path) + + def __del__(self): + AgentManager().unregister_agent(self._agent_path) + + def _on_release(self, _agent): + raise Exception(self._agent_path + " was released unexpectedly") + + def _on_action(self, _notification, action): + dprint(action) + + if action == "accept": + self.transfers[self._pending_transfer['transfer_path']] = { + 'path': self._pending_transfer['root'] + '/' + os.path.basename(self._pending_transfer['filename']), + 'size': self._pending_transfer['size'], + 'name': self._pending_transfer['name'] + } + self._agent.reply(self.transfers[self._pending_transfer['transfer_path']]['path']) + self._allowed_devices.append(self._pending_transfer['address']) + GObject.timeout_add(60000, self._allowed_devices.remove, self._pending_transfer['address']) + else: + self._agent.reply_rejected() + + def _on_authorize(self, _agent, transfer_path, address=None, filename=None, size=None): + if address and filename and size: + # stand-alone obexd + # FIXME: /tmp is only the default. Can we get the actual root + # directory from stand-alone obexd? + root = '/tmp' + else: + # BlueZ 5 integrated obexd + transfer = Transfer(transfer_path) + session = Session(transfer.session) + root = session.root + address = session.address + filename = transfer.name + size = transfer.size + name = subprocess.check_output(["hcitool", "name", session.address]).strip() + + self._pending_transfer = {'transfer_path': transfer_path, 'address': address, 'root': root, + 'filename': filename, 'size': size, 'name': name} + + + # This device was not allowed yet -> ask for confirmation + if address not in self._allowed_devices: + self._notification = NotificationBubble(_("Incoming file over Bluetooth"), + _("Incoming file %(0)s from %(1)s") % {"0": "" + filename + "", + "1": "" + name + ""}, + 30000, [["accept", _("Accept"), "help-about"], ["reject", _("Reject"), "help-about"]], self._on_action) + # Device was already allowed, larger file -> display a notification, but auto-accept + elif size > 350000: + self._notification = NotificationBubble(_("Receiving file"), + _("Receiving file %(0)s from %(1)s") % {"0": "" + filename + "", + "1": "" + name + ""}) + self._on_action(self._notification, 'accept') + # Device was already allowed. very small file -> auto-accept and transfer silently + else: + self._notification = None + self._on_action(self._notification, "accept") + + def _on_cancel(self, agent): + self._notification.close() + agent.reply_cancelled() + + +class TransferService(): + _silent_transfers = 0 + _normal_transfers = 0 + + _manager = None + _agent = None + _watch = None + + def load(self): + self._manager = Manager() + self._manager.connect("transfer-started", self._on_transfer_started) + self._manager.connect("transfer-completed", self._on_transfer_completed) + self._manager.connect('session-removed', self._on_session_removed) + + self._watch = dbus.SessionBus().watch_name_owner("org.bluez.obex", self._on_obex_owner_changed) + + def unload(self): + if self._watch: + self._watch.cancel() + + self._agent = None + + def on_manager_state_changed(self, state): + if not state: + self._agent = None + + def _on_obex_owner_changed(self, owner): + dprint("obex owner changed:", owner) + if owner == "": + self._agent = None + else: + self._agent = _Agent() + + def _on_transfer_started(self, _manager, transfer_path): + if transfer_path not in self._agent.transfers: + # This is not an incoming transfer we authorized + return + + if self._agent.transfers[transfer_path]['size'] > 350000: + self._normal_transfers += 1 + else: + self._silent_transfers += 1 + + @staticmethod + def _add_open(n, name, path): + if NotificationBubble.actions_supported(): + print("adding action") + + def on_open(*_args): + print("open") + subprocess.Popen(['xdg-open', path]) + + n.add_action("open", name, on_open, None) + n.show() + + def _on_transfer_completed(self, _manager, transfer_path, success): + try: + attributes = self._agent.transfers[transfer_path] + except KeyError: + # This is probably not an incoming transfer we authorized + return + + src = attributes['path'] + dest_dir = SHARED_PATH + filename = os.path.basename(src) + + # We get bytes from pygobject under python 2.7 + if hasattr(dest_dir, "upper",) and hasattr(dest_dir, "decode"): + dest_dir = dest_dir.decode("UTF-8") + + if os.path.exists(os.path.join(dest_dir, filename)): + now = datetime.now() + filename = "%s_%s" % (now.strftime("%Y%m%d%H%M%S"), filename) + dprint("Destination file exists, renaming to: %s" % filename) + + dest = os.path.join(dest_dir, filename) + shutil.move(src, dest) + + if success: + n = NotificationBubble(_("File received"), + _("File %(0)s from %(1)s successfully received") % { + "0": "" + filename + "", + "1": "" + attributes['name'] + ""}) + self._add_open(n, "Open", dest) + elif not success: + NotificationBubble(_("Transfer failed"), + _("Transfer of file %(0)s failed") % { + "0": "" + filename + "", + "1": "" + attributes['name'] + ""}) + + if attributes['size'] > 350000: + self._normal_transfers -= 1 + else: + self._silent_transfers -= 1 + + del self._agent.transfers[transfer_path] + + def _on_session_removed(self, _manager, _session_path): + if self._silent_transfers == 0: + return + + if self._normal_transfers == 0: + n = NotificationBubble(_("Files received"), + ngettext("Received %d file in the background", "Received %d files in the background", + self._silent_transfers) % self._silent_transfers) + + self._add_open(n, "Open Location", SHARED_PATH) + else: + n = NotificationBubble(_("Files received"), + ngettext("Received %d more file in the background", + "Received %d more files in the background", + self._silent_transfers) % self._silent_transfers) + self._add_open(n, "Open Location", SHARED_PATH) + +class ObexdNotFoundError(Exception): + pass + +class Base(GObject.GObject): + interface_version = None + + @staticmethod + def get_interface_version(): + if not Base.interface_version: + obj = dbus.SessionBus().get_object('org.bluez.obex', '/') + introspection = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable').Introspect() + if 'org.freedesktop.DBus.ObjectManager' in introspection: + dprint('Detected BlueZ integrated obexd') + Base.interface_version = [5] + elif 'org.bluez.obex.Manager' in introspection: + dprint('Detected standalone obexd') + Base.interface_version = [4] + else: + raise ObexdNotFoundError('Could not find any compatible version of obexd') + + return Base.interface_version + + def __init__(self, interface_name, obj_path, legacy_client_bus=False): + self.__signals = SignalTracker() + self.__obj_path = obj_path + self.__interface_name = interface_name + self.__bus = dbus.SessionBus() + self.__bus_name = 'org.bluez.obex.client' if legacy_client_bus else 'org.bluez.obex' + self.__dbus_proxy = self.__bus.get_object(self.__bus_name, obj_path, follow_name_owner_changes=True) + self.__interface = dbus.Interface(self.__dbus_proxy, interface_name) + super(Base, self).__init__() + + def __del__(self): + self.__signals.DisconnectAll() + + def _handle_signal(self, handler, signal): + self.__signals.Handle('dbus', self.__bus, handler, signal, self.__interface_name, self.__bus_name, + self.__obj_path) + + @property + def _interface(self): + return self.__interface + + @property + def object_path(self): + return self.__obj_path + +class Session(Base): + def __init__(self, session_path): + if self.__class__.get_interface_version()[0] < 5: + super(Session, self).__init__('org.bluez.obex.Session', session_path) + else: + super(Session, self).__init__('org.freedesktop.DBus.Properties', session_path) + + @property + def address(self): + if self.__class__.get_interface_version()[0] < 5: + return self._interface.GetProperties()['Address'] + else: + return self._interface.Get('org.bluez.obex.Session1', 'Destination') + + @property + def target(self): + if self.__class__.get_interface_version()[0] < 5: + return self.address() + else: + return self._interface.Get('org.bluez.obex.Session1', 'Destination') + + @property + def root(self): + if self.__class__.get_interface_version()[0] < 5: + raise NotImplementedError() + else: + return self._interface.Get('org.bluez.obex.Session1', 'Root') + +class Transfer(Base): + __gsignals__ = { + str('progress'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + str('completed'): (GObject.SignalFlags.NO_HOOKS, None, ()), + str('error'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)) + } + + def __init__(self, transfer_path): + if self.__class__.get_interface_version()[0] < 5: + super(Transfer, self).__init__('org.bluez.obex.Transfer', transfer_path, True) + + handlers = { + 'PropertyChanged': self._on_property_changed, + 'Complete': self._on_complete, + 'Error': self._on_error + } + + for signal, handler in handlers.items(): + self._handle_signal(handler, signal) + else: + super(Transfer, self).__init__('org.freedesktop.DBus.Properties', transfer_path) + self._handle_signal(self._on_properties_changed, 'PropertiesChanged') + + def __getattr__(self, name): + if name in ('filename', 'name', 'session', 'size'): + if self.__class__.get_interface_version()[0] < 5: + raise NotImplementedError() + else: + return self._interface.Get('org.bluez.obex.Transfer1', name.capitalize()) + + def _on_property_changed(self, name, value): + if name == 'Progress': + dprint(self.object_path, name, value) + self.emit('progress', value) + + def _on_complete(self): + dprint(self.object_path) + self.emit('completed') + + def _on_error(self, code, message): + dprint(self.object_path, code, message) + self.emit('error', message) + + def _on_properties_changed(self, interface_name, changed_properties, _invalidated_properties): + if interface_name != 'org.bluez.obex.Transfer1': + return + + for name, value in changed_properties.items(): + dprint(self.object_path, name, value) + if name == 'Transferred': + self.emit('progress', value) + elif name == 'Status': + if value == 'complete': + self.emit('completed') + elif value == 'error': + self.emit('error', None) + +class Manager(Base): + __gsignals__ = { + str('session-removed'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + str('transfer-started'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + str('transfer-completed'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)), + } + + def __init__(self): + if self.__class__.get_interface_version()[0] < 5: + super(Manager, self).__init__('org.bluez.obex.Manager', '/') + handlers = { + 'SessionRemoved': self._on_session_removed, + 'TransferStarted': self._on_transfer_started, + 'TransferCompleted': self._on_transfer_completed + } + + for signal, handler in handlers.items(): + self._handle_signal(handler, signal, ) + + else: + super(Manager, self).__init__('org.freedesktop.DBus.ObjectManager', '/') + + self._transfers = {} + + def on_interfaces_added(object_path, interfaces): + if 'org.bluez.obex.Transfer1' in interfaces: + def on_tranfer_completed(_transfer): + self._on_transfer_completed(object_path, True) + + def on_tranfer_error(_transfer, _msg): + self._on_transfer_completed(object_path, False) + + self._transfers[object_path] = Transfer(object_path) + self._transfers[object_path].connect('completed', on_tranfer_completed) + self._transfers[object_path].connect('error', on_tranfer_error) + self._on_transfer_started(object_path) + + self._handle_signal(on_interfaces_added, 'InterfacesAdded') + + def on_interfaces_removed(object_path, interfaces): + if 'org.bluez.obex.Session1' in interfaces: + self._on_session_removed(object_path) + + self._handle_signal(on_interfaces_removed, 'InterfacesRemoved') + + def _on_session_removed(self, session_path): + dprint(session_path) + self.emit('session-removed', session_path) + + def _on_transfer_started(self, transfer_path): + dprint(transfer_path) + self.emit('transfer-started', transfer_path) + + def _on_transfer_completed(self, transfer_path, success): + dprint(transfer_path, success) + self.emit('transfer-completed', transfer_path, success) + +class ObjectPush(Base): + __gsignals__ = { + str('transfer-started'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT,)), + str('transfer-failed'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + } + + def __init__(self, session_path): + if self.__class__.get_interface_version()[0] < 5: + super(ObjectPush, self).__init__('org.bluez.obex.ObjectPush', session_path, True) + else: + super(ObjectPush, self).__init__('org.bluez.obex.ObjectPush1', session_path) + + def send_file(self, file_path): + def on_transfer_started(*params): + transfer_path, props = params[0] if self.__class__.get_interface_version()[0] < 5 else params + dprint(self.object_path, file_path, transfer_path) + self.emit('transfer-started', transfer_path, props['Filename']) + + def on_transfer_error(error): + dprint(file_path, error) + self.emit('transfer-failed', error) + + self._interface.SendFile(file_path, reply_handler=on_transfer_started, error_handler=on_transfer_error) + + def get_session_path(self): + return self.object_path + +class Client(Base): + __gsignals__ = { + str('session-created'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + str('session-failed'): (GObject.SignalFlags.NO_HOOKS, None, (GObject.TYPE_PYOBJECT,)), + str('session-removed'): (GObject.SignalFlags.NO_HOOKS, None, ()), + } + + def __init__(self): + if self.__class__.get_interface_version()[0] < 5: + super(Client, self).__init__('org.bluez.obex.Client', '/', True) + else: + super(Client, self).__init__('org.bluez.obex.Client1', '/org/bluez/obex') + + def create_session(self, dest_addr, source_addr="00:00:00:00:00:00", pattern="opp"): + def on_session_created(session_path): + dprint(dest_addr, source_addr, pattern, session_path) + self.emit("session-created", session_path) + + def on_session_failed(error): + dprint(dest_addr, source_addr, pattern, error) + self.emit("session-failed", error) + + self._interface.CreateSession(dest_addr, {"Source": source_addr, "Target": pattern}, + reply_handler=on_session_created, error_handler=on_session_failed) + + def remove_session(self, session_path): + def on_session_removed(): + dprint(session_path) + self.emit('session-removed') + + def on_session_remove_failed(error): + dprint(session_path, error) + + self._interface.RemoveSession(session_path, reply_handler=on_session_removed, + error_handler=on_session_remove_failed) + +class AgentManager(Base): + def __init__(self): + if self.__class__.get_interface_version()[0] < 5: + super(AgentManager, self).__init__('org.bluez.obex.Manager', '/') + else: + super(AgentManager, self).__init__('org.bluez.obex.AgentManager1', '/org/bluez/obex') + + def register_agent(self, agent_path): + def on_registered(): + dprint(agent_path) + + def on_register_failed(error): + dprint(agent_path, error) + + self._interface.RegisterAgent(agent_path, reply_handler=on_registered, error_handler=on_register_failed) + + def unregister_agent(self, agent_path): + def on_unregistered(): + dprint(agent_path) + + def on_unregister_failed(error): + dprint(agent_path, error) + + self._interface.UnregisterAgent(agent_path, reply_handler=on_unregistered, error_handler=on_unregister_failed) + +if __name__ == '__main__': + settings = Gio.Settings("org.blueberry") + if settings.get_boolean("obex-enabled"): + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + mainloop = GObject.MainLoop() + service = TransferService() + service.load() + cont = True + while cont: + try: + mainloop.run() + except KeyboardInterrupt: + service.unload() + cont = False + else: + dprint("org.blueberry obex-enabled is False, exiting.") diff -Nru blueberry-1.1.9/usr/lib/blueberry/blueberry.py blueberry-1.1.10/usr/lib/blueberry/blueberry.py --- blueberry-1.1.9/usr/lib/blueberry/blueberry.py 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/usr/lib/blueberry/blueberry.py 2017-02-17 11:52:21.000000000 +0000 @@ -4,14 +4,12 @@ import gettext import rfkillMagic import subprocess -import blueberrySettings - +from BlueberrySettingsWidgets import SettingsPage, SettingsBox, SettingsRow import gi gi.require_version('Gtk', '3.0') gi.require_version('GnomeBluetooth', '1.0') from gi.repository import Gtk, GnomeBluetooth, Gio - BLUETOOTH_DISABLED_PAGE = "disabled-page" BLUETOOTH_HW_DISABLED_PAGE = "hw-disabled-page" BLUETOOTH_NO_DEVICES_PAGE = "no-devices-page" @@ -20,38 +18,12 @@ # i18n gettext.install("blueberry", "/usr/share/locale") -# detect the DE environment -wm_info = commands.getoutput("wmctrl -m") -if "XDG_CURRENT_DESKTOP" in os.environ: - xdg_current_desktop = os.environ["XDG_CURRENT_DESKTOP"] -else: - xdg_current_desktop = "" - -if "Marco" in wm_info or xdg_current_desktop == "MATE": - CONF_TOOLS = {"sound": "mate-volume-control", "keyboard": "mate-keyboard-properties", "mouse": "mate-mouse-properties"} -elif "Xfwm4" in wm_info or xdg_current_desktop == "XFCE": - CONF_TOOLS = {"keyboard": "xfce4-keyboard-settings", "mouse": "xfce4-mouse-settings"} - if os.path.exists("/usr/bin/pavucontrol"): - CONF_TOOLS["sound"] = "pavucontrol" - else: - CONF_TOOLS["sound"] = "xfce4-mixer" -elif "Muffin" in wm_info or xdg_current_desktop == "X-Cinnamon": - CONF_TOOLS = {"sound": "cinnamon-settings sound", "keyboard": "cinnamon-settings keyboard", "mouse": "cinnamon-settings mouse"} -elif "Mutter" in wm_info or "GNOME" in xdg_current_desktop: - CONF_TOOLS = {"sound": "gnome-control-center sound", "keyboard": "gnome-control-center keyboard", "mouse": "gnome-control-center mouse"} -elif "Unity" in wm_info or xdg_current_desktop == "Unity": - CONF_TOOLS = {"sound": "unity-control-center sound", "keyboard": "unity-control-center keyboard", "mouse": "unity-control-center mouse"} -elif xdg_current_desktop == "LXDE": - CONF_TOOLS = {"sound": "pavucontrol", "keyboard": "lxinput", "mouse": "lxinput"} -else: - print "Warning: DE could not be detected!" - CONF_TOOLS = {} - class Blueberry(Gtk.Application): ''' Create the UI ''' def __init__(self): Gtk.Application.__init__(self, application_id='com.linuxmint.blueberry', flags=Gio.ApplicationFlags.FLAGS_NONE) + self.detect_desktop_environment() self.connect("activate", self.on_activate) def on_activate(self, data=None): @@ -61,98 +33,234 @@ self.get_active_window().present() else: self.create_window() - + + def detect_desktop_environment(self): + wm_info = commands.getoutput("wmctrl -m") + if "XDG_CURRENT_DESKTOP" in os.environ: + xdg_current_desktop = os.environ["XDG_CURRENT_DESKTOP"] + else: + xdg_current_desktop = "" + + if "Marco" in wm_info or xdg_current_desktop == "MATE": + self.de = "Mate" + self.configuration_tools = {"sound": "mate-volume-control", "keyboard": "mate-keyboard-properties", "mouse": "mate-mouse-properties"} + elif "Xfwm4" in wm_info or xdg_current_desktop == "XFCE": + self.de = "Xfce" + self.configuration_tools = {"keyboard": "xfce4-keyboard-settings", "mouse": "xfce4-mouse-settings"} + if os.path.exists("/usr/bin/pavucontrol"): + self.configuration_tools["sound"] = "pavucontrol" + else: + self.configuration_tools["sound"] = "xfce4-mixer" + elif "Muffin" in wm_info or xdg_current_desktop == "X-Cinnamon": + self.de = "Cinnamon" + self.configuration_tools = {"sound": "cinnamon-settings sound", "keyboard": "cinnamon-settings keyboard", "mouse": "cinnamon-settings mouse"} + elif "Mutter" in wm_info or "GNOME" in xdg_current_desktop: + self.de = "Gnome" + self.configuration_tools = {"sound": "gnome-control-center sound", "keyboard": "gnome-control-center keyboard", "mouse": "gnome-control-center mouse"} + elif "Unity" in wm_info or xdg_current_desktop == "Unity": + self.de = "Unity" + self.configuration_tools = {"sound": "unity-control-center sound", "keyboard": "unity-control-center keyboard", "mouse": "unity-control-center mouse"} + elif xdg_current_desktop == "LXDE": + self.de = "LXDE" + self.configuration_tools = {"sound": "pavucontrol", "keyboard": "lxinput", "mouse": "lxinput"} + else: + self.de = "Unknown" + print "Warning: DE could not be detected!" + self.configuration_tools = {} + if os.path.exists("/usr/bin/pavucontrol"): + self.configuration_tools["sound"] = "pavucontrol" + def create_window(self): self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) self.window.set_title(_("Bluetooth")) self.window.set_icon_name("bluetooth") self.window.connect("destroy", self.terminate) - self.window.set_default_size(640, 480) + self.window.set_default_size(640, 400) self.main_box = Gtk.VBox() + # Toolbar + toolbar = Gtk.Toolbar() + toolbar.get_style_context().add_class("primary-toolbar") + self.main_box.pack_start(toolbar, False, False, 0) + + self.main_stack = Gtk.Stack() + self.main_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.main_stack.set_transition_duration(150) + self.main_box.pack_start(self.main_stack, True, True, 0) + + stack_switcher = Gtk.StackSwitcher() + stack_switcher.set_stack(self.main_stack) + + tool_item = Gtk.ToolItem() + tool_item.set_expand(True) + tool_item.get_style_context().add_class("raised") + toolbar.insert(tool_item, 0) + switch_holder = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + switch_holder.set_border_width(1) + tool_item.add(switch_holder) + switch_holder.pack_start(stack_switcher, True, True, 0) + stack_switcher.set_halign(Gtk.Align.CENTER) + toolbar.show_all() + + self.settings = Gio.Settings("org.blueberry") + + debug = False + if len(sys.argv) > 1 and sys.argv[1] == "debug": + debug = True + + # Devices + self.devices_box = Gtk.VBox() + self.devices_box.set_border_width(12) + + self.rf_switch = Gtk.Switch() + self.rfkill = rfkillMagic.Interface(self.update_ui_callback, debug) + self.rf_handler_id = self.rf_switch.connect("state-set", self.on_switch_changed) + self.status_image = Gtk.Image() self.status_image.set_from_icon_name("blueberry", Gtk.IconSize.DIALOG) self.status_image.show() self.stack = Gtk.Stack() - self.rf_switch = Gtk.Switch() - self.add_stack_page(_("Bluetooth is disabled"), BLUETOOTH_DISABLED_PAGE); self.add_stack_page(_("No Bluetooth adapters found"), BLUETOOTH_NO_DEVICES_PAGE); self.add_stack_page(_("Bluetooth is disabled by hardware switch"), BLUETOOTH_HW_DISABLED_PAGE); self.lib_widget = GnomeBluetooth.SettingsWidget.new(); - self.lib_widget.connect("panel-changed", self.panel_changed); - self.stack.add_named(self.lib_widget, BLUETOOTH_WORKING_PAGE) - self.lib_widget.show() self.stack.show(); - self.main_box.show(); - self.main_box.set_border_width(12) switchbox = Gtk.VBox() hbox = Gtk.HBox() - hbox.pack_end(self.rf_switch, False, False, 0) + hbox.pack_end(self.rf_switch, False, False, 10) switchbox.pack_start(hbox, False, False, 0) - switchbox.pack_start(self.status_image, False, False, 10) + switchbox.pack_start(self.status_image, False, False, 0) switchbox.show_all() - self.main_box.pack_start(switchbox, False, False, 10) - self.main_box.pack_start(self.stack, True, True, 10) + self.devices_box.pack_start(switchbox, False, False, 0) + self.devices_box.pack_start(self.stack, True, True, 0) - self.window.add(self.main_box) - - debug = False - if len(sys.argv) > 1 and sys.argv[1] == "debug": - debug = True + self.main_stack.add_titled(self.devices_box, "devices", _("Devices")) - self.rfkill = rfkillMagic.Interface(self.update_ui_callback, debug) - self.rf_handler_id = self.rf_switch.connect("state-set", self.on_switch_changed) + # Settings - self.settings = blueberrySettings.Settings() + page = SettingsPage() - traybox = Gtk.HBox() - self.traybutton = Gtk.CheckButton(label=_("Show a tray icon")) - self.traybutton.set_active(self.settings.get_tray_enabled()) - self.traybutton.connect("toggled", self.on_tray_button_toggled) - self.settings.gsettings.connect("changed::tray-enabled", self.on_settings_changed) + section = page.add_section(_("Bluetooth settings")) + self.adapter_name_entry = Gtk.Entry() + self.adapter_name_entry.set_text(self.get_default_adapter_name()) + self.adapter_name_entry.connect("changed", self.on_adapter_name_changed) + row = SettingsRow(Gtk.Label(_("Name")), self.adapter_name_entry) + row.set_tooltip_text(_("This is the Bluetooth name of your computer")) + section.add_row(row) + + self.obex_switch = Gtk.Switch() + self.obex_switch.set_active(self.settings.get_boolean("obex-enabled")) + self.obex_switch.connect("notify::active", self.on_obex_switch_toggled) + self.settings.connect("changed", self.on_settings_changed) + row = SettingsRow(Gtk.Label(_("Receive files from remote devices")), self.obex_switch) + row.set_tooltip_text(_("This option allows your computer to receive files transferred over Bluetooth (OBEX)")) + section.add_row(row) + + self.tray_switch = Gtk.Switch() + self.tray_switch.set_active(self.settings.get_boolean("tray-enabled")) + self.tray_switch.connect("notify::active", self.on_tray_switch_toggled) + self.settings.connect("changed", self.on_settings_changed) + if self.de != "Cinnamon": + # In Cinnamon we're using an applet instead + section.add_row(SettingsRow(Gtk.Label(_("Show a tray icon")), self.tray_switch)) - traybox.pack_start(self.traybutton, False, False, 0) - traybox.show_all() + self.window.add(self.main_box) - self.main_box.pack_start(traybox, False, False, 0) + self.main_stack.add_titled(page, "settings", _("Settings")) - self.window.show() + self.devices_box.show_all() self.update_ui_callback() self.add_window(self.window) + self.window.show_all() + + self.client = GnomeBluetooth.Client() + self.model = self.client.get_model() + self.model.connect('row-changed', self.update_status) + self.model.connect('row-deleted', self.update_status) + self.model.connect('row-inserted', self.update_status) + self.update_status() def panel_changed(self, widget, panel): - if not panel in CONF_TOOLS: + if not panel in self.configuration_tools: print "Warning, no configuration tool known for panel '%s'" % panel else: - os.system("%s &" % CONF_TOOLS[panel]) + os.system("%s &" % self.configuration_tools[panel]) - def on_tray_button_toggled(self, widget, data=None): + def on_tray_switch_toggled(self, widget, data=None): if widget.get_active(): - self.settings.set_tray_enabled(True) + self.settings.set_boolean("tray-enabled", True) subprocess.Popen(["blueberry-tray"]) else: - self.settings.set_tray_enabled(False) + self.settings.set_boolean("tray-enabled", False) + + def on_obex_switch_toggled(self, widget, data=None): + if widget.get_active(): + self.settings.set_boolean("obex-enabled", True) + os.system("/usr/lib/blueberry/blueberry-obex-agent.py &") + else: + self.settings.set_boolean("obex-enabled", False) + os.system("killall -9 blueberry-obex-agent"); + self.update_status() + + def on_adapter_name_changed(self, entry): + subprocess.call(["bt-adapter", "--set", "Alias", entry.get_text()]) + self.update_status() def on_settings_changed(self, settings, key): - self.traybutton.set_active(self.settings.get_tray_enabled()) + self.tray_switch.set_active(self.settings.get_boolean("tray-enabled")) + self.obex_switch.set_active(self.settings.get_boolean("obex-enabled")) def add_stack_page(self, message, name): label = Gtk.Label(message) self.stack.add_named(label, name) label.show() + def get_default_adapter_name(self): + name = None + output = subprocess.check_output(["bt-adapter", "-i"]).strip() + for line in output.split("\n"): + line = line.strip() + if line.startswith("Alias: "): + name = line.replace("Alias: ", "").replace(" [rw]", "").replace(" [ro]", "") + break + return name + + def update_status(self, path=None, iter=None, data=None): + try: + # In version 3.18, gnome_bluetooth_settings_widget + # doesn't give explicit access to its label via gi + # but it's a composite widget and its hierarchy is: + # scrolledwindow -> viewport -> vbox -> explanation-label + scrolledwindow = self.lib_widget.get_children()[0] + scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + viewport = scrolledwindow.get_children()[0] + vbox = viewport.get_children()[0] + explanation_label = vbox.get_children()[0] + name = self.get_default_adapter_name() + if name is not None: + if self.settings.get_boolean('obex-enabled'): + text = _("Visible as %s and available for Bluetooth file transfers.") + else: + text = _("Visible as %s.") + text = "%s\n" % text + explanation_label.set_markup(text % "\"%s\"" % name) + else: + explanation_label.set_label("") + except Exception as e: + print (e) + return None + def update_ui_callback(self): powered = False sensitive = False diff -Nru blueberry-1.1.9/usr/lib/blueberry/blueberrySettings.py blueberry-1.1.10/usr/lib/blueberry/blueberrySettings.py --- blueberry-1.1.9/usr/lib/blueberry/blueberrySettings.py 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/usr/lib/blueberry/blueberrySettings.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -#!/usr/bin/env python2 - -from gi.repository import Gio - -SETTINGS_SCHEMA = "org.blueberry" -TRAY_KEY = "tray-enabled" - -class Settings(): - def __init__(self): - self.gsettings = Gio.Settings.new(SETTINGS_SCHEMA) - - def get_tray_enabled(self): - return self.gsettings.get_boolean(TRAY_KEY) - - def set_tray_enabled(self, enabled): - self.gsettings.set_boolean(TRAY_KEY, enabled) - - - diff -Nru blueberry-1.1.9/usr/lib/blueberry/BlueberrySettingsWidgets.py blueberry-1.1.10/usr/lib/blueberry/BlueberrySettingsWidgets.py --- blueberry-1.1.9/usr/lib/blueberry/BlueberrySettingsWidgets.py 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/lib/blueberry/BlueberrySettingsWidgets.py 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,110 @@ +#!/usr/bin/env python2 + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +def list_header_func(row, before, user_data): + if before and not row.get_header(): + row.set_header(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) + +class SettingsPage(Gtk.Box): + + def __init__(self): + Gtk.Box.__init__(self) + self.set_orientation(Gtk.Orientation.VERTICAL) + self.set_spacing(15) + self.set_margin_left(80) + self.set_margin_right(80) + self.set_margin_top(15) + self.set_margin_bottom(15) + + def add_section(self, title): + section = SettingsBox(title) + self.pack_start(section, False, False, 0) + + return section + +class SettingsBox(Gtk.Frame): + + def __init__(self, title): + Gtk.Frame.__init__(self) + self.set_shadow_type(Gtk.ShadowType.IN) + frame_style = self.get_style_context() + frame_style.add_class("view") + self.size_group = Gtk.SizeGroup() + self.size_group.set_mode(Gtk.SizeGroupMode.VERTICAL) + + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.add(self.box) + + toolbar = Gtk.Toolbar.new() + toolbar_context = toolbar.get_style_context() + Gtk.StyleContext.add_class(Gtk.Widget.get_style_context(toolbar), "cs-header") + + label = Gtk.Label.new() + label.set_markup("%s" % title) + title_holder = Gtk.ToolItem() + title_holder.add(label) + toolbar.add(title_holder) + self.box.add(toolbar) + + toolbar_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + self.box.add(toolbar_separator) + separator_context = toolbar_separator.get_style_context() + frame_color = frame_style.get_border_color(Gtk.StateFlags.NORMAL).to_string() + css_provider = Gtk.CssProvider() + css_provider.load_from_data(".separator { -GtkWidget-wide-separators: 0; \ + color: %s; \ + }" % frame_color) + separator_context.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + self.list_box = Gtk.ListBox() + self.list_box.set_selection_mode(Gtk.SelectionMode.NONE) + self.list_box.set_header_func(list_header_func, None) + self.box.add(self.list_box) + + def add_row(self, row): + self.list_box.add(row) + + +class SettingsRow(Gtk.ListBoxRow): + + def __init__(self, label, main_widget, alternative_widget=None): + + self.main_widget = main_widget + self.alternative_widget = alternative_widget + self.label = label + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.stack.set_transition_duration(1000) + + self.stack.add_named(main_widget, "main_widget") + if alternative_widget is not None: + self.stack.add_named(self.alternative_widget, "alternative_widget") + + Gtk.ListBoxRow.__init__(self) + + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + hbox.set_border_width(5) + hbox.set_margin_left(20) + hbox.set_margin_right(20) + self.add(hbox) + + grid = Gtk.Grid() + grid.set_column_spacing(15) + hbox.pack_start(grid, True, True, 0) + + self.description_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.description_box.props.hexpand = True + self.description_box.props.halign = Gtk.Align.START + self.description_box.props.valign = Gtk.Align.CENTER + self.label.props.xalign = 0.0 + self.description_box.add(self.label) + + grid.attach(self.description_box, 0, 0, 1, 1) + grid.attach_next_to(self.stack, self.description_box, Gtk.PositionType.RIGHT, 1, 1) + + def show_alternative_widget(self): + if self.alternative_widget is not None: + self.stack.set_visible_child(self.alternative_widget) \ No newline at end of file diff -Nru blueberry-1.1.9/usr/lib/blueberry/blueberry-tray.py blueberry-1.1.10/usr/lib/blueberry/blueberry-tray.py --- blueberry-1.1.9/usr/lib/blueberry/blueberry-tray.py 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/usr/lib/blueberry/blueberry-tray.py 2017-02-17 11:52:21.000000000 +0000 @@ -7,7 +7,6 @@ gi.require_version('GnomeBluetooth', '1.0') from gi.repository import Gtk, Gdk, GnomeBluetooth, Gio import rfkillMagic -import blueberrySettings import subprocess # i18n @@ -20,11 +19,11 @@ debug = True self.rfkill = rfkillMagic.Interface(self.update_icon_callback, debug) - self.settings = blueberrySettings.Settings() - self.settings.gsettings.connect("changed::tray-enabled", self.on_settings_changed_cb) + self.settings = Gio.Settings("org.blueberry") + self.settings.connect("changed::tray-enabled", self.on_settings_changed_cb) # If we have no adapter, or disabled tray, end early - if (not self.rfkill.have_adapter) or (not self.settings.get_tray_enabled()): + if (not self.rfkill.have_adapter) or (not self.settings.get_boolean("tray-enabled")): self.rfkill.terminate() sys.exit(0) @@ -42,7 +41,7 @@ self.update_icon_callback(None, None, None) def on_settings_changed_cb(self, setting, key, data=None): - if not self.settings.get_tray_enabled(): + if not self.settings.get_boolean("tray-enabled"): self.terminate() def update_icon_callback(self, path=None, iter=None, data=None): @@ -52,24 +51,23 @@ if self.rfkill.hard_block or self.rfkill.soft_block: self.icon.set_from_icon_name("blueberry-tray-disabled") - self.icon.set_tooltip_text(_("Bluetooth")) + self.icon.set_tooltip_text(_("Bluetooth is disabled")) else: self.icon.set_from_icon_name("blueberry-tray") self.update_connected_state() def update_connected_state(self): - n_devices = self.get_n_devices() + connected_devices = self.get_connected_devices() - if n_devices > 0: + if len(connected_devices) > 0: self.icon.set_from_icon_name("blueberry-tray-active") - self.icon.set_tooltip_text(gettext.ngettext("Bluetooth: %d device connected" % n_devices, - "Bluetooth: %d devices connected" % n_devices, - n_devices)) + self.icon.set_tooltip_text(_("Bluetooth: Connected to %s") % (", ".join(connected_devices))) else: self.icon.set_from_icon_name("blueberry-tray") self.icon.set_tooltip_text(_("Bluetooth")) - def get_n_devices(self): + def get_connected_devices(self): + connected_devices = [] default_iter = None iter = self.model.get_iter_first() @@ -81,20 +79,17 @@ break iter = self.model.iter_next(iter) - if default_iter == None: - return False - - n_devices = 0 + if default_iter != None: + iter = self.model.iter_children(default_iter) + while iter: + connected = self.model.get_value(iter, GnomeBluetooth.Column.CONNECTED) + if connected: + name = self.model.get_value(iter, GnomeBluetooth.Column.NAME) + connected_devices.append(name) - iter = self.model.iter_children(default_iter) - while iter: - connected = self.model.get_value(iter, GnomeBluetooth.Column.CONNECTED) - if connected: - n_devices += 1 - - iter = self.model.iter_next(iter) + iter = self.model.iter_next(iter) - return n_devices + return connected_devices def on_activate(self, icon, data=None): subprocess.Popen(["blueberry"]) diff -Nru blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/applet.js blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/applet.js --- blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/applet.js 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/applet.js 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,179 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Applet = imports.ui.applet; +const Lang = imports.lang; +const St = imports.gi.St; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; +const Main = imports.ui.main; +const GnomeBluetooth = imports.gi.GnomeBluetooth; +const GLib = imports.gi.GLib; + +// Override Gettext localization +const Gettext = imports.gettext; +Gettext.bindtextdomain('blueberry', '/usr/share/locale'); + +function gettextBT(string) { + return Gettext.dgettext("blueberry", string); +} + +function MyApplet(metadata, orientation, panel_height, instance_id) { + this._init(metadata, orientation, panel_height, instance_id); +} + +MyApplet.prototype = { + __proto__: Applet.TextIconApplet.prototype, + + _init: function(metadata, orientation, panel_height, instance_id) { + Applet.TextIconApplet.prototype._init.call(this, orientation, panel_height, instance_id); + + this.setAllowedLayout(Applet.AllowedLayout.BOTH); + + this.metadata = metadata; + Main.systrayManager.registerRole("blueberry-tray.py", metadata.uuid); + this.set_applet_icon_symbolic_name('blueberry-applet'); + this.set_applet_tooltip(gettextBT("Bluetooth")); + + try { + this.menuManager = new PopupMenu.PopupMenuManager(this); + this.menu = new Applet.AppletPopupMenu(this, orientation); + this.menuManager.addMenu(this.menu); + + let item = new PopupMenu.PopupIconMenuItem(gettextBT("Send files to a device"), "send-to", St.IconType.SYMBOLIC); + item.connect('activate', Lang.bind(this, function() { + Util.spawnCommandLine("bluetooth-sendto"); + })); + this.menu.addMenuItem(item); + + item = new PopupMenu.PopupIconMenuItem(gettextBT("Open Bluetooth device manager"), "preferences-system", St.IconType.SYMBOLIC); + item.connect('activate', Lang.bind(this, function() { + Util.spawnCommandLine("blueberry"); + })); + this.menu.addMenuItem(item); + + this.on_orientation_changed(orientation); + + this._client = new GnomeBluetooth.Client(); + this._model = this._client.get_model(); + this._model.connect('row-changed', Lang.bind(this, this._sync)); + this._model.connect('row-deleted', Lang.bind(this, this._sync)); + this._model.connect('row-inserted', Lang.bind(this, this._sync)); + this._sync(); + } + catch (e) { + global.logError(e); + } + }, + + on_applet_clicked: function(event) { + this.menu.toggle(); + }, + + on_applet_removed_from_panel: function() { + Main.systrayManager.unregisterId(this.metadata.uuid); + }, + + on_orientation_changed: function(orientation) { + if (orientation == St.Side.LEFT || orientation == St.Side.RIGHT) + this.hide_applet_label(true); + else + this.hide_applet_label(false); + }, + + _getDefaultAdapter: function() { + let [ret, iter] = this._model.get_iter_first(); + while (ret) { + let isDefault = this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT); + let isPowered = this._model.get_value(iter, GnomeBluetooth.Column.POWERED); + if (isDefault && isPowered) { + return iter; + } + ret = this._model.iter_next(iter); + } + return null; + }, + + _get_connected_devices: function() { + let nDevices = 0; + let connected_devices = new Array(); + + let adapter = this._getDefaultAdapter(); + if (!adapter) + return [-1, connected_devices]; + + let [ret, iter] = this._model.iter_children(adapter); + while (ret) { + let isConnected = this._model.get_value(iter, GnomeBluetooth.Column.CONNECTED); + if (isConnected) { + let name = this._model.get_value(iter, GnomeBluetooth.Column.NAME); + connected_devices.push(name); + } + let isPaired = this._model.get_value(iter, GnomeBluetooth.Column.PAIRED); + let isTrusted = this._model.get_value(iter, GnomeBluetooth.Column.TRUSTED); + if (isPaired || isTrusted) { + nDevices++; + } + ret = this._model.iter_next(iter); + } + + return [nDevices, connected_devices]; + }, + + _computer_has_a_bt_adapter: function() { + try { + let [result, stdout, stderr] = GLib.spawn_command_line_sync("/usr/sbin/rfkill list bluetooth"); + if (stdout != null) { + let output = stdout.toString(); + let lines = output.split('\n'); + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if (line.search("Bluetooth") != -1) { + global.logError("Found BT Adapter: " + line); + return true; + } + } + } + } + catch (e) { + global.logError(e); + } + return false; + }, + + _sync: function() { + + try { + if (this._computer_has_a_bt_adapter()) { + this.set_applet_enabled(true); + let [ nDevices, connected_devices ] = this._get_connected_devices(); + if (nDevices >= 0) { + if (connected_devices.length > 0) { + this.set_applet_icon_symbolic_name('blueberry-applet-connected'); + let text = gettextBT("Bluetooth: Connected to %s"); + text = text.replace("%s", connected_devices.join(", ")); + this.set_applet_tooltip(text); + } + else { + this.set_applet_icon_symbolic_name('blueberry-applet'); + this.set_applet_tooltip(gettextBT("Bluetooth")); + } + } + else { + this.set_applet_icon_symbolic_name('blueberry-applet-disabled'); + this.set_applet_tooltip(gettextBT("Bluetooth is disabled")); + } + } + else { + this.set_applet_enabled(false); + } + } + catch (e) { + global.logError(e); + } + } +}; + +function main(metadata, orientation, panel_height, instance_id) { + let myApplet = new MyApplet(metadata, orientation, panel_height, instance_id); + return myApplet; +} diff -Nru blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-connected-symbolic.svg blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-connected-symbolic.svg --- blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-connected-symbolic.svg 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-connected-symbolic.svg 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,4 @@ + + + + diff -Nru blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-disabled-symbolic.svg blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-disabled-symbolic.svg --- blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-disabled-symbolic.svg 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-disabled-symbolic.svg 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff -Nru blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-symbolic.svg blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-symbolic.svg --- blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-symbolic.svg 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/icons/blueberry-applet-symbolic.svg 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,4 @@ + + + + diff -Nru blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/metadata.json blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/metadata.json --- blueberry-1.1.9/usr/share/cinnamon/applets/blueberry@cinnamon.org/metadata.json 1970-01-01 00:00:00.000000000 +0000 +++ blueberry-1.1.10/usr/share/cinnamon/applets/blueberry@cinnamon.org/metadata.json 2017-02-17 11:52:21.000000000 +0000 @@ -0,0 +1,7 @@ +{ + "dangerous": false, + "description": "Blueberry applet", + "uuid": "blueberry@cinnamon.org", + "name": "Bluetooth", + "last-edited": "1355436562" +} \ No newline at end of file diff -Nru blueberry-1.1.9/usr/share/glib-2.0/schemas/org.blueberry.gschema.xml blueberry-1.1.10/usr/share/glib-2.0/schemas/org.blueberry.gschema.xml --- blueberry-1.1.9/usr/share/glib-2.0/schemas/org.blueberry.gschema.xml 2016-12-12 12:33:36.000000000 +0000 +++ blueberry-1.1.10/usr/share/glib-2.0/schemas/org.blueberry.gschema.xml 2017-02-17 11:52:21.000000000 +0000 @@ -4,6 +4,10 @@ true Show a tray icon when bluetooth is enabled or connected + + true + Allow remote devices to send files to this computer via bluetooth +