mirror of https://github.com/tuhiproject/tuhi.git
commit
3e44e28707
|
@ -3,4 +3,3 @@ flatpak*
|
|||
tuhi.egg-info
|
||||
__pycache__
|
||||
*.swp
|
||||
*.svg
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
DBus Interface
|
||||
--------------
|
||||
|
||||
Tuhi has two main components. A DBus session daemon that handles
|
||||
communication with the device and a GTK application that provides the
|
||||
graphical user interface.
|
||||
|
||||
|
||||
The DBus session daemon uses the following interface:
|
||||
|
||||
```
|
||||
org.freedesktop.tuhi1.Manager
|
||||
|
||||
Property: Devices (ao)
|
||||
Array of object paths to known (previously registered, but not necessarily
|
||||
connected) devices. Note that a "registered" device is one that has been
|
||||
initialized via the Wacom SmartPad custom protocol. A device does not
|
||||
need to be paired over Bluetooth to register.
|
||||
|
||||
Property: Searching (b)
|
||||
Indicates whether the daemon is currently searching for devices.
|
||||
|
||||
This property is set to True when a StartSearching() request initiates
|
||||
the search for device connections. When the StartSearching() request
|
||||
completes upon timeout, or when StopSearching() is called, the property
|
||||
is set to False.
|
||||
|
||||
When a pariable device is found, the UnregisteredDevice signal is sent to
|
||||
the caller that initiated the search process.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: JSONDataVersions (au)
|
||||
Specifies the JSON file format versions the server supports. The
|
||||
client must request one of these versions in Device.GetJSONData().
|
||||
|
||||
Read-only, constant
|
||||
|
||||
Method: StartSearch() -> ()
|
||||
Start searching for available devices ready for registering
|
||||
for an unspecified timeout. When the timeout expires or an error
|
||||
occurs, a SearchStopped signal is sent indicating success or error.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartSearching() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
Method: StopSearch() -> ()
|
||||
Stop listening to available devices ready for registering. If called after
|
||||
StartSearch() and before a SearchStopped signal has been received,
|
||||
this method triggers the SearchStopped signal. That signal indicates
|
||||
success or an error.
|
||||
|
||||
If this method is called before StartSearch() or after the
|
||||
SearchStopped signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopSearch() and the SearchStopped signal
|
||||
arriving, UnregisteredDevice signals may still arrive.
|
||||
|
||||
Signal: UnregisteredDevice(o)
|
||||
Indicates that a device can be registered. This signal may be
|
||||
sent after a StartSearch() call and before SearchStopped(). This
|
||||
signal is sent once per available device and only to the client that
|
||||
initiated the search process with StartSearch.
|
||||
|
||||
When this signal is sent, a org.freedesktop.tuhi1.Device object was
|
||||
created, the object path is the argument to this signal.
|
||||
|
||||
A client must immediately call Register() on that object if
|
||||
registering with that object is desired. See the documentation for
|
||||
that interface for details.
|
||||
|
||||
When the search timeout expires, the device may be removed by the
|
||||
daemon again. Note that until the device is registered, the device is not
|
||||
listed in the managers Devices property.
|
||||
|
||||
Signal: SearchStopped(i)
|
||||
Sent when the search has stopped. An argument of 0 indicates a
|
||||
successful termination of the search process, either when a device
|
||||
has been registered or the timeout expired.
|
||||
|
||||
If the errno is -EAGAIN, the daemon is already searching for devices
|
||||
on behalf of another client. In this case, this client should wait for
|
||||
the Searching property to change and StartSearching() once the
|
||||
property is set to False.
|
||||
|
||||
Once this signal has been sent, all devices announced through
|
||||
UnregisteredDevice signals should be considered invalidated. Attempting to
|
||||
Register() one of the devices after the SearchStopped() signal may result
|
||||
in an error.
|
||||
|
||||
In case of error, the argument is a negative errno.
|
||||
|
||||
org.freedesktop.tuhi1.Device
|
||||
|
||||
Interface to a device known by tuhi. Each object in Manager.Devices
|
||||
implements this interface.
|
||||
|
||||
Property: BlueZDevice (o)
|
||||
Object path to the org.bluez.Device1 device that is this device.
|
||||
Read-only
|
||||
|
||||
Property: Dimensions (uu)
|
||||
The physical dimensions (width, height) in µm
|
||||
Read-only
|
||||
|
||||
Property: BatteryPercent (u)
|
||||
The last known battery charge level in percent. This charge level is
|
||||
only accurate when the BatteryState is other than Unknown.
|
||||
|
||||
When the BatteryState is Unknown and BatteryPercent is nonzero, the
|
||||
value is the last known percentage value.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: BatteryState (u)
|
||||
An enum describing the battery state. Permitted enum values are
|
||||
|
||||
0: Unknown
|
||||
1: Charging
|
||||
2: Discharging
|
||||
|
||||
'Unknown' may refer to a state that could not be read, a state
|
||||
that has not yet been updated, or a state that has not updated within
|
||||
a daemon-internal time period. Thus, a device that is connected but
|
||||
does not regularly send battery updates may eventually switch to
|
||||
'Unknown'.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: DrawingsAvailable (at)
|
||||
An array of timestamps of the available drawings. The timestamp of
|
||||
each drawing can be used as argument to GetJSONData(). Timestamps are
|
||||
in seconds since the Epoch and may be used to display information to
|
||||
the user or sort data.
|
||||
Read-only
|
||||
|
||||
Property: Listening (b)
|
||||
Indicates whether the daemon is currently listening for the device.
|
||||
|
||||
This property is set to True when a StartListening() request initiates
|
||||
the search for device connections. When the StartListening() request
|
||||
completes upon timeout, or when StopListening() is called, the property
|
||||
is set to False.
|
||||
|
||||
When the user press the button on the device, the daemon connects
|
||||
to the device, downloads all drawings from the device and disconnects
|
||||
from the device.
|
||||
|
||||
If successfull, the drawings are deleted from the device. The data is
|
||||
held by the daemon in non-persistent storage until the daemon is stopped
|
||||
or we run out of memory, whichever happens earlier.
|
||||
Use GetJSONData() to retrieve the data from the daemon.
|
||||
|
||||
DO NOT RELY ON THE DAEMON FOR PERMANENT STORAGE
|
||||
|
||||
When drawings become available from the device, the DrawingsAvailable
|
||||
property updates to the number of available drawings.
|
||||
When the button is pressed multiple times, any new data is appended
|
||||
to the existing list of drawings as long as this property is True.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: Live(b)
|
||||
Indicates whether the device is currently in Live mode. When in live
|
||||
mode, the device does not store drawings internally for a later sync
|
||||
but instead fowards the events immediately, similar to a traditional
|
||||
graphics tablet. See StartLive() for more details.
|
||||
|
||||
Read-only
|
||||
|
||||
Method: Register() -> (i)
|
||||
Register the device. If the device is already registered, calls to
|
||||
this method immediately return success.
|
||||
|
||||
Otherwise, the device is registered and this function returns success (0)
|
||||
or a negative errno on failure.
|
||||
|
||||
Method: StartListening() -> ()
|
||||
Listen for data from this device and connect to the device when it
|
||||
becomes available. The daemon listens to the device until the client
|
||||
calls StopListening() or the client disconnects, whichever happens
|
||||
earlier.
|
||||
|
||||
The ListeningStopped signal is sent when the listening terminates,
|
||||
either on success or with an error. A client should handle this signal
|
||||
to be notified of any errors.
|
||||
|
||||
When the daemon starts listening, the Listening property is updated
|
||||
accordingly.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartListening() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
Method: StopListening() -> ()
|
||||
Stop listening for data on this device. If called after
|
||||
StartListening(), this method triggers the ListenStopped signal.
|
||||
That signal indicates success or an error.
|
||||
|
||||
If this method is called before StartListening() or after the
|
||||
ListeningStopped signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopListening() and the ListeningStopped
|
||||
signal arriving, the property DrawingsAvailable may still be updated
|
||||
and it's the responsibility of the client to fetch the JSON data.
|
||||
|
||||
Method: StartLive(fd: h) -> (i)
|
||||
Starts live mode on this device. This disables offline storage of
|
||||
drawing data on the device and instead switches the device to a mode
|
||||
where it immediately reports the pen data, similar to a traditional
|
||||
graphics tablet.
|
||||
|
||||
The LiveStopped signal is sent when live mode terminates, either on
|
||||
success or with an error. A client should handle this signal to be
|
||||
notified of any errors.
|
||||
|
||||
When live mode enables, the Live property is updated accordingly.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartListening() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
The fd argument is a file descriptor that will be used to forward
|
||||
events to. The format is the one used by the Linux kernel's UHID
|
||||
device, see linux/uhid.h for details.
|
||||
|
||||
Method: StopLive() - >()
|
||||
Stop live mode on this device. If called after StartLive(), this
|
||||
method triggers the LiveStopped signal. That signal indicates
|
||||
success or an error.
|
||||
|
||||
If this method is called before StartLive() or after the LiveStopped
|
||||
signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopLive() and the LiveStopped signal
|
||||
arriving, the device may still send events. It's the responsibility of
|
||||
the client to handle events until the LiveStopped signal arrives.
|
||||
|
||||
Method: GetJSONData(file-version: u, timestamp: t) -> (s)
|
||||
Returns a JSON file with the drawings specified by the timestamp
|
||||
argument. The requested timestamp must be one of the entries in the
|
||||
DrawingsAvailable property value. The file-version argument specifies
|
||||
the file format version the client requests. See section JSON FILE
|
||||
FORMAT for the format of the returned data.
|
||||
|
||||
Returns a string representing the JSON data from the last drawings or
|
||||
the empty string if the timestamp is not available or the file format
|
||||
version is outside the server-supported range advertised in
|
||||
Manager.JSONDataVersions.
|
||||
|
||||
Signal: ButtonPressRequired()
|
||||
Sent when the user is expected to press the physical button on the
|
||||
device. A client should display a notification in response, if the
|
||||
user does not press the button during the (firmware-specific) timeout
|
||||
the current operation will fail.
|
||||
|
||||
Signal: ListeningStopped(i)
|
||||
Sent when the listen process has stopped. An argument of 0 indicates a
|
||||
successful termination, i.e. in response to the client calling
|
||||
StopListening(). Otherwise, the argument is a negative errno
|
||||
indicating the type of error.
|
||||
|
||||
If the errno is -EAGAIN, the daemon is already listening to the device
|
||||
on behalf of another client. In this case, this client should wait for
|
||||
the Listening property to change and StartListening() once the
|
||||
property is set to False.
|
||||
|
||||
If the error is -EBADE, the device is not ready for registering/in
|
||||
listening mode and registration/listening was requested. In
|
||||
this case, the client should indicate to the user that the device
|
||||
needs to be registered first or switched to listening mode.
|
||||
|
||||
If the error is -EACCES, the device is not registered with the daemon
|
||||
or incorrectly registered. This may happen when the device was
|
||||
registered with another host since the last connection.
|
||||
|
||||
The following other errnos may be sent by the daemon:
|
||||
-EPROTO: the daemon has encountered a protocol error with the device.
|
||||
-ETIME: timeout while communicating with the device.
|
||||
|
||||
These errnos indicate a bug in the daemon, and the client should
|
||||
display a message to that effect.
|
||||
|
||||
Signal: LiveStopped(i)
|
||||
Sent when live mode is stopped. An argument of 0 indicates a
|
||||
successful termination, i.e. in response to the client calling
|
||||
StopLive(). Otherwise, the argument is a negative errno
|
||||
indicating the type of error.
|
||||
|
||||
If the errno is -EAGAIN, the daemon has already enabled live mode on
|
||||
device on behalf of another client. In this case, this client should
|
||||
wait for the Live property to change and StartLive() once the property
|
||||
is set to False.
|
||||
|
||||
If the error is -EBADE, the device is not ready for live mode, most
|
||||
likely because it is in registration mode. In this case, the client
|
||||
should indicate to the user that the device needs to be registered
|
||||
first.
|
||||
|
||||
If the error is -EACCES, the device is not registered with the daemon
|
||||
or incorrectly registered. This may happen when the device was
|
||||
registered with another host since the last connection.
|
||||
|
||||
The following other errnos may be sent by the daemon:
|
||||
-EPROTO: the daemon has encountered a protocol error with the device.
|
||||
-ETIME: timeout while communicating with the device.
|
||||
|
||||
These errnos indicate a bug in the daemon, and the client should
|
||||
display a message to that effect.
|
||||
|
||||
Signal: SyncState(i)
|
||||
An enum to represent the current synchronization state of the device.
|
||||
When on (1), Tuhi is currently trying to download data from the
|
||||
device. When off (0), Tuhi is not currently connecting to the device.
|
||||
|
||||
This signal should be used for UI feedback.
|
||||
|
||||
This signal is only send when the device is **not** in Live mode.
|
||||
```
|
||||
|
||||
JSON File Format
|
||||
----------------
|
||||
|
||||
The current file format version is 1. A server may only support a subset of
|
||||
historical file formats, this subset is advertized as list of versions in
|
||||
the **org.freedesktop.tuhi1.Manager.JSONDataVersions** property. Likewise, a
|
||||
client may only support a subset of the possible formats. A client should
|
||||
always pick the highest format supported by both the client and the server.
|
||||
|
||||
Below is the example file format (with comments, not present in the real
|
||||
files). The JSON objects are "drawing" (the root object), "strokes",
|
||||
"points". Pseudo-code is used to illustrate the objects in the file.
|
||||
|
||||
```
|
||||
class Drawing {
|
||||
version: uint32
|
||||
devicename: string
|
||||
dimensions: [uint32, uint32] // x/y physical dimensions in µm
|
||||
timestamp: uint64
|
||||
strokes: [ Stroke, Stroke, ...]
|
||||
}
|
||||
```
|
||||
|
||||
The **strokes** list contains all strokes of a single drawing, each stroke
|
||||
consisting of a number of **points**.
|
||||
|
||||
```
|
||||
class Stroke {
|
||||
points: [Point, Point, ...]
|
||||
}
|
||||
```
|
||||
|
||||
The **points** list contains the actual pen data.
|
||||
|
||||
```
|
||||
class Point {
|
||||
toffset: uint32
|
||||
position: [uint32, uint32]
|
||||
pressure: uint32
|
||||
}
|
||||
```
|
||||
|
||||
An expanded file looks like this:
|
||||
|
||||
```
|
||||
{
|
||||
"version" : 1, // JSON file format version number
|
||||
"devicename": "Wacom Bamboo Spark",
|
||||
"dimensions": [ 100000, 200000], // width/height in µm
|
||||
"timestamp" : 12345,
|
||||
"strokes" : [
|
||||
{
|
||||
"points": [
|
||||
// all items in a point are optional. Unknown dictionary
|
||||
// entries must be ignored as future devices may add
|
||||
// new axes.
|
||||
{ "toffset" : 12366, "position" : [ 100, 200 ], "pressure" : 1000 },
|
||||
{ "toffset" : 12368, "pressure" : 800 },
|
||||
{ "toffset" : 12366, "position" : [ 120, 202 ] },
|
||||
]
|
||||
},
|
||||
{ "points" : ... }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Units used by this interface
|
||||
----------------------------
|
||||
|
||||
* Physical distances for x/y axes are in µm from the sensor's top-left
|
||||
position. (Note that on the Spark and on the Slate at least, the sensor
|
||||
is turned 90 degrees clockwise, so (0,0) is at the 'natural' top-right
|
||||
corner)
|
||||
* Stylus pressure is normalized to a range of [0, 0xffff], inclusive.
|
||||
* Timestamps are in seconds in unix epoch, time offsets are in ms after the
|
||||
most recent timestamp.
|
||||
|
469
README.md
469
README.md
|
@ -1,10 +1,9 @@
|
|||
TUHI
|
||||
Tuhi
|
||||
=====
|
||||
|
||||
Tuhi is a DBus session daemon that connects to and fetches the data from the
|
||||
Wacom ink range (Spark, Slate, Folio, Intuos Paper, ...). The data is
|
||||
provided to clients in the form of JSON, any conversion to other formats
|
||||
like SVG must be done by the clients.
|
||||
Tuhi is a GTK application that connects to and fetches the data from the
|
||||
Wacom ink range (Spark, Slate, Folio, Intuos Paper, ...). Users can save the
|
||||
data as SVGs.
|
||||
|
||||
Tuhi is the Māori word for "to draw".
|
||||
|
||||
|
@ -17,6 +16,61 @@ Devices tested and known to be supported:
|
|||
* Bamboo Slate
|
||||
* Intuos Pro Paper
|
||||
|
||||
Building Tuhi
|
||||
-------------
|
||||
|
||||
To build and run Tuhi from the repository directly:
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhi
|
||||
$> cd tuhi
|
||||
$> meson builddir
|
||||
$> ninja -C builddir
|
||||
$> ./builddir/tuhi.devel
|
||||
```
|
||||
|
||||
Tuhi requires Python v3.6 or above.
|
||||
|
||||
Install Tuhi
|
||||
---------------
|
||||
|
||||
To install and run Tuhi:
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhi
|
||||
$> cd tuhi
|
||||
$> meson builddir
|
||||
$> ninja -C builddir install
|
||||
```
|
||||
|
||||
Run Tuhi with:
|
||||
|
||||
```
|
||||
$> tuhi
|
||||
```
|
||||
|
||||
Tuhi requires Python v3.6 or above.
|
||||
|
||||
Flatpak
|
||||
-------
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhi
|
||||
$> cd tuhi
|
||||
$> flatpak-builder flatpak_builddir org.freedesktop.Tuhi.json --install --user --force-clean
|
||||
$> flatpak run org.freedesktop.Tuhi
|
||||
```
|
||||
|
||||
Note that Flatpak's containers use different XDG directories. This affects
|
||||
Tuhi being able to remember devices and the data storage. Switching between
|
||||
the Flatpak and a normal installation requires re-registering the device and
|
||||
previously downloaded drawings may become inaccessible.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Tuhi is licensed under the GPLv2 or later.
|
||||
|
||||
Registering devices
|
||||
-------------------
|
||||
|
||||
|
@ -50,411 +104,6 @@ Packages
|
|||
|
||||
Arch Linux: [tuhi-git](https://aur.archlinux.org/packages/tuhi-git/)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhi
|
||||
$> cd tuhi
|
||||
$> python3 setup.py install
|
||||
$> tuhi
|
||||
```
|
||||
|
||||
Tuhi requires Python v3.6 or above.
|
||||
|
||||
Units used by this interface
|
||||
----------------------------
|
||||
|
||||
* Physical distances for x/y axes are in µm from the sensor's top-left
|
||||
position. (Note that on the Spark and on the Slate at least, the sensor
|
||||
is turned 90 degrees clockwise, so (0,0) is at the 'natural' top-right
|
||||
corner)
|
||||
* Stylus pressure is normalized to a range of [0, 0xffff], inclusive.
|
||||
* Timestamps are in seconds in unix epoch, time offsets are in ms after the
|
||||
most recent timestamp.
|
||||
|
||||
DBus Interface
|
||||
--------------
|
||||
|
||||
The following interfaces are provided:
|
||||
|
||||
```
|
||||
org.freedesktop.tuhi1.Manager
|
||||
|
||||
Property: Devices (ao)
|
||||
Array of object paths to known (previously registered, but not necessarily
|
||||
connected) devices. Note that a "registered" device is one that has been
|
||||
initialized via the Wacom SmartPad custom protocol. A device does not
|
||||
need to be paired over Bluetooth to register.
|
||||
|
||||
Property: Searching (b)
|
||||
Indicates whether the daemon is currently searching for devices.
|
||||
|
||||
This property is set to True when a StartSearching() request initiates
|
||||
the search for device connections. When the StartSearching() request
|
||||
completes upon timeout, or when StopSearching() is called, the property
|
||||
is set to False.
|
||||
|
||||
When a pariable device is found, the UnregisteredDevice signal is sent to
|
||||
the caller that initiated the search process.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: JSONDataVersions (au)
|
||||
Specifies the JSON file format versions the server supports. The
|
||||
client must request one of these versions in Device.GetJSONData().
|
||||
|
||||
Read-only, constant
|
||||
|
||||
Method: StartSearch() -> ()
|
||||
Start searching for available devices ready for registering
|
||||
for an unspecified timeout. When the timeout expires or an error
|
||||
occurs, a SearchStopped signal is sent indicating success or error.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartSearching() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
Method: StopSearch() -> ()
|
||||
Stop listening to available devices ready for registering. If called after
|
||||
StartSearch() and before a SearchStopped signal has been received,
|
||||
this method triggers the SearchStopped signal. That signal indicates
|
||||
success or an error.
|
||||
|
||||
If this method is called before StartSearch() or after the
|
||||
SearchStopped signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopSearch() and the SearchStopped signal
|
||||
arriving, UnregisteredDevice signals may still arrive.
|
||||
|
||||
Signal: UnregisteredDevice(o)
|
||||
Indicates that a device can be registered. This signal may be
|
||||
sent after a StartSearch() call and before SearchStopped(). This
|
||||
signal is sent once per available device and only to the client that
|
||||
initiated the search process with StartSearch.
|
||||
|
||||
When this signal is sent, a org.freedesktop.tuhi1.Device object was
|
||||
created, the object path is the argument to this signal.
|
||||
|
||||
A client must immediately call Register() on that object if
|
||||
registering with that object is desired. See the documentation for
|
||||
that interface for details.
|
||||
|
||||
When the search timeout expires, the device may be removed by the
|
||||
daemon again. Note that until the device is registered, the device is not
|
||||
listed in the managers Devices property.
|
||||
|
||||
Signal: SearchStopped(i)
|
||||
Sent when the search has stopped. An argument of 0 indicates a
|
||||
successful termination of the search process, either when a device
|
||||
has been registered or the timeout expired.
|
||||
|
||||
If the errno is -EAGAIN, the daemon is already searching for devices
|
||||
on behalf of another client. In this case, this client should wait for
|
||||
the Searching property to change and StartSearching() once the
|
||||
property is set to False.
|
||||
|
||||
Once this signal has been sent, all devices announced through
|
||||
UnregisteredDevice signals should be considered invalidated. Attempting to
|
||||
Register() one of the devices after the SearchStopped() signal may result
|
||||
in an error.
|
||||
|
||||
In case of error, the argument is a negative errno.
|
||||
|
||||
org.freedesktop.tuhi1.Device
|
||||
|
||||
Interface to a device known by tuhi. Each object in Manager.Devices
|
||||
implements this interface.
|
||||
|
||||
Property: BlueZDevice (o)
|
||||
Object path to the org.bluez.Device1 device that is this device.
|
||||
Read-only
|
||||
|
||||
Property: Dimensions (uu)
|
||||
The physical dimensions (width, height) in µm
|
||||
Read-only
|
||||
|
||||
Property: BatteryPercent (u)
|
||||
The last known battery charge level in percent. This charge level is
|
||||
only accurate when the BatteryState is other than Unknown.
|
||||
|
||||
When the BatteryState is Unknown and BatteryPercent is nonzero, the
|
||||
value is the last known percentage value.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: BatteryState (u)
|
||||
An enum describing the battery state. Permitted enum values are
|
||||
|
||||
0: Unknown
|
||||
1: Charging
|
||||
2: Discharging
|
||||
|
||||
'Unknown' may refer to a state that could not be read, a state
|
||||
that has not yet been updated, or a state that has not updated within
|
||||
a daemon-internal time period. Thus, a device that is connected but
|
||||
does not regularly send battery updates may eventually switch to
|
||||
'Unknown'.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: DrawingsAvailable (at)
|
||||
An array of timestamps of the available drawings. The timestamp of
|
||||
each drawing can be used as argument to GetJSONData(). Timestamps are
|
||||
in seconds since the Epoch and may be used to display information to
|
||||
the user or sort data.
|
||||
Read-only
|
||||
|
||||
Property: Listening (b)
|
||||
Indicates whether the daemon is currently listening for the device.
|
||||
|
||||
This property is set to True when a StartListening() request initiates
|
||||
the search for device connections. When the StartListening() request
|
||||
completes upon timeout, or when StopListening() is called, the property
|
||||
is set to False.
|
||||
|
||||
When the user press the button on the device, the daemon connects
|
||||
to the device, downloads all drawings from the device and disconnects
|
||||
from the device.
|
||||
|
||||
If successfull, the drawings are deleted from the device. The data is
|
||||
held by the daemon in non-persistent storage until the daemon is stopped
|
||||
or we run out of memory, whichever happens earlier.
|
||||
Use GetJSONData() to retrieve the data from the daemon.
|
||||
|
||||
DO NOT RELY ON THE DAEMON FOR PERMANENT STORAGE
|
||||
|
||||
When drawings become available from the device, the DrawingsAvailable
|
||||
property updates to the number of available drawings.
|
||||
When the button is pressed multiple times, any new data is appended
|
||||
to the existing list of drawings as long as this property is True.
|
||||
|
||||
Read-only
|
||||
|
||||
Property: Live(b)
|
||||
Indicates whether the device is currently in Live mode. When in live
|
||||
mode, the device does not store drawings internally for a later sync
|
||||
but instead fowards the events immediately, similar to a traditional
|
||||
graphics tablet. See StartLive() for more details.
|
||||
|
||||
Read-only
|
||||
|
||||
Method: Register() -> (i)
|
||||
Register the device. If the device is already registered, calls to
|
||||
this method immediately return success.
|
||||
|
||||
Otherwise, the device is registered and this function returns success (0)
|
||||
or a negative errno on failure.
|
||||
|
||||
Method: StartListening() -> ()
|
||||
Listen for data from this device and connect to the device when it
|
||||
becomes available. The daemon listens to the device until the client
|
||||
calls StopListening() or the client disconnects, whichever happens
|
||||
earlier.
|
||||
|
||||
The ListeningStopped signal is sent when the listening terminates,
|
||||
either on success or with an error. A client should handle this signal
|
||||
to be notified of any errors.
|
||||
|
||||
When the daemon starts listening, the Listening property is updated
|
||||
accordingly.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartListening() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
Method: StopListening() -> ()
|
||||
Stop listening for data on this device. If called after
|
||||
StartListening(), this method triggers the ListenStopped signal.
|
||||
That signal indicates success or an error.
|
||||
|
||||
If this method is called before StartListening() or after the
|
||||
ListeningStopped signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopListening() and the ListeningStopped
|
||||
signal arriving, the property DrawingsAvailable may still be updated
|
||||
and it's the responsibility of the client to fetch the JSON data.
|
||||
|
||||
Method: StartLive(fd: h) -> (i)
|
||||
Starts live mode on this device. This disables offline storage of
|
||||
drawing data on the device and instead switches the device to a mode
|
||||
where it immediately reports the pen data, similar to a traditional
|
||||
graphics tablet.
|
||||
|
||||
The LiveStopped signal is sent when live mode terminates, either on
|
||||
success or with an error. A client should handle this signal to be
|
||||
notified of any errors.
|
||||
|
||||
When live mode enables, the Live property is updated accordingly.
|
||||
|
||||
If a client that successfully initated a listening process calls
|
||||
StartListening() again, that call is ignored and no signal is
|
||||
generated for that call.
|
||||
|
||||
The fd argument is a file descriptor that will be used to forward
|
||||
events to. The format is the one used by the Linux kernel's UHID
|
||||
device, see linux/uhid.h for details.
|
||||
|
||||
Method: StopLive() - >()
|
||||
Stop live mode on this device. If called after StartLive(), this
|
||||
method triggers the LiveStopped signal. That signal indicates
|
||||
success or an error.
|
||||
|
||||
If this method is called before StartLive() or after the LiveStopped
|
||||
signal, it is ignored and no signal is generated.
|
||||
|
||||
Note that between calling StopLive() and the LiveStopped signal
|
||||
arriving, the device may still send events. It's the responsibility of
|
||||
the client to handle events until the LiveStopped signal arrives.
|
||||
|
||||
Method: GetJSONData(file-version: u, timestamp: t) -> (s)
|
||||
Returns a JSON file with the drawings specified by the timestamp
|
||||
argument. The requested timestamp must be one of the entries in the
|
||||
DrawingsAvailable property value. The file-version argument specifies
|
||||
the file format version the client requests. See section JSON FILE
|
||||
FORMAT for the format of the returned data.
|
||||
|
||||
Returns a string representing the JSON data from the last drawings or
|
||||
the empty string if the timestamp is not available or the file format
|
||||
version is outside the server-supported range advertised in
|
||||
Manager.JSONDataVersions.
|
||||
|
||||
Signal: ButtonPressRequired()
|
||||
Sent when the user is expected to press the physical button on the
|
||||
device. A client should display a notification in response, if the
|
||||
user does not press the button during the (firmware-specific) timeout
|
||||
the current operation will fail.
|
||||
|
||||
Signal: ListeningStopped(i)
|
||||
Sent when the listen process has stopped. An argument of 0 indicates a
|
||||
successful termination, i.e. in response to the client calling
|
||||
StopListening(). Otherwise, the argument is a negative errno
|
||||
indicating the type of error.
|
||||
|
||||
If the errno is -EAGAIN, the daemon is already listening to the device
|
||||
on behalf of another client. In this case, this client should wait for
|
||||
the Listening property to change and StartListening() once the
|
||||
property is set to False.
|
||||
|
||||
If the error is -EBADE, the device is not ready for registering/in
|
||||
listening mode and registration/listening was requested. In
|
||||
this case, the client should indicate to the user that the device
|
||||
needs to be registered first or switched to listening mode.
|
||||
|
||||
If the error is -EACCES, the device is not registered with the daemon
|
||||
or incorrectly registered. This may happen when the device was
|
||||
registered with another host since the last connection.
|
||||
|
||||
The following other errnos may be sent by the daemon:
|
||||
-EPROTO: the daemon has encountered a protocol error with the device.
|
||||
-ETIME: timeout while communicating with the device.
|
||||
|
||||
These errnos indicate a bug in the daemon, and the client should
|
||||
display a message to that effect.
|
||||
|
||||
Signal: LiveStopped(i)
|
||||
Sent when live mode is stopped. An argument of 0 indicates a
|
||||
successful termination, i.e. in response to the client calling
|
||||
StopLive(). Otherwise, the argument is a negative errno
|
||||
indicating the type of error.
|
||||
|
||||
If the errno is -EAGAIN, the daemon has already enabled live mode on
|
||||
device on behalf of another client. In this case, this client should
|
||||
wait for the Live property to change and StartLive() once the property
|
||||
is set to False.
|
||||
|
||||
If the error is -EBADE, the device is not ready for live mode, most
|
||||
likely because it is in registration mode. In this case, the client
|
||||
should indicate to the user that the device needs to be registered
|
||||
first.
|
||||
|
||||
If the error is -EACCES, the device is not registered with the daemon
|
||||
or incorrectly registered. This may happen when the device was
|
||||
registered with another host since the last connection.
|
||||
|
||||
The following other errnos may be sent by the daemon:
|
||||
-EPROTO: the daemon has encountered a protocol error with the device.
|
||||
-ETIME: timeout while communicating with the device.
|
||||
|
||||
These errnos indicate a bug in the daemon, and the client should
|
||||
display a message to that effect.
|
||||
|
||||
Signal: SyncState(i)
|
||||
An enum to represent the current synchronization state of the device.
|
||||
When on (1), Tuhi is currently trying to download data from the
|
||||
device. When off (0), Tuhi is not currently connecting to the device.
|
||||
|
||||
This signal should be used for UI feedback.
|
||||
|
||||
This signal is only send when the device is **not** in Live mode.
|
||||
```
|
||||
|
||||
JSON File Format
|
||||
----------------
|
||||
|
||||
The current file format version is 1. A server may only support a subset of
|
||||
historical file formats, this subset is advertized as list of versions in
|
||||
the **org.freedesktop.tuhi1.Manager.JSONDataVersions** property. Likewise, a
|
||||
client may only support a subset of the possible formats. A client should
|
||||
always pick the highest format supported by both the client and the server.
|
||||
|
||||
Below is the example file format (with comments, not present in the real
|
||||
files). The JSON objects are "drawing" (the root object), "strokes",
|
||||
"points". Pseudo-code is used to illustrate the objects in the file.
|
||||
|
||||
```
|
||||
class Drawing {
|
||||
version: uint32
|
||||
devicename: string
|
||||
dimensions: [uint32, uint32] // x/y physical dimensions in µm
|
||||
timestamp: uint64
|
||||
strokes: [ Stroke, Stroke, ...]
|
||||
}
|
||||
```
|
||||
|
||||
The **strokes** list contains all strokes of a single drawing, each stroke
|
||||
consisting of a number of **points**.
|
||||
|
||||
```
|
||||
class Stroke {
|
||||
points: [Point, Point, ...]
|
||||
}
|
||||
```
|
||||
|
||||
The **points** list contains the actual pen data.
|
||||
|
||||
```
|
||||
class Point {
|
||||
toffset: uint32
|
||||
position: [uint32, uint32]
|
||||
pressure: uint32
|
||||
}
|
||||
```
|
||||
|
||||
An expanded file looks like this:
|
||||
|
||||
```
|
||||
{
|
||||
"version" : 1, // JSON file format version number
|
||||
"devicename": "Wacom Bamboo Spark",
|
||||
"dimensions": [ 100000, 200000], // width/height in µm
|
||||
"timestamp" : 12345,
|
||||
"strokes" : [
|
||||
{
|
||||
"points": [
|
||||
// all items in a point are optional. Unknown dictionary
|
||||
// entries must be ignored as future devices may add
|
||||
// new axes.
|
||||
{ "toffset" : 12366, "position" : [ 100, 200 ], "pressure" : 1000 },
|
||||
{ "toffset" : 12368, "pressure" : 800 },
|
||||
{ "toffset" : 12366, "position" : [ 120, 202 ] },
|
||||
]
|
||||
},
|
||||
{ "points" : ... }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Device notes
|
||||
============
|
||||
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
project('tuhi',
|
||||
version: '0.1',
|
||||
license: 'GPLv2',
|
||||
meson_version: '>= 0.48.0')
|
||||
# The tag date of the project_version(), update when the version bumps.
|
||||
version_date='2019-07-10'
|
||||
|
||||
# Dependencies
|
||||
dependency('python3', required: true)
|
||||
dependency('pygobject-3.0', required: true)
|
||||
|
||||
# Gtk version required
|
||||
gtk_major_version = 3
|
||||
gtk_minor_version = 22
|
||||
|
||||
prefix = get_option('prefix')
|
||||
datadir = join_paths(prefix, get_option('datadir'))
|
||||
localedir = join_paths(prefix, get_option('localedir'))
|
||||
pkgdatadir = join_paths(datadir, meson.project_name())
|
||||
bindir = join_paths(prefix, get_option('bindir'))
|
||||
podir = join_paths(meson.source_root(), 'po')
|
||||
desktopdir = join_paths(datadir, 'applications')
|
||||
icondir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'apps')
|
||||
metainfodir = join_paths(datadir, 'metainfo')
|
||||
libexecdir = join_paths(get_option('prefix'), get_option('libexecdir'), 'tuhi')
|
||||
|
||||
|
||||
i18n = import('i18n')
|
||||
|
||||
subdir('po')
|
||||
subdir('tuhigui/data')
|
||||
|
||||
# Find the directory to install our Python code
|
||||
pymod = import('python')
|
||||
py3 = pymod.find_installation()
|
||||
python_dir = py3.get_install_dir()
|
||||
install_subdir('tuhigui',
|
||||
install_dir: python_dir,
|
||||
exclude_directories: ['__pycache__', 'data'])
|
||||
install_subdir('tuhi',
|
||||
install_dir: python_dir,
|
||||
exclude_directories: '__pycache__')
|
||||
|
||||
# We have three startup scripts:
|
||||
# - tuhi: starts server and GUI
|
||||
# - tuhi-gui: starts the GUI only
|
||||
# - tuhi-server: starts the server only
|
||||
#
|
||||
# tuhi-server can run as-is, we don't need meson for it. But for the other
|
||||
# two we build a {name}.devel version that uses the in-tree files.
|
||||
# For that we need to replace a few paths, in the installed versions we just
|
||||
# use the normal dirs.
|
||||
#
|
||||
config_tuhi = configuration_data()
|
||||
config_tuhi.set_quoted('libexecdir', libexecdir)
|
||||
config_tuhi.set('devel', '')
|
||||
|
||||
config_tuhi_devel = configuration_data()
|
||||
config_tuhi_devel.set_quoted('libexecdir', '')
|
||||
config_tuhi_devel.set('devel', '''
|
||||
tuhi_gui = '@1@/tuhi-gui.devel'
|
||||
tuhi_server = '@0@/tuhi-server.py'
|
||||
print('Running from source tree, using local files')
|
||||
'''.format(meson.source_root(), meson.build_root()))
|
||||
|
||||
config_tuhigui = configuration_data()
|
||||
config_tuhigui.set('pkgdatadir', pkgdatadir)
|
||||
config_tuhigui.set('localedir', localedir)
|
||||
config_tuhigui.set('devel', '')
|
||||
|
||||
config_tuhigui_devel = config_tuhigui
|
||||
config_tuhigui_devel.set('pkgdatadir', join_paths(meson.build_root(), 'tuhigui', 'data'))
|
||||
config_tuhigui_devel.set('localedir', join_paths(meson.build_root(), 'po'))
|
||||
config_tuhigui_devel.set('devel', '''
|
||||
sys.path.insert(1, '@0@')
|
||||
print('Running from source tree, using local files')
|
||||
'''.format(meson.source_root(), meson.build_root()))
|
||||
|
||||
configure_file(input: 'tuhi.in',
|
||||
output: 'tuhi',
|
||||
configuration: config_tuhi,
|
||||
install_dir: bindir)
|
||||
|
||||
configure_file(input: 'tuhi.in',
|
||||
output: 'tuhi.devel',
|
||||
configuration: config_tuhi_devel)
|
||||
|
||||
configure_file(input: 'tuhi-gui.in',
|
||||
output: 'tuhi-gui',
|
||||
configuration: config_tuhigui,
|
||||
install_dir: libexecdir)
|
||||
|
||||
configure_file(input: 'tuhi-gui.in',
|
||||
output: 'tuhi-gui.devel',
|
||||
configuration: config_tuhigui_devel)
|
||||
|
||||
configure_file(input: 'tuhi-server.py',
|
||||
output: 'tuhi-server',
|
||||
copy: true,
|
||||
install_dir: libexecdir)
|
||||
|
||||
meson.add_install_script('meson_install.sh')
|
||||
|
||||
desktop_file = i18n.merge_file(input: 'tuhigui/data/org.freedesktop.Tuhi.desktop.in',
|
||||
output: 'org.freedesktop.Tuhi.desktop',
|
||||
type: 'desktop',
|
||||
po_dir: podir,
|
||||
install: true,
|
||||
install_dir: desktopdir)
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('version', meson.project_version())
|
||||
conf.set('url', 'https://github.com/tuhiproject/tuhi')
|
||||
conf.set('version_date', version_date)
|
||||
|
||||
appdata_intl = configure_file(input: 'tuhigui/data/org.freedesktop.Tuhi.appdata.xml.in.in',
|
||||
output: 'org.freedesktop.Tuhi.appdata.xml.in',
|
||||
configuration: conf)
|
||||
|
||||
appdata = i18n.merge_file(input: appdata_intl,
|
||||
output: 'org.freedesktop.Tuhi.appdata.xml',
|
||||
type: 'xml',
|
||||
po_dir: podir,
|
||||
install: true,
|
||||
install_dir: metainfodir)
|
||||
|
||||
install_data('tuhigui/data/org.freedesktop.Tuhi.svg', install_dir: icondir)
|
||||
|
||||
flake8 = find_program('flake8-3', required: false)
|
||||
if flake8.found()
|
||||
test('flake8', flake8,
|
||||
args: ['--ignore=E501,W504',
|
||||
join_paths(meson.source_root(), 'tuhigui/')])
|
||||
endif
|
||||
|
||||
desktop_validate = find_program('desktop-file-validate', required: false)
|
||||
if desktop_validate.found()
|
||||
test('desktop-file-validate', desktop_validate, args: [desktop_file])
|
||||
endif
|
||||
|
||||
appstream_util = find_program('appstream-util', required: false)
|
||||
if appstream_util.found()
|
||||
test('appstream-util validate-relax', appstream_util,
|
||||
args: ['validate-relax', appdata])
|
||||
endif
|
||||
|
||||
# A wrapper to start tuhi at the same time as tuhigui, used by the flatpak
|
||||
configure_file(input: 'tools/tuhi-gui-flatpak.py',
|
||||
output: 'tuhi-gui-flatpak.py',
|
||||
copy: true)
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
if [ -z $DESTDIR ]; then
|
||||
PREFIX=${MESON_INSTALL_PREFIX:-/usr}
|
||||
|
||||
# Update icon cache
|
||||
gtk-update-icon-cache -f -t $PREFIX/share/icons/hicolor
|
||||
|
||||
# Install new schemas
|
||||
#glib-compile-schemas $PREFIX/share/glib-2.0/schemas/
|
||||
fi
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"app-id": "org.freedesktop.Tuhi",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "3.30",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "tuhi",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--talk-name=org.freedesktop.tuhi1",
|
||||
"--own-name=org.freedesktop.tuhi1",
|
||||
"--system-talk-name=org.bluez"
|
||||
],
|
||||
"modules": [
|
||||
{
|
||||
"name": "pyxdg",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "git://anongit.freedesktop.org/xdg/pyxdg"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python-pyparsing",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.0/pyparsing-2.4.0.tar.gz",
|
||||
"sha512": "71877dc006cce5c1b1d45e7cc89cd60e03cb80353387fb0c6498cfc0d69af465dc574d1bceb87248033e7a82694aa940e9fce1ca80b2ef538a8df51f697ef530"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python-svgwrite",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozman/svgwrite.git"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tuhi",
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
{
|
||||
"id": "org.freedesktop.tuhi",
|
||||
"command": "/app/bin/tuhi",
|
||||
"runtime": "org.freedesktop.Platform",
|
||||
"sdk": "org.freedesktop.Sdk",
|
||||
"runtime-version": "18.08",
|
||||
"cleanup": [ "/cache",
|
||||
"/include",
|
||||
"/lib/pkgconfig",
|
||||
"/man",
|
||||
"/share/aclocal",
|
||||
"/share/devhelp",
|
||||
"/share/gir-1.0",
|
||||
"/share/gtk-doc",
|
||||
"/share/man",
|
||||
"/share/pkgconfig",
|
||||
"/share/vala",
|
||||
"/lib/systemd",
|
||||
"*.la", "*.a" ],
|
||||
"build-options" : {
|
||||
"cflags": "-O2 -g",
|
||||
"cxxflags": "-O2 -g"
|
||||
},
|
||||
"finish-args": [
|
||||
"--system-talk-name=org.bluez",
|
||||
"--own-name=org.freedesktop.tuhi1"
|
||||
],
|
||||
"modules": [
|
||||
{
|
||||
"name": "pygobject",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"python3 setup.py install --prefix=/app"
|
||||
],
|
||||
"build-options": {
|
||||
"env": {
|
||||
"PYGOBJECT_WITHOUT_PYCAIRO" : "1"
|
||||
}
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://ftp.gnome.org/pub/GNOME/sources/pygobject/3.32/pygobject-3.32.1.tar.xz",
|
||||
"sha256": "32c99def94b8dea5ce9e4bc99576ef87591ea779b4db77cfdca7af81b76d04d8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pyxdg",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "git://anongit.freedesktop.org/xdg/pyxdg"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python-pyparsing",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/pyparsing/pyparsing/releases/download/pyparsing_2.4.0/pyparsing-2.4.0.tar.gz",
|
||||
"sha512": "71877dc006cce5c1b1d45e7cc89cd60e03cb80353387fb0c6498cfc0d69af465dc574d1bceb87248033e7a82694aa940e9fce1ca80b2ef538a8df51f697ef530"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "python-svgwrite",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozman/svgwrite.git"
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tuhi",
|
||||
"buildsystem": "simple",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} ."
|
||||
],
|
||||
"post-install": [
|
||||
"cp tools/kete.py /app/lib/python3.7/site-packages/kete.py",
|
||||
"cp tools/tuhi-kete-sandboxed.py /app/bin/tuhi-kete",
|
||||
"chmod +x /app/bin/tuhi-kete"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
# Language list must be in alphabetical order
|
|
@ -0,0 +1,17 @@
|
|||
data/org.freedesktop.Tuhi.appdata.xml.in.in
|
||||
data/org.freedesktop.Tuhi.desktop.in
|
||||
|
||||
data/ui/AboutDialog.ui.in
|
||||
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
|
|
@ -0,0 +1,37 @@
|
|||
i18n
|
||||
====
|
||||
|
||||
This directory contains the translations of Tuhi
|
||||
|
||||
For errors in translations, please [file an
|
||||
issue](https://github.com/tuhiproject/tuhi/issues/new).
|
||||
|
||||
New or updated translations are always welcome. To start a new translation, run:
|
||||
|
||||
$ meson translation-build
|
||||
$ ninja -C translation-build tuhi-pot
|
||||
# Now you can optionally remove the build directory
|
||||
$ rm -rf translation-build
|
||||
$ cp po/tuhi.pot po/$lang.po
|
||||
|
||||
where `$lang` is the language code of your target language, e.g. `nl` for Dutch
|
||||
or `en_GB` for British English. Edit the
|
||||
[LINGUAS](https://github.com/tuhiproject/tuhi/blob/master/tuhigui/po/LINGUAS) file and
|
||||
add your language code, keeping the list sorted alphabetically. Finally, open
|
||||
the `.po` file you just created and translate all the strings. Don't forget to
|
||||
fill in the information in the header!
|
||||
|
||||
To update an existing translation, run:
|
||||
|
||||
$ meson translation-build
|
||||
$ ninja -C translation-build tuhi-update-po
|
||||
# Now you can optionally remove the build directory
|
||||
$ rm -rf translation-build
|
||||
|
||||
and update the `po/$lang.po` file of your target language.
|
||||
|
||||
When you are done translating, file a pull request on
|
||||
[GitHub](https://github.com/tuhiproject/tuhi) or, if you don't know how to, [open
|
||||
an issue](https://github.com/tuhiproject/tuhi/issues/new) and attach the `.po`
|
||||
file there.
|
||||
|
|
@ -0,0 +1 @@
|
|||
i18n.gettext(meson.project_name(), preset: 'glib')
|
|
@ -0,0 +1,32 @@
|
|||
#!/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.
|
||||
#
|
||||
|
||||
import subprocess
|
||||
from multiprocessing import Process
|
||||
|
||||
|
||||
def start_tuhi():
|
||||
subprocess.run('tuhi')
|
||||
|
||||
|
||||
def start_tuhigui():
|
||||
subprocess.run('tuhi-gui')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
tuhi = Process(target=start_tuhi)
|
||||
tuhi.daemon = True
|
||||
tuhi.start()
|
||||
tuhigui = Process(target=start_tuhigui)
|
||||
tuhigui.start()
|
||||
tuhigui.join()
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gi
|
||||
import sys
|
||||
import os
|
||||
|
||||
gi.require_version('Gio', '2.0')
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gio, Gtk, Gdk
|
||||
|
||||
@devel@
|
||||
resource = Gio.resource_load(os.path.join('@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 tuhigui.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)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#!/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.
|
||||
#
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
tuhi_server = os.path.join(@libexecdir@, 'tuhi-server')
|
||||
tuhi_gui = os.path.join(@libexecdir@, 'tuhi-gui')
|
||||
|
||||
@devel@
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
tuhi = subprocess.Popen(tuhi_server)
|
||||
try:
|
||||
subprocess.run(tuhi_gui)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
tuhi.terminate()
|
13
tuhi/base.py
13
tuhi/base.py
|
@ -284,6 +284,8 @@ class Tuhi(GObject.Object):
|
|||
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'device-connected':
|
||||
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'terminate':
|
||||
(GObject.SignalFlags.RUN_FIRST, None, ()),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
@ -302,7 +304,6 @@ class Tuhi(GObject.Object):
|
|||
self.devices = {}
|
||||
|
||||
self._search_stop_handler = None
|
||||
self.mainloop = GLib.MainLoop()
|
||||
|
||||
def _on_tuhi_bus_name_acquired(self, dbus_server):
|
||||
self.bluez.connect_to_bluez()
|
||||
|
@ -315,7 +316,7 @@ class Tuhi(GObject.Object):
|
|||
lambda mgr, dev: self._add_device(mgr, dev, True))
|
||||
|
||||
def _on_tuhi_bus_name_lost(self, dbus_server):
|
||||
self.mainloop.quit()
|
||||
self.emit('terminate')
|
||||
|
||||
def _on_start_search_requested(self, dbus_server, stop_handler):
|
||||
self._search_stop_handler = stop_handler
|
||||
|
@ -415,9 +416,6 @@ class Tuhi(GObject.Object):
|
|||
else:
|
||||
self.bluez.stop_discovery()
|
||||
|
||||
def run(self):
|
||||
self.mainloop.run()
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
if sys.version_info < (3, 6):
|
||||
|
@ -435,6 +433,9 @@ def main(args=sys.argv):
|
|||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
try:
|
||||
Tuhi().run()
|
||||
mainloop = GLib.MainLoop()
|
||||
tuhi = Tuhi()
|
||||
tuhi.connect('terminate', lambda tuhi: mainloop.quit())
|
||||
mainloop.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
TuhiGui
|
||||
=======
|
||||
|
||||
Tuhi is a GUI to the Tuhi DBus daemon that connects to and fetches the data
|
||||
from the Wacom ink range (Spark, Slate, Folio, Intuos Paper, ...). The data
|
||||
is converted to SVG and users can save it on disk.
|
||||
|
||||
For more info about Tuhi see: https://github.com/tuhiproject/tuhi
|
||||
|
||||
|
||||
Building TuhiGUI
|
||||
----------------
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhigui
|
||||
$> cd tuhigui
|
||||
$> meson builddir
|
||||
$> ninja -C builddir
|
||||
$> ./builddir/tuhigui.devel
|
||||
```
|
||||
|
||||
TuhiGui requires Python v3.6 or above.
|
||||
|
||||
Install TuhiGUI
|
||||
---------------
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhigui
|
||||
$> cd tuhigui
|
||||
$> meson builddir
|
||||
$> ninja -C builddir install
|
||||
$> tuhigui
|
||||
```
|
||||
|
||||
TuhiGui requires Python v3.6 or above.
|
||||
|
||||
Flatpak
|
||||
-------
|
||||
|
||||
```
|
||||
$> git clone http://github.com/tuhiproject/tuhigui
|
||||
$> cd tuhigui
|
||||
$> flatpak-builder flatpak_builddir org.freedesktop.TuhiGui.json --install --user --force-clean
|
||||
$> flatpak run org.freedesktop.TuhiGui
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
TuhiGui is licensed under the GPLv2 or later.
|
|
@ -0,0 +1,12 @@
|
|||
#!/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.
|
||||
#
|
|
@ -0,0 +1,60 @@
|
|||
#!/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 Gio, GLib, Gtk
|
||||
from .window import MainWindow
|
||||
|
||||
import gi
|
||||
gi.require_version("Gio", "2.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id='org.freedesktop.Tuhi',
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
||||
GLib.set_application_name('Tuhi')
|
||||
self._tuhi = None
|
||||
|
||||
def do_startup(self):
|
||||
Gtk.Application.do_startup(self)
|
||||
self._build_app_menu()
|
||||
|
||||
def do_activate(self):
|
||||
window = MainWindow(application=self)
|
||||
window.present()
|
||||
|
||||
def _build_app_menu(self):
|
||||
actions = [('about', self._about),
|
||||
('quit', self._quit),
|
||||
('help', self._help)]
|
||||
for (name, callback) in actions:
|
||||
action = Gio.SimpleAction.new(name, None)
|
||||
action.connect('activate', callback)
|
||||
self.add_action(action)
|
||||
|
||||
def _about(self, action, param):
|
||||
builder = Gtk.Builder().new_from_resource('/org/freedesktop/Tuhi/AboutDialog.ui')
|
||||
about = builder.get_object('about_dialog')
|
||||
about.set_transient_for(self.get_active_window())
|
||||
about.connect('response', lambda about, param: about.destroy())
|
||||
about.show()
|
||||
|
||||
def _quit(self, action, param):
|
||||
windows = self.get_windows()
|
||||
for window in windows:
|
||||
window.destroy()
|
||||
|
||||
def _help(self, action, param):
|
||||
import time
|
||||
Gtk.show_uri(None, 'https://github.com/tuhiproject/tuhi/wiki', time.time())
|
|
@ -0,0 +1,128 @@
|
|||
#!/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 xdg.BaseDirectory
|
||||
import configparser
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger('tuhi.gui.config')
|
||||
|
||||
ROOT_PATH = Path(xdg.BaseDirectory.xdg_data_home, 'tuhi')
|
||||
|
||||
|
||||
class Config(GObject.Object):
|
||||
_config_obj = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.path = Path(ROOT_PATH, 'tuhigui.ini')
|
||||
self.config = configparser.ConfigParser()
|
||||
# Don't lowercase options
|
||||
self.config.optionxform = str
|
||||
self._drawings = []
|
||||
self._load()
|
||||
self._load_cached_drawings()
|
||||
|
||||
def _load(self):
|
||||
if not self.path.exists():
|
||||
return
|
||||
|
||||
logger.debug(f'configuration found')
|
||||
self.config.read(self.path)
|
||||
|
||||
def _load_cached_drawings(self):
|
||||
if not ROOT_PATH.exists():
|
||||
return
|
||||
|
||||
for filename in ROOT_PATH.glob('*.json'):
|
||||
with open(filename) as fd:
|
||||
self._drawings.append(json.load(fd))
|
||||
self.notify('drawings')
|
||||
|
||||
def _write(self):
|
||||
self.path.resolve().parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.path, 'w') as fd:
|
||||
self.config.write(fd)
|
||||
|
||||
def _add_key(self, section, key, value):
|
||||
if section not in self.config:
|
||||
self.config[section] = {}
|
||||
self.config[section][key] = value
|
||||
self._write()
|
||||
|
||||
@GObject.property
|
||||
def orientation(self):
|
||||
try:
|
||||
return self.config['Device']['Orientation']
|
||||
except KeyError:
|
||||
return 'landscape'
|
||||
|
||||
@orientation.setter
|
||||
def orientation(self, orientation):
|
||||
assert(orientation in ['landscape', 'portrait'])
|
||||
self._add_key('Device', 'Orientation', orientation)
|
||||
|
||||
@GObject.property
|
||||
def drawings(self):
|
||||
return self._drawings
|
||||
|
||||
def add_drawing(self, timestamp, json_string):
|
||||
'''Add a drawing JSON with the given timestamp to the backend
|
||||
storage. This will update self.drawings.'''
|
||||
ROOT_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
path = Path(ROOT_PATH, f'{timestamp}.json')
|
||||
if path.exists():
|
||||
return
|
||||
|
||||
# Tuhi may still cache files we've 'deleted' locally. These need to
|
||||
# be ignored because they're still technically deleted.
|
||||
deleted = Path(ROOT_PATH, f'{timestamp}.json.deleted')
|
||||
if deleted.exists():
|
||||
return
|
||||
|
||||
with open(path, 'w') as fd:
|
||||
fd.write(json_string)
|
||||
|
||||
self._drawings.append(json.loads(json_string))
|
||||
self.notify('drawings')
|
||||
|
||||
def delete_drawing(self, timestamp):
|
||||
# We don't delete json files immediately, we just rename them
|
||||
# so we can resurrect them in the future if need be.
|
||||
path = Path(ROOT_PATH, f'{timestamp}.json')
|
||||
target = Path(ROOT_PATH, f'{timestamp}.json.deleted')
|
||||
path.rename(target)
|
||||
|
||||
self._drawings = [d for d in self._drawings if d['timestamp'] != timestamp]
|
||||
self.notify('drawings')
|
||||
|
||||
def undelete_drawing(self, timestamp):
|
||||
path = Path(ROOT_PATH, f'{timestamp}.json')
|
||||
target = Path(ROOT_PATH, f'{timestamp}.json.deleted')
|
||||
target.rename(path)
|
||||
|
||||
with open(path) as fd:
|
||||
self._drawings.append(json.load(fd))
|
||||
self.notify('drawings')
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
if cls._config_obj is None:
|
||||
cls._config_obj = Config()
|
||||
return cls._config_obj
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
sodipodi:docname="input-tablet-missing-symbolic.svg"
|
||||
inkscape:version="0.92.4 (unknown)">
|
||||
<metadata
|
||||
id="metadata16">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs14" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview12"
|
||||
showgrid="false"
|
||||
inkscape:zoom="29.5"
|
||||
inkscape:cx="15.281847"
|
||||
inkscape:cy="7.8893951"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10" />
|
||||
<g
|
||||
color="#000"
|
||||
font-weight="400"
|
||||
font-family="sans-serif"
|
||||
white-space="normal"
|
||||
fill="#2e3436"
|
||||
id="g8"
|
||||
style="fill:#474747;fill-opacity:1;opacity:0.35">
|
||||
<path
|
||||
d="M12.403 4.15L7.45 9.102 7 11l1.87-.39 5-5c.549-.593.63-1.11.13-1.61s-1.17-.278-1.597.15z"
|
||||
style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none;fill:#474747;fill-opacity:1"
|
||||
clip-rule="evenodd"
|
||||
overflow="visible"
|
||||
fill-rule="evenodd"
|
||||
image-rendering="optimizeQuality"
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M2.191 6L.263 15h15.473L14 6.897 12.316 8.58l.946 4.42H2.738l1.07-5h3.33l1.999-2zm9.776 0l-2 2h.1l2-2z"
|
||||
style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;fill:#474747;fill-opacity:1"
|
||||
overflow="visible"
|
||||
fill-rule="evenodd"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M8 0v1.5a.506.506 0 0 1-.147.356.484.484 0 0 1-.345.144H4.496a1.498 1.498 0 0 0-1.297 2.248c.266.463.761.752 1.295.752h3.01c.18 0 .34.092.43.248A.506.506 0 0 1 8 5.5V6h1v-.5A1.498 1.498 0 0 0 7.504 4h-3.01a.491.491 0 0 1-.428-.248.507.507 0 0 1 0-.504.488.488 0 0 1 .43-.248h3.012c.396 0 .777-.16 1.056-.441C8.844 2.277 9 1.897 9 1.5V0z"
|
||||
style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;fill:#474747;fill-opacity:1"
|
||||
overflow="visible"
|
||||
id="path6" />
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4581"
|
||||
overflow="visible"
|
||||
style="color:#bebebe;overflow:visible;fill:#474747;marker:none"
|
||||
d="m 11,11 h 1.375 L 13.5,12.094 14.594,11 H 16 v 1.469 L 14.906,13.531 16,14.594 V 16 H 14.562 L 13.5,14.937 12.437,16 H 11 V 14.594 L 12.063,13.531 11,12.47 Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,42 @@
|
|||
gnome = import('gnome')
|
||||
|
||||
desktopdir = join_paths(datadir, 'applications')
|
||||
icondir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'apps')
|
||||
metainfodir = join_paths(datadir, 'metainfo')
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('version', meson.project_version())
|
||||
conf.set('url', 'https://github.com/tuhiproject/tuhi')
|
||||
conf.set('version_date', version_date)
|
||||
|
||||
about_dialog = configure_file(input: 'ui/AboutDialog.ui.in',
|
||||
output: 'AboutDialog.ui',
|
||||
configuration: conf)
|
||||
|
||||
install_data('org.freedesktop.Tuhi.svg', install_dir: icondir)
|
||||
|
||||
i18n.merge_file(input: 'org.freedesktop.Tuhi.desktop.in',
|
||||
output: 'org.freedesktop.Tuhi.desktop',
|
||||
type: 'desktop',
|
||||
po_dir: podir,
|
||||
install: true,
|
||||
install_dir: desktopdir)
|
||||
|
||||
appdata = configure_file(input: 'org.freedesktop.Tuhi.appdata.xml.in.in',
|
||||
output: 'org.freedesktop.Tuhi.appdata.xml.in',
|
||||
configuration: conf)
|
||||
|
||||
i18n.merge_file(input: appdata,
|
||||
output: 'org.freedesktop.Tuhi.appdata.xml',
|
||||
type: 'xml',
|
||||
po_dir: podir,
|
||||
install: true,
|
||||
install_dir: metainfodir)
|
||||
|
||||
|
||||
gnome.compile_resources('tuhi', 'tuhi.gresource.xml',
|
||||
source_dir: '.',
|
||||
dependencies: [about_dialog],
|
||||
gresource_bundle: true,
|
||||
install: true,
|
||||
install_dir: pkgdatadir)
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>org.freedesktop.Tuhi</id>
|
||||
<metadata_license>FSFAP</metadata_license>
|
||||
<project_license>GPL-2.0+</project_license>
|
||||
<content_rating type="oars-1.0" />
|
||||
<name>Tuhi</name>
|
||||
<summary>Utility to download drawings from the Wacom Ink range of devices</summary>
|
||||
<description>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<kudos>
|
||||
<kudo>AppMenu</kudo>
|
||||
<kudo>HiDpiIcon</kudo>
|
||||
<kudo>ModernToolkit</kudo>
|
||||
</kudos>
|
||||
|
||||
<launchable type="desktop-id">org.freedesktop.Tuhi.desktop</launchable>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>The button configuraton page</caption>
|
||||
<image>https://github.com/libratbag/piper/raw/wiki/screenshots/flathub/piper-buttonpage.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>The LED configuraton page</caption>
|
||||
<image>https://github.com/libratbag/piper/raw/wiki/screenshots/flathub/piper-ledpage.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>The resolution configuraton page</caption>
|
||||
<image>https://github.com/libratbag/piper/raw/wiki/screenshots/flathub/piper-resolutionpage.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<url type="homepage">https://github.com/tuhiproject/tuhi/</url>
|
||||
<url type="bugtracker">https://github.com/tuhiproject/tuhi/issues</url>
|
||||
<url type="help">https://github.com/tuhiproject/tuhi/wiki</url>
|
||||
<project_group>GNOME</project_group>
|
||||
|
||||
<translation type="gettext">tuhi</translation>
|
||||
|
||||
<provides>
|
||||
<binary>tuhi</binary>
|
||||
</provides>
|
||||
|
||||
<releases>
|
||||
<release version="@version@" date="@version_date@"/>
|
||||
</releases>
|
||||
</component>
|
|
@ -0,0 +1,11 @@
|
|||
[Desktop Entry]
|
||||
Name=Tuhi
|
||||
Comment=Utility to download drawings from the Wacom Ink range of devices
|
||||
Exec=tuhi
|
||||
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
|
||||
Icon=org.freedesktop.Tuhi
|
||||
Type=Application
|
||||
StartupNotify=true
|
||||
Categories=GTK;GNOME;Utility;
|
||||
# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
|
||||
Keywords=tablet;wacom;ink;
|
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="org.freedesktop.Tuhi.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="-40.019796"
|
||||
inkscape:cy="593.90888"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
ry="5.1324711"
|
||||
rx="5.7588129"
|
||||
y="32.736401"
|
||||
x="22.058018"
|
||||
height="240.71727"
|
||||
width="179.96317"
|
||||
id="rect871"
|
||||
style="fill:#d1d1d1;fill-opacity:1;stroke:#000000;stroke-width:5.85741472;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
style="fill:#f8f8f8;fill-opacity:1;stroke:#000000;stroke-width:4.56920719;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect869"
|
||||
width="125.8467"
|
||||
height="209.46855"
|
||||
x="66.17778"
|
||||
y="51.012478"
|
||||
rx="4.0270891"
|
||||
ry="4.4661989" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:4.07743645;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 115.44566,153.57828 c -4.40264,6.5677 -10.62856,11.54273 -17.558895,15.19366 -4.67694,2.6307 -11.830989,10.60781 -6.551949,15.24865 7.445719,1.96193 15.315034,0.39747 22.254454,-2.65498 7.10422,-3.13141 18.42523,-0.36024 16.74675,9.32995 -2.37548,7.66511 -10.18845,11.53571 -14.93484,17.43238 l -0.47636,1.28811 0.0592,1.3909"
|
||||
id="path843"
|
||||
inkscape:connector-curvature="0" />
|
||||
<circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:4.56920719;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path873"
|
||||
cx="43.762691"
|
||||
cy="160.30058"
|
||||
r="8.0724049" />
|
||||
<g
|
||||
id="g951"
|
||||
transform="translate(-0.29412526,-24.83401)">
|
||||
<g
|
||||
transform="matrix(0.58929876,0.50248108,-0.50248108,0.58929876,143.7747,-3.51721)"
|
||||
id="g830">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect819"
|
||||
width="16.570711"
|
||||
height="104.50238"
|
||||
x="116.52951"
|
||||
y="81.046227"
|
||||
ry="5.7670002"
|
||||
rx="5.1999998" />
|
||||
<path
|
||||
sodipodi:nodetypes="scccccs"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path821"
|
||||
d="m 123.14735,202.7875 c -1.68724,-0.16712 -3.66116,-6.50505 -3.66116,-6.50505 l -6.29172,-10.73384 7.46411,0.0819 12.44164,-0.0819 -5.80754,9.44957 c 0,0 -2.71186,7.9313 -4.14533,7.78932 z"
|
||||
inkscape:transform-center-y="2.8731452"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(0.83245854,0,0,0.99999988,22.299806,-5.8799047)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect825"
|
||||
width="7.2162771"
|
||||
height="7.2162771"
|
||||
x="121.07309"
|
||||
y="78.106255"
|
||||
rx="5.1999884"
|
||||
ry="5.7669902" />
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path929"
|
||||
d="m 126.29892,160.02213 44.11346,-51.29277"
|
||||
style="fill:none;stroke:#878787;stroke-width:1.59743071;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/freedesktop/Tuhi">
|
||||
<file preprocess="xml-stripblanks">AboutDialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/Drawing.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/DrawingPerspective.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/Flowbox.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/MainWindow.ui</file>
|
||||
<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>
|
||||
</gresource>
|
||||
</gresources>
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.20.0 -->
|
||||
<interface domain="tuhi">
|
||||
<requires lib="gtk+" version="3.4"/>
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<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="website">@url@</property>
|
||||
<property name="website_label" translatable="yes">Visit Tuhi’s website</property>
|
||||
<property name="logo_icon_name">org.freedesktop.Tuhi</property>
|
||||
<property name="license_type">gpl-2-0</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
|
@ -0,0 +1,151 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkImage" id="icon_download">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-save-as-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="icon_remove">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-delete-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image_rotate_left">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">object-rotate-left-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="image_rotate_right">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">object-rotate-right-symbolic</property>
|
||||
</object>
|
||||
<template class="Drawing" parent="GtkEventBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<signal name="enter-notify-event" handler="_on_enter" swapped="no"/>
|
||||
<signal name="leave-notify-event" handler="_on_leave" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkBox" id="box_drawing">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">20</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="box_toolbar">
|
||||
<property name="height_request">20</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_rotate_left">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="image">image_rotate_left</property>
|
||||
<signal name="clicked" handler="_on_rotate_button_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_rotate_right">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="image">image_rotate_right</property>
|
||||
<signal name="clicked" handler="_on_rotate_button_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_download">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="image">icon_download</property>
|
||||
<signal name="clicked" handler="_on_download_button_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_remove">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="image">icon_remove</property>
|
||||
<signal name="clicked" handler="_on_delete_button_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="image_svg">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
<style>
|
||||
<class name="bg-paper"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="drawing"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="DrawingPerspective" parent="GtkStack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="baseline_position">top</property>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkViewport" id="viewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="box_all_drawings">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="bg-white"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="index">-1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkRevealer" id="overlay_undo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="transition_type">none</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_right">4</property>
|
||||
<property name="margin_start">12</property>
|
||||
<property name="margin_end">4</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="notification_delete_undo">
|
||||
<property name="label" translatable="yes">Undo delete drawing</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="margin_right">6</property>
|
||||
<property name="margin_end">6</property>
|
||||
<signal name="clicked" handler="_on_undo_clicked" swapped="no"/>
|
||||
<style>
|
||||
<class name="text-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="notification_delete_close">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<signal name="clicked" handler="_on_undo_close_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">window-close-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="image-button"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="app-notification"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_sync">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Press the button on the device to synchronize drawings</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title" translatable="yes">page0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="ErrorPerspective" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">TuhiGUI is an interactive GUI to download data from Tuhi.
|
||||
|
||||
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>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">55</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">20</property>
|
||||
<property name="margin_bottom">20</property>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">30</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Connecting to Tuhi</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">This should take less than a second. Make sure the Tuhi DBus server is running.</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">55</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="Flowbox" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_date">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="hexpand">False</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="size" value="20000"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">20</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="margin_bottom">20</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFlowBox" id="flowbox_drawings">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,133 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkAdjustment" id="adjustment_zoom">
|
||||
<property name="upper">15</property>
|
||||
<property name="step_increment">1.0000000002235174</property>
|
||||
<property name="page_increment">1</property>
|
||||
<signal name="value-changed" handler="_on_zoom_changed" swapped="no"/>
|
||||
</object>
|
||||
<template class="MainWindow" parent="GtkApplicationWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="default_width">1000</property>
|
||||
<property name="default_height">700</property>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="headerbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="image_missing_tablet">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="resource">/org/freedesktop/Tuhi/input-tablet-missing-symbolic.svg</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="spinner_sync">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="title">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="menubutton1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">open-menu-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="image_battery">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="icon_name">battery-missing-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">zoom-out-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale_zoom">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="adjustment">adjustment_zoom</property>
|
||||
<property name="round_digits">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">zoom-in-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack_perspectives">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="transition_type">crossfade</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,275 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="SetupDialog" parent="GtkDialog">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Initial Device Setup</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="gravity">center</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btn_quit">
|
||||
<property name="label" translatable="yes">Quit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_bottom">100</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="baseline_position">bottom</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes">Hold the button on the device until the blue light is flashing.</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Searching for device</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title" translatable="yes">page0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="baseline_position">bottom</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_devicename_p1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Connecting to LE Paper</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">55</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Connecting to device...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title" translatable="yes">page1</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="baseline_position">bottom</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes">Press the button on the device now!</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSpinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">waiting for reply</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page2</property>
|
||||
<property name="title" translatable="yes">page2</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">btn_quit</action-widget>
|
||||
</action-widgets>
|
||||
</template>
|
||||
</interface>
|
|
@ -0,0 +1,145 @@
|
|||
#!/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 gettext import gettext as _
|
||||
from gi.repository import GObject, Gtk, GdkPixbuf, Gdk
|
||||
|
||||
from .config import Config
|
||||
from .svg import JsonSvg
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
@Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/Drawing.ui')
|
||||
class Drawing(Gtk.EventBox):
|
||||
__gtype_name__ = "Drawing"
|
||||
|
||||
box_toolbar = Gtk.Template.Child()
|
||||
image_svg = Gtk.Template.Child()
|
||||
btn_rotate_left = Gtk.Template.Child()
|
||||
btn_rotate_right = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, json_data, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.orientation = Config.instance().orientation
|
||||
Config.instance().connect('notify::orientation', self._on_orientation_changed)
|
||||
|
||||
self.json_data = json_data
|
||||
self._zoom = 0
|
||||
self.refresh() # sets self.svg
|
||||
|
||||
self.timestamp = self.svg.timestamp
|
||||
self.box_toolbar.set_opacity(0)
|
||||
|
||||
def _on_orientation_changed(self, config, pspec):
|
||||
self.orientation = config.orientation
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.svg = JsonSvg(self.json_data, self.orientation)
|
||||
width, height = -1, -1
|
||||
if 'portrait' in self.orientation:
|
||||
height = 1000
|
||||
else:
|
||||
width = 1000
|
||||
self.pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self.svg.filename,
|
||||
width=width,
|
||||
height=height,
|
||||
preserve_aspect_ratio=True)
|
||||
self.redraw()
|
||||
|
||||
def redraw(self):
|
||||
ratio = self.pixbuf.get_height() / self.pixbuf.get_width()
|
||||
base = 250 + self.zoom * 50
|
||||
if 'portrait' in self.orientation:
|
||||
width = base / ratio
|
||||
height = base
|
||||
else:
|
||||
width = base
|
||||
height = base * ratio
|
||||
pb = self.pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||
self.image_svg.set_from_pixbuf(pb)
|
||||
|
||||
@GObject.Property
|
||||
def name(self):
|
||||
return "drawing"
|
||||
|
||||
@GObject.Property
|
||||
def zoom(self):
|
||||
return self._zoom
|
||||
|
||||
@zoom.setter
|
||||
def zoom(self, zoom):
|
||||
if zoom == self._zoom:
|
||||
return
|
||||
self._zoom = zoom
|
||||
self.redraw()
|
||||
|
||||
@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.set_do_overwrite_confirmation(True)
|
||||
# Translators: the default filename to save to
|
||||
dialog.set_current_name(_('untitled.svg'))
|
||||
|
||||
filter_any = Gtk.FileFilter()
|
||||
# Translators: filter name to show all/any files
|
||||
filter_any.set_name(_('Any files'))
|
||||
filter_any.add_pattern('*')
|
||||
filter_svg = Gtk.FileFilter()
|
||||
# Translators: filter to show svg files only
|
||||
filter_svg.set_name(_('SVG files'))
|
||||
filter_svg.add_pattern('*.svg')
|
||||
dialog.add_filter(filter_svg)
|
||||
dialog.add_filter(filter_any)
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
import shutil
|
||||
file = dialog.get_filename()
|
||||
shutil.copyfile(self.svg.filename, file)
|
||||
# FIXME: error handling
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
@Gtk.Template.Callback('_on_delete_button_clicked')
|
||||
def _on_delete_button_clicked(self, button):
|
||||
Config.instance().delete_drawing(self.timestamp)
|
||||
|
||||
@Gtk.Template.Callback('_on_rotate_button_clicked')
|
||||
def _on_rotate_button_clicked(self, button):
|
||||
if button == self.btn_rotate_left:
|
||||
advance = 1
|
||||
else:
|
||||
advance = 3
|
||||
|
||||
orientations = ['portrait', 'landscape', 'reverse-portrait', 'reverse-landscape'] * 3
|
||||
o = orientations[orientations.index(self.orientation) + advance]
|
||||
self.orientation = o
|
||||
self.refresh()
|
||||
|
||||
@Gtk.Template.Callback('_on_enter')
|
||||
def _on_enter(self, *args):
|
||||
self.box_toolbar.set_opacity(100)
|
||||
|
||||
@Gtk.Template.Callback('_on_leave')
|
||||
def _on_leave(self, drawing, event):
|
||||
if event.detail == Gdk.NotifyType.INFERIOR:
|
||||
return
|
||||
self.box_toolbar.set_opacity(0)
|
|
@ -0,0 +1,184 @@
|
|||
#!/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, Gtk
|
||||
from .drawing import Drawing
|
||||
from .config import Config
|
||||
|
||||
import time
|
||||
import gi
|
||||
import logging
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger('tuhi.gui.drawingperspective')
|
||||
|
||||
|
||||
@Gtk.Template(resource_path="/org/freedesktop/Tuhi/ui/Flowbox.ui")
|
||||
class Flowbox(Gtk.Box):
|
||||
__gtype_name__ = "Flowbox"
|
||||
|
||||
label_date = Gtk.Template.Child()
|
||||
flowbox_drawings = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, timestruct, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.time = timestruct
|
||||
self.label_date.set_text(time.strftime('%B %Y', self.time))
|
||||
|
||||
def insert(self, drawing):
|
||||
# We don't know which order we get drawings from the device, so
|
||||
# let's do a sorted insert here
|
||||
index = 0
|
||||
child = self.flowbox_drawings.get_child_at_index(index)
|
||||
while child is not None:
|
||||
if child.get_child().timestamp < drawing.timestamp:
|
||||
break
|
||||
index += 1
|
||||
child = self.flowbox_drawings.get_child_at_index(index)
|
||||
|
||||
self.flowbox_drawings.insert(drawing, index)
|
||||
|
||||
def delete(self, drawing):
|
||||
def delete_matching_child(child, drawing):
|
||||
if child.get_child() == drawing:
|
||||
self.flowbox_drawings.remove(child)
|
||||
self.flowbox_drawings.foreach(delete_matching_child, drawing)
|
||||
|
||||
@GObject.property
|
||||
def is_empty(self):
|
||||
return not self.flowbox_drawings.get_children()
|
||||
|
||||
|
||||
@Gtk.Template(resource_path="/org/freedesktop/Tuhi/ui/DrawingPerspective.ui")
|
||||
class DrawingPerspective(Gtk.Stack):
|
||||
__gtype_name__ = "DrawingPerspective"
|
||||
|
||||
viewport = Gtk.Template.Child()
|
||||
overlay_undo = Gtk.Template.Child()
|
||||
notification_delete_undo = Gtk.Template.Child()
|
||||
notification_delete_close = Gtk.Template.Child()
|
||||
box_all_drawings = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.known_drawings = {} # type {timestamp: Drawing()}
|
||||
self.flowboxes = {}
|
||||
self._zoom = 0
|
||||
|
||||
def _cache_drawings(self, device, pspec):
|
||||
# The config backend filters duplicates anyway, so don't care here
|
||||
for ts in self.device.drawings_available:
|
||||
json_string = self.device.json(ts)
|
||||
Config.instance().add_drawing(ts, json_string)
|
||||
|
||||
def _update_drawings(self, config, pspec):
|
||||
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']):
|
||||
ts = js['timestamp']
|
||||
if ts in self.known_drawings:
|
||||
continue
|
||||
|
||||
drawing = Drawing(js)
|
||||
self.known_drawings[ts] = drawing
|
||||
|
||||
# Now pick the right monthly flowbox to insert into
|
||||
key = _hash(drawing)
|
||||
try:
|
||||
fb = self.flowboxes[key]
|
||||
except KeyError:
|
||||
fb = Flowbox(time.gmtime(drawing.timestamp))
|
||||
self.flowboxes[key] = fb
|
||||
self.box_all_drawings.add(fb)
|
||||
finally:
|
||||
fb.insert(drawing)
|
||||
|
||||
# Remove deleted drawings
|
||||
deleted = [ts for ts in self.known_drawings if ts not in [js['timestamp'] for js in config.drawings]]
|
||||
for ts in deleted:
|
||||
drawing = self.known_drawings[ts]
|
||||
fb = self.flowboxes[_hash(drawing)]
|
||||
fb.delete(drawing)
|
||||
if fb.is_empty:
|
||||
del self.flowboxes[_hash(drawing)]
|
||||
self.box_all_drawings.remove(fb)
|
||||
del self.known_drawings[ts]
|
||||
self.notification_delete_undo.deleted_drawing = drawing.timestamp
|
||||
self.overlay_undo.set_reveal_child(True)
|
||||
|
||||
@GObject.Property
|
||||
def device(self):
|
||||
return self._device
|
||||
|
||||
@device.setter
|
||||
def device(self, device):
|
||||
self._device = device
|
||||
|
||||
device.connect('notify::connected', self._on_connected)
|
||||
device.connect('notify::listening', self._on_listening_stopped)
|
||||
|
||||
# This is a bit convoluted. We need to cache all drawings
|
||||
# because Tuhi doesn't have guaranteed storage. So any json that
|
||||
# comes in from Tuhi, we pass to our config backend to save
|
||||
# somewhere.
|
||||
# The config backend adds the json file and emits a notify for the
|
||||
# json itself (once cached) that we then actually use for SVG
|
||||
# generation.
|
||||
device.connect('notify::drawings-available', self._cache_drawings)
|
||||
Config.instance().connect('notify::drawings', self._update_drawings)
|
||||
|
||||
self._update_drawings(Config.instance(), None)
|
||||
|
||||
# We always want to sync on startup
|
||||
logger.debug(f'{device.name} - starting to listen')
|
||||
device.start_listening()
|
||||
|
||||
@GObject.Property
|
||||
def name(self):
|
||||
return "drawing_perspective"
|
||||
|
||||
@GObject.Property
|
||||
def zoom(self):
|
||||
return self._zoom
|
||||
|
||||
@zoom.setter
|
||||
def zoom(self, zoom):
|
||||
if zoom == self._zoom:
|
||||
return
|
||||
|
||||
self._zoom = zoom
|
||||
for ts, drawing in self.known_drawings.items():
|
||||
drawing.zoom = zoom
|
||||
|
||||
def _on_connected(self, device, pspec):
|
||||
# Turns out we don't really care about whether the device is
|
||||
# connected or not, it has little effect on how we work here
|
||||
pass
|
||||
|
||||
def _on_listening_stopped(self, device, pspec):
|
||||
if not device.listening:
|
||||
logger.debug(f'{device.name} - listening stopped, restarting')
|
||||
# We never want to stop listening
|
||||
device.start_listening()
|
||||
|
||||
@Gtk.Template.Callback('_on_undo_close_clicked')
|
||||
def _on_undo_close_clicked(self, button):
|
||||
self.overlay_undo.set_reveal_child(False)
|
||||
|
||||
@Gtk.Template.Callback('_on_undo_clicked')
|
||||
def _on_undo_clicked(self, button):
|
||||
Config.instance().undelete_drawing(button.deleted_drawing)
|
||||
self.overlay_undo.set_reveal_child(False)
|
|
@ -0,0 +1,89 @@
|
|||
#!/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 xdg.BaseDirectory
|
||||
import svgwrite
|
||||
import os
|
||||
|
||||
DATA_PATH = os.path.join(xdg.BaseDirectory.xdg_data_home, 'tuhi', 'svg')
|
||||
|
||||
|
||||
class JsonSvg(GObject.Object):
|
||||
def __init__(self, json, orientation, *args, **kwargs):
|
||||
self.json = json
|
||||
try:
|
||||
os.mkdir(DATA_PATH)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
self.timestamp = json['timestamp']
|
||||
self.filename = os.path.join(DATA_PATH, f'{self.timestamp}.svg')
|
||||
self.orientation = orientation
|
||||
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] / 100, dimensions[1] / 100
|
||||
|
||||
if self.orientation in ['portrait', 'reverse-Portrait']:
|
||||
size = (height, width)
|
||||
else:
|
||||
size = (width, height)
|
||||
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 / 100, y / 100
|
||||
|
||||
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, yp),
|
||||
end=(x, y),
|
||||
stroke_width=stroke_width,
|
||||
style='fill:none'
|
||||
)
|
||||
)
|
||||
g.add(lines)
|
||||
|
||||
svg.add(g)
|
||||
svg.save()
|
|
@ -0,0 +1,397 @@
|
|||
#!/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, Gio, GLib
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import xdg.BaseDirectory
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger('tuhi.gui.dbus')
|
||||
|
||||
CONFIG_PATH = os.path.join(xdg.BaseDirectory.xdg_data_home, 'tuhi-kete')
|
||||
|
||||
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'
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# this is not handled asynchronously because if we fail to
|
||||
# get the session bus, we have other issues
|
||||
if _DBusObject._connection is None:
|
||||
self._connect_to_session()
|
||||
|
||||
self.interface = interface
|
||||
self.objpath = objpath
|
||||
self._online = False
|
||||
self._name = name
|
||||
try:
|
||||
self._connect()
|
||||
except DBusError:
|
||||
self._reconnect_timer = GObject.timeout_add_seconds(2, self._on_reconnect_timer)
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
self.proxy = Gio.DBusProxy.new_sync(self._connection,
|
||||
Gio.DBusProxyFlags.NONE, None,
|
||||
self._name, self.objpath,
|
||||
self.interface, None)
|
||||
if self.proxy.get_name_owner() is None:
|
||||
raise DBusError(f'No-one is handling {self._name}, is the daemon running?')
|
||||
|
||||
self._online = True
|
||||
self.notify('online')
|
||||
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
|
||||
|
||||
self.proxy.connect('g-properties-changed', self._on_properties_changed)
|
||||
self.proxy.connect('g-signal', self._on_signal_received)
|
||||
|
||||
def _on_reconnect_timer(self):
|
||||
try:
|
||||
logger.debug('reconnecting')
|
||||
self._connect()
|
||||
return False
|
||||
except DBusError:
|
||||
return True
|
||||
|
||||
@GObject.Property
|
||||
def online(self):
|
||||
return self._online
|
||||
|
||||
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):
|
||||
__gsignals__ = {
|
||||
'button-press-required':
|
||||
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
'registered':
|
||||
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
}
|
||||
|
||||
def __init__(self, manager, objpath):
|
||||
_DBusObject.__init__(self, TUHI_DBUS_NAME,
|
||||
ORG_FREEDESKTOP_TUHI1_DEVICE,
|
||||
objpath)
|
||||
self.manager = manager
|
||||
self.is_registering = False
|
||||
self._bluez_device = BlueZDevice(self.property('BlueZDevice'))
|
||||
self._bluez_device.connect('notify::connected', self._on_connected)
|
||||
self._sync_state = 0
|
||||
|
||||
@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 dimensions(self):
|
||||
return self.property('Dimensions')
|
||||
|
||||
@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
|
||||
|
||||
@GObject.Property
|
||||
def sync_state(self):
|
||||
return self._sync_state
|
||||
|
||||
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 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')
|
||||
self.emit('button-press-required', self)
|
||||
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':
|
||||
self._sync_state = parameters[0]
|
||||
self.notify('sync-state')
|
||||
|
||||
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')
|
||||
self.emit('registered', self)
|
||||
|
||||
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,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
_DBusObject.__init__(self, TUHI_DBUS_NAME,
|
||||
ORG_FREEDESKTOP_TUHI1_MANAGER,
|
||||
ROOT_PATH)
|
||||
|
||||
self._devices = {}
|
||||
self._unregistered_devices = {}
|
||||
logger.info('starting up')
|
||||
|
||||
if not self.online:
|
||||
self.connect('notify::online', self._init)
|
||||
else:
|
||||
self._init()
|
||||
|
||||
def _init(self, *args, **kwargs):
|
||||
logger.info('manager is online')
|
||||
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):
|
||||
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 = {}
|
||||
|
||||
def terminate(self):
|
||||
for dev in self._devices.values():
|
||||
dev.terminate()
|
||||
self._devices = {}
|
||||
self._unregistered_devices = {}
|
||||
super(TuhiKeteManager, self).terminate()
|
||||
|
||||
def _on_properties_changed(self, proxy, changed_props, invalidated_props):
|
||||
if changed_props is None:
|
||||
return
|
||||
|
||||
changed_props = changed_props.unpack()
|
||||
|
||||
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 _handle_unregistered_device(self, objpath):
|
||||
for addr, dev in self._devices.items():
|
||||
if dev.objpath == objpath:
|
||||
self.emit('unregistered-device', dev)
|
||||
return
|
||||
|
||||
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]
|
|
@ -0,0 +1,243 @@
|
|||
#!/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 gettext import gettext as _
|
||||
from gi.repository import Gtk, Gio, GLib, GObject
|
||||
|
||||
from .drawingperspective import DrawingPerspective
|
||||
from .tuhi import TuhiKeteManager
|
||||
from .config import Config
|
||||
|
||||
import logging
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
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):
|
||||
'''
|
||||
The page loaded when we cannot connect to the Tuhi DBus server.
|
||||
'''
|
||||
__gtype_name__ = "ErrorPerspective"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@GObject.Property
|
||||
def name(self):
|
||||
return "error_perspective"
|
||||
|
||||
|
||||
@Gtk.Template(resource_path="/org/freedesktop/Tuhi/ui/SetupPerspective.ui")
|
||||
class SetupDialog(Gtk.Dialog):
|
||||
'''
|
||||
The setup dialog when we don't yet have a registered device with Tuhi.
|
||||
'''
|
||||
__gtype_name__ = "SetupDialog"
|
||||
__gsignals__ = {
|
||||
'new-device':
|
||||
(GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
||||
}
|
||||
|
||||
stack = Gtk.Template.Child()
|
||||
label_devicename_p1 = Gtk.Template.Child()
|
||||
btn_quit = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, tuhi, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._tuhi = tuhi
|
||||
self._sig = tuhi.connect('unregistered-device', self._on_unregistered_device)
|
||||
tuhi.start_search()
|
||||
self.device = None
|
||||
|
||||
def _on_unregistered_device(self, tuhi, device):
|
||||
tuhi.disconnect(self._sig)
|
||||
|
||||
self.label_devicename_p1.set_text(_(f'Connecting to {device.name}'))
|
||||
self.stack.set_visible_child_name('page1')
|
||||
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)
|
||||
|
||||
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)
|
||||
self.device = device
|
||||
self.response(Gtk.ResponseType.OK)
|
||||
|
||||
@GObject.Property
|
||||
def name(self):
|
||||
return "setup_dialog"
|
||||
|
||||
|
||||
@Gtk.Template(resource_path='/org/freedesktop/Tuhi/ui/MainWindow.ui')
|
||||
class MainWindow(Gtk.ApplicationWindow):
|
||||
__gtype_name__ = 'MainWindow'
|
||||
|
||||
stack_perspectives = Gtk.Template.Child()
|
||||
headerbar = Gtk.Template.Child()
|
||||
menubutton1 = Gtk.Template.Child()
|
||||
spinner_sync = Gtk.Template.Child()
|
||||
image_battery = Gtk.Template.Child()
|
||||
image_missing_tablet = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.maximize()
|
||||
self._tuhi = TuhiKeteManager()
|
||||
|
||||
action = Gio.SimpleAction.new_stateful('orientation', GLib.VariantType('s'),
|
||||
GLib.Variant('s', 'landscape'))
|
||||
action.connect('activate', self._on_orientation_changed)
|
||||
action.set_state(GLib.Variant.new_string(Config.instance().orientation))
|
||||
self.add_action(action)
|
||||
|
||||
builder = Gtk.Builder.new_from_string(MENU_XML, -1)
|
||||
menu = builder.get_object("primary-menu")
|
||||
self.menubutton1.set_menu_model(menu)
|
||||
|
||||
ep = ErrorPerspective()
|
||||
self._add_perspective(ep)
|
||||
self.stack_perspectives.set_visible_child_name(ep.name)
|
||||
|
||||
# the dbus bindings need more async...
|
||||
if not self._tuhi.online:
|
||||
self._tuhi.connect('notify::online', self._on_dbus_online)
|
||||
else:
|
||||
self._on_dbus_online()
|
||||
|
||||
def _on_dbus_online(self, *args, **kwargs):
|
||||
logger.debug('dbus is online')
|
||||
|
||||
dp = DrawingPerspective()
|
||||
self._add_perspective(dp)
|
||||
self.headerbar.set_title(f'Tuhi')
|
||||
self.stack_perspectives.set_visible_child_name(dp.name)
|
||||
|
||||
if not self._tuhi.devices:
|
||||
dialog = SetupDialog(self._tuhi)
|
||||
dialog.set_transient_for(self)
|
||||
dialog.connect('response', self._on_setup_dialog_closed)
|
||||
dialog.show()
|
||||
else:
|
||||
device = self._tuhi.devices[0]
|
||||
self._init_device(device)
|
||||
dp.device = device
|
||||
self.headerbar.set_title(f'Tuhi - {dp.device.name}')
|
||||
|
||||
def _init_device(self, device):
|
||||
device.connect('notify::sync-state', self._on_sync_state)
|
||||
device.connect('notify::battery-percent', self._on_battery_changed)
|
||||
device.connect('notify::battery-state', self._on_battery_changed)
|
||||
self._on_battery_changed(device, None)
|
||||
|
||||
def _on_battery_changed(self, device, pspec):
|
||||
if device.battery_percent > 80:
|
||||
fill = 'full'
|
||||
elif device.battery_percent > 40:
|
||||
fill = 'good'
|
||||
elif device.battery_percent > 10:
|
||||
fill = 'low'
|
||||
else:
|
||||
fill = 'caution'
|
||||
|
||||
if device.battery_state == 1:
|
||||
state = '-charging'
|
||||
elif device.battery_state == 0: # unknown
|
||||
fill = 'missing'
|
||||
state = ''
|
||||
else:
|
||||
state = ''
|
||||
batt_icon_name = f'battery-{fill}{state}-symbolic'
|
||||
_, isize = self.image_battery.get_icon_name()
|
||||
self.image_battery.set_from_icon_name(batt_icon_name, isize)
|
||||
self.image_battery.set_tooltip_text(f'{device.battery_percent}%')
|
||||
|
||||
def _on_sync_state(self, device, pspec):
|
||||
self.image_missing_tablet.set_visible(False)
|
||||
if device.sync_state:
|
||||
self.spinner_sync.start()
|
||||
else:
|
||||
self.spinner_sync.stop()
|
||||
|
||||
def _on_setup_dialog_closed(self, dialog, response):
|
||||
device = dialog.device
|
||||
dialog.destroy()
|
||||
|
||||
if response != Gtk.ResponseType.OK or device is None:
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
logger.debug('device was registered')
|
||||
self.headerbar.set_title(f'Tuhi - {device.name}')
|
||||
|
||||
dp = self._get_child('drawing_perspective')
|
||||
dp.device = device
|
||||
self._init_device(device)
|
||||
self.stack_perspectives.set_visible_child_name(dp.name)
|
||||
|
||||
def _add_perspective(self, perspective):
|
||||
self.stack_perspectives.add_named(perspective, perspective.name)
|
||||
|
||||
def _get_child(self, name):
|
||||
return self.stack_perspectives.get_child_by_name(name)
|
||||
|
||||
def _on_reconnect_tuhi(self, tuhi):
|
||||
self._tuhi = tuhi
|
||||
|
||||
def _on_orientation_changed(self, action, label):
|
||||
action.set_state(label)
|
||||
Config.instance().orientation = label.get_string() # this is a GVariant
|
||||
|
||||
@Gtk.Template.Callback('_on_zoom_changed')
|
||||
def _on_zoom_changed(self, adjustment):
|
||||
dp = self._get_child('drawing_perspective')
|
||||
dp.zoom = int(adjustment.get_value())
|
Loading…
Reference in New Issue