Compare commits

...

94 Commits
0.2 ... master

Author SHA1 Message Date
Gerard Braad 17f7c3e53f Update meson setup command 2023-03-19 18:21:00 +10:00
Peter Hutterer 02991b7fd9 CI: update some actions to v3
Node 12 is deprecated, so let's bump to a newer version
https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-12-07 09:00:38 +10:00
Peter Hutterer e44e982717 tuhi 0.6 2022-04-28 10:12:08 +10:00
Peter Hutterer 38e6fc29a7 flatpak: update the gnome runtime and to the latest pyxdg and svgwrite
pyparsing has new releases too but someone needs to check that...
2022-04-28 10:11:33 +10:00
Peter Hutterer 576d9d9fd2 Update translations
A few things were removed, let's keep the pot/po files up-to-date

$ meson compile -C translation-build tuhi-pot
$ meson compile -C translation-build tuhi-update-po
2022-04-28 10:06:45 +10:00
Peter Hutterer 008f65ca6b data: DrawingPerspective - don't translate the internal name page0
Same as 4facceebc4
2022-04-28 10:06:45 +10:00
albanobattistella e59fa5258d Update it.po 2022-04-28 09:13:54 +10:00
Peter Hutterer 57a2e9e7b7 CI: run apt-get update before apt-get install
Make sure our packageset is up-to-date

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-04-26 09:34:40 +10:00
Martin Owens 0b02f5f9c2 Change SVG export to use paths instead of line segments 2022-02-01 16:42:11 +10:00
Peter Hutterer 1362b89394 tools: add a basic SVG/PNG exporter tool
A commandline tool to convert the JSON files the GUI stores in
$XDG/tuhi/*.json. Makes testing SVG changes a bit easier.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-01-31 12:30:14 +10:00
Peter Hutterer 4ff7da5d41 protocol: handle the pen id packet correctly
We were re-using the header byte of the stroke header to count the bits.
Where that header is anything but 0xff we got out of sync and raised an
error. Fix this by renaming things so we don't accidentally use the
wrong fields.

Fixes #283

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2022-01-31 12:29:31 +10:00
Gündüzhan Gündüz f05fc2d80f
po: tr translation fixes (#277) 2021-02-08 13:57:36 +10:00
Gündüzhan Gündüz df7b92643e
Add turkish language translation (#275) 2021-02-04 15:58:19 +10:00
Peter Hutterer 7141412d14 Fix a typo in the ErrorPerspective
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-02-03 10:29:27 +10:00
Peter Hutterer c59730cf1f Replace Circle Ci with github workflows
It has a better integration to github (to no-ones suprise), even though its
use of docker containers isn't as convenient.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 14:02:12 +10:00
Peter Hutterer 16b0f9d2d5 meson.build: fall back to flake8 where flake8-3 does not exist
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 14:02:12 +10:00
Peter Hutterer 8ab43bf0b7 meson.build: explicitly check for gettext to be present
There's a meson issue where loading i18n produces a warning about missing
gettext but then proceeds to fail with confusing error messages where the i18n
module is used, see https://github.com/mesonbuild/meson/issues/6165

Work around this by explicitly checking for gettext after loading i18n.

Fixes #270

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-25 13:08:18 +10:00
Peter Hutterer 643099b858 Fix flake8 errors - ambiguous variable name 'l'
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
Peter Hutterer b54ac6e99e Fix flake8 errors - module import not at top of the file
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
Peter Hutterer 27164aba52 Fix flake8 errors - f-string without placeholders
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2021-01-22 17:09:27 +10:00
albanobattistella 9d36e3bad0
Add Italian translation (#263) 2020-09-09 07:14:06 +10:00
Peter Hutterer 4facceebc4 data: don't make the internal page0, page1 etc names translatable
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-04 09:43:15 +10:00
Peter Hutterer 4ce8d011d1 tuhi 0.5
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:24:42 +10:00
Peter Hutterer 9b4c0ca5ef Update the copyright year in the about dialog
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:24:42 +10:00
Peter Hutterer 02252b68b9 Update flatpak dependencies to latest versions
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-09-02 10:12:08 +10:00
Peter Hutterer eef877d6ce tuhi: use a Gtk.FileChooserNative dialog
This one hooks into the correct portal APIs, hopefully fixing the issue with
the Flatpak version not saving files correctly.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer ea5177027c Update flatpak GNOME version to 3.36
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer ba0424c6df gui: fix a flake8 complaint
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 11:03:26 +10:00
Peter Hutterer 59dfce5b8d gui: add an expanding label to the end, but don't expand flowboxes
pack_end, introduced in 29761204a9 means they
align to the bottom of the window. Where there aren't enough drawings to fill
the window, everything is bottom aligned. Hack around this by adding an empty
label to the bottom that expands to the maximum available size and stop
expanding the rest. This new label thus pushes everything up to the top
position.

Fixes 29761204a9

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 10:33:07 +10:00
Peter Hutterer 29761204a9 gui: pack the flowboxes with pack_end
The first drawings we load are from disk. If we sync a drawing from the device
with a different key (i.e. created in a different month), the new flowbox for
that drawing was appended to the list and in last place. This caused
out-of-order sorting (though a restart of Tuhi would fix it).

So simply change the behavior to sort by oldest timestamp first and pack_end()
for all of them. If we pack all our flowboxes with pack_end, they are
effectively in reverse order, i.e. last one added is first in the list.

Fixes #244

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-08-26 09:34:08 +10:00
Peter Hutterer 927fc0216b tuhi 0.4
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-07-21 16:54:52 +10:00
Taihsiang Ho (tai271828) 3c1a84e1ab protocol: rm unused class WacomPacket 2020-04-21 15:23:44 +10:00
Robert Schütz 72d9dee37a live: keep $PATH and $DISPLAY 2020-04-06 15:38:17 +02:00
Peter Hutterer 7a881de9bb test: switch the stroke tests to use pytest
We're already running the meson test through pytest anyway and pytest is more
powerful than unittest. So let's switch, it's just a search/replace away.

Plus, this way the approach to dynamically create the tests based on the test
logs in the user's home directory is a lot saner.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:39:45 +01:00
Peter Hutterer fcdacb7187 test: switch the message tests from unittest to pytest
We're already running the meson test through pytest anyway and pytest is more
powerfull than unittest. So let's switch, it's just a search/replace away.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:39:45 +01:00
Peter Hutterer acc459f003 meson.build: check for 3.30 as minimum pygobject version
Fixes #238

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:12:24 +10:00
Peter Hutterer 53e2cebaf8 protocol: fix a flake8 issue
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:08:36 +10:00
Peter Hutterer 785c1e5f58 test: append the required \0xa to the spark messages
f5ea08f171 added this effective requirement for
the Spark and older generations, so let's make sure the test passes.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-08 10:08:36 +10:00
Niclas Hoyer f5ea08f171 Add support for long device names
If the device name is longer than one reply, the registration will crash.
This adds support for a variable number of replies until the reply ends
with `0x0a`.
2020-01-07 11:05:13 +10:00
Niclas Hoyer a0566e0dc0 Workaround for Spark register problems
This is a workaround for registering problems with Spark.
The registration hangs at "Connecting to device...".

Internally the `GENERAL_ERROR` occurs, which is expected
when registering a Spark device, but this should also
raise an AuthorizeException, but it instead reaches the
handler as DeviceError.

It is currently unclear why the `GENERAL_ERROR` passes to
this handler, so this catches any `GENERAL_ERROR` while
registering a Spark device.
2020-01-07 11:00:11 +10:00
Peter Hutterer 820f168f43 meson.build: drop python-devel dependency
This isn't needed. We need python but if we can run meson we can rely on
python being available anyway. And we don't actually use the python header
files here.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-03 10:35:15 +10:00
Peter Hutterer 110c77745c flatpak: allow access to $HOME
The whole purpose of Tuhi is to save the files after downloading and the
flatpak sandbox quietly dropped any file we saved in the target directory.

We could use xdg-download or something as allowed directory but let's open up
home itself since we don't want to limit where the files can be saved.

Fixes #230

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2020-01-03 10:34:50 +10:00
Niclas Hoyer 67eb54c908 Add company ID for Augmented Paper 2020-01-03 10:34:25 +10:00
Peter Hutterer 170b2c0d8a meson.build: add a custom message for the python3 dependency.
It's confusing to users because they don't get any indication that it's the
development package we need, not the normal python package.

This requires bumping meson's minimum version to 0.50 but hey, we can live
with that.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-11-04 09:43:08 +01:00
Piotr Drąg a11d51c42c Update Polish translation 2019-10-24 16:54:46 +02:00
Peter Hutterer f6fdf86649 meson.build: drop yaml from the required python module list
This isn't required for the flatpak, the yaml module is only needed in some of
the debugging/recovery tools but not in Tuhi itself.
2019-09-30 11:36:52 +02:00
Peter Hutterer d5cb1930e5 export: drop unused import
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-09-30 18:05:39 +10:00
Ishak BELAHMAR ddd896b1f6 tools: Add choices to fetch.format argument 2019-09-30 09:50:03 +02:00
Ishak BELAHMAR ea1dc798b1 Remove unused svg module 2019-09-30 09:50:03 +02:00
Ishak BELAHMAR bbff179a4d Add png export feature 2019-09-30 09:50:03 +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
38 changed files with 1624 additions and 1116 deletions

View File

@ -1,18 +0,0 @@
version: 2
jobs:
build:
working_directory: ~/tuhi
docker:
- image: fedora:latest
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
- checkout
- run:
command: |
meson builddir
ninja -C builddir test
- store_artifacts:
path: ~/tuhi/builddir/meson-logs

26
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,26 @@
on: [ push, pull_request ]
env:
CFLAGS: "-Werror -Wall -Wextra -Wno-error=sign-compare -Wno-error=unused-parameter -Wno-error=missing-field-initializers"
UBUNTU_PACKAGES: meson gettext python3-dev python-gi-dev flake8 desktop-file-utils libappstream-glib-dev appstream-util python3-pytest python3-xdg python3-yaml python3-svgwrite python3-cairo
jobs:
meson_test:
runs-on: ubuntu-20.04
steps:
- name: Install dependencies
run: |
sudo apt-get update -yq
sudo apt-get install -yq --no-install-suggests --no-install-recommends $UBUNTU_PACKAGES
- uses: actions/checkout@v3
- name: meson
run: meson builddir
- name: ninja
run: ninja -C builddir test
- name: capture build logs
uses: actions/upload-artifact@v3
if: ${{ always() }} # even if we fail
with:
name: meson logs
path: |
builddir/meson-logs

View File

@ -26,7 +26,7 @@ To build and run Tuhi from the repository directly:
```
$> git clone http://github.com/tuhiproject/tuhi
$> cd tuhi
$> meson builddir
$> meson setup builddir
$> ninja -C builddir
$> ./builddir/tuhi.devel
```
@ -41,7 +41,7 @@ To install and run Tuhi:
```
$> git clone http://github.com/tuhiproject/tuhi
$> cd tuhi
$> meson builddir
$> meson setup builddir
$> ninja -C builddir install
```

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>

View File

@ -8,7 +8,7 @@
<property name="type_hint">normal</property>
<property name="program_name">Tuhi</property>
<property name="version">@version@</property>
<property name="copyright">Copyright © 2019 Tuhi Developers</property>
<property name="copyright">Copyright © 2020 Tuhi Developers</property>
<property name="website">@url@</property>
<property name="website_label" translatable="yes">Visit Tuhis website</property>
<property name="logo_icon_name">org.freedesktop.Tuhi</property>

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

@ -140,7 +140,7 @@
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
<property name="title">page0</property>
</packing>
</child>
</template>

View File

@ -22,7 +22,7 @@
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.
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.</property>
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 responding and needs to be restarted.</property>
<property name="wrap">True</property>
<property name="max_width_chars">55</property>
</object>

View File

@ -118,7 +118,7 @@
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
<property name="title">page0</property>
</packing>
</child>
<child>
@ -185,7 +185,7 @@
</object>
<packing>
<property name="name">page1</property>
<property name="title" translatable="yes">page1</property>
<property name="title">page1</property>
<property name="position">1</property>
</packing>
</child>
@ -255,7 +255,7 @@
</object>
<packing>
<property name="name">page2</property>
<property name="title" translatable="yes">page2</property>
<property name="title">page2</property>
<property name="position">2</property>
</packing>
</child>

View File

@ -1,13 +1,12 @@
project('tuhi',
version: '0.2',
version: '0.6',
license: 'GPLv2',
meson_version: '>= 0.48.0')
meson_version: '>= 0.50.0')
# The tag date of the project_version(), update when the version bumps.
version_date='2019-08-26'
version_date='2022-04-28'
# Dependencies
dependency('python3', required: true)
dependency('pygobject-3.0', required: true)
dependency('pygobject-3.0', version: '>= 3.30', required: true)
prefix = get_option('prefix')
datadir = join_paths(prefix, get_option('datadir'))
@ -24,13 +23,32 @@ libexecdir = join_paths(get_option('prefix'), get_option('libexecdir'), 'tuhi')
i18n = import('i18n')
# Workaround for https://github.com/mesonbuild/meson/issues/6165
find_program('gettext')
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',
'cairo',
]
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,
@ -121,7 +139,7 @@ appdata = i18n.merge_file(input: appdata_intl,
install_data('data/org.freedesktop.Tuhi.svg', install_dir: icondir)
flake8 = find_program('flake8-3', required: false)
flake8 = find_program('flake8-3', 'flake8', required: false)
if flake8.found()
test('flake8', flake8,
args: ['--ignore=E501,W504',

View File

@ -1,17 +1,18 @@
{
"app-id": "org.freedesktop.Tuhi",
"runtime": "org.gnome.Platform",
"runtime-version": "3.32",
"runtime-version": "42",
"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"
],
"--system-talk-name=org.bluez",
"--filesystem=home"
],
"modules": [
{
"name": "pyxdg",
@ -19,7 +20,9 @@
"sources": [
{
"type": "git",
"url": "git://anongit.freedesktop.org/xdg/pyxdg"
"url": "https://gitlab.freedesktop.org/xdg/pyxdg.git",
"tag": "rel-0.27",
"commit": "f097a66923a65e93640c48da83e6e9cfbddd86ba"
}
],
"build-commands": [
@ -32,8 +35,8 @@
"sources": [
{
"type": "archive",
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.2/pyparsing-2.4.2.tar.gz",
"sha512": "27e5959eb1cf0c4d899746d2d32f5f000c3753278bdbbb670d24a077053e5c08caf8429f684186c502f6d9bf358702e0a8b3fea40cd2b50807cf02ea38c750dd"
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.7/pyparsing-2.4.7.tar.gz",
"sha512": "0b9f8f18907f65cb3af1b48ed57989e183f28d71646f2b2f820e772476f596ca15ee1a689f3042f18458206457f4683d10daa6e73dfd3ae82d5e4405882f9dd2"
}
],
"build-commands": [
@ -46,7 +49,9 @@
"sources": [
{
"type": "git",
"url": "https://github.com/mozman/svgwrite.git"
"url": "https://github.com/mozman/svgwrite.git",
"tag": "v1.4.2",
"commit": "e2617741ab018956e638e18aa21827405bd8edd1"
}
],
"build-commands": [

View File

@ -1 +1,4 @@
# Language list must be in alphabetical order
it
pl
tr

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

193
po/it.po Normal file
View File

@ -0,0 +1,193 @@
# Italian translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# ALBANO BATTISTELLA <albano_battistella@hotmail.com>, 2020,2022.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: 2022-04-25 18:23+0100\n"
"Last-Translator: Albano Battistella <piotrd>\n"
"Language-Team: Italian <>\n"
"Language: it\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 ""
"Utilità per scaricare i disegni dalla gamma Inchiostro di dispositivi Wacom"
#: 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 è un'interfaccia utente grafica per scaricare disegni archiviati su "
"tablet dispositivi della gamma di Inchiostro Wacom, ad esempio Intuos Pro "
"Paper o Bamboo Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
#, fuzzy
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 ""
"Tuhi richiede Tuhi, il demone per comunicare effettivamente con i "
"dispositivi.ThiGui è semplicemente un front-end di Tuhi, Tuhi deve essere "
"installato e funzionante quando Tuhi viene lanciato."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Finestra principale di Tuhi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Finestra principale di Tuhi (ingrandita)"
#. 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;inchiostro;"
#: data/ui/AboutDialog.ui.in:13
msgid "Visit Tuhis website"
msgstr "Visita il sito web di Tuhi"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Ritratto"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Paesaggio"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Aiuto"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "Informazioni"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Annulla cancellazione del disegno"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Premere il pulsante sul dispositivo per sincronizzare i disegni"
#: data/ui/ErrorPerspective.ui:21
#, fuzzy
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 responding and needs "
"to be restarted."
msgstr ""
"TuhiGUI è una GUI interattiva per scaricare dati da Tuhi.\n"
"\n"
"Tuhi si collega ai tablet della gamma Wacom. Ti permette di scaricare i "
"disegni memorizzati su tali dispositivi come SVG per l'elaborazione "
"successiva.\n"
"\n"
"Tuhi è un server DBus che deve essere in esecuzione perché la GUI Tuhi possa "
"connettersi ad esso. La connessione al server DBus dovrebbe richiedere meno "
"di un secondo. Se hai letto fin qui, il tuo server Tuhi DBus non è in "
"esecuzione o non risponde e ha bisogno di essere riavviato."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Connessione a Tuhi"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"Questo dovrebbe richiedere meno di un secondo. Assicurati che il server Tuhi "
"DBus sia in esecuzione."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Errore di autorizzazione durante la connessione al dispositivo "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Registro"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "Configurazione iniziale del dispositivo"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Esci"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr ""
"Tieni premuto il pulsante sul dispositivo finché la luce blu non lampeggia."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Ricerca del dispositivo"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "Collegamento a LE Paper"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Connessione al dispositivo..."
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Premi ora il pulsante sul dispositivo!"
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "in attesa di risposta"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "senza titolo.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Qualsiasi file"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "File SVG"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "File PNG"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "Connessione a {device.name}"

193
po/pl.po Normal file
View File

@ -0,0 +1,193 @@
# 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: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: 2019-10-24 16:23+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: 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/ErrorPerspective.ui:21
#, fuzzy
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 responding 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: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ź"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "bez tytułu.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Wszystkie pliki"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "Pliki SVG"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "Pliki PNG"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "Łączenie z urządzeniem {device.name}"

190
po/tr.po Normal file
View File

@ -0,0 +1,190 @@
# Turkish translation for tuhi.
# Copyright © 2019 the tuhi authors.
# This file is distributed under the same license as the tuhi package.
# Gündüzhan Gündüz <gunduzhan@gmail.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: tuhi\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-28 09:34+1000\n"
"PO-Revision-Date: \n"
"Last-Translator: Gündüzhan Gündüz <gunduzhan@gmail.com>\n"
"Language-Team: Turkish <gnome-turk@gnome.org>\n"
"Language: tr\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"
"X-Generator: Poedit 2.4.1\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 "Wacom Ink cihazlarınızdan çizimlerinizi indirmek için bir araç"
#: 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, Wacom Ink serisi cihazlarda depolanan çizimlerinizi indirmenize olanak "
"sağlayan bir grafik kullanıcı arayüzüdür. Ör. Intuos Pro Paper veya Bamboo "
"Slate."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:15
#, fuzzy
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 ""
"Tuhi cihazınız ile iletişim kurmak için arka plan programı Tuhi'ye ihtiyaç "
"duyar. ThiGui, Tuhi için sadece bir başlangıç aşamasıdır. Tuhi "
"başlatıldığında kurulmalı ve çalıştırılmalıdır."
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:32
msgid "Tuhi's main window"
msgstr "Tuhi ana penceresi"
#: data/org.freedesktop.Tuhi.appdata.xml.in.in:36
msgid "Tuhi's main window (zoomed)"
msgstr "Tuhi ana penceresi (yakın)"
#. 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 "Tuhi'nin sitesini ziyaret edin"
#: data/ui/AppMenu.ui:5
msgid "Portrait"
msgstr "Portre"
#: data/ui/AppMenu.ui:10
msgid "Landscape"
msgstr "Yatay"
#: data/ui/AppMenu.ui:17
msgid "Help"
msgstr "Yardım"
#: data/ui/AppMenu.ui:21
msgid "About"
msgstr "Hakkında"
#: data/ui/DrawingPerspective.ui:68
msgid "Undo delete drawing"
msgstr "Silinen çizimi geri al"
#: data/ui/DrawingPerspective.ui:132
msgid "Press the button on the device to synchronize drawings"
msgstr "Çizimlerinizi senkron etmek için cihazdaki düğmeye basın"
#: data/ui/ErrorPerspective.ui:21
#, fuzzy
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 responding and needs "
"to be restarted."
msgstr ""
"TuhiGUI, Tuhi'den çizimlerinizi indirmek için bir araçtır.\n"
"\n"
"Tuhi Wacom Ink serisi cihazlara bağlanır. Ayrıca cihazlarda depolanan "
"çizimlerinizi işlemek üzere SVG olarak indirmenizi sağlar.\n"
"\n"
"Tuhi, TuhiGUI'nin bağlanabilmesi için çalışan bir DBus sunucusudur. DBus "
"sunucusuna bağlanmak saniyeden çok daha kısa sürede gerçekleşir. Eğer buraya "
"kadar okuduysanız, Tuhi DBus sunucusu çalışmıyor, yanıt vermiyor ve yeniden "
"başlatılması gerekiyordur."
#: data/ui/ErrorPerspective.ui:69
msgid "Connecting to Tuhi"
msgstr "Tuhi'ye bağlanılıyor"
#: data/ui/ErrorPerspective.ui:96
msgid ""
"This should take less than a second. Make sure the Tuhi DBus server is "
"running."
msgstr ""
"Bir saniyeden daha kısa sürer. Tuhi DBus sunucusunun çalıştığından emin olun."
#: data/ui/MainWindow.ui:166
msgid "Authorization error while connecting to the device "
msgstr "Cihaza bağlanırken yetkilendirme hatası oluştu "
#: data/ui/MainWindow.ui:176
msgid "Register"
msgstr "Kayıt"
#: data/ui/SetupPerspective.ui:7
msgid "Initial Device Setup"
msgstr "İlk Cihaz Kurulumu"
#: data/ui/SetupPerspective.ui:30
msgid "Quit"
msgstr "Çıkış"
#: data/ui/SetupPerspective.ui:70
msgid "Hold the button on the device until the blue light is flashing."
msgstr "Mavı ışık yanıp sönmeye başlayana kadar düğmeye basılı tutun."
#: data/ui/SetupPerspective.ui:103
msgid "Searching for device"
msgstr "Cihaz aranıyor"
#: data/ui/SetupPerspective.ui:137
msgid "Connecting to LE Paper"
msgstr "LE Paper'a bağlanıyor"
#: data/ui/SetupPerspective.ui:170
msgid "Connecting to device..."
msgstr "Cihaza bağlanıyor ..."
#: data/ui/SetupPerspective.ui:206
msgid "Press the button on the device now!"
msgstr "Şimdi cihazdaki düğmeye basın!"
#: data/ui/SetupPerspective.ui:240
msgid "waiting for reply"
msgstr "yanıt bekleniyor"
#. Translators: the default filename to save to
#: tuhi/gui/drawing.py:121
msgid "untitled.svg"
msgstr "yenibelge.svg"
#. Translators: filter name to show all/any files
#: tuhi/gui/drawing.py:125
msgid "Any files"
msgstr "Herhangi bir dosya"
#. Translators: filter to show svg files only
#: tuhi/gui/drawing.py:129
msgid "SVG files"
msgstr "SVG dosyaları"
#. Translators: filter to show png files only
#: tuhi/gui/drawing.py:133
msgid "PNG files"
msgstr "PNG dosyaları"
#: tuhi/gui/window.py:68
#, python-brace-format
msgid "Connecting to {device.name}"
msgstr "{device.name} cihaz bağlantısı kuruluyor"

269
test/test_messages.py Normal file → Executable file
View File

@ -19,19 +19,19 @@
import calendar
import os
import pytest
import sys
import unittest
import time
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.protocol import *
from tuhi.protocol import * # noqa
SUCCESS = NordicData([0xb3, 0x1, 0x00])
class TestUtils(unittest.TestCase):
class TestUtils(object):
def test_hex_string(self):
values = [
([0x00, 0x12], '00 12'),
@ -44,12 +44,12 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(as_hex_string(v[0]), v[1])
assert as_hex_string(v[0]) == v[1]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
as_hex_string(1)
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
as_hex_string('0x00')
def test_protocol_version(self):
@ -64,14 +64,14 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(ProtocolVersion.from_string(v[0]), v[1])
assert ProtocolVersion.from_string(v[0]) == v[1]
# No real reason we couldn't support those but right now they
# aren't, so let's test for it.
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
ProtocolVersion.from_string('Slate')
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
ProtocolVersion.from_string('IntuosPro')
def test_little_u16(self):
@ -81,12 +81,12 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(little_u16(v[0]), bytes(v[1]))
self.assertEqual(little_u16(v[1]), v[0])
assert little_u16(v[0]) == bytes(v[1])
assert little_u16(v[1]) == v[0]
invalid = [0x10000, -1, [0x00, 0x00, 0x00]]
for v in invalid:
with self.assertRaises(AssertionError):
with pytest.raises(AssertionError):
little_u16(v)
def test_little_u32(self):
@ -98,20 +98,20 @@ class TestUtils(unittest.TestCase):
]
for v in values:
self.assertEqual(little_u32(v[0]), bytes(v[1]))
self.assertEqual(little_u32(v[1]), v[0])
assert little_u32(v[0]) == bytes(v[1])
assert little_u32(v[1]) == v[0]
invalid = [0x100000000, -1, [0x00, 0x00, 0x00, 0x00, 0x00]]
for v in invalid:
with self.assertRaises(AssertionError):
with pytest.raises(AssertionError):
little_u32(v)
class TestProtocolAny(unittest.TestCase):
class TestProtocolAny(object):
protocol_version = ProtocolVersion.ANY
def test_get_protocol(self):
self.assertIsNotNone(Protocol(self.protocol_version, callback=None))
assert Protocol(self.protocol_version, callback=None) is not None
def test_has_all_messages(self):
p = Protocol(self.protocol_version, callback=None)
@ -136,47 +136,47 @@ class TestProtocolAny(unittest.TestCase):
def test_connect(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe6
assert request.length == 6
return SUCCESS
if cb is None:
cb = _cb
p = Protocol(self.protocol_version, callback=cb)
with self.assertRaises(TypeError):
with pytest.raises(TypeError):
p.execute(Interactions.CONNECT) # missing argument
uuid = 'abcdef123456'
msg = p.execute(Interactions.CONNECT, uuid)
self.assertEqual(msg.uuid, uuid)
assert msg.uuid == uuid
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
p.execute(Interactions.CONNECT, 'too-long-an-id')
with self.assertRaises(binascii.Error):
with pytest.raises(binascii.Error):
uuid = 'uvwxyz123456'
p.execute(Interactions.CONNECT, uuid)
def test_get_name(self, cb=None, name='test dev name'):
def test_get_name(self, cb=None, name='test dev name\x0a'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xbb)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xbb
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xbc, len(name)] + list(bytes(name, encoding='ascii')))
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_NAME)
self.assertEqual(msg.name, name)
assert msg.name == name
def test_set_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xbb)
self.assertEqual(request.length, len(name) + 1)
self.assertEqual(request[-1], 0xa) # spark needs a trailing linebreak
self.assertEqual(bytes(request[:-1]).decode('utf-8'), name)
assert request.opcode == 0xbb
assert request.length == len(name) + 1
assert request[-1] == 0xa # spark needs a trailing linebreak
assert bytes(request[:-1]).decode('utf-8') == name
return SUCCESS
cb = cb or _cb
@ -186,8 +186,8 @@ class TestProtocolAny(unittest.TestCase):
def test_get_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb6
assert request.length == 1
t = time.strftime('%y%m%d%H%M%S', time.gmtime(ts))
t = [int(i) for i in binascii.unhexlify(t)]
return NordicData([0xbd, len(t)] + t)
@ -196,15 +196,15 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_TIME)
self.assertEqual(msg.timestamp, int(ts))
assert msg.timestamp == int(ts)
def test_set_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xb6
assert request.length == 6
str_timestamp = ''.join([f'{b:02x}' for b in request])
t = calendar.timegm(time.strptime(str_timestamp, '%y%m%d%H%M%S'))
self.assertEqual(int(t), int(ts))
assert int(t) == int(ts)
return SUCCESS
cb = cb or _cb
@ -214,8 +214,8 @@ class TestProtocolAny(unittest.TestCase):
def test_get_fw(self, cb=None, fw='abcdef-123456'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb7)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb7
assert request.length == 1
data = [int(c, 16) for c in fw.split('-')[request[0]]]
return NordicData([0xb8, len(data) + 1, 0x00] + data)
@ -223,43 +223,43 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_FIRMWARE)
self.assertEqual(msg.firmware, fw)
assert msg.firmware == fw
def test_get_battery(self, cb=None, battery=(1, 78)):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb9)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb9
assert request.length == 1
return NordicData([0xba, 2, battery[1], battery[0]])
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_BATTERY)
self.assertEqual(msg.battery_is_charging, battery[0])
self.assertEqual(msg.battery_percent, battery[1])
assert msg.battery_is_charging == battery[0]
assert msg.battery_percent == battery[1]
def test_get_width(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_WIDTH)
self.assertEqual(msg.width, 21000)
assert msg.width == 21000
def test_get_height(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_HEIGHT)
self.assertEqual(msg.height, 14800)
assert msg.height == 14800
def test_get_point_size(self, cb=None):
# this is hardcoded for the spark
p = Protocol(self.protocol_version, callback=None)
msg = p.execute(Interactions.GET_POINT_SIZE)
self.assertEqual(msg.point_size, 10)
assert msg.point_size == 10
def test_unknown_e3(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe3)
self.assertEqual(request.length, 1)
assert request.opcode == 0xe3
assert request.length == 1
return SUCCESS
cb = cb or _cb
@ -269,9 +269,9 @@ class TestProtocolAny(unittest.TestCase):
def test_filetransfer_reporting_type(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xec)
self.assertEqual(request.length, 6)
self.assertEqual(request, [0x06, 0x00, 0x00, 0x00, 0x00, 0x00])
assert request.opcode == 0xec
assert request.length == 6
assert request, [0x06, 0x00, 0x00, 0x00, 0x00 == 0x00]
return SUCCESS
cb = cb or _cb
@ -284,9 +284,9 @@ class TestProtocolAny(unittest.TestCase):
mode = Mode.LIVE
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], mode)
assert request.opcode == 0xb1
assert request.length == 1
assert request[0] == mode
return SUCCESS
cb = cb or _cb
@ -299,9 +299,9 @@ class TestProtocolAny(unittest.TestCase):
# this is a weird double call, see the protocol
# We reply 0xc7 first, and then 0xcd
if request is not None:
self.assertEqual(request.opcode, 0xc5)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc5
assert request.length == 1
assert request[0] == 0x00
data = list(count.to_bytes(4, byteorder='big'))
return NordicData([0xc7, len(data)] + data)
else:
@ -313,14 +313,14 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_STROKES)
self.assertEqual(msg.count, count)
self.assertEqual(msg.timestamp, int(ts))
assert msg.count == count
assert msg.timestamp == int(ts)
def test_available_files_count(self, cb=None, ndata=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc1
assert request.length == 1
assert request[0] == 0x00
data = list(ndata.to_bytes(2, byteorder='big'))
return NordicData([0xc2, len(data)] + data)
@ -328,13 +328,13 @@ class TestProtocolAny(unittest.TestCase):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.AVAILABLE_FILES_COUNT)
self.assertEqual(msg.count, ndata)
assert msg.count == ndata
def test_download_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc3)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc3
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xc8, 1, 0xbe])
cb = cb or _cb
@ -344,9 +344,9 @@ class TestProtocolAny(unittest.TestCase):
def test_delete_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xca)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xca
assert request.length == 1
assert request[0] == 0x00
# no reply
cb = cb or _cb
@ -356,9 +356,9 @@ class TestProtocolAny(unittest.TestCase):
def test_register_complete(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe5)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xe5
assert request.length == 1
assert request[0] == 0x00
return SUCCESS
cb = cb or _cb
@ -368,16 +368,35 @@ class TestProtocolAny(unittest.TestCase):
def test_register_press_button(self, cb=None, uuid=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe3)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x01)
assert request.opcode == 0xe3
assert request.length == 1
assert request[0] == 0x01
# no reply
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.REGISTER_PRESS_BUTTON, uuid=uuid)
self.assertEqual(msg.uuid, uuid)
assert 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 pytest.raises(DeviceError) as cm:
p.execute(Interactions.CONNECT, uuid='abcdef123456')
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
with pytest.raises(DeviceError) as cm:
p.execute(Interactions.GET_STROKES)
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
with pytest.raises(DeviceError) as cm:
p.execute(Interactions.SET_MODE, Mode.PAPER)
assert cm.value.errorcode == DeviceError.ErrorCode.GENERAL_ERROR
class TestProtocolSpark(TestProtocolAny):
@ -385,14 +404,14 @@ class TestProtocolSpark(TestProtocolAny):
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0xe4, 0x00])
cb = cb or _cb
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.REGISTER_WAIT_FOR_BUTTON)
self.assertEqual(msg.protocol_version, self.protocol_version)
assert msg.protocol_version == self.protocol_version
class TestProtocolSlate(TestProtocolSpark):
@ -400,9 +419,9 @@ class TestProtocolSlate(TestProtocolSpark):
def test_get_width(self, cb=None, width=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 3)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 3
data = [0x03, 0x00] + list(width.to_bytes(4, byteorder='little'))
return NordicData([0xeb, len(data)] + data)
@ -411,13 +430,13 @@ class TestProtocolSlate(TestProtocolSpark):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_WIDTH)
self.assertEqual(msg.width, width)
assert msg.width == width
def test_get_height(self, cb=None, height=4321):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 4)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 4
data = [0x04, 0x00] + list(height.to_bytes(4, byteorder='little'))
return NordicData([0xeb, len(data)] + data)
@ -426,13 +445,13 @@ class TestProtocolSlate(TestProtocolSpark):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_HEIGHT)
self.assertEqual(msg.height, height)
assert msg.height == height
def test_get_strokes(self, cb=None, count=1024, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xcc)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xcc
assert request.length == 1
assert request[0] == 0x00
c = list(count.to_bytes(4, byteorder='little'))
t = time.strftime('%y%m%d%H%M%S', time.gmtime(ts))
t = [int(i) for i in binascii.unhexlify(t)]
@ -443,9 +462,9 @@ class TestProtocolSlate(TestProtocolSpark):
def test_available_files_count(self, cb=None, ndata=1234):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xc1)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xc1
assert request.length == 1
assert request[0] == 0x00
data = list(ndata.to_bytes(2, byteorder='little'))
return NordicData([0xc2, len(data)] + data)
@ -453,24 +472,24 @@ class TestProtocolSlate(TestProtocolSpark):
def test_delete_oldest_file(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xca)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xca
assert request.length == 1
assert request[0] == 0x00
return SUCCESS
super().test_delete_oldest_file(cb or _cb)
def test_register_press_button(self, cb=None, uuid='abcdef123456'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe7)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe7
assert request.length == 6
# no reply
super().test_register_press_button(cb or _cb, uuid)
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0xe4, 0x00])
super().test_register_wait_for_button(cb or _cb)
@ -481,34 +500,34 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_connect(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xe6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xe6
assert request.length == 6
return NordicData([0x50, 0x06] + request) # replies with the uuid
super().test_connect(cb or _cb)
def test_get_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xdb)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xdb
assert request.length == 1
assert request[0] == 0x00
return NordicData([0xbc, len(name)] + list(bytes(name, encoding='ascii')))
super().test_get_name(cb or _cb, name=name)
def test_set_name(self, cb=None, name='test dev name'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xdb)
self.assertEqual(request.length, len(name))
self.assertEqual(bytes(request).decode('utf-8'), name)
assert request.opcode == 0xdb
assert request.length == len(name)
assert bytes(request).decode('utf-8') == name
return SUCCESS
super().test_set_name(cb or _cb, name=name)
def test_get_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xd6)
self.assertEqual(request.length, 1)
assert request.opcode == 0xd6
assert request.length == 1
t = list(int(ts).to_bytes(length=4, byteorder='little')) + [0x00, 0x00]
return NordicData([0xbd, len(t)] + t)
@ -516,18 +535,18 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_set_time(self, cb=None, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb6)
self.assertEqual(request.length, 6)
assert request.opcode == 0xb6
assert request.length == 6
t = int.from_bytes(request[0:4], byteorder='little')
self.assertEqual(int(t), int(ts))
assert int(t) == int(ts)
return SUCCESS
super().test_set_time(cb or _cb, ts=ts)
def test_get_fw(self, cb=None, fw='anything-string'):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xb7)
self.assertEqual(request.length, 1)
assert request.opcode == 0xb7
assert request.length == 1
data = bytes(fw.split('-')[request[0]].encode('utf8'))
return NordicData([0xb8, len(data) + 1, 0x00] + list(data))
@ -535,9 +554,9 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_get_strokes(self, cb=None, count=1024, ts=time.time()):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xcc)
self.assertEqual(request.length, 1)
self.assertEqual(request[0], 0x00)
assert request.opcode == 0xcc
assert request.length == 1
assert request[0] == 0x00
c = list(count.to_bytes(4, byteorder='little'))
t = list(int(ts).to_bytes(4, byteorder='little'))
data = c + t
@ -547,16 +566,16 @@ class TestProtocolIntuosPro(TestProtocolSlate):
def test_register_wait_for_button(self, cb=None):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertIsNone(request)
assert request is None
return NordicData([0x53, 0x00])
super().test_register_wait_for_button(cb or _cb)
def test_get_point_size(self, cb=None, pointsize=12):
def _cb(request, requires_reply=True, userdata=None, timeout=5):
self.assertEqual(request.opcode, 0xea)
self.assertEqual(request.length, 2)
self.assertEqual(request[0], 0x14)
assert request.opcode == 0xea
assert request.length == 2
assert request[0] == 0x14
ps = little_u32(pointsize)
return NordicData([0xeb, 6, 0x14, 0x00] + list(ps))
@ -564,8 +583,4 @@ class TestProtocolIntuosPro(TestProtocolSlate):
p = Protocol(self.protocol_version, callback=cb)
msg = p.execute(Interactions.GET_POINT_SIZE)
self.assertEqual(msg.point_size, pointsize - 1)
if __name__ == "__main__":
unittest.main(sys.argv[1:])
assert msg.point_size == pointsize - 1

View File

@ -18,8 +18,8 @@
#
import os
import pytest
import sys
import unittest
import xdg.BaseDirectory
from pathlib import Path
import yaml
@ -27,39 +27,54 @@ import logging
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.protocol import *
from tuhi.util import flatten
from tuhi.protocol import * # noqa
from tuhi.util import flatten # noqa
logger = logging.getLogger('tuhi') # piggyback the debug messages
logger.setLevel(logging.DEBUG)
class TestLogFiles(unittest.TestCase):
'''
Special test class that loads a yaml file created by tuhi compiles
the StrokeData from it. This class autogenerates its own tests, see
the main() handling.
'''
def load_pen_data(self, filename):
def pytest_generate_tests(metafunc):
# for any test function that takes a "logfile" argument return the list
# of all current logfiles in XDG_DATA_HOME/tuhi
# This means the test gets better the more logfiles are present on the
# user's machine.
if 'logfile' in metafunc.fixturenames:
basedir = Path(xdg.BaseDirectory.xdg_data_home) / 'tuhi'
def loads_and_has_data(filename):
with open(filename) as fd:
try:
yml = yaml.load(fd, Loader=yaml.Loader)
return yml is not None
except Exception as e:
logger.error(f'Exception triggered by file {filename}')
raise e
logfiles = [f for f in basedir.glob('**/raw/log-*.yaml') if loads_and_has_data(f)]
metafunc.parametrize('logfile', logfiles)
def test_log_files(logfile):
def load_pen_data(filename):
with open(filename) as fd:
yml = yaml.load(fd, Loader=yaml.Loader)
# all recv lists that have source PEN
pendata = [d['recv'] for d in yml['data'] if 'recv' in d and 'source' in d and d['source'] == 'PEN']
return list(flatten(pendata))
def _test_file(self, fname):
data = self.load_pen_data(fname)
if not data: # Recordings without Pen data can be skipped
raise unittest.SkipTest()
StrokeFile(data)
data = load_pen_data(logfile)
if not data: # Recordings without Pen data can be skipped
pytest.skip('Recording without pen data')
StrokeFile(data)
class TestStrokeParsers(unittest.TestCase):
class TestStrokeParsers(object):
def test_identify_file_header(self):
data = [0x67, 0x82, 0x69, 0x65]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.FILE_HEADER
data = [0x62, 0x38, 0x62, 0x74]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.FILE_HEADER
others = [
# with header
@ -77,54 +92,54 @@ class TestStrokeParsers(unittest.TestCase):
[0x62, 0x38, 0x62, 0x73],
]
for data in others:
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.FILE_HEADER, msg=data)
assert StrokeDataType.identify(data) != StrokeDataType.FILE_HEADER, data
def test_identify_stroke_header(self):
data = [0xff, 0xfa] # two bytes are enough to identify
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_HEADER
data = [0x3, 0xfa] # lowest bits set, not a correct packet but identify doesn't care
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_HEADER
data = [0xfc, 0xfa] # lowest bits unset, must be something else
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_HEADER)
assert StrokeDataType.identify(data) != StrokeDataType.STROKE_HEADER
def test_identify_stroke_point(self):
data = [0xff, 0xff, 0xff] # three bytes are enough to identify
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
data = [0xff, 0xff, 0xff, 1, 2, 3, 4, 5, 6]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
# wrong header, but observed in the wild
data = [0xbf, 0xff, 0xff, 1, 2, 3, 4, 5, 6]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) == StrokeDataType.POINT
data = [0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] # stroke end
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) != StrokeDataType.POINT
data = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] # EOF
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.POINT)
assert StrokeDataType.identify(data) != StrokeDataType.POINT
def test_identify_stroke_lost_point(self):
data = [0xff, 0xdd, 0xdd]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.LOST_POINT)
assert StrokeDataType.identify(data) == StrokeDataType.LOST_POINT
def test_identify_eof(self):
data = [0xff] * 9
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.EOF)
assert StrokeDataType.identify(data) == StrokeDataType.EOF
def test_identify_stroke_end(self):
data = [0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.STROKE_END)
assert StrokeDataType.identify(data) == StrokeDataType.STROKE_END
def test_identify_delta(self):
for i in range(256):
data = [i, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
if i & 0x3 == 0:
self.assertEqual(StrokeDataType.identify(data), StrokeDataType.DELTA, f'packet: {data}')
assert StrokeDataType.identify(data), StrokeDataType.DELTA == f'packet: {data}'
else:
self.assertNotEqual(StrokeDataType.identify(data), StrokeDataType.DELTA, f'packet: {data}')
assert StrokeDataType.identify(data), StrokeDataType.DELTA != f'packet: {data}'
def test_parse_stroke_header(self):
F_NEW_LAYER = 0x40
@ -134,81 +149,81 @@ class TestStrokeParsers(unittest.TestCase):
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 1
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# new layer off
flags = pen_type
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 0)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 0
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# pen type change
pen_type = 1
flags = F_NEW_LAYER | pen_type
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01]
packet = StrokeHeader(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 9
assert packet.is_new_layer == 1
assert packet.pen_id == 0
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
# with pen id
flags = F_NEW_LAYER | F_PEN_ID | pen_type
pen_id = [0xff, 0x0a, 0x87, 0x75, 0x80, 0x28, 0x42, 0x00, 0x10]
data = [0xff, 0xfa, flags, 0x1f, 0x73, 0x53, 0x5d, 0x2e, 0x01] + pen_id
packet = StrokeHeader(data)
self.assertEqual(packet.size, 18)
self.assertEqual(packet.is_new_layer, 1)
self.assertEqual(packet.pen_id, 0x100042288075870a)
self.assertEqual(packet.pen_type, pen_type)
self.assertEqual(packet.timestamp, 1565750047)
assert packet.size == 18
assert packet.is_new_layer == 1
assert packet.pen_id == 0x100042288075870a
assert packet.pen_type == pen_type
assert packet.timestamp == 1565750047
def test_parse_stroke_point(self):
# 0xff means 2 bytes each for abs coords
data = [0xff, 0xff, 0xff, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokePoint(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertEqual(packet.p, 0x0605)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == 9
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p == 0x0605
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
# 0xbf means: 1 byte for pressure delta, i.e. the 0x6 is skipped
data = [0xbf, 0xff, 0xff, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokePoint(data)
self.assertEqual(packet.size, 8)
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 0x5)
assert packet.size == 8
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p is None
assert packet.dx is None
assert packet.dy is None
assert packet.dp == 0x5
def test_parse_lost_point(self):
data = [0xff, 0xdd, 0xdd, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
packet = StrokeLostPoint(data)
self.assertEqual(packet.size, 9)
self.assertEqual(packet.nlost, 0x0201)
assert packet.size == 9
assert packet.nlost == 0x0201
def test_parse_eof(self):
data = [0xff] * 9
packet = StrokeEOF(data)
self.assertEqual(packet.size, 9)
assert packet.size == 9
data = [0xfc] + [0xff] * 6
packet = StrokeEOF(data)
self.assertEqual(packet.size, 7)
assert packet.size == 7
def test_parse_delta(self):
x_delta = 0b00001000 # noqa
@ -221,93 +236,93 @@ class TestStrokeParsers(unittest.TestCase):
flags = x_delta
data = [flags, 1]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 1)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 1
assert packet.dy is None
assert packet.dp is None
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = y_delta
data = [flags, 2]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.dx)
self.assertEqual(packet.dy, 2)
self.assertIsNone(packet.dp)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx is None
assert packet.dy == 2
assert packet.dp is None
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = p_delta
data = [flags, 3]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 3)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx is None
assert packet.dy is None
assert packet.dp == 3
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_delta | p_delta
data = [flags, 3, 5]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 3)
self.assertIsNone(packet.dy)
self.assertEqual(packet.dp, 5)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 3
assert packet.dy is None
assert packet.dp == 5
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_delta | y_delta | p_delta
data = [flags, 3, 5, 7]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.dx, 3)
self.assertEqual(packet.dy, 5)
self.assertEqual(packet.dp, 7)
self.assertIsNone(packet.x)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
assert packet.size == len(data)
assert packet.dx == 3
assert packet.dy == 5
assert packet.dp == 7
assert packet.x is None
assert packet.y is None
assert packet.p is None
flags = x_abs | y_abs | p_abs
data = [flags, 1, 2, 3, 4, 5, 6]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.x, 0x0201)
self.assertEqual(packet.y, 0x0403)
self.assertEqual(packet.p, 0x0605)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == len(data)
assert packet.x == 0x0201
assert packet.y == 0x0403
assert packet.p == 0x0605
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
flags = y_abs
data = [flags, 2, 3]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertIsNone(packet.x)
self.assertEqual(packet.y, 0x0302)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertIsNone(packet.dy)
self.assertIsNone(packet.dp)
assert packet.size == len(data)
assert packet.x is None
assert packet.y == 0x0302
assert packet.p is None
assert packet.dx is None
assert packet.dy is None
assert packet.dp is None
flags = x_abs | y_delta | p_delta
data = [flags, 2, 3, 4, 5]
packet = StrokeDelta(data)
self.assertEqual(packet.size, len(data))
self.assertEqual(packet.x, 0x0302)
self.assertIsNone(packet.y)
self.assertIsNone(packet.p)
self.assertIsNone(packet.dx)
self.assertEqual(packet.dy, 4)
self.assertEqual(packet.dp, 5)
assert packet.size == len(data)
assert packet.x == 0x0302
assert packet.y is None
assert packet.p is None
assert packet.dx is None
assert packet.dy == 4
assert packet.dp == 5
class TestStrokes(unittest.TestCase):
class TestStrokes(object):
def test_single_stroke(self):
data = '''
67 82 69 65 22 73 53 5d 00 00 02 00 00 00 00 00 ff fa c3 1f
@ -428,39 +443,3 @@ class TestStrokes(unittest.TestCase):
p = Protocol(ProtocolVersion.INTUOS_PRO, None, None)
p.parse_pen_data(b)
# How does this work?
# The test generater spits out a simple test function that just calls the
# real test function
#
# Then we search for all yaml files with logs we have and generate a unique
# test name (based on the timestamp) for that file. Result: we have
# a unittests function for each log file found in the directory.
def generator(logfile):
def test(self):
self._test_file(logfile)
return test
def search_for_tests():
basedir = Path(xdg.BaseDirectory.xdg_data_home) / 'tuhi'
for logfile in basedir.glob('**/raw/log-*.yaml'):
with open(logfile) as fd:
try:
yml = yaml.load(fd, Loader=yaml.Loader)
if not yml:
continue
timestamp = yml['time']
test_name = f'test_log_{timestamp}'
test = generator(logfile)
setattr(TestLogFiles, test_name, test)
except Exception as e:
logger.error(f'Exception triggered by file {logfile}')
raise e
search_for_tests()
if __name__ == '__main__':
unittest.main()

49
tools/exporter.py Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 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.
#
from pathlib import Path
import argparse
import json
import os
import sys
# This tool isn't installed, so we can assume that the tuhi module is always
# in the parent directory
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.export import JsonSvg, JsonPng
parser = argparse.ArgumentParser(description='Converter tool from Tuhi JSON files to SVG or PNG.')
parser.add_argument('filename', help='The JSON file to export ($HOME/.local/share/tuhi/*.json)')
parser.add_argument('--format',
help='The format to generate. Default: svg',
default='svg',
choices=['svg', 'png'])
parser.add_argument('--output',
type=str,
help='The output file name. Default: "$PWD/inputfile.suffix"',
default=None)
parser.add_argument('--orientation',
help='The orientation of the image',
default='landscape',
choices=['landscape', 'portrait', 'reverse-landscape', 'reverse-portrait'])
ns = parser.parse_args()
if ns.output is None:
ns.output = f"{Path(ns.filename).stem}.{ns.format}"
js = json.load(open(ns.filename))
if ns.format == 'svg':
JsonSvg(js, ns.orientation, ns.output)
elif ns.format == 'png':
JsonPng(js, ns.orientation, ns.output)

View File

@ -20,7 +20,6 @@ import errno
import os
import json
import logging
import re
import readline
import struct
import threading
@ -30,13 +29,15 @@ import configparser
from pathlib import Path
try:
from tuhi.svg import JsonSvg
from tuhi.export import JsonSvg, JsonPng
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
from tuhi.export import JsonSvg, JsonPng
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):
@ -502,7 +208,7 @@ class Searcher(Worker):
logger.error('Another client is already searching')
return
logger.debug(f'Starting searching')
logger.debug('Starting searching')
self.manager.start_search()
def stop(self):
@ -583,6 +289,7 @@ class Fetcher(Worker):
super(Fetcher, self).__init__(manager)
self.device = None
self.timestamps = None
self.format = args.format
address = args.address
index = args.index
@ -620,8 +327,12 @@ class Fetcher(Worker):
data = json.loads(jsondata)
t = time.localtime(data['timestamp'])
t = time.strftime('%Y-%m-%d-%H-%M', t)
path = f'{data["devicename"]}-{t}.svg'
JsonSvg(data, self.orientation, filename=path)
if self.format == 'png':
path = f'{data["devicename"]}-{t}.png'
JsonPng(data, self.orientation, filename=path)
else:
path = f'{data["devicename"]}-{t}.svg'
JsonSvg(data, self.orientation, filename=path)
logger.info(f'{data["devicename"]}: saved file "{path}"')
@ -743,7 +454,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 +586,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,13 +697,17 @@ 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}',
type=is_index_or_all,
const='all', nargs='?', default='all',
help='the index of the drawing to fetch or a literal "all"')
parser.add_argument('--format', metavar='{svg|png}',
default='svg',
choices=['svg', 'png'],
help='output file format')
try:
parsed_args = parser.parse_args(args.split())
@ -1083,7 +798,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 +852,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 +904,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 +961,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

@ -28,11 +28,11 @@ import logging
# This tool isn't installed, so we can assume that the tuhi module is always
# in the parent directory
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)) + '/..') # noqa
from tuhi.util import flatten
from tuhi.drawing import Drawing
from tuhi.protocol import StrokeFile
from tuhi.svg import JsonSvg
from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate
from tuhi.util import flatten # noqa
from tuhi.drawing import Drawing # noqa
from tuhi.protocol import StrokeFile # noqa
from tuhi.export import JsonSvg, JsonPng # noqa
from tuhi.wacom import WacomProtocolSpark, WacomProtocolIntuosPro, WacomProtocolSlate # noqa
logging.basicConfig(format='%(asctime)s %(levelname)s: %(name)s: %(message)s',
level=logging.INFO,
@ -68,6 +68,7 @@ def parse_file(filename, file_format, tablet_model, orientation):
# gotta convert to Drawings, then to json string, then to json, then
# to svg. ffs.
svgname = f'{stem}.svg'
pngname = f'{stem}.png'
jsonname = f'{stem}.json'
d = Drawing(svgname, (width * point_size, height * point_size), timestamp)
@ -85,10 +86,13 @@ def parse_file(filename, file_format, tablet_model, orientation):
with open(jsonname, 'w') as fd:
fd.write(d.to_json())
return
from io import StringIO
js = json.load(StringIO(d.to_json()))
JsonSvg(js, orientation, d.name)
else:
from io import StringIO
js = json.load(StringIO(d.to_json()))
if file_format == 'svg':
JsonSvg(js, orientation, d.name)
elif file_format == 'png':
JsonPng(js, orientation, pngname)
def fetch_files():
@ -142,7 +146,7 @@ def main(args=sys.argv):
parser.add_argument('--format',
help='The format to generate. Default: svg',
default='svg',
choices=['svg', 'json'])
choices=['svg', 'png', 'json'])
ns = parser.parse_args(args[1:])
if ns.verbose:

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}')
@ -178,30 +183,33 @@ def drop_privileges():
os.setresuid(uid, uid, uid)
pw = pwd.getpwuid(uid)
path = os.environ['PATH']
display = os.environ['DISPLAY']
# we completely clear the environment and start a new and controlled one
os.environ.clear()
os.environ['XDG_RUNTIME_DIR'] = f'/run/user/{uid}'
os.environ['HOME'] = pw.pw_dir
os.environ['PATH'] = path
os.environ['DISPLAY'] = display
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 +219,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

@ -21,11 +21,11 @@ from pathlib import Path
try:
from gi.repository import GObject, GLib
except Exception as e:
print(f'************ Importing gi.repository failed **********')
print(f'* This is an issue with the gi module, not with tuhi *')
print(f'******************************************************')
print(f'The full exception is below:')
print(f'')
print('************ Importing gi.repository failed **********')
print('* This is an issue with the gi module, not with tuhi *')
print('******************************************************')
print('The full exception is below:')
print('')
raise e
from tuhi.dbusserver import TuhiDBusServer
@ -37,7 +37,7 @@ DEFAULT_CONFIG_PATH = Path(xdg.BaseDirectory.xdg_data_home, 'tuhi')
logger = logging.getLogger('tuhi')
WACOM_COMPANY_IDS = [0x4755, 0x4157]
WACOM_COMPANY_IDS = [0x4755, 0x4157, 0x424d]
class TuhiDevice(GObject.Object):

View File

@ -206,11 +206,11 @@ class BlueZDevice(GObject.Object):
'''
i = self.obj.get_interface(ORG_BLUEZ_DEVICE1)
if self.connected:
self.logger.info(f'Device is already connected')
self.logger.info('Device is already connected')
self.emit('connected')
return
self.logger.info(f'Connecting')
self.logger.debug('Connecting')
i.Connect(result_handler=self._on_connect_result)
def _on_connect_result(self, obj, result, user_data):
@ -219,7 +219,7 @@ class BlueZDevice(GObject.Object):
result.code == Gio.IOErrorEnum.DBUS_ERROR and
Gio.dbus_error_get_remote_error(result) == 'org.bluez.Error.Failed' and
'Operation already in progress' in result.message):
self.logger.debug(f'Already connecting')
self.logger.debug('Already connecting')
elif isinstance(result, Exception):
self.logger.error(f'Connection failed: {result}')
@ -230,11 +230,11 @@ class BlueZDevice(GObject.Object):
'''
i = self.obj.get_interface(ORG_BLUEZ_DEVICE1)
if not i.get_cached_property('Connected').get_boolean():
self.logger.info(f'Device is already disconnected')
self.logger.info('Device is already disconnected')
self.emit('disconnected')
return
self.logger.info(f'Disconnecting')
self.logger.debug('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

@ -426,7 +426,7 @@ class TuhiDBusDevice(_TuhiDBus):
fds_list = message.get_unix_fd_list()
if fds_list is None or fds_list.get_length() != 1:
logger.error(f'uhid fds not provided')
logger.error('uhid fds not provided')
result = GLib.Variant.new_int32(-errno.EINVAL)
invocation.return_value(GLib.Variant.new_tuple(result))
return

160
tuhi/export.py Normal file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 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.
#
from gi.repository import GObject
import svgwrite
from svgwrite import mm
import cairo
class ImageExportBase(GObject.Object):
def __init__(self, json, orientation, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json = json
self.timestamp = json['timestamp']
self.filename = filename
self.orientation = orientation.lower()
self._convert()
@property
def output_dimensions(self):
dimensions = self.json['dimensions']
if dimensions == [0, 0]:
width, height = 100, 100
else:
# Original dimensions are too big for most Standards
# so we scale them down
width = dimensions[0] / self._output_scaling_factor
height = dimensions[1] / self._output_scaling_factor
if self.orientation in ['portrait', 'reverse-portrait']:
return height, width
else:
return width, height
@property
def output_strokes(self):
width, height = self.output_dimensions
strokes = []
for s in self.json['strokes']:
points_with_sk_width = []
for p in s['points']:
x, y = p['position']
# Scaling coordinates
x = x / self._output_scaling_factor
y = y / self._output_scaling_factor
if self.orientation == 'reverse-portrait':
x, y = y, height - x
elif self.orientation == 'portrait':
x, y = width - y, x
elif self.orientation == 'reverse-landscape':
x, y = width - x, height - y
# Pressure normalized range is [0, 0xffff]
delta = (p['pressure'] - 0x8000) / 0x8000
stroke_width = self._base_pen_width + self._pen_pressure_width_factor * delta
points_with_sk_width.append((x, y, stroke_width))
strokes.append(points_with_sk_width)
return strokes
class JsonSvg(ImageExportBase):
_output_scaling_factor = 1000
_base_pen_width = 0.4
_pen_pressure_width_factor = 0.2
# Change this value down to reduce size, change it up to improve accuracy. measured in px
_width_precision = 10
def _convert(self):
width, height = self.output_dimensions
size = width * mm, height * mm
# Make sure to set viewBox here so mm doesn't have to be specified in all later parts
svg = svgwrite.Drawing(filename=self.filename, size=size, viewBox=(f'0 0 {width} {height}'))
g = svgwrite.container.Group(id='layer0')
for sk_num, stroke_points in enumerate(self.output_strokes):
path = None
stroke_width_p = None
for i, (x, y, stroke_width) in enumerate(stroke_points):
if not x or not y:
continue
# Reduce precision of the width
stroke_width = int(stroke_width * self._width_precision) / self._width_precision
# Create a new path per object and per unique width
if stroke_width_p != stroke_width:
if path:
g.add(path)
# Reduce width by mm to px at 96dpi (see SVG/CSS specification)
width_px = stroke_width * 0.26458
path = svg.path(id=f'sk_{sk_num}_{i}', style=f'fill:none;stroke:black;stroke-width:{width_px}')
stroke_width_p = stroke_width
path.push("M", f'{x:.2f}', f'{y:.2f}')
else:
# Continue writing segment line with next coords
path.push("L", f'{x:.2f}', f'{y:.2f}')
if path:
g.add(path)
svg.add(g)
svg.save()
class JsonPng(ImageExportBase):
_output_scaling_factor = 100
_base_pen_width = 3
_pen_pressure_width_factor = 1
def _convert(self):
width, height = self.output_dimensions
width, height = int(width), int(height)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
# Paint a transparent background
ctx.set_source_rgba(0, 0, 0, 0)
ctx.paint()
ctx.set_antialias(cairo.Antialias.DEFAULT)
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
ctx.set_source_rgb(0, 0, 0)
for sk_num, stroke_points in enumerate(self.output_strokes):
for i, (x, y, stroke_width) in enumerate(stroke_points):
ctx.set_line_width(stroke_width)
if i == 0:
ctx.move_to(x, y)
else:
ctx.line_to(x, y)
ctx.stroke()
surface.write_to_png(self.filename)

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

@ -46,7 +46,7 @@ class Config(GObject.Object):
if not self.path.exists():
return
logger.debug(f'configuration found')
logger.debug('configuration found')
self.config.read(self.path)
def _load_cached_drawings(self):
@ -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,18 +12,21 @@
#
from gettext import gettext as _
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk
import xdg.BaseDirectory
import os
from pathlib import Path
from .config import Config
from tuhi.svg import JsonSvg
from tuhi.export import JsonSvg, JsonPng
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')
DATA_PATH = Path(xdg.BaseDirectory.xdg_cache_home, 'tuhi')
SVG_DATA_PATH = Path(DATA_PATH, 'svg')
PNG_DATA_PATH = Path(DATA_PATH, 'png')
@Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/Drawing.ui')
@ -39,7 +42,8 @@ class Drawing(Gtk.EventBox):
super().__init__()
self.orientation = Config().orientation
Config().connect('notify::orientation', self._on_orientation_changed)
DATA_PATH.mkdir(parents=True, exist_ok=True)
SVG_DATA_PATH.mkdir(parents=True, exist_ok=True)
PNG_DATA_PATH.mkdir(parents=True, exist_ok=True)
self.json_data = json_data
self._zoom = zoom
@ -55,8 +59,12 @@ class Drawing(Gtk.EventBox):
self.redraw()
def process_svg(self):
path = os.fspath(Path(DATA_PATH, f'{self.json_data["timestamp"]}.svg'))
self.svg = JsonSvg(self.json_data, self.orientation, path)
path = os.fspath(Path(SVG_DATA_PATH, f'{self.json_data["timestamp"]}.svg'))
self.svg = JsonSvg(
self.json_data,
self.orientation,
path
)
width, height = -1, -1
if 'portrait' in self.orientation:
height = 1000
@ -67,6 +75,14 @@ class Drawing(Gtk.EventBox):
height=height,
preserve_aspect_ratio=True)
def process_png(self):
path = os.fspath(Path(PNG_DATA_PATH, f'{self.json_data["timestamp"]}.png'))
self.png = JsonPng(
self.json_data,
self.orientation,
path
)
def redraw(self):
ratio = self.pixbuf.get_height() / self.pixbuf.get_width()
base = 250 + self.zoom * 50
@ -96,11 +112,9 @@ class Drawing(Gtk.EventBox):
@Gtk.Template.Callback('_on_download_button_clicked')
def _on_download_button_clicked(self, button):
dialog = Gtk.FileChooserDialog(_('Please choose a file'),
None,
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
dialog = Gtk.FileChooserNative()
dialog.set_action(Gtk.FileChooserAction.SAVE)
dialog.set_transient_for(self.get_toplevel())
dialog.set_do_overwrite_confirmation(True)
# Translators: the default filename to save to
@ -114,21 +128,33 @@ class Drawing(Gtk.EventBox):
# Translators: filter to show svg files only
filter_svg.set_name(_('SVG files'))
filter_svg.add_pattern('*.svg')
filter_png = Gtk.FileFilter()
# Translators: filter to show png files only
filter_png.set_name(_('PNG files'))
filter_png.add_pattern('*.png')
dialog.add_filter(filter_svg)
dialog.add_filter(filter_png)
dialog.add_filter(filter_any)
response = dialog.run()
if response == Gtk.ResponseType.OK:
if response == Gtk.ResponseType.ACCEPT:
import shutil
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
file = dialog.get_filename()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling
if file.lower().endswith('.png'):
# regenerate the PNG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_png()
shutil.move(self.png.filename, file)
else:
# regenerate the SVG based on the current rotation.
# where we used the orientation buttons, we haven't updated the
# file itself.
self.process_svg()
shutil.copyfile(self.svg.filename, file)
# FIXME: error handling
dialog.destroy()

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()
@ -74,7 +74,13 @@ class DrawingPerspective(Gtk.Stack):
super().__init__(*args, **kwargs)
self.known_drawings = {} # type {timestamp: Drawing()}
self.flowboxes = {}
# Add an expanding emtpy label to the bottom - this pushes all the
# real stuff up to the top, forcing a nice alignment
fake_label = Gtk.Label("")
fake_label.show()
self.box_all_drawings.pack_end(fake_label, expand=True, fill=True, padding=100)
self._zoom = 0
self._want_listen = True
def _cache_drawings(self, device, pspec):
# The config backend filters duplicates anyway, so don't care here
@ -86,7 +92,7 @@ class DrawingPerspective(Gtk.Stack):
def _hash(drawing):
return time.strftime('%Y%m', time.gmtime(drawing.timestamp))
for js in sorted(config.drawings, reverse=True, key=lambda j: j['timestamp']):
for js in sorted(config.drawings, key=lambda j: j['timestamp']):
ts = js['timestamp']
if ts in self.known_drawings:
continue
@ -101,7 +107,7 @@ class DrawingPerspective(Gtk.Stack):
except KeyError:
fb = Flowbox(time.gmtime(drawing.timestamp))
self.flowboxes[key] = fb
self.box_all_drawings.add(fb)
self.box_all_drawings.pack_end(fb, expand=False, fill=True, padding=0)
finally:
fb.insert(drawing)
@ -126,8 +132,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 +179,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)
@ -167,7 +137,7 @@ class MainWindow(Gtk.ApplicationWindow):
dp = DrawingPerspective()
self._add_perspective(dp)
self.headerbar.set_title(f'Tuhi')
self.headerbar.set_title('Tuhi')
self.stack_perspectives.set_visible_child_name(dp.name)
if not self._tuhi.devices:
@ -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):
@ -649,10 +651,18 @@ class MsgGetName(Msg):
opcode = 0xbb
protocol = ProtocolVersion.ANY
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = ""
def _handle_reply(self, reply):
if reply.opcode != 0xbc:
raise UnexpectedReply(f'Unknown reply: {reply.opcode}')
self.name = bytes(reply).decode('utf-8')
self.name += bytes(reply).decode('utf-8')
if bytes(reply)[-1] != 0x0a:
self.requires_request = False
self.execute()
self.requires_request = True
class MsgGetNameIntuosPro(Msg):
@ -1374,16 +1384,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()
@ -1609,7 +1616,7 @@ class StrokeFile(object):
points.append(last_point)
else:
# should never get here
raise StrokeParsingError(f'Failed to parse', data[:16])
raise StrokeParsingError('Failed to parse', data[:16])
logger.debug(f'Offset {consumed}: {packet}')
consumed += packet.size
@ -1636,7 +1643,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)}'
@ -1670,9 +1677,9 @@ class StrokeFileHeader(StrokePacket):
func = file_formats[key]
func(data)
except KeyError:
raise StrokeParsingError(f'Unknown file format:', data[:4])
raise StrokeParsingError('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}'
@ -1719,7 +1726,7 @@ class StrokeHeader(StrokePacket):
elif payload[0:3] == [0xff, 0xee, 0xee]:
self._parse_slate(data, header, payload)
else:
raise StrokeParsingError(f'Invalid StrokeHeader, expected ff fa or ff ee.', data[:8])
raise StrokeParsingError('Invalid StrokeHeader, expected ff fa or ff ee.', data[:8])
def _parse_slate(self, data, header, payload):
self.pen_id = 0
@ -1747,21 +1754,22 @@ class StrokeHeader(StrokePacket):
# if the pen id flag is set, the pen ID comes in the next 8-byte
# packet (plus 0xff header)
if needs_pen_id:
pen_packet = data[self.size + 1:]
pen_packet = data[self.size:] # header + 8 bytes payload
if not pen_packet:
raise StrokeParsingError('Missing pen ID packet')
header = data[0]
if header != 0xff:
raise StrokeParsingError(f'Unexpected pen id packet header: {header}.', data[:9])
pen_header = pen_packet[0]
if pen_header != 0xff:
raise StrokeParsingError(f'Unexpected pen id packet header: {pen_header}.', pen_packet[:9])
nbytes = bin(header).count('1')
self.pen_id = little_u64(pen_packet[:8])
pen_payload = pen_packet[1:]
nbytes = bin(pen_header).count('1')
self.pen_id = little_u64(pen_payload[: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))
t = time.strftime('%y%m%d%H%M%S', time.gmtime(self.timestamp))
else:
t = time.strftime(f'boot+{self.time_offset/1000}s')
return f'StrokeHeader: time: {t} new layer: {self.is_new_layer}, pen type: {self.pen_type}, pen id: {self.pen_id:#x}'
@ -1815,7 +1823,7 @@ class StrokeDelta(object):
# 8 bit delta
delta = int.from_bytes(bytes([databytes[0]]), byteorder='little', signed=True)
if delta == 0:
raise StrokeParsingError(f'StrokeDelta: invalid delta of zero', data)
raise StrokeParsingError('StrokeDelta: invalid delta of zero', data)
assert delta != 0
size = 1
elif mask == 3:
@ -1825,7 +1833,7 @@ class StrokeDelta(object):
return value, delta, size
if (data[0] & 0b11) != 0:
raise NotImplementedError(f'LSB two bits set in mask - this is not supposed to happen')
raise NotImplementedError('LSB two bits set in mask - this is not supposed to happen')
xmask = (data[0] & 0b00001100) >> 2
ymask = (data[0] & 0b00110000) >> 4
@ -1849,7 +1857,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 \
@ -1870,7 +1878,7 @@ class StrokePoint(StrokeDelta):
header = data[0]
payload = data[1:]
if payload[:2] != [0xff, 0xff]:
raise StrokeParsingError(f'Invalid StrokePoint, expected ff ff ff', data[:9])
raise StrokeParsingError('Invalid StrokePoint, expected ff ff ff', data[:9])
# This is a wrapper around StrokeDelta which does the mask parsing.
# In theory the StrokePoint would be a separate packet but it
@ -1888,7 +1896,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}'
@ -1898,7 +1906,7 @@ class StrokeEOF(StrokePacket):
payload = data[1:]
nbytes = bin(header).count('1')
if payload[:nbytes] != [0xff] * nbytes:
raise StrokeParsingError(f'Invalid EOF, expected 0xff only', data[:9])
raise StrokeParsingError('Invalid EOF, expected 0xff only', data[:9])
self.size = nbytes + 1
@ -1908,11 +1916,11 @@ class StrokeEndOfStroke(StrokePacket):
payload = data[1:]
nbytes = bin(header).count('1')
if payload[:nbytes] != [0xff] * nbytes:
raise StrokeParsingError(f'Invalid EndOfStroke, expected 0xff only', data[:9])
raise StrokeParsingError('Invalid EndOfStroke, expected 0xff only', data[:9])
self.size = nbytes + 1
self.data = data[:self.size]
def __repr__(self):
def __str__(self):
return f'EndOfStroke: {list2hex(self.data)}'
@ -1929,6 +1937,6 @@ class StrokeLostPoint(StrokePacket):
header = data[0]
payload = data[1:]
if payload[:2] != [0xdd, 0xdd]:
raise StrokeParsingError(f'Invalid StrokeLostPoint, expected ff dd dd', data[:9])
raise StrokeParsingError('Invalid StrokeLostPoint, expected ff dd dd', data[:9])
self.nlost = little_u16(payload[2:4])
self.size = bin(header).count('1') + 1

View File

@ -1,82 +0,0 @@
#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 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.
#
from gi.repository import GObject
import svgwrite
from svgwrite import mm
class JsonSvg(GObject.Object):
def __init__(self, json, orientation, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json = json
self.timestamp = json['timestamp']
self.filename = filename
self.orientation = orientation.lower()
self._convert()
def _convert(self):
js = self.json
dimensions = js['dimensions']
if dimensions == [0, 0]:
width, height = 100, 100
else:
# Original dimensions are too big for SVG Standard
# so we normalize them
width, height = dimensions[0] / 1000, dimensions[1] / 1000
if self.orientation in ['portrait', 'reverse-portrait']:
size = (height * mm, width * mm)
else:
size = (width * mm, height * mm)
svg = svgwrite.Drawing(filename=self.filename, size=size)
g = svgwrite.container.Group(id='layer0')
for stroke_num, s in enumerate(js['strokes']):
points_with_sk_width = []
for p in s['points']:
x, y = p['position']
# Normalize coordinates too
x, y = x / 1000, y / 1000
if self.orientation == 'reverse-portrait':
x, y = y, width - x
elif self.orientation == 'portrait':
x, y = height - y, x
elif self.orientation == 'reverse-landscape':
x, y = width - x, height - y
# Pressure normalized range is [0, 0xffff]
delta = (p['pressure'] - 0x8000) / 0x8000
stroke_width = 0.4 + 0.20 * delta
points_with_sk_width.append((x, y, stroke_width))
lines = svgwrite.container.Group(id=f'strokes_{stroke_num}', stroke='black')
for i, (x, y, stroke_width) in enumerate(points_with_sk_width):
if i != 0:
xp, yp, stroke_width_p = points_with_sk_width[i - 1]
lines.add(
svg.line(
start=(xp * mm, yp * mm),
end=(x * mm, y * mm),
stroke_width=stroke_width,
style='fill:none'
)
)
g.add(lines)
svg.add(g)
svg.save()

View File

@ -12,13 +12,13 @@
#
def list2hex(l, groupsize=8):
def list2hex(lst, groupsize=8):
'''Converts a list of integers to a two-letter hex string in the form
"1a 2b c3"'''
slices = []
for idx in range(0, len(l), groupsize):
s = ' '.join([f'{x:02x}' for x in l[idx:idx + groupsize]])
for idx in range(0, len(lst), groupsize):
s = ' '.join([f'{x:02x}' for x in lst[idx:idx + groupsize]])
slices.append(s)
return ' '.join(slices)

View File

@ -113,10 +113,10 @@ def b2hex(bs):
return ' '.join([''.join(s) for s in zip(hx[::2], hx[1::2])])
def list2hexlist(l):
def list2hexlist(lst):
'''Converts a list of integers to a two-letter prefixed hex string in the form
"[0x1a, 0x32, 0xab]"'''
return '[' + ', '.join([f'{x:#04x}' for x in l]) + ']'
return '[' + ', '.join([f'{x:#04x}' for x in lst]) + ']'
class DataLogger(object):
@ -204,7 +204,7 @@ class DataLogger(object):
self.logfile.write(f'name: {self.device.name}\n')
self.logfile.write(f'bluetooth: {self.btaddr}\n')
self.logfile.write(f'time: {timestamp} # host time: {time.strftime("%Y-%m-%d %H:%M:%S")}\n')
self.logfile.write(f'data:\n')
self.logfile.write('data:\n')
def _close_file(self):
if self.logfile is None:
@ -268,78 +268,6 @@ class DataLogger(object):
self._in_context = False
class WacomPacket(GObject.Object):
'''
A single protocol packet of variable length. The protocol format is a
single-byte bitmask followed by up to 8 bytes (depending on the number
of 1-bits in the bitmask). Each byte represents the matching bit in the
bitmask, i.e. the data is non-sparse.
If the bitmask has 0x1 and/or 0x2 set, those two bytes make up the
opcode of the command. So the possible layouts are:
| bitmask | opcode1 | opcode2 | payload ...
| bitmask | opcode1 | payload ...
| bitmask | opcode2 | payload ...
| bitmask | payload
On most normal packets containing motion data, the opcode is not
present.
Attributes:
bitmask .. single byte with a bitmask denoting the contents
opcode ... the 16-bit opcode or None for 'special' packets. Note that
the opcode is converted into an integer from the
little-endian protocol format
bytes .... a list of the payload bytes as sent by the device. This is
a non-sparse list matching the number of set bits in the
bitmask. it does not include the bitmask.
args ..... a sparse list of the payload bytes, expanded to match the
bitmask so that args[x] is the value for each bit x in
bitmask. it does not include the bitmask.
length ... length of the packet in bytes, including bitmask
'''
def __init__(self, data):
self.bitmask = data[0]
nbytes = bin(self.bitmask).count('1')
self.bytes = data[1:1 + nbytes]
self.length = nbytes + 1 # for the bitmask
idx = 0
# 2-byte opcode, but only if the bitmask is set for either byte
opcode = 0
if self.bitmask & 0x1:
opcode |= self.bytes[idx]
idx += 1
if self.bitmask & 0x2:
opcode |= self.bytes[idx] << 8
idx += 1
self.opcode = opcode if opcode else None
self.args = []
vals = self.bytes.copy()
mask = self.bitmask
while mask != 0:
self.args.append(vals.pop(0) if mask & 0x1 else 0x00)
mask >>= 1
def __repr__(self):
debug_data = []
debug_data.append(f'{self.bitmask:02x} ({self.bitmask:08b}) |')
if self.opcode:
debug_data.append(f'{self.opcode:04x} |')
else:
debug_data.append(f' |')
for i in range(2, 8): # start at 2 to skip the opcode
if self.bitmask & (1 << i):
debug_data.append(f'{self.args[i]:02x}')
else:
debug_data.append(' ')
return " ".join(debug_data)
class WacomProtocolLowLevelComm(GObject.Object):
'''
Internal class to handle the communication with the Wacom device.
@ -423,6 +351,13 @@ class WacomRegisterHelper(WacomProtocolLowLevelComm):
except AuthorizationError:
# this is expected
pass
except Exception as e:
logger.exception('Got other Exception while registering Spark device')
if e.errorcode == DeviceError.ErrorCode.GENERAL_ERROR:
logger.debug('Got GENERAL_ERROR while registering Spark device')
pass
else:
raise
# The "press button now command" on the spark
self.p.execute(Interactions.REGISTER_PRESS_BUTTON)
@ -479,7 +414,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)
@ -506,7 +441,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
data = value[2:]
while data:
if bytes(data) == b'\xff\xff\xff\xff\xff\xff':
logger.debug(f'Pen left proximity')
logger.debug('Pen left proximity')
if self._uhid_device is not None:
self._uhid_device.call_input_event([1, 0, 0, 0, 0, 0, 0, 0])
@ -550,7 +485,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
tdelta = time.mktime(time.gmtime()) - time.mktime(t)
if abs(tdelta) > 300:
logger.error(f'device time is out by more than 5 minutes')
logger.error('device time is out by more than 5 minutes')
def get_battery_info(self):
msg = self.p.execute(Interactions.GET_BATTERY)
@ -717,7 +652,7 @@ class WacomProtocolBase(WacomProtocolLowLevelComm):
self.emit('drawing', drawing)
file_count -= 1
if TuhiConfig().peek_at_drawing:
logger.info(f'Not deleting drawing from device')
logger.info('Not deleting drawing from device')
if file_count > 0:
logger.info(f'{file_count} more files on device but I can only download the oldest one')
break
@ -805,12 +740,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)
@ -950,7 +886,7 @@ class WacomDevice(GObject.Object):
protocol = ProtocolVersion.from_string(self._config['Protocol'])
self._init_protocol(protocol)
except (KeyError, ValueError):
logger.error(f'Missing or invalid Protocol entry in config file. Treating this device as unregistered')
logger.error('Missing or invalid Protocol entry in config file. Treating this device as unregistered')
self._uuid = None
def _init_protocol(self, protocol):
@ -1027,7 +963,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:
@ -1044,7 +980,7 @@ class WacomDevice(GObject.Object):
logger.error(f'**** Exception: {e} ****')
exception = e
except AuthorizationError as e:
logger.error(f'Authorization failed, device needs to be re-registered')
logger.error('Authorization failed, device needs to be re-registered')
exception = e
finally:
self.sync_state = 0