Compare commits

...

45 Commits
0.2 ... master

Author SHA1 Message Date
nixo add6955bb8 Add tuhi-live to output 2019-09-21 08:38:44 +02:00
Piotr Drąg 538480f022 Add Polish translation 2019-09-16 08:32:02 +10:00
Peter Hutterer d4dcccf36d tuhi 0.3
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-12 11:20:42 +10:00
Peter Hutterer a6a33ce6b4 live: add flatpak compatibility mode argument
Duplicated from tuhi.in because it's easier this way.

Fixes #217

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-12 11:18:56 +10:00
Peter Hutterer 2cc9fc778f live: pass our arguments through to the tuhi server
Any args that we don't handle in live mode directly (none right now) should be
passed through to the tuhi server.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-12 11:18:56 +10:00
Peter Hutterer cb72111910 gui: make the app menu translatable
Fixes #216

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-10 15:28:20 +10:00
Piotr Drąg f01651bf5e po: update POTFILES 2019-09-09 09:12:26 +10:00
Peter Hutterer b1b0be84ea tuhi: add a flatpak compatibility mode
This only modifies the XDG directories to point to the ones flatpak uses.
Makes it easier to switch back and forth.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:43 +10:00
Peter Hutterer 13830e02c5 Add the python 3.6 check to all entry points
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:43 +10:00
Peter Hutterer 0e12369866 wacom: set the x/y min/max to the correct device units
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 74ffe763b6 ble: downgrade three more debug messages
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 1c893d14e8 wacom: make a debug message more informative
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer fde36b1271 tools: tuhi-live's tuhi server needs to ignore SIGINT
Handling SIGINT in the freshly-started server leads to a race condition:
on Ctrl+C, Tuhi is killed before tuhi-live can call StopLive, leaving the
device in live mode. On the next start of tuhi-live nothing works because the
device will complain about invalid state.

Simplest solution here is to ignore SIGINT in the Tuhi instance and instead
rely on python's multiprocessing module to take us down when the main process
exits.

Fixes #206

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 960904d481 wacom: don't fetch the dimensions if we're stopping live mode
There's just no point to that and if the tablet is currently in live mode, it
refuses the GET_WIDTH request anyway.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 9eedbe108a tools: remove write-only variable from tuhi-live
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer b40dee5900 tools: stop live before starting it
If for some reason the device is still in live mode from an earlier
invocation, stop live mode first before we attempt to start a new one.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 43c844ea1e dbusclient: make the live property mirror the dbus one
We have a dbus property for this so let's just use that as a backend for our
live property instead of emulating our own on top of it.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer 2d95848cee tools: make tuhi-live --verbose actually work
The argparser in tuhi.base.main() drops argv[0], so we have to add a fake
argument here.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-03 16:22:18 +10:00
Peter Hutterer bffe41fbb6 protocol: handle 0xb3 automatically
0xb3 is the generic error code (or success, where applicable). Let's handle
those by default so that the rest of the messages only have to care about
replies with message-specific opcodes.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 12:38:21 +10:00
Peter Hutterer 1dc1acaba3 gui: move most of the tuhi-gui startup script to application.py
Add a new main() there so we have a single entry point for the gui instead of
a complex script that needs to be called. That start up script should really
just be the minimum bits possible.

It's still not perfect because we won't work without the gresources and they
require that the pkgdatadir is set. But at least it moves all the logic out of
the startup script.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 10:42:44 +10:00
Peter Hutterer bb9f9b6c26 gui: reshuffle the includes until gi and flake8 are happy
We have implied inclusion orders here, specifically with gi. This works as
long as the files are included in the right order from other files but its not
generic enough - we really do need the gi.require_version() bit
everywhere to avoid issues.

Of course, once we do this flake8 complains about everything so let's
reshuffle things in an order it's happy with. This seems to be local modules
first, then ... whatever except when not?

application.py is excluded from this patch because it's about to get changed
in the next one and I'm too lazy to separate those out.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 10:42:44 +10:00
Peter Hutterer e37016dc7b Fix deprecated use of @GObject.property → @GObject.Property
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 10:42:44 +10:00
Peter Hutterer 97a4ab04d0 ble: downgrade a message from info to debug
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 10:42:10 +10:00
Peter Hutterer 90ac2c7150 tools: call directly into Tuhi's main from tuhi-live
This sets up the config dir, loggers, etc. for us. Fixes regressions
introduced a while ago when Tuhi.run() was removed and then more obvious
breakage now that we require the Config path to be set up.

Fixes #206

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-29 10:42:10 +10:00
Peter Hutterer 658fe44d76 tools: remove noop statement from tuhi-live
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 08afa02690 tools: use the dbus client bindings from tuhi-live too
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer cd84de4f32 tools: use the shared dbus client bindings for kete
Since most of the signals are propagated by the bindings we largely just need
to hook up simple methods to log what is happening.

One nasty hack: we import some of the bits into the kete namespace so
tuhi-live continues to work. This can be fixed up in a follow-up commit.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 07cefa3c26 dbusclient: add the start/stop live from kete to the bindings
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 695b7c43be dbusclient: drop the duplicate device argument
The first argument in any signal is the object that emits it, so we don't have
to add it again.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 71ca2ca569 gui: fix two signal disconnections
Both of the signals here are connect to the device, not Tuhi. The only reason
this worked was because the signal sent by the dbus bindings had the device
twice in the arguments and 'tuhi' was actually the device.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 2d142b4be3 dbusclient: drop the word Kete from the generic dbus client bindings
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer 28f92882ec dbusclient: fix a few super() calls
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer f477e86a96 gui: move the dbus client handler bits to the tuhi module
This is used by the GUI and by kete and it's just an abstraction of the
handling anyway with little actual logic. Let's make this sharable.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:17:38 +02:00
Peter Hutterer ed29c0daae gui: stop listening on an authorization error
This conflicts with trying to register the device on authentication errors and
making re-registration unreliable. Tuhi would keep connecting to the device
while holding the button down, so we'd get another failed connection and a
disconnect from the device. The Register signal would come in and get lost
somehow.

Fixes #195

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 14:55:12 +02:00
Peter Hutterer 488512231c gui: reset the signal list after disconnecting them
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 14:55:12 +02:00
Peter Hutterer c0628f1f19 meson.build: check for missing python modules
Check those modules that aren't part of the python installation and bail out
of meson where they're missing. This is technically wrong because we don't
need them at build-time and only at run-time but pragmatically sensible
because we waste too much time dealing with those bugs.

Fixes #200

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 17:36:16 +10:00
Peter Hutterer 9c399c8eac flatpak: fix the pyxdg path
So we don't need to be redirected every time.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 17:35:52 +10:00
Peter Hutterer 505f1c786f flatpak: use specific versions of svgwrite and pyxdg
flathub requires us to use specific versions, so let's do that here too so
we're consistent in testing.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 17:35:52 +10:00
Peter Hutterer cab716938f flatpak: indentation fix 2019-08-28 17:35:52 +10:00
Peter Hutterer 6c0ddd96bc protocol: add missing space for NordicData's str()
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:33:28 +10:00
Peter Hutterer 827ccbfa07 protcol: just print the class name during str()
<class '....'> doesn't make things easier to debug

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:33:28 +10:00
Peter Hutterer 1fb36ebc40 protocol: fix typo in printing a NordicData object
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:33:28 +10:00
Peter Hutterer db87390c48 protocol: use __str__, not __repr__ for the various messages
str() is the preferred one, though I can't find a good explanation when to use
what.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-28 15:33:28 +10:00
Peter Hutterer f24967ad12 gui: require pygi version 3.30
This was the one that introduced the Gtk.Template class that we use. And
better to have a meaningful exit than a crash.

Fixes #197

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-27 19:31:00 +10:00
Peter Hutterer e6ea60d6c1 gui: hook up ctrl+q for app.quit
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-08-26 13:18:28 +10:00
22 changed files with 605 additions and 564 deletions

View File

