Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pinner plugin #1308

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5020967
Add Pinner plugin
andy5995 Feb 13, 2024
5def3d7
Check for duplicates
andy5995 Feb 14, 2024
3a29307
check if doc eq NULL
andy5995 Feb 14, 2024
4bb86a9
use g_strdup instead of strlcpy, point better
andy5995 Feb 14, 2024
18b58b3
Add non-working popup menu and func to clear list
andy5995 Feb 14, 2024
73322e4
add header to Makefile.am
andy5995 Feb 14, 2024
c1899e4
Make list global
andy5995 Feb 14, 2024
afeeb59
Remove header, make all functions static
andy5995 Feb 14, 2024
093184d
Add UNpin function
andy5995 Feb 15, 2024
6a96d0c
Make documents clickable and open them
andy5995 Feb 15, 2024
b8d66c4
Don't auto-switch to the "Pinned" tab
andy5995 Feb 15, 2024
a1fe957
fix double-free error when freeing list
andy5995 Feb 16, 2024
cf40001
fix 'stack-use-after-return` in pin_cleanup
andy5995 Feb 16, 2024
5391654
implement unpin, use hashtable instead of GSList
andy5995 Feb 16, 2024
87b8b4e
Add right-click menu/option to clear pinned list
andy5995 Feb 16, 2024
295737b
Conditionally add sanitize flag
andy5995 Feb 16, 2024
a0f4bc8
Add credit for ChatGPT
andy5995 Feb 16, 2024
259571e
remove sanitize flag
andy5995 Feb 16, 2024
e0d734c
Don't use 'with_mnemonic' (fix missing underscores)
andy5995 Feb 16, 2024
7aa35e5
Acommodate for long filenames by using an ellipse
andy5995 Feb 17, 2024
cd1f687
Implment keybindings and add README.md
andy5995 Feb 17, 2024
b3c3b67
Add "unpin document" to right-click menu
andy5995 Feb 17, 2024
b58b98b
Add "Unpin" item to right-click popup menu
andy5995 Feb 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ if ENABLE_PAIRTAGHIGHLIGHTER
SUBDIRS += pairtaghighlighter
endif

if ENABLE_PINNER
SUBDIRS += pinner
endif

if ENABLE_POHELPER
SUBDIRS += pohelper
endif
Expand Down
9 changes: 9 additions & 0 deletions build/pinner.m4
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
AC_DEFUN([GP_CHECK_PINNER],
[
GP_ARG_DISABLE([pinner], [auto])
GP_COMMIT_PLUGIN_STATUS([Pinner])

AC_CONFIG_FILES([
pinner/Makefile
])
])
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ GP_CHECK_LIPSUM
GP_CHECK_MARKDOWN
GP_CHECK_OVERVIEW
GP_CHECK_PAIRTAGHIGHLIGHTER
GP_CHECK_PINNER
GP_CHECK_POHELPER
GP_CHECK_PRETTYPRINTER
GP_CHECK_PROJECTORGANIZER
Expand Down
28 changes: 28 additions & 0 deletions pinner/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
include $(top_srcdir)/build/vars.build.mk

plugin = pinner

geanyplugins_LTLIBRARIES = pinner.la

pinner_la_SOURCES = pinner.c

pinner_la_CPPFLAGS = $(AM_CPPFLAGS) \
-DG_LOG_DOMAIN=\"Pinner\"

#if MINGW
# ASAN_FLAG =
#else
# ASAN_FLAG = -fsanitize=address,undefined
#endif

pinner_la_CFLAGS = \
$(AM_CFLAGS) $(ASAN_FLAG)

pinner_la_LDFLAGS = \
$(AM_LDFLAGS) $(ASAN_FLAG)

pinner_la_LIBADD = \
$(COMMONLIBS)

AM_CPPCHECKFLAGS = -DSCE_PAS_DEFAULT=0
include $(top_srcdir)/build/cppcheck.mk
16 changes: 16 additions & 0 deletions pinner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Pinner

