diff options
-rwxr-xr-x | .extract.sh | 29 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | README.md | 59 | ||||
-rw-r--r-- | Settings.ui | 507 | ||||
-rw-r--r-- | bluetooth.js | 109 | ||||
-rw-r--r-- | bluetooth_legacy.js | 124 | ||||
-rw-r--r-- | convenience.js | 93 | ||||
-rw-r--r-- | debian/changelog | 159 | ||||
-rw-r--r-- | debian/clean | 1 | ||||
-rw-r--r-- | debian/control | 17 | ||||
-rw-r--r-- | debian/copyright | 38 | ||||
-rw-r--r-- | debian/install | 5 | ||||
-rwxr-xr-x | debian/rules | 7 | ||||
-rw-r--r-- | debian/upstream/metadata | 4 | ||||
-rw-r--r-- | debian/watch | 2 | ||||
-rw-r--r-- | extension.js | 305 | ||||
-rw-r--r-- | metadata.json | 7 | ||||
-rw-r--r-- | po/bluetooth-quick-connect.pot | 66 | ||||
-rw-r--r-- | po/cs.po | 67 | ||||
-rw-r--r-- | po/de.po | 66 | ||||
-rw-r--r-- | po/el.po | 67 | ||||
-rw-r--r-- | po/es.po | 69 | ||||
-rw-r--r-- | po/fr.po | 67 | ||||
-rw-r--r-- | po/nl.po | 66 | ||||
-rw-r--r-- | po/oc.po | 66 | ||||
-rw-r--r-- | po/pl.po | 68 | ||||
-rw-r--r-- | power.js | 37 | ||||
-rw-r--r-- | prefs.js | 48 | ||||
-rw-r--r-- | schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml | 20 | ||||
-rw-r--r-- | settings.js | 72 | ||||
-rw-r--r-- | ui.js | 298 | ||||
-rw-r--r-- | utils.js | 82 |
33 files changed, 2179 insertions, 467 deletions
diff --git a/.extract.sh b/.extract.sh new file mode 100755 index 0000000..b26f890 --- /dev/null +++ b/.extract.sh @@ -0,0 +1,29 @@ +#!/bin/bash +if [[ $# -ne 1 ]]; then + echo "usage $0 dir" >&2 + exit 1 +fi + +dir="$1" + +if [[ -e $dir ]]; then + echo "Error: $dir already exists" >&2 + exit 1 +fi + +mkdir -p "$dir" +cd "$dir" + +GS=/usr/lib64/gnome-shell/libgnome-shell.so + +for r in $(gresource list $GS); do + t="${r/#\/org\/gnome\/shell\/}" + mkdir -p $(dirname $t) + echo Extracting $t + gresource extract $GS $r >$t +done + +echo +echo "Now add the following to /etc/environment and restart gnome-shell" +echo "if you want to run with these extracted source files." +echo "GNOME_SHELL_JS=$PWD" @@ -1,2 +1,4 @@ /.idea /schemas/gschemas.compiled +/.gnome-shell +/bluetooth-quick-connect@bjarosze.gmail.com.shell-extension.zip
\ No newline at end of file @@ -2,13 +2,20 @@ # SPDX-License-Identifier: GPL-2.0-or-later # (see extension.js for details) -all: - glib-compile-schemas --strict --targetdir=schemas schemas - -dist: all - zip gnome-bluetooth-quick-connect.zip -9r * +dist: + gnome-extensions pack -f --extra-source=bluetooth.js --extra-source=bluetooth_legacy.js \ + --extra-source=power.js --extra-source=quickSettings.js --extra-source=settings.js \ + --extra-source=Settings.ui --extra-source=ui.js --extra-source=utils.js -o . clean: rm -fr gnome-bluetooth-quick-connect.zip schemas/gschemas.compiled -.PHONY: all clean dist +install: dist + gnome-extensions install -f bluetooth-quick-connect@bjarosze.gmail.com.shell-extension.zip + +translation: + mkdir -p po + xgettext --from-code=UTF-8 extension.js quickSettings.js ui.js Settings.ui \ + -o po/bluetooth-quick-connect.pot + +.PHONY: clean dist @@ -1,19 +1,64 @@ -# Bluetooth quick connect +# Bluetooth Quick Connect This extension allows paired Bluetooth devices to be connected and -disconnected via the GNOME system menu, without having to enter the +disconnected via the GNOME system menu, without need to enter the Settings app every time. -## Installation from extensions.gnome.org +## Installation + +### Requirements + + * bluez (on ubuntu: `sudo apt install bluez`) + +### Installation from extensions.gnome.org https://extensions.gnome.org/extension/1401/bluetooth-quick-connect/ -## Installation from source code +### Installation from source code ``` git clone https://github.com/bjarosze/gnome-bluetooth-quick-connect cd gnome-bluetooth-quick-connect -make -rm -r ~/.local/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com -cp -r gnome-bluetooth-quick-connect ~/.local/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com +make install +``` + +## Battery level + +Headset battery (currently) requires enabling experimental features in bluez. +See https://github.com/bjarosze/gnome-bluetooth-quick-connect/pull/42 for more details. + +## Troubleshooting + +### Connecting and disconnecting does not work + +This extensions calls `bluetoothctl` under the hood. If something does not work +you can try to execute `bluetoothctl` command in terminal and see what is wrong. + +#### Paired devices +```bash +bluetoothctl -- paired-devices +``` + +#### Connecting +```bash +bluetoothctl -- connect <mac address> +``` + +#### Disconnecting +```bash +bluetoothctl -- disconnect <mac address> +``` + +#### Reconnecting +```bash +bluetoothctl -- disconnect <mac> && sleep 7 && bluetoothctl -- connect <mac> +``` + +### Reconnecting does not work + +Not sure why, but sometimes bluetoothctl does not want to connect +device after it was disconnected. Reinstalling bluez and rebooting system helped on my ubuntu. +``` +$ sudo apt purge bluez gnome-bluetooth pulseaudio-module-bluetooth +$ sudo apt install bluez gnome-bluetooth pulseaudio-module-bluetooth ``` diff --git a/Settings.ui b/Settings.ui index 047b045..3743838 100644 --- a/Settings.ui +++ b/Settings.ui @@ -1,79 +1,69 @@ <?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 --> <!-- Copyright 2018 Bartosz Jaroszewski SPDX-License-Identifier: GPL-2.0-or-later (see extension.js for details) --> -<!-- Generated with glade 3.20.0 --> -<interface> - <requires lib="gtk+" version="3.12"/> +<interface domain="bluetooth-quick-connect"> + <requires lib="gtk+" version="3.12" /> <object class="GtkAdjustment" id="auto_power_off_interval_adjustment"> <property name="lower">1</property> <property name="upper">3600</property> - <property name="step_increment">1</property> - <property name="page_increment">5</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> </object> <object class="GtkBox" id="auto_power_off_settings"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">24</property> - <property name="margin_right">24</property> - <property name="margin_top">24</property> - <property name="margin_bottom">24</property> + <property name="can-focus">False</property> + <property name="margin-start">24</property> + <property name="margin-end">24</property> + <property name="margin-top">24</property> + <property name="margin-bottom">24</property> <property name="orientation">vertical</property> <property name="spacing">24</property> <child> <object class="GtkFrame"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label_xalign">0</property> - <property name="shadow_type">none</property> + <property name="can-focus">False</property> + <property name="label-xalign">0</property> <child> <object class="GtkListBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="selection_mode">none</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> <child> <object class="GtkListBoxRow"> <property name="visible">True</property> - <property name="can_focus">True</property> + <property name="can-focus">True</property> <child> - <object class="GtkGrid"> + <object class="GtkBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">12</property> - <property name="margin_right">12</property> - <property name="margin_top">12</property> - <property name="margin_bottom">12</property> - <property name="column_spacing">32</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> <child> <object class="GtkLabel"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">Checking idle inteval (seconds)</property> - <property name="xalign">0</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Checking idle interval (seconds)</property> </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> </child> <child> <object class="GtkSpinButton" id="auto_power_off_interval"> <property name="visible">True</property> - <property name="can_focus">True</property> + <property name="can-focus">True</property> <property name="halign">end</property> <property name="hexpand">True</property> <property name="text">60</property> <property name="adjustment">auto_power_off_interval_adjustment</property> - <property name="climb_rate">1</property> - <property name="snap_to_ticks">True</property> + <property name="climb-rate">1</property> + <property name="snap-to-ticks">True</property> <property name="numeric">True</property> </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> + </child> </object> </child> @@ -82,167 +72,380 @@ SPDX-License-Identifier: GPL-2.0-or-later </object> </child> </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> </child> </object> - <object class="GtkBox" id="bluetooth_quick_connect_settings"> + <object class="GtkScrolledWindow" id="items_container"> + <!-- <property name="width-request">450</property> --> + <!-- <property name="height-request">692</property> --> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">24</property> - <property name="margin_right">24</property> - <property name="margin_top">24</property> - <property name="margin_bottom">24</property> - <property name="orientation">vertical</property> - <property name="spacing">24</property> + <property name="can-focus">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar-policy">never</property> <child> - <object class="GtkFrame"> + <object class="GtkViewport" id="main_prefs_vp"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label_xalign">0</property> - <property name="shadow_type">in</property> + <property name="can-focus">False</property> <child> - <object class="GtkListBox"> + <object class="GtkBox" id="bluetooth_quick_connect_settings"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="selection_mode">none</property> + <property name="can-focus">False</property> + <property name="margin-start">24</property> + <property name="margin-end">24</property> + <property name="margin-top">24</property> + <property name="margin-bottom">24</property> + <property name="orientation">vertical</property> + <property name="spacing">24</property> <child> - <object class="GtkListBoxRow"> + <object class="GtkFrame"> <property name="visible">True</property> - <property name="can_focus">True</property> + <property name="can-focus">False</property> + <property name="label-xalign">0</property> <child> - <object class="GtkGrid"> + <object class="GtkListBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">12</property> - <property name="margin_right">12</property> - <property name="margin_top">12</property> - <property name="margin_bottom">12</property> - <property name="column_spacing">32</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> <child> - <object class="GtkSwitch" id="auto_power_on_switch"> + <object class="GtkListBoxRow"> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="halign">end</property> - <property name="valign">center</property> + <property name="can-focus">True</property> + <child> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkSwitch" id="auto_power_on_switch"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="auto_power_on_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Enable bluetooth when menu opened</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> </child> <child> - <object class="GtkLabel" id="auto_power_on_label"> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="label" translatable="yes">Enable bluetooth when menu opened</property> - <property name="xalign">0</property> + <property name="can-focus">True</property> + <child> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkLabel" id="auto_power_off_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Disable bluetooth if idle</property> + <property name="use-markup">True</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="auto_power_off_settings_button"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="valign">center</property> + <child> + <object class="GtkImage" id="image_window_previews_options2"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">emblem-system-symbolic</property> + </object> + </child> + <style> + <class name="circular" /> + </style> + </object> + + </child> + <child> + <object class="GtkSwitch" id="auto_power_off_switch"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + </object> + + </child> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> </child> - </object> - </child> - </object> - </child> - <child> - <object class="GtkListBoxRow"> - <property name="width_request">100</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <child> - <object class="GtkGrid"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="margin_left">12</property> - <property name="margin_right">12</property> - <property name="margin_top">12</property> - <property name="margin_bottom">12</property> - <property name="column_spacing">32</property> <child> - <object class="GtkLabel" id="auto_power_off_label"> + <object class="GtkListBoxRow"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="label" translatable="yes">Disable bluetooth if idle</property> - <property name="use_markup">True</property> - <property name="xalign">0</property> + <property name="can-focus">True</property> + <child> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkSwitch" id="keep_menu_on_toggle"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="keep_menu_on_toggle_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Keep the menu open after toggling the connection (restart required)</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - </packing> </child> <child> - <object class="GtkBox"> + <object class="GtkListBoxRow"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">6</property> + <property name="can-focus">True</property> <child> - <object class="GtkButton" id="auto_power_off_settings_button"> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="halign">center</property> - <property name="valign">center</property> - <property name="xalign">0.46000000834465027</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> <child> - <object class="GtkImage" id="image_window_previews_options2"> + <object class="GtkSwitch" id="refresh_button_on"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="icon_name">emblem-system-symbolic</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="refresh_button_on_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Show reconnect button (restart required)</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> </object> </child> - <style> - <class name="circular"/> - </style> </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can-focus">True</property> <child> - <object class="GtkSwitch" id="auto_power_off_switch"> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="halign">end</property> - <property name="valign">center</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkSwitch" id="debug_mode_on"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="debug_mode_on_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Debug mode (restart required)</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkSwitch" id="show_battery_value_on"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="show_battery_value_on_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Show battery value (restart required)</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <!-- n-columns=2 n-rows=1 --> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">32</property> + <child> + <object class="GtkSwitch" id="show_battery_icon_on"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="show_battery_icon_on_label"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" translatable="yes">Show battery icon (restart required)</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> </child> </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> </child> </object> </child> + <child type="label_item"> + <placeholder /> + </child> </object> + </child> </object> </child> - <child type="label_item"> - <placeholder/> - </child> </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> </child> </object> </interface> diff --git a/bluetooth.js b/bluetooth.js new file mode 100644 index 0000000..d9dff16 --- /dev/null +++ b/bluetooth.js @@ -0,0 +1,109 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const GnomeBluetooth = imports.gi.GnomeBluetooth; +const Signals = imports.signals; +const GLib = imports.gi.GLib; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +var BluetoothController = class { + constructor() { + this._client = new GnomeBluetooth.Client(); + this._deviceNotifyConnected = new Set(); + this._store = this._client.get_devices(); + } + + enable() { + this._client.connect('notify::default-adapter', () => { + this._deviceNotifyConnected.clear(); + this.emit('default-adapter-changed'); + }); + this._client.connect('notify::default-adapter-powered', () => { + this._deviceNotifyConnected.clear(); + this.emit('default-adapter-changed'); + }); + this._client.connect('device-removed', (c, path) => { + this._deviceNotifyConnected.delete(path); + this.emit('device-deleted'); + }); + this._client.connect('device-added', (c, device) => { + this._connectDeviceNotify(device); + this.emit('device-inserted', new BluetoothDevice(device)); + }); + } + + _connectDeviceNotify(device) { + const path = device.get_object_path(); + + if (this._deviceNotifyConnected.has(path)) + return; + + device.connect('notify', (device) => { + this.emit('device-changed', new BluetoothDevice(device)); + }); + } + + getDevices() { + let devices = []; + + for (let i = 0; i < this._store.get_n_items(); i++) { + let device = new BluetoothDevice(this._store.get_item(i)); + devices.push(device); + } + + return devices; + } + + getConnectedDevices() { + return this.getDevices().filter((device) => { + return device.isConnected; + }); + } + + destroy() { + this._disconnectSignals(); + } +} + +Signals.addSignalMethods(BluetoothController.prototype); +Utils.addSignalsHelperMethods(BluetoothController.prototype); + +var BluetoothDevice = class { + constructor(dev) { + this.update(dev); + } + + update(dev) { + this.name = dev.alias || dev.name; + this.isConnected = dev.connected; + this.isPaired = dev.paired; + this.mac = dev.address; + } + + disconnect() { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac}`) + } + + connect() { + Utils.spawn(`bluetoothctl -- connect ${this.mac}`) + } + + reconnect() { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac} && sleep 7 && bluetoothctl -- connect ${this.mac}`) + } +} diff --git a/bluetooth_legacy.js b/bluetooth_legacy.js new file mode 100644 index 0000000..e266187 --- /dev/null +++ b/bluetooth_legacy.js @@ -0,0 +1,124 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const GnomeBluetooth = imports.gi.GnomeBluetooth; +const Signals = imports.signals; +const GLib = imports.gi.GLib; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +var BluetoothController = class { + constructor() { + this._client = new GnomeBluetooth.Client(); + this._model = this._client.get_model(); + } + + enable() { + this._connectSignal(this._model, 'row-changed', (arg0, arg1, iter) => { + if (iter) { + let device = this._buildDevice(iter); + if (device.isDefault) + this.emit('default-adapter-changed', device); + else + this.emit('device-changed', device); + } + + }); + this._connectSignal(this._model, 'row-deleted', () => { + this.emit('device-deleted'); + }); + this._connectSignal(this._model, 'row-inserted', (arg0, arg1, iter) => { + if (iter) { + let device = this._buildDevice(iter); + this.emit('device-inserted', device); + } + }); + } + + getDevices() { + let adapter = this._getDefaultAdapter(); + if (!adapter) + return []; + + let devices = []; + + let [ret, iter] = this._model.iter_children(adapter); + while (ret) { + let device = this._buildDevice(iter); + devices.push(device); + ret = this._model.iter_next(iter); + } + + return devices; + } + + getConnectedDevices() { + return this.getDevices().filter((device) => { + return device.isConnected; + }); + } + + destroy() { + this._disconnectSignals(); + } + + _getDefaultAdapter() { + 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; + } + + _buildDevice(iter) { + return new BluetoothDevice(this._model, iter); + } +} + +Signals.addSignalMethods(BluetoothController.prototype); +Utils.addSignalsHelperMethods(BluetoothController.prototype); + +var BluetoothDevice = class { + constructor(model, iter) { + this._model = model; + this.update(iter); + } + + update(iter) { + this.name = this._model.get_value(iter, GnomeBluetooth.Column.ALIAS) || this._model.get_value(iter, GnomeBluetooth.Column.NAME); + this.isConnected = this._model.get_value(iter, GnomeBluetooth.Column.CONNECTED); + this.isPaired = this._model.get_value(iter, GnomeBluetooth.Column.PAIRED); + this.mac = this._model.get_value(iter, GnomeBluetooth.Column.ADDRESS); + this.isDefault = this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT); + } + + disconnect() { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac}`) + } + + connect() { + Utils.spawn(`bluetoothctl -- connect ${this.mac}`) + } + + reconnect() { + Utils.spawn(`bluetoothctl -- disconnect ${this.mac} && sleep 7 && bluetoothctl -- connect ${this.mac}`) + } +} diff --git a/convenience.js b/convenience.js deleted file mode 100644 index 1c866b2..0000000 --- a/convenience.js +++ /dev/null @@ -1,93 +0,0 @@ -/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ -/* - Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com> - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the GNOME nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -const Gettext = imports.gettext; -const Gio = imports.gi.Gio; - -const Config = imports.misc.config; -const ExtensionUtils = imports.misc.extensionUtils; - -/** - * initTranslations: - * @domain: (optional): the gettext domain to use - * - * Initialize Gettext to load translations from extensionsdir/locale. - * If @domain is not provided, it will be taken from metadata['gettext-domain'] - */ -function initTranslations(domain) { - let extension = ExtensionUtils.getCurrentExtension(); - - domain = domain || extension.metadata['gettext-domain']; - - // check if this extension was built with "make zip-file", and thus - // has the locale files in a subfolder - // otherwise assume that extension has been installed in the - // same prefix as gnome-shell - let localeDir = extension.dir.get_child('locale'); - if (localeDir.query_exists(null)) - Gettext.bindtextdomain(domain, localeDir.get_path()); - else - Gettext.bindtextdomain(domain, Config.LOCALEDIR); -} - -/** - * getSettings: - * @schema: (optional): the GSettings schema id - * - * Builds and return a GSettings schema for @schema, using schema files - * in extensionsdir/schemas. If @schema is not provided, it is taken from - * metadata['settings-schema']. - */ -function getSettings(schema) { - let extension = ExtensionUtils.getCurrentExtension(); - - schema = schema || extension.metadata['settings-schema']; - - const GioSSS = Gio.SettingsSchemaSource; - - // check if this extension was built with "make zip-file", and thus - // has the schema files in a subfolder - // otherwise assume that extension has been installed in the - // same prefix as gnome-shell (and therefore schemas are available - // in the standard folders) - let schemaDir = extension.dir.get_child('schemas'); - let schemaSource; - - if (schemaDir.query_exists(null)) - schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), - GioSSS.get_default(), - false); - else - schemaSource = GioSSS.get_default(); - - let schemaObj = schemaSource.lookup(schema, true); - if (!schemaObj) - throw new Error('Schema ' + schema + ' could not be found for extension ' - + extension.metadata.uuid + '. Please check your installation.'); - - return new Gio.Settings({ settings_schema: schemaObj }); -} diff --git a/debian/changelog b/debian/changelog index dd4244e..0fb7df9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,162 @@ +gnome-shell-extension-bluetooth-quick-connect (36-2) unstable; urgency=medium + + * d/clean: Fix FTBFS-twice-in-a-row by deleting build result + (Closes: #1044877) + * Upload to unstable (part of transition: #1043144) + + -- Simon McVittie <smcv@debian.org> Sun, 20 Aug 2023 23:06:19 +0100 + +gnome-shell-extension-bluetooth-quick-connect (36-1) experimental; urgency=medium + + * New upstream release + * d/control: gnome-shell 44 is required for this version + (Closes: #1041566) + + -- Simon McVittie <smcv@debian.org> Thu, 03 Aug 2023 18:42:54 +0100 + +gnome-shell-extension-bluetooth-quick-connect (33-1) unstable; urgency=medium + + * New upstream release + - New translation: oc + - No code changes + + -- Simon McVittie <smcv@debian.org> Fri, 24 Feb 2023 16:49:25 +0000 + +gnome-shell-extension-bluetooth-quick-connect (32-1) unstable; urgency=medium + + * New upstream release + - Sort devices in the menu + - Initial translation support: de, es, fr, nl, pl + * d/copyright: Update + * Standards-Version: 4.6.2 (no changes required) + * Build-depend on gnome-shell, for the gnome-extensions tool + * d/rules: Include all files from upstream's `make dist` + + -- Simon McVittie <smcv@debian.org> Mon, 13 Feb 2023 10:08:49 +0000 + +gnome-shell-extension-bluetooth-quick-connect (30-1) unstable; urgency=medium + + * New upstream release + * Drop patch, applied upstream + * Fix attribution in previous changelog entry + + -- Simon McVittie <smcv@debian.org> Mon, 26 Sep 2022 19:48:32 +0100 + +gnome-shell-extension-bluetooth-quick-connect (29-3) unstable; urgency=medium + + * Fix "this._proxy is undefined" error with some configuration options. + Thanks to frosth555 and WorkingRobot on Github + + -- Simon McVittie <smcv@debian.org> Sat, 24 Sep 2022 20:35:20 +0100 + +gnome-shell-extension-bluetooth-quick-connect (29-2) unstable; urgency=medium + + * Add proposed patch to integrate with GNOME Shell 43 (Closes: #1018186) + * Standards-Version: 4.6.1 (no changes required) + + -- Simon McVittie <smcv@debian.org> Sun, 18 Sep 2022 20:22:29 +0100 + +gnome-shell-extension-bluetooth-quick-connect (29-1) unstable; urgency=medium + + * New upstream release + * Drop patches, applied upstream + + -- Simon McVittie <smcv@debian.org> Sun, 17 Apr 2022 16:40:23 +0100 + +gnome-shell-extension-bluetooth-quick-connect (26-2) unstable; urgency=medium + + * Add patches for compatibility with GNOME Shell 42 (Closes: #1008120) + + -- Simon McVittie <smcv@debian.org> Fri, 01 Apr 2022 12:00:02 +0100 + +gnome-shell-extension-bluetooth-quick-connect (26-1) unstable; urgency=medium + + * New upstream release + - Drop patch for compatibility with GNOME 41, applied upstream + + -- Simon McVittie <smcv@debian.org> Mon, 13 Dec 2021 12:15:27 +0000 + +gnome-shell-extension-bluetooth-quick-connect (23-2) unstable; urgency=medium + + * Declare compatibility with GNOME Shell 41 (Closes: #996047) + + -- Simon McVittie <smcv@debian.org> Sun, 17 Oct 2021 11:54:25 +0100 + +gnome-shell-extension-bluetooth-quick-connect (23-1) unstable; urgency=medium + + * New upstream release + * Suggest using gnome-extensions-app instead of gnome-shell-extension-prefs + * Add missing dependency on bluez + * Update standards version to 4.6.0 (no changes needed) + + -- Simon McVittie <smcv@debian.org> Wed, 06 Oct 2021 15:57:28 +0100 + +gnome-shell-extension-bluetooth-quick-connect (20-2) unstable; urgency=medium + + * Release to unstable + + -- Simon McVittie <smcv@debian.org> Sat, 11 Sep 2021 22:15:35 +0100 + +gnome-shell-extension-bluetooth-quick-connect (20-1) experimental; urgency=medium + + * New upstream release for GNOME 40 (Closes: #993057) + * d/watch: Update for Github changes + * Use debhelper compat level 13 + * Disable dh_auto_install. + It doesn't do the right thing for upstream's `make install`. + + -- Simon McVittie <smcv@debian.org> Tue, 31 Aug 2021 10:33:02 +0100 + +gnome-shell-extension-bluetooth-quick-connect (16-1) unstable; urgency=medium + + * New upstream release + - Drop patch series + - The code is identical to the patched versions 13-2 and 13-3 + + -- Simon McVittie <smcv@debian.org> Sat, 31 Oct 2020 11:32:12 +0000 + +gnome-shell-extension-bluetooth-quick-connect (13-3) unstable; urgency=medium + + * Release to unstable + + -- Simon McVittie <smcv@debian.org> Sat, 26 Sep 2020 11:30:25 +0100 + +gnome-shell-extension-bluetooth-quick-connect (13-2) experimental; urgency=medium + + * d/patches: Update to upstream v13-7-g662250e + - Drop compatibility with GNOME Shell < 3.37.x + - Be compatible with GNOME Shell >= 3.37.x + + -- Simon McVittie <smcv@debian.org> Mon, 07 Sep 2020 18:07:52 +0100 + +gnome-shell-extension-bluetooth-quick-connect (13-1) unstable; urgency=medium + + * New upstream release + - d/copyright: Update + - d/control: Adjust dependencies. + This version drops compatibility with GNOME Shell older than 3.36. + - Drop patch to declare compatibility with GNOME 3.36, no longer required + - Install schemas to expected location. + The new version is more specific about where to find them. + + -- Simon McVittie <smcv@debian.org> Mon, 01 Jun 2020 11:55:44 +0100 + +gnome-shell-extension-bluetooth-quick-connect (10-3) unstable; urgency=medium + + * Bump maximum GNOME Shell version to 3.36 + * Declare GNOME Shell 3.34, 3.36 compatibility in metadata + * Standards-Version: 4.5.0 (no changes required) + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + -- Simon McVittie <smcv@debian.org> Tue, 24 Mar 2020 20:06:38 +0000 + +gnome-shell-extension-bluetooth-quick-connect (10-2) unstable; urgency=medium + + * Expand supported version range to include GNOME Shell 3.34 + + -- Simon McVittie <smcv@debian.org> Wed, 11 Sep 2019 12:06:27 +0100 + gnome-shell-extension-bluetooth-quick-connect (10-1) unstable; urgency=medium * New upstream release diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..c4c4ffc --- /dev/null +++ b/debian/clean @@ -0,0 +1 @@ +*.zip diff --git a/debian/control b/debian/control index 537c312..bc06615 100644 --- a/debian/control +++ b/debian/control @@ -4,10 +4,13 @@ Priority: optional Maintainer: Simon McVittie <smcv@debian.org> Uploaders: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org> Build-Depends: - debhelper-compat (= 12), + debhelper-compat (= 13), gettext, + gnome-shell (>= 44~), + gnome-shell (<< 45~), libglib2.0-bin, -Standards-Version: 4.4.0 + unzip, +Standards-Version: 4.6.2 Homepage: https://github.com/bjarosze/gnome-bluetooth-quick-connect Vcs-Git: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-bluetooth-quick-connect.git Vcs-Browser: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-bluetooth-quick-connect @@ -16,15 +19,17 @@ Rules-Requires-Root: no Package: gnome-shell-extension-bluetooth-quick-connect Architecture: all Depends: - gnome-shell (<< 3.33), - gnome-shell (>= 3.28), + bluez, + gnome-shell (<< 45~), + gnome-shell (>= 44~), ${misc:Depends}, Recommends: - gnome-tweaks, + gnome-shell-extension-prefs, Description: GNOME Shell extension to connect paired Bluetooth devices This GNOME Shell extension adds entries to the shell's System menu to provide a quick way to connect and disconnect Bluetooth devices that were previously paired with the computer. . Please note that each user will need to enable the extension manually, for - example using the gnome-tweaks application. + example using gnome-extensions-app, which is part of the + gnome-shell-extension-prefs package. diff --git a/debian/copyright b/debian/copyright index c92cff3..f8c1e9b 100644 --- a/debian/copyright +++ b/debian/copyright @@ -4,19 +4,18 @@ Source: https://github.com/bjarosze/gnome-bluetooth-quick-connect Files: * Copyright: - © 2018 Bartosz Jaroszewski + © 2018-2023 Bartosz Jaroszewski + © 2010-2022 GNOME Shell contributors + © 2022 Simon McVittie + © 2023 Óscar Fernández Díaz + © 2023 Heimen Stoffels + © 2023 Robin Grenet License: GPL-2+ -Files: - convenience.js -Copyright: - © 2011-2012 Giovanni Campagna -License: BSD-3-clause - Files: debian/* Copyright: © 2014-2015 Tobias Frost - © 2016-2019 Simon McVittie + © 2016-2023 Simon McVittie License: GPL-2+ License: GPL-2+ @@ -32,26 +31,3 @@ License: GPL-2+ Comment: On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". - -License: BSD-3-clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the GNOME nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/install b/debian/install index 4f2d4c2..151c5ef 100644 --- a/debian/install +++ b/debian/install @@ -1,4 +1 @@ -*.js usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com -*.json usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com -*.ui usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com -schemas/*.xml usr/share/glib-2.0/schemas +usr/share/gnome-shell/extensions/bluetooth-quick-connect@bjarosze.gmail.com diff --git a/debian/rules b/debian/rules index 3c746ff..90c8f49 100755 --- a/debian/rules +++ b/debian/rules @@ -3,5 +3,12 @@ include /usr/share/dpkg/default.mk +extension_id=bluetooth-quick-connect@bjarosze.gmail.com +install_dir=$(CURDIR)/debian/tmp/usr/share/gnome-shell/extensions/$(extension_id) + %: dh $@ + +override_dh_auto_install: + install -d $(install_dir) + unzip -d $(install_dir) $(extension_id).shell-extension.zip diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 0000000..53ad048 --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/bjarosze/gnome-bluetooth-quick-connect/issues +Bug-Submit: https://github.com/bjarosze/gnome-bluetooth-quick-connect/issues/new +Repository: https://github.com/bjarosze/gnome-bluetooth-quick-connect.git +Repository-Browse: https://github.com/bjarosze/gnome-bluetooth-quick-connect diff --git a/debian/watch b/debian/watch index f64ea64..0382f1e 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,3 @@ version=4 opts=filenamemangle=s/.+\/v?@ANY_VERSION@(@ARCHIVE_EXT@)/@PACKAGE@-$1$2/,dversionmangle=s/\+git[0-9.]+$// \ -https://github.com/bjarosze/gnome-bluetooth-quick-connect/tags .*/archive/v?@ANY_VERSION@@ARCHIVE_EXT@ +https://github.com/bjarosze/gnome-bluetooth-quick-connect/tags .*/archive/refs/tags/v?@ANY_VERSION@@ARCHIVE_EXT@ diff --git a/extension.js b/extension.js index 403d956..7935288 100644 --- a/extension.js +++ b/extension.js @@ -15,227 +15,248 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. const Main = imports.ui.main; -const GnomeBluetooth = imports.gi.GnomeBluetooth; -const PopupMenu = imports.ui.popupMenu; -const Util = imports.misc.util; const GLib = imports.gi.GLib; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = Me.imports.convenience; - -class BluetoothDevice { - constructor(model, device) { - this._name = model.get_value(device, GnomeBluetooth.Column.NAME); - this._isConnected = model.get_value(device, GnomeBluetooth.Column.CONNECTED); - this._isPaired = model.get_value(device, GnomeBluetooth.Column.PAIRED); - this._mac = model.get_value(device, GnomeBluetooth.Column.ADDRESS); - } - - get name() { - return this._name; - } +const UiExtension = Me.imports.ui; - get isConnected() { - return this._isConnected; - } +const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); +const _ = Gettext.gettext; - get isPaired() { - return this._isPaired; - } +const Bluetooth = imports.gi.GnomeBluetooth.Client.prototype.get_devices === undefined ? + Me.imports.bluetooth_legacy : + Me.imports.bluetooth; - get mac() { - return this._mac; - } +const Utils = Me.imports.utils; +const Settings = Me.imports.settings.Settings; +const BatteryProvider = Me.imports.power.UPowerBatteryProvider; - get item() { - if (!this._item) - this._buildMenuItem(); - - return this._item; - } +const PopupMenu = imports.ui.popupMenu; - _buildMenuItem() { - this._item = new PopupMenu.PopupSwitchMenuItem(this.name, this.isConnected); - this._item.isDeviceSwitcher = true; - this._item.connect('toggled', (item, state) => { - if (state) - this._connect(); - else - this._disconnect(); - }); - } - _disconnect() { - this._call_bluetoothctl(`disconnect ${this.mac}`) - } +class BluetoothQuickConnect { + constructor(quickSettings, bluetooth, settings) { + this._logger = new Utils.Logger(settings); + this._logger.info('Initializing extension'); + if (quickSettings) { + let btIndicator = quickSettings._bluetooth; + let bluetoothToggle = btIndicator.quickSettingsItems[0]; + bluetoothToggle._updateDeviceVisibility = () => { + bluetoothToggle._deviceSection.actor.visible = false; + bluetoothToggle._placeholderItem.actor.visible = false; + } + bluetoothToggle._updateDeviceVisibility(); - _connect() { - this._call_bluetoothctl(`connect ${this.mac}`) - } + this._proxy = bluetoothToggle._client._proxy; + this._menu = new PopupMenu.PopupMenuSection(); - _call_bluetoothctl(command) { - let btctl_command = `echo -e "${command}\\n" | bluetoothctl`; - Util.spawn(['/usr/bin/env', 'bash', '-c', btctl_command]); - } -} - -class BluetoothQuickConnect { - constructor(bluetooth, settings) { - this._menu = bluetooth._item.menu; - this._proxy = bluetooth._proxy; + bluetoothToggle.menu.addMenuItem(this._menu, 0); + } else { + this._menu = bluetooth._item.menu; + this._proxy = bluetooth._proxy; + } + this._controller = new Bluetooth.BluetoothController(); this._settings = settings; + this._battery_provider = new BatteryProvider(this._logger); - this._signals = []; + this._items = {}; } enable() { - this._loadBluetoothModel(); + this._logger.info('Enabling extension'); + this._controller.enable(); + this._refresh(); + this._connectControllerSignals(); + this._connectIdleMonitor(); + this._connectMenuSignals(); + } + + _connectMenuSignals() { this._connectSignal(this._menu, 'open-state-changed', (menu, isOpen) => { - if (isOpen && this._autoPowerOnEnabled()) + this._logger.info(`Menu toggled: ${isOpen}`); + if (isOpen) + this._disconnectIdleMonitor(); + else + this._connectIdleMonitor(); + + if (isOpen && this._settings.isAutoPowerOnEnabled() && this._proxy.BluetoothAirplaneMode) { + this._logger.info('Disabling airplane mode'); this._proxy.BluetoothAirplaneMode = false; + } }); - - this._connectSignal(this._model, 'row-changed', () => this._sync()); - this._connectSignal(this._model, 'row-deleted', () => this._sync()); - this._connectSignal(this._model, 'row-inserted', () => this._sync()); - - this._idleMonitor(); - if (!this._proxy.BluetoothAirplaneMode) { - this._sync(); - } } disable() { + this._logger.info('Disabling extension'); this._destroy(); } test() { try { + this._logger.info('Testing bluetoothctl'); GLib.spawn_command_line_sync("bluetoothctl --version"); - } catch(error) { - Main.notifyError(`Bluetooth quick connect: error trying to execute "bluetoothctl"`); + this._logger.info('Test succeeded'); + } catch (error) { + Main.notifyError(_("Bluetooth Quick Connect"), _("Error trying to execute \"bluetoothctl\"")); + this._logger.info('Test failed'); } } - _idleMonitor() { - this._idleMonitorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._autoPowerOffCheckingInterval() * 1000, () => { - if (this._autoPowerOffEnabled() && this._getConnectedDevices().length === 0) - this._proxy.BluetoothAirplaneMode = true; + _connectControllerSignals() { + this._logger.info('Connecting bluetooth controller signals'); - return true; + this._connectSignal(this._controller, 'default-adapter-changed', (ctrl) => { + this._logger.info('Default adapter changed event'); + this._refresh(); }); - } - _connectSignal(subject, signal_name, method) { - let signal_id = subject.connect(signal_name, method); - this._signals.push({ - subject: subject, - signal_id: signal_id + this._connectSignal(this._controller, 'device-inserted', (ctrl, device) => { + this._logger.info(`Device inserted event: ${device.name}`); + if (device.isPaired) { + this._addMenuItem(device); + } else { + this._logger.info(`Device ${device.name} not paired, ignoring`); + } + }); + + this._connectSignal(this._controller, 'device-changed', (ctrl, device) => { + this._logger.info(`Device changed event: ${device.name}`); + if (device.isPaired) + this._syncMenuItem(device); + else + this._logger.info(`Skipping change event for unpaired device ${device.name}`); + }); + + this._connectSignal(this._controller, 'device-deleted', () => { + this._logger.info(`Device deleted event`); + this._refresh(); }); } - _loadBluetoothModel() { - this._client = new GnomeBluetooth.Client(); - this._model = this._client.get_model(); + _syncMenuItem(device) { + this._logger.info(`Synchronizing device menu item: ${device.name}`); + let item = this._items[device.mac] || this._addMenuItem(device); + + item.sync(device); } - _getDefaultAdapter() { - 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; + _addMenuItem(device) { + this._logger.info(`Adding device menu item: ${device.name} ${device.mac}`); + + let menuItem = new UiExtension.PopupBluetoothDeviceMenuItem( + device, + this._battery_provider, + this._logger, + { + showRefreshButton: this._settings.isShowRefreshButtonEnabled(), + closeMenuOnAction: !this._settings.isKeepMenuOnToggleEnabled(), + showBatteryValue: this._settings.isShowBatteryValueEnabled(), + showBatteryIcon: this._settings.isShowBatteryIconEnabled() + } + ); + + this._items[device.mac] = menuItem; + this._menu.addMenuItem(menuItem); + + return menuItem; } - _getDevices() { - let adapter = this._getDefaultAdapter(); - if (!adapter) - return []; + _connectIdleMonitor() { + if (this._idleMonitorId) return; - let devices = []; + this._logger.info('Connecting idle monitor'); - let [ret, iter] = this._model.iter_children(adapter); - while (ret) { - devices.push(new BluetoothDevice(this._model, iter)); - ret = this._model.iter_next(iter); - } + this._idleMonitorId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._settings.autoPowerOffCheckingInterval() * 1000, () => { + if (this._settings.isAutoPowerOffEnabled() && this._controller.getConnectedDevices().length === 0) + this._proxy.BluetoothAirplaneMode = true; - return devices; + return true; + }); } - _getPairedDevices() { - return this._getDevices().filter((device) => { - return device.isPaired || device.isConnected; - }); + _disconnectIdleMonitor() { + if (!this._idleMonitorId) return; + + this._logger.info('Disconnecting idle monitor'); + + GLib.Source.remove(this._idleMonitorId); + this._idleMonitorId = null; } - _getConnectedDevices() { - return this._getDevices().filter((device) => { - return device.isConnected; + _connectSignal(subject, signal_name, method) { + let signal_id = subject.connect(signal_name, method); + this._signals.push({ + subject: subject, + signal_id: signal_id }); } - _sync() { + _refresh() { this._removeDevicesFromMenu(); this._addDevicesToMenu(); + + this._logger.info('Refreshing devices list'); } _addDevicesToMenu() { - this._getPairedDevices().forEach((device) => { - this._menu.addMenuItem(device.item, 1); + this._controller.getDevices().sort((a, b) => { + return a.name.localeCompare(b.name); + }).forEach((device) => { + if (device.isPaired) { + let item = this._addMenuItem(device); + } else { + this._logger.info(`skipping adding device ${device.name}`); + } }); } _removeDevicesFromMenu() { - this._menu._getMenuItems().forEach((item) => { - if (item.isDeviceSwitcher) { - item.destroy(); - } + Object.values(this._items).forEach((item) => { + item.disconnectSignals(); + item.destroy(); }); + + this._items = {}; } _destroy() { - this._signals.forEach((signal) => { - signal.subject.disconnect(signal.signal_id); - }); - this._signals = []; + this._disconnectSignals(); this._removeDevicesFromMenu(); - - if (this._idleMonitorId) - GLib.Source.remove(this._idleMonitorId); - } - - _autoPowerOnEnabled() { - return this._settings.get_boolean('bluetooth-auto-power-on'); - } - - _autoPowerOffEnabled() { - return this._settings.get_boolean('bluetooth-auto-power-off'); - } - - _autoPowerOffCheckingInterval() { - return this._settings.get_int('bluetooth-auto-power-off-interval'); + this._disconnectIdleMonitor(); + if (this._controller) + this._controller.destroy(); } } +Utils.addSignalsHelperMethods(BluetoothQuickConnect.prototype); + let bluetoothQuickConnect = null; function init() { - let bluetooth = Main.panel.statusArea.aggregateMenu._bluetooth; - let settings = Convenience.getSettings(); - bluetoothQuickConnect = new BluetoothQuickConnect(bluetooth, settings); + ExtensionUtils.initTranslations(Me.metadata['gettext-domain']); } function enable() { + if (Main.panel.statusArea.quickSettings) { + bluetoothQuickConnect = new BluetoothQuickConnect( + Main.panel.statusArea.quickSettings, + null, + new Settings() + ); + } else { + bluetoothQuickConnect = new BluetoothQuickConnect( + null, + Main.panel.statusArea.aggregateMenu._bluetooth, + new Settings() + ); + } + bluetoothQuickConnect.test(); bluetoothQuickConnect.enable(); } function disable() { bluetoothQuickConnect.disable(); + bluetoothQuickConnect = null; } diff --git a/metadata.json b/metadata.json index dea519a..70b3688 100644 --- a/metadata.json +++ b/metadata.json @@ -1,14 +1,11 @@ { - "name": "Bluetooth quick connect", + "name": "Bluetooth Quick Connect", "description": "Allow to connect to paired devices from gnome control panel.\n", "uuid": "bluetooth-quick-connect@bjarosze.gmail.com", "url": "https://github.com/bjarosze/gnome-bluetooth-quick-connect", "settings-schema": "org.gnome.shell.extensions.bluetooth-quick-connect", "gettext-domain": "bluetooth-quick-connect", "shell-version": [ - "3.26.2", - "3.28", - "3.30", - "3.32" + "44" ] } diff --git a/po/bluetooth-quick-connect.pot b/po/bluetooth-quick-connect.pot new file mode 100644 index 0000000..6c31ed6 --- /dev/null +++ b/po/bluetooth-quick-connect.pot @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "" + +#: ui.js:197 +msgid "Wait" +msgstr "" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "" diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 0000000..e537edf --- /dev/null +++ b/po/cs.po @@ -0,0 +1,67 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR Amerey.eu <info@amerey.eu>, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-02-28 14:39+0100\n" +"Last-Translator: Amerey.eu <info@amerey.eu>\n" +"Language-Team: \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);\n" +"X-Generator: Poedit 3.1.1\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Rychlé připojení Bluetooth" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Chyba při pokusu o spuštění \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Nastavení Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Počkejte" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Kontrola intervalu nečinnosti (v sekundách)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Při otevření nabídky povolit bluetooth" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Při nečinnosti deaktivovat bluetooth" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Po přepnutí připojení nechte nabídku otevřenou (vyžaduje restart)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Zobrazit tlačítko pro opětovné připojení (vyžaduje restart)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Režim ladění (vyžaduje restart)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Zobrazit hodnotu baterie (vyžaduje restart)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Zobrazit ikonu baterie (vyžaduje restart)" diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..b4aa518 --- /dev/null +++ b/po/de.po @@ -0,0 +1,66 @@ +# German translation for Bluetooth Quick Connect. +# Copyright (C) 2023 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# <j.r@jugendhacker.de>, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-02-05 13:18+0100\n" +"Last-Translator: <j.r@jugendhacker.de>\n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth Quick Connect" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Fehler beim Ausführen von \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Bluetooth Einstellungen" + +#: ui.js:197 +msgid "Wait" +msgstr "Warte" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Intervall für die Inaktivitätsprüfung (Sekunden)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Bluetooth beim Öffnen des Menüs aktivieren" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Bluetooth bei Inaktivität deaktivieren" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Das Menü beim Umschalten einer Verbindung offen halten (Neustart benötigt)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Knopf zum Neuverbinden anzeigen (Neustart benötigt)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Debug Modus (Neustart benötigt)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Batteriestand anzeigen (Neustart benötigt)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Batteriesymbol anziegen (Neustart benötigt)" diff --git a/po/el.po b/po/el.po new file mode 100644 index 0000000..b0d3586 --- /dev/null +++ b/po/el.po @@ -0,0 +1,67 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-03-23 15:46+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Γρήγορη σύνδεση Bluetooth" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Σφάλμα κατά την προσπάθεια εκτέλεσης του \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Ρυθμίσεις Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Αναμονή" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Έλεγχος διαστήματος αδράνειας (δευτερόλεπτα)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Ενεργοποίηση bluetooth όταν ανοίγει το μενού" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Απενεργοποίηση του bluetooth σε αδράνεια" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Διατηρήστε το μενού ανοιχτό μετά την εναλλαγή της σύνδεσης (απαιτείται επανεκκίνηση)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Εμφάνιση κουμπιού επανασύνδεσης (απαιτείται επανεκκίνηση)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Λειτουργία εντοπισμού σφαλμάτων (απαιτείται επανεκκίνηση)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Εμφάνιση τιμής μπαταρίας (απαιτείται επανεκκίνηση)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Εμφάνιση εικονιδίου μπαταρίας (απαιτείται επανεκκίνηση)" diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..953944f --- /dev/null +++ b/po/es.po @@ -0,0 +1,69 @@ +# Spanish translation for Bluetooth Quick Connect. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Óscar Fernández Díaz <42654671+oscfdezdz@users.noreply.github.com>, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:25+0100\n" +"PO-Revision-Date: 2023-01-26 17:28+0100\n" +"Last-Translator: Óscar Fernández Díaz <42654671+oscfdezdz@users.noreply." +"github.com>\n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth Quick Connect" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Error al intentar ejecutar \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Configuración de Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Espere" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Comprobar intervalo de inactividad (segundos)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Activar Bluetooth al abrir el menú" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Desactivar Bluetooth si está inactivo" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "" +"Mantener el menú abierto después de cambiar la conexión (requiere reinicio)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Mostrar botón de reconexión (requiere reinicio)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Modo depuración (requiere reinicio)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Mostrar valor de la batería (requiere reinicio)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Mostrar icono de batería (requiere reinicio)" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 0000000..ce746fe --- /dev/null +++ b/po/fr.po @@ -0,0 +1,67 @@ +# French translation for Bluetooth Quick Connect. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Robin Grenet <robin.grenet@wanadoo.fr>, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-02-02 15:20+0100\n" +"Last-Translator: Robin Grenet <robin.grenet@wanadoo.fr>\n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.2.2\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth Quick Connect" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Erreur lors de l'exécution de \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Paramètres Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Patientez" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Intervalle de vérification de l'inactivité (secondes)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Activer le Bluetooth à l'ouverture du menu" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Désactiver le Bluetooth en cas d'inactivité" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Garder le menu ouvert après la connexion/déconnexion (redémarrage requis)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Afficher le bouton de reconnexion (redémarrage requis)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Mode débogage (redémarrage requis)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Afficher le pourcentage de batterie (redémarrage requis)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Afficher l'icône de niveau de batterie (redémarrage requis)" diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 0000000..28bef51 --- /dev/null +++ b/po/nl.po @@ -0,0 +1,66 @@ +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Heimen Stoffels <vistausss@fastmail.com>, 2023. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-02-01 19:21+0100\n" +"Last-Translator: Heimen Stoffels <vistausss@fastmail.com>\n" +"Language-Team: Dutch\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 22.12.1\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth - snel verbinden" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "‘bluetoothctl’ kan niet worden uitgevoerd" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Bluetoothvoorkeuren" + +#: ui.js:197 +msgid "Wait" +msgstr "Wachten" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Controle tijdens inactiviteit (in seconden)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Bluetooth inschakelen na openen van menu" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Bluetooth uitschakelen tijdens inactiviteit" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Menu openhouden na in-/uitschakelen (herstart vereist)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Knop ‘Opnieuw verbinden’ tonen (herstart vereist)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Foutopsporingsmodus (herstart vereist)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Accupercentage tonen (herstart vereist)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Accupictogram tonen (herstart vereist)" diff --git a/po/oc.po b/po/oc.po new file mode 100644 index 0000000..77fcb9e --- /dev/null +++ b/po/oc.po @@ -0,0 +1,66 @@ +# Occitan locale file. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Quentin PAGÈS, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:28+0100\n" +"PO-Revision-Date: 2023-02-10 09:35+0100\n" +"Last-Translator: Quentin PAGÈS\n" +"Language-Team: \n" +"Language: oc\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.2.2\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth Quick Connect" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Error en ensajant d’executar « bluetoothctl »" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Paramètres Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Esperatz" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Interval de verificacion d’inactivitat (en segondas)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Activar lo bluetooth quand lo menú es dobèrt" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Desactivar lo bluetooth en inactivitat" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "Gardar lo menú dobèrt aprèp aver basculat la connexion (reaviada requerida)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Afichar lo boton de reconnexion (reaviada requerida)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Mòde de desbugatge (reaviada requerida)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Afichar la valor de la batariá (reaviada requerida)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Afichar l’icòna de la batariá (reaviada requerida)" diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 0000000..8010db3 --- /dev/null +++ b/po/pl.po @@ -0,0 +1,68 @@ +# Spanish translation for Bluetooth Quick Connect. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Bartosz Jaroszewski, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-26 17:25+0100\n" +"PO-Revision-Date: 2023-01-26 17:28+0100\n" +"Last-Translator: Bartosz Jaroszewski\n" +"Language-Team: \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#: extension.js:97 +msgid "Bluetooth Quick Connect" +msgstr "Bluetooth Quick Connect" + +#: extension.js:97 +msgid "Error trying to execute \"bluetoothctl\"" +msgstr "Błąd wywołania \"bluetoothctl\"" + +#: quickSettings.js:43 +msgid "Bluetooth Settings" +msgstr "Ustawienia Bluetooth" + +#: ui.js:197 +msgid "Wait" +msgstr "Czekaj" + +#: Settings.ui:51 +msgid "Checking idle interval (seconds)" +msgstr "Sprawdzanie bezczynności co (sekundy)" + +#: Settings.ui:140 +msgid "Enable bluetooth when menu opened" +msgstr "Rozwinięcie listy włącza bluetooth jeśli wyłączony" + +#: Settings.ui:172 +msgid "Disable bluetooth if idle" +msgstr "Wyłącz bluetooth jeśli bezczynny" + +#: Settings.ui:255 +msgid "Keep the menu open after toggling the connection (restart required)" +msgstr "" +"Nie zamykaj listy po połączeniu/rozłączeniu urządzenia (wymagany restart)" + +#: Settings.ui:298 +msgid "Show reconnect button (restart required)" +msgstr "Pokaż guzik ponownego połączenia urządzenia (wymagany restart)" + +#: Settings.ui:341 +msgid "Debug mode (restart required)" +msgstr "Tryb debugowania (wymagany restart)" + +#: Settings.ui:384 +msgid "Show battery value (restart required)" +msgstr "Pokaż procentowy poziom baterii (wymagany restart)" + +#: Settings.ui:427 +msgid "Show battery icon (restart required)" +msgstr "Pokaż ikonę poziomu baterii (wymagany restart)" diff --git a/power.js b/power.js new file mode 100644 index 0000000..e15e24b --- /dev/null +++ b/power.js @@ -0,0 +1,37 @@ +const ExtensionUtils = imports.misc.extensionUtils; +const UPower = imports.gi.UPowerGlib; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +var UPowerBatteryProvider = class { + constructor(logger) { + this._upower_client = UPower.Client.new(); + this._logger = logger; + } + + locateBatteryDevice(device) { + // upower has no field in the devices that indicate that a battery is + // from a bluetooth device, so we must try and find by the provided mac. + // Problem is, the native_path field has macs in all kinds of forms ... + let _mac_addrs = [ + device.mac.toUpperCase(), + device.mac.replace(/:/g, "_").toUpperCase(), + ]; + + let _battery_devices = this._upower_client.get_devices(); + let _bateries = _battery_devices.filter(bat => { + let _native_path = bat.native_path.toUpperCase(); + return _mac_addrs.some(mac => _native_path.includes(mac)); + }); + + if (_bateries.length > 1) { + this._logger.warn(`device ${device.name} matched more than one UPower device by native_path`); + _bateries = []; + } + + let _battery_native_path = _bateries.map(bat => bat.native_path)[0] || "NOT-FOUND"; + this._logger.info(`battery: ${_battery_native_path}`); + + return _bateries; + } +} @@ -5,48 +5,42 @@ const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; -const Gettext = imports.gettext.domain('gnome-shell-extensions'); -const _ = Gettext.gettext; - const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = Me.imports.convenience; +const Settings = Me.imports.settings.Settings; +const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); +const _ = Gettext.gettext; class SettingsBuilder { - constructor() { - this._settings = new Convenience.getSettings(); + this._settings = new Settings().settings; this._builder = new Gtk.Builder(); } build() { this._builder.add_from_file(Me.path + '/Settings.ui'); - this._settingsBox = this._builder.get_object('bluetooth_quick_connect_settings'); - - - this._viewport = new Gtk.Viewport(); - this._viewport.add(this._settingsBox); - this._widget = new Gtk.ScrolledWindow(); - this._widget.add(this._viewport); + this._widget = this._builder.get_object('items_container') this._builder.get_object('auto_power_off_settings_button').connect('clicked', () => { - let dialog = new Gtk.Dialog({ title: 'Auto power off settings', - transient_for: this._widget.get_toplevel(), + let dialog = new Gtk.Dialog({ + title: 'Auto power off settings', + transient_for: this._widget.get_ancestor(Gtk.Window), use_header_bar: true, - modal: true }); + modal: true + }); let box = this._builder.get_object('auto_power_off_settings'); - dialog.get_content_area().add(box); + dialog.get_content_area().append(box); dialog.connect('response', (dialog) => { dialog.get_content_area().remove(box); dialog.destroy(); }); - dialog.show_all(); + dialog.show(); }); @@ -64,18 +58,32 @@ class SettingsBuilder { let autoPowerOffInterval = this._builder.get_object('auto_power_off_interval'); this._settings.bind('bluetooth-auto-power-off-interval', autoPowerOffInterval, 'value', Gio.SettingsBindFlags.DEFAULT); + + let keepMenuOnToggleSwitch = this._builder.get_object('keep_menu_on_toggle'); + this._settings.bind('keep-menu-on-toggle', keepMenuOnToggleSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let refreshButtonOnSwitch = this._builder.get_object('refresh_button_on'); + this._settings.bind('refresh-button-on', refreshButtonOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let debugModeOnSwitch = this._builder.get_object('debug_mode_on'); + this._settings.bind('debug-mode-on', debugModeOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let batteryValueOnSwitch = this._builder.get_object('show_battery_value_on'); + this._settings.bind('show-battery-value-on', batteryValueOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + let batteryIconOnSwitch = this._builder.get_object('show_battery_icon_on'); + this._settings.bind('show-battery-icon-on', batteryIconOnSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); } } function init() { - // Convenience.initTranslations(); + ExtensionUtils.initTranslations(Me.metadata['gettext-domain']); } function buildPrefsWidget() { let settings = new SettingsBuilder(); let widget = settings.build(); - widget.show_all(); return widget; } diff --git a/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml b/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml index 684af11..8640537 100644 --- a/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml +++ b/schemas/org.gnome.shell.extensions.bluetooth-quick-connect.gschema.xml @@ -18,5 +18,25 @@ SPDX-License-Identifier: GPL-2.0-or-later <default>60</default> <summary>Checking interval for power off idling bluetooth</summary> </key> + <key name="keep-menu-on-toggle" type="b"> + <default>false</default> + <summary>Keep the menu open after toggling the connection</summary> + </key> + <key name="refresh-button-on" type="b"> + <default>false</default> + <summary>Show reconnect button</summary> + </key> + <key name="debug-mode-on" type="b"> + <default>false</default> + <summary>Debug mode</summary> + </key> + <key name="show-battery-value-on" type="b"> + <default>false</default> + <summary>Show battery level as value</summary> + </key> + <key name="show-battery-icon-on" type="b"> + <default>true</default> + <summary>Show battery level as icon</summary> + </key> </schema> </schemalist> diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..d77d98e --- /dev/null +++ b/settings.js @@ -0,0 +1,72 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const ExtensionUtils = imports.misc.extensionUtils; +const Gio = imports.gi.Gio; +const GioSSS = Gio.SettingsSchemaSource; + +var Settings = class Settings { + constructor() { + this.settings = this._loadSettings(); + } + + isAutoPowerOnEnabled() { + return this.settings.get_boolean('bluetooth-auto-power-on'); + } + + isAutoPowerOffEnabled() { + return this.settings.get_boolean('bluetooth-auto-power-off'); + } + + autoPowerOffCheckingInterval() { + return this.settings.get_int('bluetooth-auto-power-off-interval'); + } + + isKeepMenuOnToggleEnabled() { + return this.settings.get_boolean('keep-menu-on-toggle'); + } + + isShowRefreshButtonEnabled() { + return this.settings.get_boolean('refresh-button-on'); + } + + isDebugModeEnabled() { + return this.settings.get_boolean('debug-mode-on'); + } + + isShowBatteryValueEnabled() { + return this.settings.get_boolean('show-battery-value-on'); + } + + isShowBatteryIconEnabled() { + return this.settings.get_boolean('show-battery-icon-on'); + } + + _loadSettings() { + let extension = ExtensionUtils.getCurrentExtension(); + let schema = extension.metadata['settings-schema']; + + let schemaSource = GioSSS.new_from_directory( + extension.dir.get_child('schemas').get_path(), + GioSSS.get_default(), + false + ); + + let schemaObj = schemaSource.lookup(schema, true); + + return new Gio.Settings({ settings_schema: schemaObj }); + } +};
\ No newline at end of file @@ -0,0 +1,298 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const Gettext = imports.gettext.domain(Me.metadata['gettext-domain']); +const _ = Gettext.gettext; + +const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; +const St = imports.gi.St; +const GLib = imports.gi.GLib; +const PopupMenu = imports.ui.popupMenu; + +var PopupBluetoothDeviceMenuItem = GObject.registerClass( + class PopupSwitchWithButtonMenuItem extends PopupMenu.PopupSwitchMenuItem { + _init(device, batteryProvider, logger, params) { + let label = device.name || '(unknown)'; + super._init(label, device.isConnected, {}); + + this._logger = logger; + + this._device = device; + this._optBatDevice = []; + this._batteryProvider = batteryProvider; + this._batteryDeviceChangeSignal = null; + this._batteryDeviceLocateTimeout = null; + + this._showRefreshButton = params.showRefreshButton; + this._closeMenuOnAction = params.closeMenuOnAction; + + this.label.x_expand = true; + this._statusBin.x_expand = false; + + this._refreshButton = this._buildRefreshButton(); + this._pendingLabel = this._buildPendingLabel(); + this._connectToggledEvent(); + + this._batteryInfo = new BatteryInfoWidget(params.showBatteryValue, params.showBatteryIcon); + this.insert_child_at_index(this._batteryInfo, this.get_n_children() - 1); + + this.insert_child_at_index(this._refreshButton, this.get_n_children() - 1); + this.add_child(this._pendingLabel); + + this.sync(device); + } + + _tryLocateBatteryWithTimeout(count = 10) { + + let device = this._device; + + this._logger.info(`looking up battery info for ${device.name}`); + + this._batteryDeviceLocateTimeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 1000, + () => { + this._logger.info(`Looking up battery info for ${device.name}`); + let optBat = this._batteryProvider.locateBatteryDevice(device); + + if (optBat.length) { + this._batteryFound(optBat); + this._batteryDeviceLocateTimeout = null; + } else if (count) { + // try again + this._tryLocateBatteryWithTimeout(count - 1); + } else { + this._logger.info(`Did not find a battery for device ${device.name}`); + this._batteryDeviceLocateTimeout = null; + } + }); + } + + _batteryFound(optBatDevice) { + this._optBatDevice = optBatDevice; + + for (const bat of this._optBatDevice) { + this._batteryInfo.show(); + this._batteryInfo.setPercentage(bat.percentage); + + this._batteryDeviceChangeSignal = bat.connect("notify", (_dev, pspec) => { + if (pspec.name == 'percentage') { + this._logger.info(`${_dev.native_path} notified ${pspec.name}, percentage is ${_dev.percentage}`); + this._batteryInfo.setPercentage(bat.percentage); + } + }); + } + } + + disconnectSignals() { + this._optBatDevice.map(bat => bat.disconnect(this._batteryDeviceChangeSignal)); + this._batteryDeviceChangeSignal = null; + + if (this._batteryDeviceLocateTimeout != null) { + GLib.Source.remove(this._batteryDeviceLocateTimeout); + this._batteryDeviceLocateTimeout = null; + } + + if (this._afterReconnectTimeout != null) { + GLib.Source.remove(this._afterReconnectTimeout); + this._afterReconnectTimeout = null + } + + if (this._afterToggleTimeout != null) { + GLib.Source.remove(this._afterToggleTimeout); + this._afterToggleTimeout = null + } + } + + sync(device) { + this.disconnectSignals(); + + this._optBatDevice = []; + this._batteryInfo.visible = false; + + this._device = device; + + this._switch.state = device.isConnected; + this.visible = device.isPaired; + if (this._showRefreshButton && device.isConnected) + this._refreshButton.show(); + else + this._refreshButton.hide(); + + this._disablePending(); + + if (device.isConnected) + this._tryLocateBatteryWithTimeout(); + + } + + _buildRefreshButton() { + let icon = new St.Icon({ + icon_name: 'view-refresh', + style_class: 'popup-menu-icon', + opacity: 155 + }); + + let button = new St.Button({ + child: icon, + x_align: St.Align.END + }); + + button.connect("enter-event", (widget) => { + widget.child.ease( { + opacity: 255, + time: 0.05, + transition: Clutter.AnimationMode.LINEAR + } + ); + }); + + button.connect("leave-event", (widget) => { + widget.child.ease( { + opacity: 155, + time: 0.05, + transition: Clutter.AnimationMode.LINEAR + } + ); + }); + + button.connect('clicked', () => { + this._enablePending(); + this._device.reconnect(); + this._afterReconnectTimeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 10000, + () => { + this._disablePending(); + this._afterReconnectTimeout = null; + } + ); + + if (this._closeMenuOnAction) + this.emit('activate', Clutter.get_current_event()); + }); + + return button; + } + + _buildPendingLabel() { + let label = new St.Label({text: _("Wait")}); + label.hide(); + + return label; + } + + _connectToggledEvent() { + this.connect('toggled', (item, state) => { + if (state) + this._device.connect(); + else + this._device.disconnect(); + + // in case there is no change on device + this._afterToggleTimeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 10000, + () => { + this._disablePending(); + this._afterToggleTimeout = null; + } + ); + }); + } + + activate(event) { + if (this._switch.mapped) { + this.toggle(); + this._switch.toggle(); // toggle back, state will be updated by signal + } + + // we allow pressing space to toggle the switch + // without closing the menu + if (event.type() == Clutter.EventType.KEY_PRESS && + event.get_key_symbol() == Clutter.KEY_space) + return; + + if (this._closeMenuOnAction) + this.emit('activate', event); + } + + toggle() { + super.toggle(); + this._enablePending(); + } + + _enablePending() { + this._refreshButton.reactive = false; + this._switch.hide(); + this._pendingLabel.show(); + this.reactive = false; + } + + _disablePending() { + this._refreshButton.reactive = true; + this._switch.show(); + this._pendingLabel.hide(); + this.reactive = true; + } + } +); + +var BatteryInfoWidget = GObject.registerClass( + class BatteryInfoWidget extends St.BoxLayout { + _init(showBatteryValue, showBatteryIcon) { + super._init({ visible: false, style: 'spacing: 3px;' }); + this._icon = new St.Icon({ style_class: 'popup-menu-icon' }); + this.add_child(this._icon); + this._icon.icon_name = null; + + // dirty trick: instantiate the label with text 100%, so we can set + // the natural width of the label in case monospace has no effect + this._label = new St.Label({ + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + style_class: 'monospace', + text: '100%' + }); + + this._label.natural_width = this._label.width; + this._label.text = ""; + + this.add_child(this._label); + + if (!showBatteryValue) this._label.hide(); + if (!showBatteryIcon) this._icon.hide(); + } + + setPercentage(value) { + if (value == null) { + this._label.text = ''; + this._icon.icon_name = 'battery-missing-symbolic'; + } else { + this._label.text = '%d%%'.format(value); + + let fillLevel = 10 * Math.floor(value / 10); + let iconName = 'battery-level-%d-symbolic'.format(fillLevel); + this._icon.icon_name = iconName; + } + } + } +); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..5c7481d --- /dev/null +++ b/utils.js @@ -0,0 +1,82 @@ +// Copyright 2018 Bartosz Jaroszewski +// SPDX-License-Identifier: GPL-2.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +const GLib = imports.gi.GLib; + +function spawn(command, callback) { + let [status, pid] = GLib.spawn_async( + null, + ['/usr/bin/env', 'bash', '-c', command], + null, + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null + ); + + // ensure we always close the pid to avoid zombie processes + GLib.child_watch_add( + GLib.PRIORITY_DEFAULT, pid, + (_pid, _status) => { + try { + if (callback) { + callback(_pid, _status); + } + } finally { + GLib.spawn_close_pid(_pid); + } + }); +} + + +function isDebugModeEnabled() { + return new Settings().isDebugModeEnabled(); +} + +var Logger = class Logger { + constructor(settings) { + this._enabled = settings.isDebugModeEnabled(); + } + + info(message) { + if (!this._enabled) return; + + log(`[bluetooth-quick-connect] ${message}`); + } + + warn(message) { + log(`[bluetooth-quick-connect WARNING] ${message}`); + } +}; + +function addSignalsHelperMethods(prototype) { + prototype._connectSignal = function (subject, signal_name, method) { + if (!this._signals) this._signals = []; + + let signal_id = subject.connect(signal_name, method); + this._signals.push({ + subject: subject, + signal_id: signal_id + }); + } + + prototype._disconnectSignals = function () { + if (!this._signals) return; + + this._signals.forEach((signal) => { + signal.subject.disconnect(signal.signal_id); + }); + this._signals = []; + }; +} |