Using the actual monitor width in pixels can lead to unsatisfying results, depending on the display orientation and physical size: on a 10" tablet with a 1280x800 resolution (scale 1), portrait orientation will be using the narrow layout. If the keyboard is sized in an optimal way (i.e. so the layout fills the whole area, with no blanks on the sides) this would result in an unnecessarily huge keyboard being displayed, therefore wasting screen estate. Using the virtual display size gives a hint about the physical size of the device, and can be used to select wide layouts even in portrait mode, while still preserving current behavior on HiDPI devices. This has been tested on PineTab, PinePhone and Librem 5.
432 lines
14 KiB
C
432 lines
14 KiB
C
/*
|
|
* Copyright (C) 2010-2011 Daiki Ueno <ueno@unixuser.org>
|
|
* Copyright (C) 2010-2011 Red Hat, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public License
|
|
* as published by the Free Software Foundation; either version 2 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA
|
|
*/
|
|
|
|
/**
|
|
* SECTION:eek-gtk-keyboard
|
|
* @short_description: a #GtkWidget displaying #EekKeyboard
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include "eek-renderer.h"
|
|
#include "eek-keyboard.h"
|
|
|
|
#include "eek-gtk-keyboard.h"
|
|
|
|
#include "eekboard/eekboard-context-service.h"
|
|
#include "src/layout.h"
|
|
#include "src/submission.h"
|
|
|
|
#define LIBFEEDBACK_USE_UNSTABLE_API
|
|
#include <libfeedback.h>
|
|
|
|
#define SQUEEKBOARD_APP_ID "sm.puri.squeekboard"
|
|
|
|
typedef struct _EekGtkKeyboardPrivate
|
|
{
|
|
EekRenderer *renderer; // owned, nullable
|
|
EekboardContextService *eekboard_context; // unowned reference
|
|
struct submission *submission; // unowned reference
|
|
|
|
struct squeek_layout_state *layout; // unowned
|
|
LevelKeyboard *keyboard; // unowned reference; it's kept in server-context
|
|
|
|
GdkEventSequence *sequence; // unowned reference
|
|
LfbEvent *event;
|
|
} EekGtkKeyboardPrivate;
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE (EekGtkKeyboard, eek_gtk_keyboard, GTK_TYPE_DRAWING_AREA)
|
|
|
|
static void
|
|
eek_gtk_keyboard_real_realize (GtkWidget *self)
|
|
{
|
|
gtk_widget_set_events (self,
|
|
GDK_EXPOSURE_MASK |
|
|
GDK_KEY_PRESS_MASK |
|
|
GDK_KEY_RELEASE_MASK |
|
|
GDK_BUTTON_PRESS_MASK |
|
|
GDK_BUTTON_RELEASE_MASK |
|
|
GDK_BUTTON_MOTION_MASK |
|
|
GDK_TOUCH_MASK);
|
|
|
|
GTK_WIDGET_CLASS (eek_gtk_keyboard_parent_class)->realize (self);
|
|
}
|
|
|
|
static gboolean
|
|
eek_gtk_keyboard_real_draw (GtkWidget *self,
|
|
cairo_t *cr)
|
|
{
|
|
EekGtkKeyboardPrivate *priv =
|
|
eek_gtk_keyboard_get_instance_private (EEK_GTK_KEYBOARD (self));
|
|
GtkAllocation allocation;
|
|
gtk_widget_get_allocation (self, &allocation);
|
|
|
|
if (!priv->keyboard) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (!priv->renderer) {
|
|
PangoContext *pcontext = gtk_widget_get_pango_context (self);
|
|
|
|
priv->renderer = eek_renderer_new (
|
|
priv->keyboard,
|
|
pcontext);
|
|
|
|
eek_renderer_set_allocation_size (priv->renderer,
|
|
priv->keyboard->layout,
|
|
allocation.width,
|
|
allocation.height);
|
|
eek_renderer_set_scale_factor (priv->renderer,
|
|
gtk_widget_get_scale_factor (self));
|
|
}
|
|
|
|
eek_renderer_render_keyboard (priv->renderer, priv->submission, cr, priv->keyboard);
|
|
return FALSE;
|
|
}
|
|
|
|
// Units of virtual pixels size
|
|
static enum squeek_arrangement_kind get_type(uint32_t width, uint32_t height) {
|
|
(void)height;
|
|
if (width < 540) {
|
|
return ARRANGEMENT_KIND_BASE;
|
|
}
|
|
return ARRANGEMENT_KIND_WIDE;
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_real_size_allocate (GtkWidget *self,
|
|
GtkAllocation *allocation)
|
|
{
|
|
EekGtkKeyboardPrivate *priv =
|
|
eek_gtk_keyboard_get_instance_private (EEK_GTK_KEYBOARD (self));
|
|
// check if the change would switch types
|
|
enum squeek_arrangement_kind new_type = get_type(
|
|
(uint32_t)(allocation->width - allocation->x),
|
|
(uint32_t)(allocation->height - allocation->y));
|
|
if (priv->layout->arrangement != new_type) {
|
|
priv->layout->arrangement = new_type;
|
|
uint32_t time = gdk_event_get_time(NULL);
|
|
eekboard_context_service_use_layout(priv->eekboard_context, priv->layout, time);
|
|
}
|
|
|
|
if (priv->renderer)
|
|
eek_renderer_set_allocation_size (priv->renderer,
|
|
priv->keyboard->layout,
|
|
allocation->width,
|
|
allocation->height);
|
|
|
|
GTK_WIDGET_CLASS (eek_gtk_keyboard_parent_class)->
|
|
size_allocate (self, allocation);
|
|
}
|
|
|
|
static void
|
|
on_event_triggered (LfbEvent *event,
|
|
GAsyncResult *res,
|
|
gpointer unused)
|
|
{
|
|
g_autoptr (GError) err = NULL;
|
|
|
|
if (!lfb_event_trigger_feedback_finish (event, res, &err)) {
|
|
g_warning ("Failed to trigger feedback for '%s': %s",
|
|
lfb_event_get_event (event), err->message);
|
|
}
|
|
}
|
|
|
|
static void depress(EekGtkKeyboard *self,
|
|
gdouble x, gdouble y, guint32 time)
|
|
{
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (self);
|
|
if (!priv->keyboard) {
|
|
return;
|
|
}
|
|
squeek_layout_depress(priv->keyboard->layout,
|
|
priv->submission,
|
|
x, y, eek_renderer_get_transformation(priv->renderer), time, self);
|
|
}
|
|
|
|
static void drag(EekGtkKeyboard *self,
|
|
gdouble x, gdouble y, guint32 time)
|
|
{
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (self);
|
|
if (!priv->keyboard) {
|
|
return;
|
|
}
|
|
squeek_layout_drag(eekboard_context_service_get_keyboard(priv->eekboard_context)->layout,
|
|
priv->submission,
|
|
x, y, eek_renderer_get_transformation(priv->renderer), time,
|
|
priv->eekboard_context, self);
|
|
}
|
|
|
|
static void release(EekGtkKeyboard *self, guint32 time)
|
|
{
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (self);
|
|
if (!priv->keyboard) {
|
|
return;
|
|
}
|
|
squeek_layout_release(eekboard_context_service_get_keyboard(priv->eekboard_context)->layout,
|
|
priv->submission,
|
|
eek_renderer_get_transformation(priv->renderer), time,
|
|
priv->eekboard_context, self);
|
|
}
|
|
|
|
static gboolean
|
|
eek_gtk_keyboard_real_button_press_event (GtkWidget *self,
|
|
GdkEventButton *event)
|
|
{
|
|
if (event->type == GDK_BUTTON_PRESS && event->button == 1) {
|
|
depress(EEK_GTK_KEYBOARD(self), event->x, event->y, event->time);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
// TODO: this belongs more in gtk_keyboard, with a way to find out which key to re-render
|
|
static gboolean
|
|
eek_gtk_keyboard_real_button_release_event (GtkWidget *self,
|
|
GdkEventButton *event)
|
|
{
|
|
if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
|
|
// TODO: can the event have different coords than the previous move event?
|
|
release(EEK_GTK_KEYBOARD(self), event->time);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
eek_gtk_keyboard_leave_event (GtkWidget *self,
|
|
GdkEventCrossing *event)
|
|
{
|
|
if (event->type == GDK_LEAVE_NOTIFY) {
|
|
// TODO: can the event have different coords than the previous move event?
|
|
release(EEK_GTK_KEYBOARD(self), event->time);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
eek_gtk_keyboard_real_motion_notify_event (GtkWidget *self,
|
|
GdkEventMotion *event)
|
|
{
|
|
if (event->state & GDK_BUTTON1_MASK) {
|
|
drag(EEK_GTK_KEYBOARD(self), event->x, event->y, event->time);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
// Only one touch stream at a time allowed. Others will be completely ignored.
|
|
static gboolean
|
|
handle_touch_event (GtkWidget *widget,
|
|
GdkEventTouch *event)
|
|
{
|
|
EekGtkKeyboard *self = EEK_GTK_KEYBOARD (widget);
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (self);
|
|
|
|
/* For each new touch, release the previous one and record the new event
|
|
sequence. */
|
|
if (event->type == GDK_TOUCH_BEGIN) {
|
|
release(self, event->time);
|
|
priv->sequence = event->sequence;
|
|
depress(self, event->x, event->y, event->time);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Only allow the latest touch point to be dragged. */
|
|
if (event->type == GDK_TOUCH_UPDATE && event->sequence == priv->sequence) {
|
|
drag(self, event->x, event->y, event->time);
|
|
}
|
|
else if (event->type == GDK_TOUCH_END || event->type == GDK_TOUCH_CANCEL) {
|
|
// TODO: can the event have different coords than the previous update event?
|
|
/* Only respond to the release of the latest touch point. Previous
|
|
touches have already been released. */
|
|
if (event->sequence == priv->sequence) {
|
|
release(self, event->time);
|
|
priv->sequence = NULL;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_real_unmap (GtkWidget *self)
|
|
{
|
|
EekGtkKeyboardPrivate *priv =
|
|
eek_gtk_keyboard_get_instance_private (EEK_GTK_KEYBOARD (self));
|
|
|
|
if (priv->keyboard) {
|
|
squeek_layout_release_all_only(
|
|
priv->keyboard->layout,
|
|
priv->submission,
|
|
gdk_event_get_time(NULL));
|
|
}
|
|
|
|
GTK_WIDGET_CLASS (eek_gtk_keyboard_parent_class)->unmap (self);
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
(void)value;
|
|
switch (prop_id) {
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_dispose (GObject *object)
|
|
{
|
|
EekGtkKeyboard *self = EEK_GTK_KEYBOARD (object);
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (self);
|
|
|
|
if (priv->renderer) {
|
|
eek_renderer_free(priv->renderer);
|
|
priv->renderer = NULL;
|
|
priv->renderer = NULL;
|
|
}
|
|
|
|
if (priv->keyboard) {
|
|
squeek_layout_release_all_only(
|
|
priv->keyboard->layout,
|
|
priv->submission,
|
|
gdk_event_get_time(NULL));
|
|
priv->keyboard = NULL;
|
|
}
|
|
|
|
if (priv->event) {
|
|
g_clear_object (&priv->event);
|
|
lfb_uninit ();
|
|
}
|
|
|
|
G_OBJECT_CLASS (eek_gtk_keyboard_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_class_init (EekGtkKeyboardClass *klass)
|
|
{
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
widget_class->realize = eek_gtk_keyboard_real_realize;
|
|
widget_class->unmap = eek_gtk_keyboard_real_unmap;
|
|
widget_class->draw = eek_gtk_keyboard_real_draw;
|
|
widget_class->size_allocate = eek_gtk_keyboard_real_size_allocate;
|
|
widget_class->button_press_event =
|
|
eek_gtk_keyboard_real_button_press_event;
|
|
widget_class->button_release_event =
|
|
eek_gtk_keyboard_real_button_release_event;
|
|
widget_class->motion_notify_event =
|
|
eek_gtk_keyboard_real_motion_notify_event;
|
|
widget_class->leave_notify_event =
|
|
eek_gtk_keyboard_leave_event;
|
|
|
|
widget_class->touch_event = handle_touch_event;
|
|
|
|
gobject_class->set_property = eek_gtk_keyboard_set_property;
|
|
gobject_class->dispose = eek_gtk_keyboard_dispose;
|
|
}
|
|
|
|
static void
|
|
eek_gtk_keyboard_init (EekGtkKeyboard *self)
|
|
{
|
|
EekGtkKeyboardPrivate *priv = eek_gtk_keyboard_get_instance_private (EEK_GTK_KEYBOARD (self));
|
|
g_autoptr(GError) err = NULL;
|
|
|
|
if (lfb_init(SQUEEKBOARD_APP_ID, &err))
|
|
priv->event = lfb_event_new ("button-pressed");
|
|
else
|
|
g_warning ("Failed to init libfeedback: %s", err->message);
|
|
|
|
GtkIconTheme *theme = gtk_icon_theme_get_default ();
|
|
|
|
gtk_icon_theme_add_resource_path (theme, "/sm/puri/squeekboard/icons");
|
|
}
|
|
|
|
static void
|
|
on_notify_keyboard (GObject *object,
|
|
GParamSpec *spec,
|
|
EekGtkKeyboard *self) {
|
|
(void)spec;
|
|
EekGtkKeyboardPrivate *priv = (EekGtkKeyboardPrivate*)eek_gtk_keyboard_get_instance_private (self);
|
|
priv->keyboard = eekboard_context_service_get_keyboard(EEKBOARD_CONTEXT_SERVICE(object));
|
|
if (priv->renderer) {
|
|
eek_renderer_free(priv->renderer);
|
|
}
|
|
priv->renderer = NULL;
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
}
|
|
|
|
/**
|
|
* Create a new #GtkWidget displaying @keyboard.
|
|
* Returns: a #GtkWidget
|
|
*/
|
|
GtkWidget *
|
|
eek_gtk_keyboard_new (EekboardContextService *eekservice,
|
|
struct submission *submission,
|
|
struct squeek_layout_state *layout)
|
|
{
|
|
EekGtkKeyboard *ret = EEK_GTK_KEYBOARD(g_object_new (EEK_TYPE_GTK_KEYBOARD, NULL));
|
|
EekGtkKeyboardPrivate *priv = (EekGtkKeyboardPrivate*)eek_gtk_keyboard_get_instance_private (ret);
|
|
priv->eekboard_context = eekservice;
|
|
priv->submission = submission;
|
|
priv->layout = layout;
|
|
priv->renderer = NULL;
|
|
g_signal_connect (eekservice,
|
|
"notify::keyboard",
|
|
G_CALLBACK(on_notify_keyboard),
|
|
ret);
|
|
on_notify_keyboard(G_OBJECT(eekservice), NULL, ret);
|
|
/* TODO: this is how a compound keyboard
|
|
* made out of a layout and a suggestion bar could start.
|
|
* GtkBox *box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
|
|
GtkEntry *fill = GTK_ENTRY(gtk_entry_new());
|
|
gtk_box_pack_start(box, GTK_WIDGET(fill), FALSE, FALSE, 0);
|
|
gtk_box_pack_start(box, GTK_WIDGET(ret), TRUE, TRUE, 0);
|
|
return GTK_WIDGET(box);*/
|
|
return GTK_WIDGET(ret);
|
|
}
|
|
|
|
/**
|
|
* eek_gtk_keyboard_emit_feedback:
|
|
*
|
|
* Emit button press haptic feedback via libfeedack.
|
|
*/
|
|
void
|
|
eek_gtk_keyboard_emit_feedback (EekGtkKeyboard *self)
|
|
{
|
|
EekGtkKeyboardPrivate *priv;
|
|
|
|
g_return_if_fail (EEK_IS_GTK_KEYBOARD (self));
|
|
|
|
priv = eek_gtk_keyboard_get_instance_private (EEK_GTK_KEYBOARD (self));
|
|
if (priv->event) {
|
|
lfb_event_trigger_feedback_async (priv->event,
|
|
NULL,
|
|
(GAsyncReadyCallback)on_event_triggered,
|
|
NULL);
|
|
}
|
|
}
|