Pinner is a [geany plugin](https://www.geany.org/support/plugins/) that
enables pinning documents to a sidebar tab.

This will add two items to the Geany tools menu:

Pin Document
Unpin Document

To clear the list, Right click on it and left-click on "Clear".

## Keybindings

When the plugin is enabled, keybindings to pin and unpin documents can be
changed from the preferences menu.
313 changes: 313 additions & 0 deletions pinner/pinner.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
/*
* pinner.c
*
* Copyright 2024 Andy Alt <arch_stanton5995@proton.me>
* With assistance from ChatGPT
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*
*/

#include <geanyplugin.h>

enum {
DO_PIN,
DO_UNPIN
};

static void destroy_widget(gpointer pdata);
static void clear_pinned_documents(void);
static GtkWidget *create_popup_menu(const gchar *file_name);
static gboolean on_button_press_cb(GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean is_duplicate(const gchar* file_name);
static void pin_activate_cb(GtkMenuItem *menuitem, gpointer pdata);
static void unpin_activate_cb(GtkMenuItem *menuitem, gpointer pdata);

static GtkWidget *pinned_view_vbox;
static gint page_number = 0;
static GHashTable *doc_to_widget_map = NULL;

static void destroy_widget(gpointer pdata)
{
GtkWidget *widget = (GtkWidget *)pdata;
gtk_widget_destroy(widget);
}


void clear_pinned_documents(void)
{
if (doc_to_widget_map != NULL)
{
// Removes all keys and their associated values from the hash table.
// This will also call the destroy functions specified for keys and values,
// thus freeing the memory for the file names and destroying the widgets.
g_hash_table_remove_all(doc_to_widget_map);
}
}


static GtkWidget *create_popup_menu(const gchar *file_name) {
GtkWidget *menu = gtk_menu_new();

// Create a menu item without a label
GtkWidget *clear_item = gtk_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item);
g_signal_connect_swapped(clear_item, "activate", G_CALLBACK(clear_pinned_documents), NULL);

// Create a box to contain the icon and label
GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); // 6 pixels spacing

// Create the icon
GtkWidget *clear_icon = gtk_image_new_from_icon_name("edit-clear", GTK_ICON_SIZE_MENU);

// Create the label
GtkWidget *label = gtk_label_new("Clear List");

// Pack the icon and label into the box
gtk_box_pack_start(GTK_BOX(hbox), clear_icon, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);

// Add the box to the menu item
gtk_container_add(GTK_CONTAINER(clear_item), hbox);

// Show all widgets
gtk_widget_show_all(clear_item);

if (file_name != NULL)
{
// Create a menu item without a label for unpinning a document
GtkWidget *unpin_item = gtk_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(menu), unpin_item);
g_signal_connect_data(unpin_item, "activate", G_CALLBACK(unpin_activate_cb), g_strdup(file_name), (GClosureNotify)g_free, 0);

// Create a horizontal box to hold the icon and label
GtkWidget *hbox_unpin = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); // 6 pixels spacing

// Create the icon
GtkWidget *unpin_icon = gtk_image_new_from_icon_name("list-remove", GTK_ICON_SIZE_MENU);

// Create the label
GtkWidget *label_unpin = gtk_label_new("Unpin Document");

// Pack the icon and label into the horizontal box
gtk_box_pack_start(GTK_BOX(hbox_unpin), unpin_icon, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox_unpin), label_unpin, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(unpin_item), hbox_unpin);

gtk_widget_show_all(unpin_item);
}
gtk_widget_show_all(menu);

return menu;
}


static gboolean is_duplicate(const gchar* file_name)
{
return g_hash_table_contains(doc_to_widget_map, file_name);
}


static void pin_document_key_cb(guint key_id)
{
pin_activate_cb(NULL, NULL);
}

static void unpin_document_key_cb(guint key_id)
{
unpin_activate_cb(NULL, NULL);
}


static void pin_activate_cb(GtkMenuItem *menuitem, gpointer pdata)
{
gchar *ptr_file_name = NULL;
if (pdata == NULL)
{
GeanyDocument *doc = document_get_current();
if (doc == NULL)
return;
else
ptr_file_name = doc->file_name;
}
else
{
ptr_file_name = pdata;
}

if (is_duplicate(ptr_file_name))
return;

/* This must be freed when nodes are removed from the list */
gchar *tmp_file_name = g_strdup(ptr_file_name);

GtkWidget *event_box = gtk_event_box_new();
g_hash_table_insert(doc_to_widget_map, tmp_file_name, event_box);

GtkWidget *label = gtk_label_new(ptr_file_name);
// Enable ellipsizing at the start of the filename
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START);
gtk_label_set_max_width_chars(GTK_LABEL(label), 30);
// Set the label's alignment to left
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
// Set margins
gtk_widget_set_margin_start(label, 10); // 20 pixels margin on the start (left)
gtk_widget_set_margin_end(label, 10); // 20 pixels margin on the end (right)

gtk_container_add(GTK_CONTAINER(event_box), label);
gtk_widget_show_all(event_box);
gtk_box_pack_start(GTK_BOX(pinned_view_vbox), event_box, FALSE, FALSE, 0);
// gtk_notebook_set_current_page(GTK_NOTEBOOK(plugin->geany_data->main_widgets->sidebar_notebook), page_number);

