Commit a3bda8e0 authored by Guido Gunther's avatar Guido Gunther

Add "cycling" buttons

These buttons cycle the symbols that are displayed on the button.

This can later be folded back into HdyDialerButton.
parent 837ddd0f
......@@ -38,6 +38,7 @@
<title>Widgets and Objects</title>
<xi:include href="xml/hdy-dialer.xml"/>
<xi:include href="xml/hdy-dialer-button.xml"/>
<xi:include href="xml/hdy-dialer-cycle-button.xml"/>
</chapter>
<chapter id="helpers">
......
......@@ -22,6 +22,7 @@ G_BEGIN_DECLS
#define HANDY_INSIDE
#include "hdy-version.h"
#include "hdy-dialer-button.h"
#include "hdy-dialer-cycle-button.h"
#include "hdy-dialer.h"
#include "hdy-string-utf8.h"
#undef HANDY_INSIDE
......
......@@ -208,4 +208,3 @@ hdy_dialer_button_get_letters(HdyDialerButton *self)
g_return_val_if_fail (HDY_IS_DIALER_BUTTON (self), NULL);
return priv->letters;
}
/*
* Copyright (C) 2017 Purism SPC
*
* SPDX-License-Identifier: LGPL-3.0+
*/
#include <glib/gi18n.h>
#include "hdy-dialer-cycle-button.h"
/**
* SECTION:hdy-dialer-cycle-button
* @short_description: A button on a #HdyDialer keypad cycling through available symbols
* @Title: HdyDialerCycleButton
*
* The #HdyDialerCycleButton widget is a single button on a #HdyDialer
* representing symbols such as regular letters or symbols like #, +
* or ☃. When the button is pressed multiple times in a row, the
* symbols are cycled through. That is a call to #get_curent_symbol
* returns another symbol each time the button is pressed. If no
* further button presses are received cycling mode ends after a
* timeout. This is configurable via the
* #HdyDialerCycleButton:cycle-timeout property.
*/
typedef struct
{
int num; /* number of button presses in the cycle */
guint32 source_id; /* timeout handler id */
gint timeout; /* timeout between button presses */
} HdyDialerCycleButtonPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (HdyDialerCycleButton, hdy_dialer_cycle_button, HDY_TYPE_DIALER_BUTTON)
enum {
PROP_0 = 0,
PROP_CYCLE_TIMEOUT,
PROP_LAST_PROP,
};
static GParamSpec *props[PROP_LAST_PROP] = { NULL, };
enum {
CYCLE_START = 0,
CYCLE_END,
LAST_SIGNAL,
};
static guint signals [LAST_SIGNAL];
static void
end_cycle(HdyDialerCycleButton *self)
{
HdyDialerCycleButtonPrivate *priv =
hdy_dialer_cycle_button_get_instance_private(HDY_DIALER_CYCLE_BUTTON (self));
priv->num = 0;
priv->source_id = 0;
g_signal_emit (self, signals[CYCLE_END], 0);
}
static gboolean
expire_cb (HdyDialerCycleButton *self)
{
g_return_val_if_fail (HDY_IS_DIALER_CYCLE_BUTTON (self), FALSE);
end_cycle (self);
return FALSE;
}
static gboolean
button_clicked_cb(HdyDialerCycleButton *self, GdkEventButton *event)
{
HdyDialerCycleButtonPrivate *priv =
hdy_dialer_cycle_button_get_instance_private(HDY_DIALER_CYCLE_BUTTON (self));
g_return_val_if_fail (HDY_IS_DIALER_CYCLE_BUTTON (self), FALSE);
/* Only cycle if we have more than one symbol */
if (!strlen(hdy_dialer_button_get_letters (HDY_DIALER_BUTTON (self))))
return FALSE;
if (hdy_dialer_cycle_button_is_cycling(self)) {
g_source_remove (priv->source_id);
priv->num++;
} else {
g_signal_emit (self, signals[CYCLE_START], 0);
}
priv->source_id = g_timeout_add (priv->timeout,
(GSourceFunc) expire_cb,
self);
return FALSE;
}
static void
hdy_dialer_cycle_button_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
HdyDialerCycleButton *self = HDY_DIALER_CYCLE_BUTTON (object);
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
switch (property_id) {
case PROP_CYCLE_TIMEOUT:
priv->timeout = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
hdy_dialer_cycle_button_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
HdyDialerCycleButton *self = HDY_DIALER_CYCLE_BUTTON (object);
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
switch (property_id) {
case PROP_CYCLE_TIMEOUT:
g_value_set_int (value, priv->timeout);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
hdy_dialer_cycle_button_dispose (GObject *object)
{
HdyDialerCycleButton *self = HDY_DIALER_CYCLE_BUTTON (object);
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
GObjectClass *parent_class = G_OBJECT_CLASS (hdy_dialer_cycle_button_parent_class);
if (priv->source_id) {
g_source_remove (priv->source_id);
priv->source_id = 0;
}
if (parent_class)
parent_class->dispose (object);
}
static void
hdy_dialer_cycle_button_class_init (HdyDialerCycleButtonClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = hdy_dialer_cycle_button_dispose;
object_class->set_property = hdy_dialer_cycle_button_set_property;
object_class->get_property = hdy_dialer_cycle_button_get_property;
props[PROP_CYCLE_TIMEOUT] = g_param_spec_int ("cycle-timeout",
"the cycle timeout",
"The timeout (in seconds) between button "
"presses after which a cycle ends",
0,
G_MAXINT,
1000,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
/**
* HdyDialerCycleButton::cycle-start:
* @self: The #HdyDialer instance.
*
* This signal is emitted when the button starts cycling (that is on
* the first button press).
*/
signals [CYCLE_START] =
g_signal_new ("cycle-start",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (HdyDialerCycleButtonClass, cycle_start),
NULL, NULL, NULL,
G_TYPE_NONE,
0);
/**
* HdyDialerCycleButton::cycle-end:
* @self: The #HdyDialer instance.
*
* This signal is emitted when the cycle ends. This can either be
* because of timeout or because #hdy_dialer_cycle_stop_cycle got
* called.
*/
signals [CYCLE_END] =
g_signal_new ("cycle-end",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (HdyDialerCycleButtonClass, cycle_end),
NULL, NULL, NULL,
G_TYPE_NONE,
0);
}
/**
* hdy_dialer_cycle_button_new:
* @symbols: the symbols displayed on the #HdyDialerCycleButton
*
* Create a new #HdyDialerCycleButton which displays @symbols. The
* symbols can by cycled through by pressing the button multiple
* times.
*
* Returns: the newly created #HdyDialerCycleButton widget
*/
GtkWidget *hdy_dialer_cycle_button_new (const gchar* symbols)
{
/* FIXME: we should call this 'symbols' in the base class already */
return g_object_new (HDY_TYPE_DIALER_CYCLE_BUTTON, "letters", symbols, NULL);
}
static void
hdy_dialer_cycle_button_init (HdyDialerCycleButton *self)
{
end_cycle (self);
g_signal_connect(self, "clicked", G_CALLBACK (button_clicked_cb), NULL);
}
/**
* hdy_dialer_cycle_button_get_current_symbol:
* @self: a #HdyDialerCycleButton
*
* Get the cycle the dialer should display
*
* Returns: a pointer to the symbol
*
*/
gunichar
hdy_dialer_cycle_button_get_current_symbol (HdyDialerCycleButton *self)
{
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
const gchar *symbols = hdy_dialer_button_get_letters (HDY_DIALER_BUTTON (self));
gint off = priv->num % g_utf8_strlen(symbols, -1);
return g_utf8_get_char(g_utf8_offset_to_pointer(symbols, off));
}
/**
* hdy_dialer_cycle_button_is_cycling:
* @self: a #HdyDialerCycleButton
*
* Check whether the button is in cycling mode.
*
* Returns: #TRUE if the in cycling mode otherwise #FALSE
*
*/
gboolean
hdy_dialer_cycle_button_is_cycling (HdyDialerCycleButton *self)
{
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
return !!priv->source_id;
}
/**
* hdy_dialer_cycle_button_stop_cycle
* @self: a #HdyDialerCycleButton
*
* Stop the cycling mode.
*
*/
void
hdy_dialer_cycle_button_stop_cycle (HdyDialerCycleButton *self)
{
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
g_return_if_fail (HDY_IS_DIALER_CYCLE_BUTTON (self));
if (priv->source_id)
g_source_remove (priv->source_id);
end_cycle(self);
}
/**
* hdy_dialer_cycle_button_get_cycle_timeout
* @self: a #HdyDialerCycleButton
*
* Get the cycle timeout in milliseconds.
*/
gint
hdy_dialer_cycle_button_get_cycle_timeout (HdyDialerCycleButton *self)
{
HdyDialerCycleButtonPrivate *priv = hdy_dialer_cycle_button_get_instance_private(self);
g_return_val_if_fail (HDY_IS_DIALER_CYCLE_BUTTON (self), 0);
return priv->timeout;
}
/**
* hdy_dialer_cycle_button_set_cycle_timeout
* @self: a #HdyDialerCycleButton
*
* Set the cycle timeout in milliseconds.
*/
void
hdy_dialer_cycle_button_set_cycle_timeout (HdyDialerCycleButton *self, gint timeout)
{
g_return_if_fail (HDY_IS_DIALER_CYCLE_BUTTON (self));
g_object_set (G_OBJECT (self), "cycle-timeout", timeout, NULL);
}
/*
* Copyright (C) 2017 Purism SPC
*
* SPDX-License-Identifier: LGPL-3.0+
*/
#ifndef HDY_DIALER_CYCLE_BUTTON_H
#define HDY_DIALER_CYCLE_BUTTON_H
#if !defined(HANDY_INSIDE) && !defined(HANDY_COMPILATION)
#error "Only <handy.h> can be included directly."
#endif
#include "hdy-dialer-button.h"
G_BEGIN_DECLS
#define HDY_TYPE_DIALER_CYCLE_BUTTON (hdy_dialer_cycle_button_get_type())
G_DECLARE_DERIVABLE_TYPE (HdyDialerCycleButton, hdy_dialer_cycle_button, HDY, DIALER_CYCLE_BUTTON, HdyDialerButton)
/**
* HdyDialerCycleButtonClass
* @parent_class: The parent classqn
* @cycle_start: Class handler for the #HdyDialerCycleButton::cycle-start signal
* @cycle_end: Class handler for the #HdyDialerCycleButton::cycle-end signal
*/
struct _HdyDialerCycleButtonClass
{
HdyDialerButtonClass parent_class;
/* Signals
*/
void (*cycle_start) (HdyDialerCycleButton *self);
void (*cycle_end) (HdyDialerCycleButton *self);
};
GtkWidget * hdy_dialer_cycle_button_new (const gchar* symbols);
gunichar hdy_dialer_cycle_button_get_current_symbol (HdyDialerCycleButton *self);
gboolean hdy_dialer_cycle_button_is_cycling (HdyDialerCycleButton *self);
void hdy_dialer_cycle_button_stop_cycle (HdyDialerCycleButton *self);
gint hdy_dialer_cycle_button_get_cycle_timeout (HdyDialerCycleButton *self);
void hdy_dialer_cycle_button_set_cycle_timeout (HdyDialerCycleButton *self,
gint timeout);
G_END_DECLS
#endif /* HDY_DIALER_CYCLE_BUTTON_H */
......@@ -8,7 +8,8 @@
#include "hdy-dialer.h"
#include "hdy-dialer-button.h"
#include "hdy-dialer-cycle-button.h"
#include "hdy-string-utf8.h"
/**
* SECTION:hdy-dialer
......@@ -22,7 +23,7 @@
typedef struct
{
HdyDialerButton *btn_0, *btn_1, *btn_2, *btn_3, *btn_4, *btn_5, *btn_6, *btn_7, *btn_8, *btn_9;
HdyDialerButton *btn_hash, *btn_star;
HdyDialerCycleButton *btn_hash, *btn_star, *cycle_btn;
GtkLabel *display;
GtkButton *btn_dial, *btn_del;
GString *number;
......@@ -45,6 +46,18 @@ enum {
static guint signals [LAST_SIGNAL];
static void
stop_cycle_mode (HdyDialer *self)
{
HdyDialerPrivate *priv = hdy_dialer_get_instance_private (self);
if (priv->cycle_btn) {
hdy_dialer_cycle_button_stop_cycle (priv->cycle_btn);
priv->cycle_btn = NULL;
}
}
static void
hdy_digit_button_clicked (HdyDialer *self, HdyDialerButton *btn)
{
......@@ -54,6 +67,8 @@ hdy_digit_button_clicked (HdyDialer *self, HdyDialerButton *btn)
g_return_if_fail (HDY_IS_DIALER (self));
g_return_if_fail (HDY_IS_DIALER_BUTTON (btn));
stop_cycle_mode (self);
d = hdy_dialer_button_get_digit(btn);
g_string_append_printf(priv->number, "%d", d);
g_object_notify_by_pspec (G_OBJECT (self), props[HDY_DIALER_PROP_NUMBER]);
......@@ -61,20 +76,48 @@ hdy_digit_button_clicked (HdyDialer *self, HdyDialerButton *btn)
static void
hdy_symbol_button_clicked (HdyDialer *self, HdyDialerButton *btn)
hdy_cycle_button_clicked (HdyDialer *self, HdyDialerCycleButton *btn)
{
HdyDialerPrivate *priv = hdy_dialer_get_instance_private (self);
char* symbols;
gunichar symbol;
g_return_if_fail (HDY_IS_DIALER (self));
g_return_if_fail (HDY_IS_DIALER_BUTTON (btn));
symbols = hdy_dialer_button_get_letters(btn);
g_string_append_printf(priv->number, "%c", symbols[0]);
if (priv->cycle_btn != btn) {
stop_cycle_mode (self);
priv->cycle_btn = btn;
} else if (priv->number->len) {
hdy_string_utf8_truncate (priv->number, hdy_string_utf8_len (priv->number)-1);
}
symbol = hdy_dialer_cycle_button_get_current_symbol (btn);
g_string_append_unichar (priv->number, symbol);
g_object_notify_by_pspec (G_OBJECT (self), props[HDY_DIALER_PROP_NUMBER]);
}
static void
hdy_cycle_button_cycle_start (HdyDialer *self, HdyDialerCycleButton *btn)
{
/* FIXME: change cursor */
}
static void
hdy_cycle_button_cycle_end (HdyDialer *self, HdyDialerCycleButton *btn)
{
HdyDialerPrivate *priv = hdy_dialer_get_instance_private (self);
/* reset cycle_btn so pressing it again produces a new character */
if (priv->cycle_btn == btn) {
priv->cycle_btn = NULL;
/* FIXME: change cursor */
}
}
static void
hdy_dial_button_clicked (HdyDialer *self, GtkButton *btn)
{
......@@ -84,6 +127,8 @@ hdy_dial_button_clicked (HdyDialer *self, GtkButton *btn)
g_return_if_fail (HDY_IS_DIALER (self));
g_return_if_fail (GTK_IS_BUTTON (btn));
stop_cycle_mode (self);
number = gtk_label_get_label(priv->display);
g_signal_emit (self, signals[DIALED], 0, number);
}
......@@ -97,10 +142,12 @@ hdy_del_button_clicked (HdyDialer *self, GtkButton *btn)
g_return_if_fail (HDY_IS_DIALER (self));
g_return_if_fail (GTK_IS_BUTTON (btn));
stop_cycle_mode (self);
if (!priv->number->len)
return;
g_string_truncate(priv->number, priv->number->len - 1);
hdy_string_utf8_truncate (priv->number, hdy_string_utf8_len (priv->number)-1);
g_object_notify_by_pspec (G_OBJECT (self), props[HDY_DIALER_PROP_NUMBER]);
}
......@@ -301,16 +348,19 @@ hdy_dialer_init (HdyDialer *self)
G_CALLBACK (hdy_digit_button_clicked),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (priv->btn_star,
"clicked",
G_CALLBACK (hdy_symbol_button_clicked),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (priv->btn_hash,
"clicked",
G_CALLBACK (hdy_symbol_button_clicked),
self,
G_CONNECT_SWAPPED);
g_object_connect (priv->btn_star,
"swapped-signal::clicked", G_CALLBACK (hdy_cycle_button_clicked), self,
"swapped-signal::cycle-start", G_CALLBACK (hdy_cycle_button_cycle_start), self,
"swapped-signal::cycle-end", G_CALLBACK (hdy_cycle_button_cycle_end), self,
NULL);
g_object_connect (priv->btn_hash,
"swapped-signal::clicked", G_CALLBACK (hdy_cycle_button_clicked), self,
"swapped-signal::cycle-start", G_CALLBACK (hdy_cycle_button_cycle_start), self,
"swapped-signal::cycle-end", G_CALLBACK (hdy_cycle_button_cycle_end), self,
NULL);
g_signal_connect_object (priv->btn_dial,
"clicked",
G_CALLBACK (hdy_dial_button_clicked),
......@@ -338,6 +388,7 @@ hdy_dialer_init (HdyDialer *self)
gtk_button_set_image(priv->btn_dial, image);
priv->number = g_string_new(NULL);
priv->cycle_btn = NULL;
}
......
......@@ -129,9 +129,9 @@
</child>
<child>
<object class="HdyDialerButton" id="btn_star">
<object class="HdyDialerCycleButton" id="btn_star">
<property name="digit">-1</property>
<property name="letters">+</property>
<property name="letters">*+</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -157,9 +157,9 @@
</child>
<child>
<object class="HdyDialerButton" id="btn_hash">
<object class="HdyDialerCycleButton" id="btn_hash">
<property name="digit">-1</property>
<property name="letters">#</property>
<property name="letters">#</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......
......@@ -35,12 +35,14 @@ libhandy_private_sources = []
src_headers = [
'hdy-dialer-button.h',
'hdy-dialer-cycle-button.h',
'hdy-dialer.h',
'hdy-string-utf8.h',
]
src_sources = [
'hdy-dialer-button.c',
'hdy-dialer-cycle-button.c',
'hdy-dialer.c',
'hdy-string-utf8.c',
]
......
......@@ -18,19 +18,19 @@ test_link_args = [
'-fPIC',
]
test_dialer = executable('test-dialer', 'test-dialer.c',
c_args: test_cflags,
link_args: test_link_args,
dependencies: libhandy_deps + [libhandy_dep],
)
test('test-dialer', test_dialer, env: test_env)
test_string_utf8 = executable('test-string-utf8', 'test-string-utf8.c',
c_args: test_cflags,
link_args: test_link_args,
dependencies: libhandy_deps + [libhandy_dep],
)
test('test-string-utf8', test_string_utf8, env: test_env)
test_names = [
'test-dialer',
'test-string-utf8',
'test-dialer-cycle-button',
]
foreach test_name : test_names
t = executable(test_name, test_name + '.c',
c_args: test_cflags,
link_args: test_link_args,
dependencies: libhandy_deps + [libhandy_dep],
)
test(test_name, t, env: test_env)
endforeach
endif
/*
* Copyright (C) 2017 Purism SPC
*
* SPDX-License-Identifier: LGPL-3.0+
*/
#include <handy.h>
gint notified;
static void
cycle_end_cb(GtkWidget *widget, gpointer data)
{
notified++;
}
static void
test_hdy_dialer_cycle_button_cycle_end(void)
{
GtkWidget *btn;
btn = hdy_dialer_cycle_button_new ("abc");
g_signal_connect (btn, "cycle-end", G_CALLBACK (cycle_end_cb), NULL);
hdy_dialer_cycle_button_stop_cycle (HDY_DIALER_CYCLE_BUTTON (btn));
g_assert_cmpint (1, ==, notified);
notified = 0;
}
static void
test_hdy_dialer_cycle_button_cycle_timeout(void)
{
HdyDialerCycleButton *btn;
btn = HDY_DIALER_CYCLE_BUTTON (hdy_dialer_cycle_button_new ("abc"));
g_assert_cmpint (1000, ==, hdy_dialer_cycle_button_get_cycle_timeout (btn));
hdy_dialer_cycle_button_set_cycle_timeout (btn, 10);
g_assert_cmpint (10, ==, hdy_dialer_cycle_button_get_cycle_timeout (btn));
}
gint
main (gint argc,
gchar *argv[])
{
g_test_init (&argc, &argv, NULL);
gtk_init(&argc, &argv);
g_test_add_func("/Handy/DialerCycleButton/cycle_end", test_hdy_dialer_cycle_button_cycle_end);
g_test_add_func("/Handy/DialerCycleButton/cycle_timeout", test_hdy_dialer_cycle_button_cycle_timeout);
return g_test_run();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment