talkatu/talkatu
Clone
Summary
Browse
Changes
Graph
Set layout managers for our GtkWidget subclasses
gtk4
2022-02-01, Gary Kramlich
bfd93f3bfba1
Set layout managers for our GtkWidget subclasses
Also disable the GtkBuildable for TalkatuScrolledWindow for now.
Testing Done:
Ran and verified the size allocation runtime warnings were gone.
Reviewed at https://reviews.imfreedom.org/r/1276/
/*
* Talkatu - GTK widgets for chat applications
* Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
*
* This library 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 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this library; if not, see <https://www.gnu.org/licenses/>.
*/
#include
<glib/gi18n-lib.h>
#include
<gtk/gtk.h>
#include
<gdk/gdkkeysyms.h>
#include
<gumbo.h>
#include
<stdio.h>
#include
"talkatu/talkatuactiongroup.h"
#include
"talkatu/talkatuattachment.h"
#include
"talkatu/talkatuattachmentdialog.h"
#include
"talkatu/talkatubuffer.h"
#include
"talkatu/talkatumarkup.h"
#include
"talkatu/talkatutag.h"
#include
"talkatu/talkatuview.h"
/**
* TalkatuViewClass:
* @open_url: The class handler for the #TalkatuView::open_url signal.
*
* The backing class to #TalkatuView instances.
*/
/**
* TalkatuView:
*
* A #GtkTextView subclass that's preconfigured with a #TalkatuBuffer.
*/
typedef
struct
{
GSimpleActionGroup
*
action_group
;
/* we cache the cursor that's displayed while hovering over a link as well
* the tag for links/anchors to avoid extra lookups.
*/
GdkCursor
*
cursor_hand
;
GtkWidget
*
menu
;
gchar
*
url
;
}
TalkatuViewPrivate
;
enum
{
SIG_FORMAT_ACTIVATE
,
SIG_OPEN_URL
,
LAST_SIGNAL
,
};
static
guint
signals
[
LAST_SIGNAL
]
=
{
0
,
};
G_DEFINE_TYPE_WITH_PRIVATE
(
TalkatuView
,
talkatu_view
,
GTK_TYPE_TEXT_VIEW
)
/******************************************************************************
* Helpers
*****************************************************************************/
static
const
gchar
*
talkatu_view_url_from_iter
(
TalkatuView
*
view
,
GtkTextIter
*
iter
)
{
GSList
*
tag
=
NULL
;
gchar
*
url
=
NULL
;
tag
=
gtk_text_iter_get_tags
(
iter
);
for
(;
tag
!=
NULL
;
tag
=
g_slist_delete_link
(
tag
,
tag
))
{
if
(
tag
->
data
==
NULL
)
{
continue
;
}
url
=
g_object_get_data
(
G_OBJECT
(
tag
->
data
),
"talkatu-anchor-url"
);
if
(
url
!=
NULL
)
{
break
;
}
}
g_slist_free
(
tag
);
return
url
;
}
/******************************************************************************
* Actions
*****************************************************************************/
static
void
talkatu_view_link_open_cb
(
GtkWidget
*
widget
,
const
gchar
*
action_name
,
GVariant
*
parameter
)
{
TalkatuView
*
view
=
TALKATU_VIEW
(
widget
);
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
view
);
g_signal_emit
(
view
,
signals
[
SIG_OPEN_URL
],
0
,
priv
->
url
);
}
static
void
talkatu_view_link_copy_cb
(
GtkWidget
*
widget
,
const
gchar
*
action_name
,
GVariant
*
parameter
)
{
TalkatuView
*
view
=
TALKATU_VIEW
(
widget
);
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
view
);
if
(
priv
->
url
!=
NULL
)
{
GdkClipboard
*
clipboard
=
NULL
;
clipboard
=
gtk_widget_get_clipboard
(
widget
);
gdk_clipboard_set_text
(
clipboard
,
priv
->
url
);
}
}
/******************************************************************************
* Signal Handlers
*****************************************************************************/
static
void
talkatu_view_released_cb
(
GtkGestureClick
*
gesture
,
guint
n_press
,
double
wx
,
double
wy
,
gpointer
data
)
{
TalkatuView
*
view
=
TALKATU_VIEW
(
data
);
GtkTextIter
iter
;
const
gchar
*
url
=
NULL
;
gint
x
,
y
;
gtk_text_view_window_to_buffer_coords
(
GTK_TEXT_VIEW
(
view
),
GTK_TEXT_WINDOW_TEXT
,
(
gint
)
wx
,
(
gint
)
wy
,
&
x
,
&
y
);
gtk_text_view_get_iter_at_location
(
GTK_TEXT_VIEW
(
view
),
&
iter
,
x
,
y
);
url
=
talkatu_view_url_from_iter
(
view
,
&
iter
);
if
(
url
!=
NULL
)
{
gint
button
=
gtk_gesture_single_get_current_button
(
GTK_GESTURE_SINGLE
(
gesture
));
if
(
button
==
1
)
{
g_signal_emit
(
view
,
signals
[
SIG_OPEN_URL
],
0
,
url
);
}
else
if
(
button
==
2
)
{
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
view
);
g_clear_pointer
(
&
priv
->
url
,
g_free
);
priv
->
url
=
g_strdup
(
url
);
gtk_popover_set_pointing_to
(
GTK_POPOVER
(
priv
->
menu
),
&
(
const
GdkRectangle
){
x
,
y
,
1
,
1
});
gtk_popover_popup
(
GTK_POPOVER
(
priv
->
menu
));
}
}
}
static
void
talkatu_view_motion_cb
(
GtkEventControllerMotion
*
controller
,
gdouble
wx
,
gdouble
wy
,
gpointer
data
)
{
TalkatuView
*
view
=
TALKATU_VIEW
(
data
);
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
view
);
GtkTextIter
iter
;
GdkCursor
*
cursor
=
NULL
;
const
gchar
*
url
=
NULL
;
gint
x
,
y
;
gtk_text_view_window_to_buffer_coords
(
GTK_TEXT_VIEW
(
view
),
GTK_TEXT_WINDOW_TEXT
,
(
gint
)
wx
,
(
gint
)
wy
,
&
x
,
&
y
);
gtk_text_view_get_iter_at_location
(
GTK_TEXT_VIEW
(
view
),
&
iter
,
x
,
y
);
url
=
talkatu_view_url_from_iter
(
view
,
&
iter
);
if
(
url
!=
NULL
)
{
cursor
=
priv
->
cursor_hand
;
}
if
(
cursor
!=
gtk_widget_get_cursor
(
GTK_WIDGET
(
view
)))
{
gtk_widget_set_cursor
(
GTK_WIDGET
(
view
),
cursor
);
}
}
static
void
talkatu_view_buffer_set_cb
(
GObject
*
view
,
GParamSpec
*
pspec
,
gpointer
data
)
{
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
TALKATU_VIEW
(
view
));
GtkTextBuffer
*
buffer
=
gtk_text_view_get_buffer
(
GTK_TEXT_VIEW
(
view
));
if
(
TALKATU_IS_BUFFER
(
buffer
))
{
priv
->
action_group
=
talkatu_buffer_get_action_group
(
TALKATU_BUFFER
(
buffer
));
}
}
/******************************************************************************
* Default Signal Handlers
*****************************************************************************/
static
gboolean
talkatu_view_query_tooltip
(
GtkWidget
*
widget
,
gint
x
,
gint
y
,
gboolean
keyboard
,
GtkTooltip
*
tooltip
)
{
GtkTextIter
iter
;
const
gchar
*
url
=
NULL
;
gint
adj_x
,
adj_y
;
if
(
keyboard
)
{
return
GTK_WIDGET_CLASS
(
talkatu_view_parent_class
)
->
query_tooltip
(
widget
,
x
,
y
,
keyboard
,
tooltip
);
}
/* convert the window coordinates to match what's visible */
gtk_text_view_window_to_buffer_coords
(
GTK_TEXT_VIEW
(
widget
),
GTK_TEXT_WINDOW_TEXT
,
x
,
y
,
&
adj_x
,
&
adj_y
);
/* now find the iter for what we're at */
gtk_text_view_get_iter_at_location
(
GTK_TEXT_VIEW
(
widget
),
&
iter
,
adj_x
,
adj_y
);
/* look for a url, if we have one, add it to tooltip */
url
=
talkatu_view_url_from_iter
(
TALKATU_VIEW
(
widget
),
&
iter
);
if
(
url
!=
NULL
)
{
gtk_tooltip_set_text
(
tooltip
,
url
);
return
TRUE
;
}
return
GTK_WIDGET_CLASS
(
talkatu_view_parent_class
)
->
query_tooltip
(
widget
,
x
,
y
,
keyboard
,
tooltip
);
}
/******************************************************************************
* GtkTextViewClass overrides
*****************************************************************************/
static
GtkTextBuffer
*
talkatu_view_create_buffer
(
GtkTextView
*
view
)
{
return
talkatu_buffer_new
(
NULL
);
}
/******************************************************************************
* GObject Stuff
*****************************************************************************/
static
void
talkatu_view_finalize
(
GObject
*
obj
)
{
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
TALKATU_VIEW
(
obj
));
g_clear_object
(
&
priv
->
cursor_hand
);
g_clear_pointer
(
&
priv
->
url
,
g_free
);
G_OBJECT_CLASS
(
talkatu_view_parent_class
)
->
finalize
(
obj
);
}
static
void
talkatu_view_init
(
TalkatuView
*
view
)
{
TalkatuViewPrivate
*
priv
=
talkatu_view_get_instance_private
(
view
);
GtkEventController
*
controller
=
NULL
;
gtk_widget_init_template
(
GTK_WIDGET
(
view
));
priv
->
cursor_hand
=
gdk_cursor_new_from_name
(
"pointer"
,
NULL
);
/* tell the widget class that we support tooltips. This is used to show
* link targets, and probably other stuff at some point.
*/
gtk_widget_set_has_tooltip
(
GTK_WIDGET
(
view
),
TRUE
);
/* Connect some signals we care about */
controller
=
gtk_event_controller_motion_new
();
g_signal_connect
(
controller
,
"motion"
,
G_CALLBACK
(
talkatu_view_motion_cb
),
view
);
gtk_widget_add_controller
(
GTK_WIDGET
(
view
),
controller
);
controller
=
GTK_EVENT_CONTROLLER
(
gtk_gesture_click_new
());
/* check all buttons */
gtk_gesture_single_set_button
(
GTK_GESTURE_SINGLE
(
controller
),
0
);
g_signal_connect
(
controller
,
"released"
,
G_CALLBACK
(
talkatu_view_released_cb
),
view
);
gtk_widget_add_controller
(
GTK_WIDGET
(
view
),
controller
);
/* we need to know when the buffer is changed in our parent so we can
* update our actions and other stuff.
*/
g_signal_connect
(
G_OBJECT
(
view
),
"notify::buffer"
,
G_CALLBACK
(
talkatu_view_buffer_set_cb
),
NULL
);
}
static
void
talkatu_view_class_init
(
TalkatuViewClass
*
klass
)
{
GObjectClass
*
obj_class
=
G_OBJECT_CLASS
(
klass
);
GtkWidgetClass
*
widget_class
=
GTK_WIDGET_CLASS
(
klass
);
GtkTextViewClass
*
text_view_class
=
GTK_TEXT_VIEW_CLASS
(
klass
);
obj_class
->
finalize
=
talkatu_view_finalize
;
widget_class
->
query_tooltip
=
talkatu_view_query_tooltip
;
gtk_widget_class_set_template_from_resource
(
widget_class
,
"/org/imfreedom/keep/talkatu/talkatu/ui/view.ui"
);
text_view_class
->
create_buffer
=
talkatu_view_create_buffer
;
/* add our signals */
/**
* TalkatuView::open-url:
* @talkatutextview: The #TalkatuView instances.
* @url: The URL to open.
* @user_data: User supplied data.
*
* Emitted when a user clicks on a link to open the url
*/
signals
[
SIG_OPEN_URL
]
=
g_signal_new
(
"open-url"
,
G_TYPE_FROM_CLASS
(
klass
),
G_SIGNAL_RUN_LAST
|
G_SIGNAL_ACTION
,
G_STRUCT_OFFSET
(
TalkatuViewClass
,
open_url
),
NULL
,
NULL
,
NULL
,
G_TYPE_NONE
,
1
,
G_TYPE_STRING
);
gtk_widget_class_bind_template_child_private
(
widget_class
,
TalkatuView
,
menu
);
/* add our actions */
gtk_widget_class_install_action
(
widget_class
,
"link.open"
,
NULL
,
talkatu_view_link_open_cb
);
gtk_widget_class_install_action
(
widget_class
,
"link.copy"
,
NULL
,
talkatu_view_link_copy_cb
);
/* add our custom keybindings */
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_b
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_BOLD
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_i
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_ITALIC
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_slash
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_STRIKETHROUGH
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_plus
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_GROW
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_equal
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_GROW
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_minus
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_SHRINK
,
NULL
);
gtk_widget_class_add_binding_action
(
widget_class
,
GDK_KEY_r
,
GDK_CONTROL_MASK
,
TALKATU_ACTION_FORMAT_RESET
,
NULL
);
gtk_widget_class_add_binding_signal
(
widget_class
,
GDK_KEY_Insert
,
GDK_META_MASK
|
GDK_SHIFT_MASK
,
"insert-at-cursor"
,
"s"
,
"🐣"
);
}
/******************************************************************************
* Public API
*****************************************************************************/
/**
* talkatu_view_new:
*
* Creates a new #TalkatuView with a #TalkatuBuffer.
*
* Returns: (transfer full): The new #TalkatuView.
*/
GtkWidget
*
talkatu_view_new
(
void
)
{
return
talkatu_view_new_with_buffer
(
talkatu_buffer_new
(
NULL
));
}
/**
* talkatu_view_new_with_buffer:
* @buffer: A #GtkTextBuffer.
*
* Creates a new #TalkatuView with @buffer.
*
* Returns: (transfer full): The new #TalkatuView.
*/
GtkWidget
*
talkatu_view_new_with_buffer
(
GtkTextBuffer
*
buffer
)
{
return
GTK_WIDGET
(
g_object_new
(
TALKATU_TYPE_VIEW
,
"buffer"
,
buffer
,
NULL
));
}