@ -7,7 +7,7 @@ jobs:
steps:
- run:
command: |
dnf install -y meson gettext python3-devel pygobject3-devel python3-flake8 desktop-file-utils libappstream-glib python3-pytest python3-pyxdg python3-pyyaml
dnf install -y meson gettext python3-devel pygobject3-devel python3-flake8 desktop-file-utils libappstream-glib python3-pytest python3-pyxdg python3-pyyaml python3-svgwrite
- checkout
- run:
command: |

View File

@ -9,5 +9,6 @@
<file preprocess="xml-stripblanks">ui/SetupPerspective.ui</file>
<file preprocess="xml-stripblanks">ui/ErrorPerspective.ui</file>
<file preprocess="xml-stripblanks">input-tablet-missing-symbolic.svg</file>
<file preprocess="xml-stripblanks">ui/AppMenu.ui</file>
</gresource>
</gresources>

26
data/ui/AppMenu.ui Normal file
View File

@ -0,0 +1,26 @@
<interface>
<menu id="primary-menu">
<section>
<item>
<attribute name="label" translatable="yes">Portrait</attribute>
<attribute name="action">win.orientation</attribute>
<attribute name="target">portrait</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Landscape</attribute>
<attribute name="action">win.orientation</attribute>
<attribute name="target">landscape</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Help</attribute>
<attribute name="action">app.help</attribute>
</item>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>

View File