g_signal_connect(event_box, "button-press-event",
G_CALLBACK(on_button_press_cb), tmp_file_name);

return;
}


static void unpin_activate_cb(GtkMenuItem *menuitem, gpointer pdata)
{
GeanyDocument *doc = document_get_current();
if (doc == NULL)
return;

gboolean removed = g_hash_table_remove(doc_to_widget_map, doc->file_name);
// If removed
if (!removed)
{
// Handle the case where the document was not found in the map
}

return;
}


static gboolean on_button_press_cb(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
{
// Check if the clicked widget is an event box
if (GTK_IS_EVENT_BOX(widget))
{
GtkWidget *label = gtk_bin_get_child(GTK_BIN(widget));

if (GTK_IS_LABEL(label))
{
const gchar *file_name = gtk_label_get_text(GTK_LABEL(label));
document_open_file(file_name, FALSE, NULL, NULL);
}
}
}
else if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY)
{
// Check if the clicked widget is an event box
if (GTK_IS_EVENT_BOX(widget))
{
GtkWidget *label = gtk_bin_get_child(GTK_BIN(widget));

if (GTK_IS_LABEL(label))
{
const gchar *file_name = gtk_label_get_text(GTK_LABEL(label));
GtkWidget *menu = create_popup_menu(file_name);
gtk_menu_popup_at_pointer(GTK_MENU(menu), (const GdkEvent *)event);
return TRUE;
}
}
else
{
GtkWidget *menu = create_popup_menu(NULL);
gtk_menu_popup_at_pointer(GTK_MENU(menu), (const GdkEvent *)event);
return TRUE;
}
}
return FALSE;
}


static gboolean pin_init(GeanyPlugin *plugin, gpointer pdata)
{
GtkWidget **tools_item = g_new0(GtkWidget*, 3); // Allocate memory for 3 pointers (2 items + NULL terminator)
tools_item[DO_PIN] = gtk_menu_item_new_with_mnemonic("Pin Document");
tools_item[DO_UNPIN] = gtk_menu_item_new_with_mnemonic("Unpin Document");
tools_item[2] = NULL; // NULL sentinel

doc_to_widget_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, destroy_widget);

gtk_widget_show(tools_item[DO_PIN]);
gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu),
tools_item[DO_PIN]);
g_signal_connect(tools_item[DO_PIN], "activate",
G_CALLBACK(pin_activate_cb), NULL);

gtk_widget_show(tools_item[DO_UNPIN]);
gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu),
tools_item[DO_UNPIN]);
g_signal_connect(tools_item[DO_UNPIN], "activate",
G_CALLBACK(unpin_activate_cb), NULL);

//g_signal_connect(event_box, "button-press-event",
//G_CALLBACK(on_button_press), NULL);
//g_signal_connect(pinned_view_vbox, "button-press-event",
//G_CALLBACK(on_button_press), NULL);

geany_plugin_set_data(plugin, tools_item, NULL);

pinned_view_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_show_all(pinned_view_vbox);
page_number = gtk_notebook_append_page(GTK_NOTEBOOK(plugin->geany_data->main_widgets->sidebar_notebook),
pinned_view_vbox, gtk_label_new(_("Pinned")));

// Keybinding setup
GeanyKeyGroup *key_group = plugin_set_key_group(plugin, "pinner_keys", 2, NULL);
keybindings_set_item(key_group, DO_PIN, pin_document_key_cb, 0, 0, "pin_document", "Pin Document", NULL);
keybindings_set_item(key_group, DO_UNPIN, unpin_document_key_cb, 0, 0, "unpin_document", "Unpin Document", NULL);

return TRUE;
}


static void pin_cleanup(GeanyPlugin *plugin, gpointer pdata)
{
if (doc_to_widget_map != NULL)
{
g_hash_table_destroy(doc_to_widget_map);
doc_to_widget_map = NULL;
}

GtkWidget **tools_item = pdata;
while (*tools_item != NULL) {
gtk_widget_destroy(*tools_item);
tools_item++;
}
g_free(pdata);
}


G_MODULE_EXPORT
void geany_load_module(GeanyPlugin *plugin)
{
plugin->info->name = "Pinner";
plugin->info->description = "Pin a document";
plugin->info->version = "0.1.0";
plugin->info->author = "Andy Alt <arch_stanton5995@proton.me>";

plugin->funcs->init = pin_init;
plugin->funcs->cleanup = pin_cleanup;

GEANY_PLUGIN_REGISTER(plugin, 225);
}
3 changes: 3 additions & 0 deletions po/POTFILES.skip
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
# geanyvc
geanyvc/src/commit.glade

# Pinner
pinner/pinner.c

# WebHelper
webhelper/src/gwh-enum-types.c