@ -1,9 +1,9 @@
project('tuhi',
version: '0.2',
version: '0.3',
license: 'GPLv2',
meson_version: '>= 0.48.0')
# The tag date of the project_version(), update when the version bumps.
version_date='2019-08-26'
version_date='2019-09-12'
# Dependencies
dependency('python3', required: true)
@ -28,9 +28,26 @@ i18n = import('i18n')
subdir('po')
subdir('data')
# Find the directory to install our Python code
pymod = import('python')
py3 = pymod.find_installation()
# external python modules that are required for running Tuhi
python_modules = [
'svgwrite',
'xdg',
'gi',
'yaml',
]
if meson.version().version_compare('>=0.51')
py3 = pymod.find_installation(modules: python_modules)
else
py3 = pymod.find_installation()
foreach module: python_modules
if run_command(py3, '-c', 'import @0@'.format(module)).returncode() != 0
error('Failed to find required python module \'@0@\'.'.format(module))
endif
endforeach
endif
python_dir = py3.get_install_dir()
install_subdir('tuhi',
install_dir: python_dir,
@ -55,6 +72,7 @@ config_tuhi_devel.set('libexecdir', '')
config_tuhi_devel.set('devel', '''
tuhi_gui = '@1@/tuhi-gui.devel'
tuhi_server = '@0@/tuhi-server.py'
tuhi_live = '@0@/tuhi-live.py'
print('Running from source tree, using local files')
'''.format(meson.source_root(), meson.build_root()))
@ -94,6 +112,11 @@ configure_file(input: 'tuhi-server.py',
copy: true,
install_dir: libexecdir)
configure_file(input: 'tuhi-live.py',
output: 'tuhi-live',
copy: true,
install_dir: libexecdir)
meson.add_install_script('meson_install.sh')
desktop_file = i18n.merge_file(input: 'data/org.freedesktop.Tuhi.desktop.in',

View File

@ -4,14 +4,14 @@
"runtime-version": "3.32",
"sdk": "org.gnome.Sdk",
"command": "tuhi",
"finish-args": [
"finish-args": [
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--talk-name=org.freedesktop.tuhi1",
"--own-name=org.freedesktop.tuhi1",
"--system-talk-name=org.bluez"
],
],
"modules": [
{
"name": "pyxdg",
@ -19,7 +19,9 @@
"sources": [
{
"type": "git",
"url": "git://anongit.freedesktop.org/xdg/pyxdg"
"url": "https://gitlab.freedesktop.org/xdg/pyxdg.git",
"tag": "rel-0.26",
"commit": "7db14dcf4c4305c3859a2d9fcf9f5da2db328330"
}
],
"build-commands": [
@ -46,7 +48,9 @@
"sources": [
{
"type": "git",
"url": "https://github.com/mozman/svgwrite.git"
"url": "https://github.com/mozman/svgwrite.git",
"tag": "v1.3.1",
"commit": "13633ad13d7a4b3253d1304d31db8fc2c8d1dd9e"
}
],
"build-commands": [

View File

@ -1 +1,2 @@
# Language list must be in alphabetical order
pl

View File

@ -2,16 +2,15 @@ data/org.freedesktop.Tuhi.appdata.xml.in.in
data/org.freedesktop.Tuhi.desktop.in
data/ui/AboutDialog.ui.in
data/ui/AppMenu.ui
data/ui/Drawing.ui
data/ui/DrawingPerspective.ui
data/ui/ErrorPerspective.ui
data/ui/MainWindow.ui
data/ui/SetupPerspective.ui
tuhigui/application.py
tuhigui/config.py
tuhigui/drawing.py
tuhigui/drawingperspective.py
tuhigui/svg.py
tuhigui/tuhi.py
tuhigui/window.py
tuhi/gui/application.py
tuhi/gui/config.py
tuhi/gui/drawing.py
tuhi/gui/drawingperspective.py
tuhi/gui/window.py

208
po/pl.po Normal file
View File

@ -0,0 +1,208 @@
# Polish translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# Piotr Drąg <piotrdrag@gmail.com>, 2019.
# Aviary.pl <community-poland@mozilla.org>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-15 12:38+0200\n"
"PO-Revision-Date: 2019-09-15 12:55+0200\n"
"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
"Language-Team: Polish <community-poland@mozilla.org>\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=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:7
#: data/org.freedesktop.Tuhi.desktop.in:3
msgid "Tuhi"
msgstr "Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:8
#: data/org.freedesktop.Tuhi.desktop.in:4
msgid "Utility to download drawings from the Wacom Ink range of devices"
msgstr "Narzędzie do pobierania rysunków z rodziny urządzeń Wacom Ink"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:10
msgid ""
"Tuhi is a graphical user interface to download drawings stored on tablet "
"devices from the Wacom Ink range, e.g. Intuos Pro Paper or Bamboo Slate."
msgstr ""
"Tuhi to graficzny interfejs użytkownika do pobierania rysunków "
"przechowywanych na tabletach z rodziny Wacom Ink, np. Intuos Pro Paper "
"i Bamboo Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
msgid ""
"Tuhi requires Tuhi, the daemon to actually communicate with the devices. "
"ThiGui is merely a front end to Tuhi, Tuhi must be installed and running "
"when Tuhi is launched."
msgstr ""
"TuhiGUI wymaga Tuhi, usługi komunikującej się z urządzeniem. TuhiGUI to "
"interfejs dla usługi Tuhi, która musi być zainstalowana i uruchomiona, aby "
"narzędzie Tuhi mogło działać."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Główne okno Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Główne okno Tuhi (powiększone)"
#. Translators: Do NOT translate or transliterate this text (this is an icon file name)!
#: data/org.freedesktop.Tuhi.desktop.in:7
msgid "org.freedesktop.Tuhi"
msgstr "org.freedesktop.Tuhi"
#. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
#: data/org.freedesktop.Tuhi.desktop.in:12
msgid "tablet;wacom;ink;"
msgstr "tablet;wacom;ink;"
#: data/ui/AboutDialog.ui.in:13
msgid "Visit Tuhis website"
msgstr "Witryna programu Tuhi"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Pionowo"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Poziomo"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Pomoc"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "O programie"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Cofnij usunięcie rysunku"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Proszę nacisnąć przycisk na urządzeniu, aby zsynchronizować rysunki"
#: data/ui/DrawingPerspective.ui:143 data/ui/SetupPerspective.ui:121
msgid "page0"
msgstr "strona 0"
#: data/ui/ErrorPerspective.ui:21
msgid ""
"TuhiGUI is an interactive GUI to download data from Tuhi.\n"
"\n"
"Tuhi connects to tablets of the Wacom Ink range. It allows you to download "
"the drawings stored on those devices as SVGs for processing later.\n"
"\n"
"Tuhi is a DBus server that needs to be running for the Tuhi GUI to connect "
"to it. Connecting to the DBus server should take less than a second. If you "
"read this far, your Tuhi DBus server is not running or responsing and needs "
"to be restarted."
msgstr ""
"TuhiGUI to interaktywny interfejs użytkownika do pobierania danych z usługi "
"Tuhi.\n"
"\n"
"Tuhi łączy się z tabletami z rodziny Wacom Ink. Umożliwia pobieranie obrazów "
"przechowywanych na tych urządzeniach jako pliki SVG do przetwarzania "
"w późniejszym czasie.\n"
"\n"
"Tuhi to serwer D-Bus, który musi być uruchomiony, aby interfejs Tuhi mógł "
"się z nim połączyć. Połączenie z serwerem D-Bus powinno zająć mniej niż "
"sekundę. Jeśli jeszcze czytasz ten tekst, to znaczy że serwer D-Bus Tuhi nie "
"jest uruchomiony lub nie odpowiada i wymaga ponownego uruchomienia."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Łączenie z usługą Tuhi"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"To powinno zająć mniej niż sekundę. Proszę się upewnić, że serwer D-Bus Tuhi "
"jest uruchomiony."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Błąd upoważnienia podczas łączenia z urządzeniem "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Zarejestruj"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "Pierwsza konfiguracja urządzenia"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Zakończ"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr ""
"Proszę przytrzymać przycisk na urządzeniu, aż niebieska dioda zacznie migać."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Wyszukiwanie urządzenia"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "Łączenie z LE Paper"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Łączenie z urządzeniem…"
#: data/ui/SetupPerspective.ui:188
msgid "page1"
msgstr "1. strona"
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Proszę teraz nacisnąć przycisk na urządzeniu."
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "oczekiwanie na odpowiedź"
#: data/ui/SetupPerspective.ui:258
msgid "page2"
msgstr "2. strona"
#: tuhi/gui/drawing.py:100
msgid "Please choose a file"
msgstr "Proszę wybrać plik"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:108
msgid "untitled.svg"
msgstr "bez tytułu.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:112
msgid "Any files"
msgstr "Wszystkie pliki"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:116
msgid "SVG files"
msgstr "Pliki SVG"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "Łączenie z urządzeniem {device.name}"

View File

@ -379,6 +379,25 @@ class TestProtocolAny(unittest.TestCase):
msg = p.execute(Interactions.REGISTER_PRESS_BUTTON, uuid=uuid)
self.assertEqual(msg.uuid, uuid)
def test_error_invalid_state(self):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
return NordicData([0xb3, 0x1, 0x1])
p = Protocol(self.protocol_version, callback=_cb)
# a "random" collection of requests that we want to check for
with self.assertRaises(DeviceError) as cm:
p.execute(Interactions.CONNECT, uuid='abcdef123456')
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
with self.assertRaises(DeviceError) as cm:
p.execute(Interactions.GET_STROKES)
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
with self.assertRaises(DeviceError) as cm:
p.execute(Interactions.SET_MODE, Mode.PAPER)
self.assertEqual(cm.exception.errorcode, DeviceError.ErrorCode.GENERAL_ERROR)
class TestProtocolSpark(TestProtocolAny):
protocol_version = ProtocolVersion.SPARK

View File

@ -20,7 +20,6 @@ import errno
import os
import json
import logging
import re
import readline
import struct
import threading
@ -31,12 +30,14 @@ from pathlib import Path
try:
from tuhi.svg import JsonSvg
import tuhi.dbusclient
except ModuleNotFoundError:
# If PYTHONPATH isn't set up or we never installed Tuhi, the module
# isn't available. And since we don't install kete, we can assume that
# we're still in the git repo, so messing with the path is "fine".
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.svg import JsonSvg
import tuhi.dbusclient
CONFIG_PATH = Path(xdg.BaseDirectory.xdg_data_home, 'tuhi-kete')
@ -98,13 +99,6 @@ logger = logging.getLogger('tuhi-kete')
logger.addHandler(logger_handler)
logger.setLevel(logging.INFO)
TUHI_DBUS_NAME = 'org.freedesktop.tuhi1'
ORG_FREEDESKTOP_TUHI1_MANAGER = 'org.freedesktop.tuhi1.Manager'
ORG_FREEDESKTOP_TUHI1_DEVICE = 'org.freedesktop.tuhi1.Device'
ROOT_PATH = '/org/freedesktop/tuhi1'
ORG_BLUEZ_DEVICE1 = 'org.bluez.Device1'
# remove ':' from the completer delimiters of readline so we can match on
# device addresses
completer_delims = readline.get_completer_delims()
@ -118,340 +112,52 @@ def b2hex(bs):
return ' '.join([''.join(s) for s in zip(hx[::2], hx[1::2])])
class DBusError(Exception):
def __init__(self, message):
self.message = message
class _DBusObject(GObject.Object):
_connection = None
def __init__(self, name, interface, objpath):
GObject.GObject.__init__(self)
if _DBusObject._connection is None:
self._connect_to_session()
self.interface = interface
self.objpath = objpath
try:
self.proxy = Gio.DBusProxy.new_sync(self._connection,
Gio.DBusProxyFlags.NONE, None,
name, objpath, interface, None)
except GLib.Error as e:
if (e.domain == 'g-io-error-quark' and
e.code == Gio.IOErrorEnum.DBUS_ERROR):
raise DBusError(e.message)
else:
raise e
if self.proxy.get_name_owner() is None:
raise DBusError(f'No-one is handling {name}, is the daemon running?')
self.proxy.connect('g-properties-changed', self._on_properties_changed)
self.proxy.connect('g-signal', self._on_signal_received)
def _connect_to_session(self):
try:
_DBusObject._connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
except GLib.Error as e:
if (e.domain == 'g-io-error-quark' and
e.code == Gio.IOErrorEnum.DBUS_ERROR):
raise DBusError(e.message)
else:
raise e
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
# Implement this in derived classes to respond to property changes
pass
def _on_signal_received(self, proxy, sender, signal, parameters):
# Implement this in derived classes to respond to signals
pass
def property(self, name):
p = self.proxy.get_cached_property(name)
if p is not None:
return p.unpack()
return p
def terminate(self):
del(self.proxy)
class _DBusSystemObject(_DBusObject):
'''
Same as the _DBusObject, but connects to the system bus instead
'''
def __init__(self, name, interface, objpath):
self._connect_to_system()
super().__init__(name, interface, objpath)
def _connect_to_system(self):
try:
self._connection = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
except GLib.Error as e:
if (e.domain == 'g-io-error-quark' and
e.code == Gio.IOErrorEnum.DBUS_ERROR):
raise DBusError(e.message)
else:
raise e
class BlueZDevice(_DBusSystemObject):
def __init__(self, objpath):
super().__init__('org.bluez', ORG_BLUEZ_DEVICE1, objpath)
self.proxy.connect('g-properties-changed', self._on_properties_changed)
@GObject.Property
def connected(self):
return self.proxy.get_cached_property('Connected').unpack()
def _on_properties_changed(self, obj, properties, invalidated_properties):
properties = properties.unpack()
if 'Connected' in properties:
self.notify('connected')
class TuhiKeteDevice(_DBusObject):
def __init__(self, manager, objpath):
_DBusObject.__init__(self, TUHI_DBUS_NAME,
ORG_FREEDESKTOP_TUHI1_DEVICE,
objpath)
self.manager = manager
self.is_registering = False
self.live = False
self._bluez_device = BlueZDevice(self.property('BlueZDevice'))
self._bluez_device.connect('notify::connected', self._on_connected)
@classmethod
def is_device_address(cls, string):
if re.match(r'[0-9a-f]{2}(:[0-9a-f]{2}){5}$', string.lower()):
return string
raise argparse.ArgumentTypeError(f'"{string}" is not a valid device address')
@GObject.Property
def address(self):
return self._bluez_device.property('Address')
@GObject.Property
def name(self):
return self._bluez_device.property('Name')
@GObject.Property
def listening(self):
return self.property('Listening')
@GObject.Property
def drawings_available(self):
return self.property('DrawingsAvailable')
@GObject.Property
def battery_percent(self):
return self.property('BatteryPercent')
@GObject.Property
def battery_state(self):
return self.property('BatteryState')
@GObject.Property
def connected(self):
return self._bluez_device.connected
def _on_connected(self, bluez_device, pspec):
self.notify('connected')
def register(self):
logger.debug(f'{self}: Register')
# FIXME: Register() doesn't return anything useful yet, so we wait until
# the device is in the Manager's Devices property
self.s1 = self.manager.connect('notify::devices', self._on_mgr_devices_updated)
self.is_registering = True
self.proxy.Register()
def start_listening(self):
self.proxy.StartListening()
def stop_listening(self):
try:
self.proxy.StopListening()
except GLib.Error as e:
if (e.domain != 'g-dbus-error-quark' or
e.code != Gio.IOErrorEnum.EXISTS or
Gio.dbus_error_get_remote_error(e) != 'org.freedesktop.DBus.Error.ServiceUnknown'):
raise e
def start_live(self, fd):
fd_list = Gio.UnixFDList.new()
fd_list.append(fd)
res, fds = self.proxy.call_with_unix_fd_list_sync('org.freedesktop.tuhi1.Device.StartLive',
GLib.Variant('(h)', (fd,)),
Gio.DBusCallFlags.NO_AUTO_START,
-1,
fd_list,
None)
if res[0] == 0:
self.live = True
def stop_live(self):
self.proxy.StopLive()
self.live = False
def json(self, timestamp):
SUPPORTED_FILE_FORMAT = 1
return self.proxy.GetJSONData('(ut)', SUPPORTED_FILE_FORMAT, timestamp)
def _on_signal_received(self, proxy, sender, signal, parameters):
if signal == 'ButtonPressRequired':
logger.info(f'{self}: Press button on device now')
elif signal == 'ListeningStopped':
err = parameters[0]
if err == -errno.EACCES:
logger.error(f'{self}: wrong device, please re-register.')
elif err < 0:
logger.error(f'{self}: an error occured: {os.strerror(-err)}')
self.notify('listening')
elif signal == 'SyncState':
state = parameters[0]
if state:
logger.debug(f'{self}: Downloading from device')
else:
logger.debug(f'{self}: Download done')
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
if changed_props is None:
return
changed_props = changed_props.unpack()
if 'DrawingsAvailable' in changed_props:
self.notify('drawings-available')
elif 'Listening' in changed_props:
self.notify('listening')
elif 'BatteryPercent' in changed_props:
self.notify('battery-percent')
elif 'BatteryState' in changed_props:
self.notify('battery-state')
def __repr__(self):
return f'{self.address} - {self.name}'
def _on_mgr_devices_updated(self, manager, pspec):
if not self.is_registering:
return
for d in manager.devices:
if d.address == self.address:
self.is_registering = False
self.manager.disconnect(self.s1)
del(self.s1)
logger.info(f'{self}: Registration successful')
def terminate(self):
try:
self.manager.disconnect(self.s1)
except AttributeError:
pass
self._bluez_device.terminate()
super(TuhiKeteDevice, self).terminate()
class TuhiKeteManager(_DBusObject):
__gsignals__ = {
'unregistered-device':
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
}
class TuhiKeteManager(tuhi.dbusclient.TuhiDBusClientManager):
def __init__(self):
_DBusObject.__init__(self, TUHI_DBUS_NAME,
ORG_FREEDESKTOP_TUHI1_MANAGER,
ROOT_PATH)
super().__init__()
self.connect('unregistered_device', self._on_unregistered_device)
self._devices = {}
self._unregistered_devices = {}
self.sigs = {}
for d in self.devices:
self.sigs[d] = []
self._connect_device(d)
for objpath in self.property('Devices'):
device = TuhiKeteDevice(self, objpath)
self._devices[device.address] = device
@GObject.Property
def devices(self):
return [v for k, v in self._devices.items()]
@GObject.Property
def unregistered_devices(self):
return [v for k, v in self._unregistered_devices.items()]
@GObject.Property
def searching(self):
return self.proxy.get_cached_property('Searching')
def start_search(self):
self._unregistered_devices = {}
self.proxy.StartSearch()
def stop_search(self):
def _disconnect_device_signals(self, device):
try:
self.proxy.StopSearch()
except GLib.Error as e:
if (e.domain != 'g-dbus-error-quark' or
e.code != Gio.IOErrorEnum.EXISTS or
Gio.dbus_error_get_remote_error(e) != 'org.freedesktop.DBus.Error.ServiceUnknown'):
raise e
self._unregistered_devices = {}
for s in self.sigs[device]:
device.disconnect(s)
self.sigs[device] = []
except KeyError:
pass
def terminate(self):
for dev in self._devices.values():
dev.terminate()
self._devices = {}
self._unregistered_devices = {}
super(TuhiKeteManager, self).terminate()
def _on_unregistered_device(self, manager, device):
self._disconnect_device_signals(device)
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
if changed_props is None:
return
def log_press_required(device):
logger.info(f'{device}: Press button on device now')
device.connect('button-press-required', log_press_required)
changed_props = changed_props.unpack()
def log_registered(device):
logger.info(f'{device}: Registration successful')
device.connect('registered', log_registered)
device.connect('registered', self._connect_device)
if 'Devices' in changed_props:
objpaths = changed_props['Devices']
for objpath in objpaths:
try:
d = self._unregistered_devices[objpath]
self._devices[d.address] = d
del self._unregistered_devices[objpath]
except KeyError:
# if we called Register() on an existing device it's not
# in unregistered devices
pass
self.notify('devices')
if 'Searching' in changed_props:
self.notify('searching')
def _connect_device(self, device):
self._disconnect_device_signals(device)
def _handle_unregistered_device(self, objpath):
for addr, dev in self._devices.items():
if dev.objpath == objpath:
self.emit('unregistered-device', dev)
return
def log_sync_state(device, pspec):
if device.sync_state:
logger.debug(f'{device}: Communicating with device')
else:
logger.debug(f'{device}: Communication complete')
device.connect('notify::sync-state', log_sync_state)
device = TuhiKeteDevice(self, objpath)
self._unregistered_devices[objpath] = device
logger.debug(f'New unregistered device: {device}')
self.emit('unregistered-device', device)
def _on_signal_received(self, proxy, sender, signal, parameters):
if signal == 'SearchStopped':
self.notify('searching')
elif signal == 'UnregisteredDevice':
objpath = parameters[0]
self._handle_unregistered_device(objpath)
def __getitem__(self, btaddr):
return self._devices[btaddr]
def log_device_error(d, err):
if err == -errno.EACCES:
logger.error(f'{device}: wrong device, please re-register.')
elif err < 0:
logger.error(f'{device}: an error occured: {os.strerror(-err)}')
device.connect('device-error', log_device_error)
class Worker(GObject.Object):
@ -743,7 +449,7 @@ class TuhiKeteShell(cmd.Cmd):
readline.set_history_length(100)
Gio.bus_watch_name(Gio.BusType.SESSION,
TUHI_DBUS_NAME,
tuhi.dbusclient.TUHI_DBUS_NAME,
Gio.BusNameWatcherFlags.NONE,
self._on_name_appeared,
self._on_name_vanished)
@ -875,7 +581,7 @@ class TuhiKeteShell(cmd.Cmd):
add_help=False)
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
type=TuhiKeteDevice.is_device_address,
type=tuhi.dbusclient.TuhiDBusClientDevice.is_device_address,
default=None,
help='the address of the device to listen to')
parser.add_argument('mode', choices=['on', 'off'], nargs='?',
@ -986,7 +692,7 @@ class TuhiKeteShell(cmd.Cmd):
add_help=False)
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
type=TuhiKeteDevice.is_device_address,
type=tuhi.dbusclient.TuhiDBusClientDevice.is_device_address,
default=None,
help='the address of the device to fetch drawing from')
parser.add_argument('index', metavar='{<index>|all}',
@ -1083,7 +789,7 @@ class TuhiKeteShell(cmd.Cmd):
add_help=False)
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
type=TuhiKeteDevice.is_device_address,
type=tuhi.dbusclient.TuhiDBusClientDevice.is_device_address,
default=None,
help='the address of the device to register')
@ -1137,7 +843,7 @@ class TuhiKeteShell(cmd.Cmd):
add_help=False)
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
type=TuhiKeteDevice.is_device_address,
type=tuhi.dbusclient.TuhiDBusClientDevice.is_device_address,
default=None, nargs='?',
help='the address of the device to listen to')
@ -1189,7 +895,7 @@ class TuhiKeteShell(cmd.Cmd):
add_help=False)
parser.add_argument('-h', action='help', help=argparse.SUPPRESS)
parser.add_argument('address', metavar='12:34:56:AB:CD:EF',
type=TuhiKeteDevice.is_device_address,
type=tuhi.dbusclient.TuhiDBusClientDevice.is_device_address,
default=None, nargs='?',
help='the address of the device to listen to')
parser.add_argument('mode', choices=['on', 'off'], nargs='?',
@ -1246,7 +952,7 @@ def main(args):
with TuhiKeteShell() as shell:
shell.run()
except DBusError as e:
except tuhi.dbusclient.DBusError as e:
logger.error(e.message)

View File

@ -19,6 +19,16 @@ import sys
import multiprocessing
from multiprocessing import reduction
try:
import tuhi.dbusclient
except ModuleNotFoundError:
# If PYTHONPATH isn't set up or we never installed Tuhi, the module
# isn't available. And since we don't install tuhi-live, we can assume that
# we're still in the git repo, so messing with the path is "fine".
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
import tuhi.dbusclient
manager = None
logger = None
@ -35,10 +45,8 @@ def open_uhid_process(queue_in, conn_out):
def maybe_start_tuhi(queue):
sys.path
try:
should_start, verbose = queue.get()
should_start, args = queue.get()
except KeyboardInterrupt:
return 0
@ -48,14 +56,15 @@ def maybe_start_tuhi(queue):
sys.path.append(os.getcwd())
import tuhi.base
if verbose:
tuhi.base.logger.setLevel(logging.DEBUG)
t = tuhi.base.Tuhi()
while True:
try:
t.run()
except KeyboardInterrupt:
pass
import signal
# we don't want to kill Tuhi on ctrl+c because we won't be able to reset
# live mode. Instead we rely on tuhi-live to take us down when it exits
signal.signal(signal.SIGINT, signal.SIG_IGN)
args = ['tuhi-live'] + args # argparse in tuhi.base.main skips argv[0]
tuhi.base.main(args)
def start_tuhi_server(args):
@ -84,7 +93,7 @@ def start_tuhi_server(args):
except GLib.Error as e:
if (e.domain == 'g-io-error-quark' and
e.code == Gio.IOErrorEnum.DBUS_ERROR):
raise kete.DBusError(e.message)
raise tuhi.dbusclient.DBusError(e.message)
else:
raise e
@ -93,50 +102,50 @@ def start_tuhi_server(args):
try:
proxy = Gio.DBusProxy.new_sync(connection,
Gio.DBusProxyFlags.NONE, None,
kete.TUHI_DBUS_NAME,
kete.ROOT_PATH,
kete.ORG_FREEDESKTOP_TUHI1_MANAGER,
tuhi.dbusclient.TUHI_DBUS_NAME,
tuhi.dbusclient.ROOT_PATH,
tuhi.dbusclient.ORG_FREEDESKTOP_TUHI1_MANAGER,
None)
except GLib.Error as e:
if (e.domain == 'g-io-error-quark' and
e.code == Gio.IOErrorEnum.DBUS_ERROR):
raise kete.DBusError(e.message)
raise tuhi.dbusclient.DBusError(e.message)
else:
raise e
started = proxy.get_name_owner() is not None
if not started:
print(f'No-one is handling {kete.TUHI_DBUS_NAME}, attempting to start a daemon')
print(f'No-one is handling {tuhi.dbusclient.TUHI_DBUS_NAME}, attempting to start a daemon')
queue.put((not started, args.verbose))
queue.put((not started, args))
def run_live(request_fd_queue, conn_fd):
import kete
from gi.repository import Gio, GLib
def on_name_appeared(connection, name, client):
global manager
logger.info('Connected to the Tuhi daemon')
manager = kete.TuhiKeteManager()
manager = tuhi.dbusclient.TuhiDBusClientManager()
for device in manager.devices:
if device.live:
logger.info(f'{device} is already live, stopping first')
device.stop_live()
logger.info(f'starting live on {device}, please press button on the device')
request_fd_queue.put(os.getpid())
fd = reduction.recv_handle(conn_fd)
device.start_live(fd)
Gio.bus_watch_name(Gio.BusType.SESSION,
kete.TUHI_DBUS_NAME,
tuhi.dbusclient.TUHI_DBUS_NAME,
Gio.BusNameWatcherFlags.NONE,
on_name_appeared,
None)
mainloop = GLib.MainLoop()
connected_devices = 0
def on_disconnect(dev, pspec):
mainloop.quit()
@ -147,10 +156,6 @@ def run_live(request_fd_queue, conn_fd):
except KeyboardInterrupt:
pass
finally:
for device in manager.devices:
if device.live:
connected_devices += 1
for device in manager.devices:
if device.live and device.connected:
logger.info(f'stopping live on {device}')
@ -186,22 +191,21 @@ def drop_privileges():
def parse(args):
desc = 'tool to start the live mode on all devices tuhi knows about'
parser = argparse.ArgumentParser(description=desc)
parser.add_argument('-v', '--verbose',
help='Show some debugging informations',
parser = argparse.ArgumentParser(description='Tool to start live mode')
parser.add_argument('--flatpak-compatibility-mode',
help='Use the flatpak xdg directories',
action='store_true',
default=False)
return parser.parse_args(args[1:])
ns, remaining_args = parser.parse_known_args(args[1:])
return ns, remaining_args
def main(args=sys.argv):
if not os.geteuid() == 0:
sys.exit('Script must be run as root')
args = parse(args)
our_args, remaining_args = parse(args)
request_fd_queue = multiprocessing.Queue()
conn_in, conn_out = multiprocessing.Pipe()
@ -211,7 +215,19 @@ def main(args=sys.argv):
drop_privileges()
start_tuhi_server(args)
if our_args.flatpak_compatibility_mode:
from pathlib import Path
# tuhi-live is usually started through sudo, so let's get to the
# user's home directory here.
userhome = Path(os.path.expanduser('~' + os.getlogin()))
basedir = userhome / '.var' / 'app' / 'org.freedesktop.Tuhi'
print(f'Using flatpak xdg dirs in {basedir}')
os.environ['XDG_DATA_HOME'] = os.fspath(basedir / 'data')
os.environ['XDG_CONFIG_HOME'] = os.fspath(basedir / 'config')
os.environ['XDG_CACHE_HOME'] = os.fspath(basedir / 'cache')
start_tuhi_server(remaining_args)
run_live(request_fd_queue, conn_in)

View File

@ -5,9 +5,15 @@ import sys
import os
from pathlib import Path
try:
# 3.30 is the first one with Gtk.Template
gi.check_version('3.30') # NOQA
except ValueError as e:
print(e, file=sys.stderr)
sys.exit(1)
gi.require_version('Gio', '2.0') # NOQA
gi.require_version('Gtk', '3.0') # NOQA
from gi.repository import Gio, Gtk, Gdk
from gi.repository import Gio
@devel@ # NOQA
@ -15,61 +21,12 @@ resource = Gio.resource_load(os.fspath(Path('@pkgdatadir@', 'tuhi.gresource')))
Gio.Resource._register(resource)
def install_excepthook():
old_hook = sys.excepthook
def new_hook(etype, evalue, etb):
old_hook(etype, evalue, etb)
while Gtk.main_level():
Gtk.main_quit()
sys.exit()
sys.excepthook = new_hook
def gtk_style():
css = b"""
flowboxchild:selected {
background-color: white;
}
.bg-white {
background-color: white;
}
.bg-paper {
border-radius: 5px;
background-color: #ebe9e8;
}
.drawing {
background-color: white;
border-radius: 5px;
}
"""
screen = Gdk.Screen.get_default()
if screen is None:
print('Error: Unable to connect to screen. Make sure DISPLAY or WAYLAND_DISPLAY are set', file=sys.stderr)
sys.exit(1)
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
Gtk.StyleContext.add_provider_for_screen(
screen,
style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
if __name__ == "__main__":
import gettext
import locale
import signal
from tuhi.gui.application import Application
install_excepthook()
gtk_style()
locale.bindtextdomain('tuhi', '@localedir@')
locale.textdomain('tuhi')
gettext.bindtextdomain('tuhi', '@localedir@')
gettext.textdomain('tuhi')
signal.signal(signal.SIGINT, signal.SIG_DFL)
exit_status = Application().run(sys.argv)
sys.exit(exit_status)
from tuhi.gui.application import main
main(sys.argv)

24
tuhi.in
View File

@ -14,6 +14,7 @@
import sys
import subprocess
from pathlib import Path
import argparse
tuhi_server = Path('@libexecdir@', 'tuhi-server')
tuhi_gui = Path('@libexecdir@', 'tuhi-gui')
@ -22,10 +23,27 @@ tuhi_gui = Path('@libexecdir@', 'tuhi-gui')
@devel@ # NOQA
if __name__ == '__main__':
args = sys.argv[1:]
tuhi = subprocess.Popen([tuhi_server] + args)
if sys.version_info < (3, 6):
sys.exit('Python 3.6 or later required')
parser = argparse.ArgumentParser(description='Tuhi')
parser.add_argument('--flatpak-compatibility-mode',
help='Use the flatpak xdg directories',
action='store_true',
default=False)
ns, remainder = parser.parse_known_args()
if ns.flatpak_compatibility_mode:
import os
basedir = Path.home() / '.var' / 'app' / 'org.freedesktop.Tuhi'
print(f'Using flatpak xdg dirs in {basedir}')
os.environ['XDG_DATA_HOME'] = os.fspath(basedir / 'data')
os.environ['XDG_CONFIG_HOME'] = os.fspath(basedir / 'config')
os.environ['XDG_CACHE_HOME'] = os.fspath(basedir / 'cache')
tuhi = subprocess.Popen([tuhi_server] + remainder)
try:
subprocess.run([tuhi_gui] + args)
subprocess.run([tuhi_gui] + remainder)
except KeyboardInterrupt:
pass
tuhi.terminate()

View File

@ -210,7 +210,7 @@ class BlueZDevice(GObject.Object):
self.emit('connected')
return
self.logger.info(f'Connecting')
self.logger.debug(f'Connecting')
i.Connect(result_handler=self._on_connect_result)
def _on_connect_result(self, obj, result, user_data):
@ -234,7 +234,7 @@ class BlueZDevice(GObject.Object):
self.emit('disconnected')
return
self.logger.info(f'Disconnecting')
self.logger.debug(f'Disconnecting')
i.Disconnect(result_handler=self._on_disconnect_result)
def _on_disconnect_result(self, obj, result, user_data):
@ -246,9 +246,9 @@ class BlueZDevice(GObject.Object):
if 'Connected' in properties:
if properties['Connected']:
self.logger.info('Connection established')
self.logger.debug('Connection established')
else:
self.logger.info('Disconnected')
self.logger.debug('Disconnected')
self.emit('disconnected')
if 'ServicesResolved' in properties:
if properties['ServicesResolved']:

View File

@ -18,7 +18,7 @@ import os
import logging
import re
logger = logging.getLogger('tuhi.gui.dbus')
logger = logging.getLogger('tuhi.dbusclient')
TUHI_DBUS_NAME = 'org.freedesktop.tuhi1'
ORG_FREEDESKTOP_TUHI1_MANAGER = 'org.freedesktop.tuhi1.Manager'
@ -37,7 +37,7 @@ class _DBusObject(GObject.Object):
_connection = None
def __init__(self, name, interface, objpath):
GObject.GObject.__init__(self)
super().__init__()
# this is not handled asynchronously because if we fail to
# get the session bus, we have other issues
@ -149,20 +149,18 @@ class BlueZDevice(_DBusSystemObject):
self.notify('connected')
class TuhiKeteDevice(_DBusObject):
class TuhiDBusClientDevice(_DBusObject):
__gsignals__ = {
'button-press-required':
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
(GObject.SignalFlags.RUN_FIRST, None, ()),
'registered':
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
(GObject.SignalFlags.RUN_FIRST, None, ()),
'device-error':
(GObject.SignalFlags.RUN_FIRST, None, (int,)),
}
def __init__(self, manager, objpath):
_DBusObject.__init__(self, TUHI_DBUS_NAME,
ORG_FREEDESKTOP_TUHI1_DEVICE,
objpath)
super().__init__(TUHI_DBUS_NAME, ORG_FREEDESKTOP_TUHI1_DEVICE, objpath)
self.manager = manager
self.is_registering = False
self._bluez_device = BlueZDevice(self.property('BlueZDevice'))
@ -211,6 +209,10 @@ class TuhiKeteDevice(_DBusObject):
def sync_state(self):
return self._sync_state
@GObject.Property
def live(self):
return self.property('Live')
def _on_connected(self, bluez_device, pspec):
self.notify('connected')
@ -241,7 +243,7 @@ class TuhiKeteDevice(_DBusObject):
def _on_signal_received(self, proxy, sender, signal, parameters):
if signal == 'ButtonPressRequired':
logger.info(f'{self}: Press button on device now')
self.emit('button-press-required', self)
self.emit('button-press-required')
elif signal == 'ListeningStopped':
err = parameters[0]
if err == -errno.EACCES:
@ -268,6 +270,8 @@ class TuhiKeteDevice(_DBusObject):
self.notify('battery-percent')
elif 'BatteryState' in changed_props:
self.notify('battery-state')
elif 'Live' in changed_props:
self.notify('live')
def __repr__(self):
return f'{self.address} - {self.name}'
@ -282,7 +286,21 @@ class TuhiKeteDevice(_DBusObject):
self.manager.disconnect(self.s1)
del(self.s1)
logger.info(f'{self}: Registration successful')
self.emit('registered', self)
self.emit('registered')
def start_live(self, fd):
fd_list = Gio.UnixFDList.new()
fd_list.append(fd)
res, fds = self.proxy.call_with_unix_fd_list_sync('org.freedesktop.tuhi1.Device.StartLive',
GLib.Variant('(h)', (fd,)),
Gio.DBusCallFlags.NO_AUTO_START,
-1,
fd_list,
None)
def stop_live(self):
self.proxy.StopLive()
def terminate(self):
try:
@ -290,19 +308,17 @@ class TuhiKeteDevice(_DBusObject):
except AttributeError:
pass
self._bluez_device.terminate()
super(TuhiKeteDevice, self).terminate()
super().terminate()
class TuhiKeteManager(_DBusObject):
class TuhiDBusClientManager(_DBusObject):
__gsignals__ = {
'unregistered-device':
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
}
def __init__(self):
_DBusObject.__init__(self, TUHI_DBUS_NAME,
ORG_FREEDESKTOP_TUHI1_MANAGER,
ROOT_PATH)
super().__init__(TUHI_DBUS_NAME, ORG_FREEDESKTOP_TUHI1_MANAGER, ROOT_PATH)
self._devices = {}
self._unregistered_devices = {}
@ -316,7 +332,7 @@ class TuhiKeteManager(_DBusObject):
def _init(self, *args, **kwargs):
logger.info('manager is online')
for objpath in self.property('Devices'):
device = TuhiKeteDevice(self, objpath)
device = TuhiDBusClientDevice(self, objpath)
self._devices[device.address] = device
@GObject.Property
@ -350,7 +366,7 @@ class TuhiKeteManager(_DBusObject):
dev.terminate()
self._devices = {}
self._unregistered_devices = {}
super(TuhiKeteManager, self).terminate()
super().terminate()
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
if changed_props is None:
@ -379,7 +395,7 @@ class TuhiKeteManager(_DBusObject):
self.emit('unregistered-device', dev)
return
device = TuhiKeteDevice(self, objpath)
device = TuhiDBusClientDevice(self, objpath)
self._unregistered_devices[objpath] = device
logger.debug(f'New unregistered device: {device}')

View File

@ -11,18 +11,17 @@
# GNU General Public License for more details.
#
from gi.repository import Gio, GLib, Gtk
import logging
from .window import MainWindow
from .config import Config
import xdg.BaseDirectory
from pathlib import Path
import logging
import sys
import xdg.BaseDirectory
import gi
gi.require_version("Gio", "2.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gio, GLib, Gtk, Gdk # NOQA
logging.basicConfig(format='%(asctime)s %(levelname)s: %(name)s: %(message)s',
level=logging.INFO,
@ -52,6 +51,9 @@ class Application(Gtk.Application):
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
'download first drawing only but do not remove it from the device')
self.set_accels_for_action('app.quit', ['<Ctrl>Q'])
self._tuhi = None
def do_startup(self):
@ -102,3 +104,61 @@ class Application(Gtk.Application):
def _help(self, action, param):
import time
Gtk.show_uri(None, 'https://github.com/tuhiproject/tuhi/wiki', time.time())
def install_excepthook():
old_hook = sys.excepthook
def new_hook(etype, evalue, etb):
old_hook(etype, evalue, etb)
while Gtk.main_level():
Gtk.main_quit()
sys.exit()
sys.excepthook = new_hook
def gtk_style():
css = b"""
flowboxchild:selected {
background-color: white;
}
.bg-white {
background-color: white;
}
.bg-paper {
border-radius: 5px;
background-color: #ebe9e8;
}
.drawing {
background-color: white;
border-radius: 5px;
}
"""
screen = Gdk.Screen.get_default()
if screen is None:
print('Error: Unable to connect to screen. Make sure DISPLAY or WAYLAND_DISPLAY are set', file=sys.stderr)
sys.exit(1)
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
Gtk.StyleContext.add_provider_for_screen(screen,
style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def main(argv):
if sys.version_info < (3, 6):
sys.exit('Python 3.6 or later required')
import gettext
import locale
import signal
install_excepthook()
gtk_style()
locale.textdomain('tuhi')
gettext.textdomain('tuhi')
signal.signal(signal.SIGINT, signal.SIG_DFL)
exit_status = Application().run(argv)
sys.exit(exit_status)

View File

@ -69,7 +69,7 @@ class Config(GObject.Object):
self.config[section][key] = value
self._write()
@GObject.property
@GObject.Property
def orientation(self):
try:
return self.config['Device']['Orientation']
@ -81,7 +81,7 @@ class Config(GObject.Object):
assert(orientation in ['landscape', 'portrait'])
self._add_key('Device', 'Orientation', orientation)
@GObject.property
@GObject.Property
def drawings(self):
return self._drawings

View File

@ -12,7 +12,6 @@
#
from gettext import gettext as _
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk
import xdg.BaseDirectory
import os
@ -22,6 +21,8 @@ from tuhi.svg import JsonSvg
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk # NOQA
DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi', 'svg')

View File

@ -11,15 +11,15 @@
# GNU General Public License for more details.
#
from gi.repository import GObject, Gtk
from .drawing import Drawing
from .config import Config
import time
import gi
import logging
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GObject, Gtk # NOQA
logger = logging.getLogger('tuhi.gui.drawingperspective')
@ -55,7 +55,7 @@ class Flowbox(Gtk.Box):
self.flowbox_drawings.remove(child)
self.flowbox_drawings.foreach(delete_matching_child, drawing)
@GObject.property
@GObject.Property
def is_empty(self):
return not self.flowbox_drawings.get_children()
@ -75,6 +75,7 @@ class DrawingPerspective(Gtk.Stack):
self.known_drawings = {} # type {timestamp: Drawing()}
self.flowboxes = {}
self._zoom = 0
self._want_listen = True
def _cache_drawings(self, device, pspec):
# The config backend filters duplicates anyway, so don't care here
@ -126,8 +127,13 @@ class DrawingPerspective(Gtk.Stack):
def device(self, device):
self._device = device
device.connect('notify::connected', self._on_connected)
device.connect('notify::listening', self._on_listening_stopped)
self._signals = []
sig = device.connect('notify::connected', self._on_connected)
self._signals.append(sig)
sig = device.connect('notify::listening', self._on_listening_stopped)
self._signals.append(sig)
sig = device.connect('device-error', self._on_device_error)
self._signals.append(sig)
# This is a bit convoluted. We need to cache all drawings
# because Tuhi doesn't have guaranteed storage. So any json that
@ -168,11 +174,20 @@ class DrawingPerspective(Gtk.Stack):
pass
def _on_listening_stopped(self, device, pspec):
if not device.listening:
if not device.listening and self._want_listen:
logger.debug(f'{device.name} - listening stopped, restarting')
# We never want to stop listening
device.start_listening()
def _on_device_error(self, device, error):
import errno
if error == -errno.EACCES:
# No point to keep getting notified
for sig in self._signals:
device.disconnect(sig)
self._signals = []
self._want_listen = False
@Gtk.Template.Callback('_on_undo_close_clicked')
def _on_undo_close_clicked(self, button):
self.overlay_undo.set_reveal_child(False)

View File

@ -11,49 +11,19 @@
# GNU General Public License for more details.
#
from gettext import gettext as _
from gi.repository import Gtk, Gio, GLib, GObject
from .drawingperspective import DrawingPerspective
from .tuhi import TuhiKeteManager
from .config import Config
from tuhi.dbusclient import TuhiDBusClientManager
from gettext import gettext as _
import logging
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib, GObject # NOQA
logger = logging.getLogger('tuhi.gui.window')
MENU_XML = """
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="primary-menu">
<section>
<item>
<attribute name="label" translatable="yes">Portrait</attribute>
<attribute name="action">win.orientation</attribute>
<attribute name="target">portrait</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Landscape</attribute>
<attribute name="action">win.orientation</attribute>
<attribute name="target">landscape</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Help</attribute>
<attribute name="action">app.help</attribute>
</item>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>
"""
@Gtk.Template(resource_path="/org/freedesktop/Tuhi/ui/ErrorPerspective.ui")
class ErrorPerspective(Gtk.Box):
@ -100,14 +70,14 @@ class SetupDialog(Gtk.Dialog):
self._sig = device.connect('button-press-required', self._on_button_press_required)
device.register()
def _on_button_press_required(self, tuhi, device):
tuhi.disconnect(self._sig)
def _on_button_press_required(self, device):
device.disconnect(self._sig)
self.stack.set_visible_child_name('page2')
self._sig = device.connect('registered', self._on_registered)
def _on_registered(self, tuhi, device):
tuhi.disconnect(self._sig)
def _on_registered(self, device):
device.disconnect(self._sig)
self.device = device
self.response(Gtk.ResponseType.OK)
@ -132,7 +102,7 @@ class MainWindow(Gtk.ApplicationWindow):
super().__init__(**kwargs)
self.maximize()
self._tuhi = TuhiKeteManager()
self._tuhi = TuhiDBusClientManager()
action = Gio.SimpleAction.new_stateful('orientation', GLib.VariantType('s'),
GLib.Variant('s', 'landscape'))
@ -140,7 +110,7 @@ class MainWindow(Gtk.ApplicationWindow):
action.set_state(GLib.Variant.new_string(Config().orientation))
self.add_action(action)
builder = Gtk.Builder.new_from_string(MENU_XML, -1)
builder = Gtk.Builder.new_from_resource('/org/freedesktop/Tuhi/ui/AppMenu.ui')
menu = builder.get_object("primary-menu")
self.menubutton1.set_menu_model(menu)
@ -244,6 +214,7 @@ class MainWindow(Gtk.ApplicationWindow):
# register.
for sig in self._signals:
device.disconnect(sig)
self._signals = []
def _add_perspective(self, perspective):
self.stack_perspectives.add_named(perspective, perspective.name)

View File

@ -144,7 +144,7 @@ def as_hex_string(data):
elif isinstance(data, list):
return ' '.join([f'{x:02x}' for x in data])
raise ValueError('Unsupported data format {data.__class__} for {data}')
raise ValueError('Unsupported data format {data.__class__.__name__} for {data}')
def _get_protocol_dictionary(protocol):
@ -337,8 +337,8 @@ class NordicData(list):
if self.length != len(data):
raise UnexpectedDataError(bs, f'Invalid data: length field {self.length}, data length is {len(data)}')
def __repr__(self):
return f'{self.name if self.Name else "UNKNOWN"}{self.opcode:02x} / {self.length:02x} / {as_hex_string(self)}'
def __str__(self):
return f'{self.name if self.name else "UNKNOWN"} {self.opcode:02x} / {self.length:02x} / {as_hex_string(self)}'
class ProtocolError(Exception):
@ -361,7 +361,7 @@ class MissingReplyError(ProtocolError):
def __init__(self, request, message=None):
self.request = request
def __repr__(self):
def __str__(self):
return f'Missing reply for request {self.request}. {self.message}'
@ -390,8 +390,8 @@ class UnexpectedReply(ProtocolError):
super().__init__(message)
self.msg = msg
def __repr__(self):
return f'{self.__class__}: {self.msg}: {self.message}'
def __str__(self):
return f'{self.__class__.__name__}: {self.msg}: {self.message}'
class UnexpectedDataError(ProtocolError):
@ -412,8 +412,8 @@ class UnexpectedDataError(ProtocolError):
super().__init__(*args, **kwargs)
self.bytes = bytes
def __repr__(self):
return f'{self.__class__}: {self.bytes} - {self.message}'
def __str__(self):
return f'{self.__class__.__name__}: {self.bytes} - {self.message}'
class DeviceError(ProtocolError):
@ -451,11 +451,8 @@ class DeviceError(ProtocolError):
if self.errorcode == DeviceError.ErrorCode.INVALID_STATE:
self.errno = errno.EBADE
def __repr__(self):
return f'DeviceError.{self.errorcode.name}'
def __str__(self):
return repr(self)
return f'DeviceError.{self.errorcode.name}'
class Msg(object):
@ -526,18 +523,16 @@ class Msg(object):
self._args = args
def _handle_reply(self, reply):
'''Override this in the subclass to handle the reply.
'''
Override this in the subclass to handle the reply. Note that the
default 0xb3 message is handled automaticaly, this is only for
non-default replies.
This is the default reply handler that deals with the 0xb3 ACK/Error
messages and throws the respective exceptions.
No return value, just throw the appropriate exception on failure.
:param reply: A :class:`NordicData` object
'''
if reply.opcode != 0xb3:
raise UnexpectedReply(self)
if reply[0] != 0x00:
raise DeviceError(reply[0])
raise NotImplementedError(f'{reply} needs customized handling')
def execute(self):
'''
@ -557,7 +552,14 @@ class Msg(object):
if self.reply is None:
raise MissingReplyError(self.request)
try:
self._handle_reply(self.reply)
# 0xb3 is always handled by us, anything else requires a
# custom reply handler
if self.reply.opcode == 0xb3:
if self.reply[0] != 0x00:
raise DeviceError(self.reply[0])
else:
self._handle_reply(self.reply)
# no exception? we can assume success
self.errorcode = DeviceError.ErrorCode.SUCCESS
except DeviceError as e:
@ -565,8 +567,8 @@ class Msg(object):
raise e
return self # allow chaining
def __repr__(self):
return f'{self.__class__}: {self.interaction} - {self.request}{self.reply}'
def __str__(self):
return f'{self.__class__.__name__}: {self.interaction} - {self.request}{self.reply}'
class MsgConnectIntuosPro(Msg):
@ -1374,16 +1376,13 @@ class StrokeParsingError(ProtocolError):
self.message = message
self.data = data
def __repr__(self):
def __str__(self):
if self.data:
datastr = f' data: {list2hex(self.data)}'
else:
datastr = ''
return f'{self.message}{datastr}'
def __str__(self):
return self.__repr__()
class StrokeDataType(enum.Enum):
UNKNOWN = enum.auto()
@ -1636,7 +1635,7 @@ class StrokePacketUnknown(StrokePacket):
self.size = 1 + nbytes
self.data = data[:self.size]
def __repr__(self):
def __str__(self):
return f'Unknown packet: {list2hex(self.data)}'
@ -1672,7 +1671,7 @@ class StrokeFileHeader(StrokePacket):
except KeyError:
raise StrokeParsingError(f'Unknown file format:', data[:4])
def __repr__(self):
def __str__(self):
t = time.strftime("%y%m%d%H%M%S", time.gmtime(self.timestamp))
return f'FileHeader: time: {t}, stroke count: {self.nstrokes}'
@ -1759,7 +1758,7 @@ class StrokeHeader(StrokePacket):
self.pen_id = little_u64(pen_packet[:8])
self.size += 1 + nbytes
def __repr__(self):
def __str__(self):
if self.timestamp is not None:
t = time.strftime(f'%y%m%d%H%M%S', time.gmtime(self.timestamp))
else:
@ -1849,7 +1848,7 @@ class StrokeDelta(object):
self.size = offset
def __repr__(self):
def __str__(self):
def printstring(delta, abs):
return f'{delta:+5d}' if delta is not None \
else f'{abs:5d}' if abs is not None \
@ -1888,7 +1887,7 @@ class StrokePoint(StrokeDelta):
# self.y = little_u16(data[4:6])
# self.pressure = little_u16(data[6:8])
def __repr__(self):
def __str__(self):
return f'StrokePoint: {self.x}/{self.y} pressure: {self.p}'
@ -1912,7 +1911,7 @@ class StrokeEndOfStroke(StrokePacket):
self.size = nbytes + 1
self.data = data[:self.size]
def __repr__(self):
def __str__(self):
return f'EndOfStroke: {list2hex(self.data)}'

View File

@ -479,7 +479,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
device.connect_gatt_value(WACOM_OFFLINE_CHRC_PEN_DATA_UUID,
self._on_pen_data_received)
@GObject.property
@GObject.Property
def dimensions(self):
return (self.width, self.height)
@ -805,12 +805,13 @@ class WacomProtocolSlate(WacomProtocolSpark):
self._on_sysevent_data_received)
def live_mode(self, mode, uhid):
# Slate tablet has two models A5 and A4
# Here, we read real tablet dimensions before
# starting live mode
self.update_dimensions()
self.x_max = self.width - 1000
self.y_max = self.height - 500
if mode:
# Slate tablet has two models A5 and A4
# Here, we read real tablet dimensions before
# starting live mode
self.update_dimensions()
self.x_max = int(self.width / self.point_size) - 1000
self.y_max = int(self.height / self.point_size) - 500
return super().live_mode(mode, uhid)
@ -1027,7 +1028,7 @@ class WacomDevice(GObject.Object):
mode = args[0]
logger.debug(f'{self._device.address}: starting')
logger.debug(f'{self._device.address}: starting for mode {mode.name}')
self._is_running = True
exception = None
try: