Coder
/* $Id$ */
static char const _copyright[] =
"Copyright © 2011-2020 Pierre Pronchery <khorben@defora.org>";
/* This file is part of DeforaOS Desktop Coder */
static char const _license[] =
"This program is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU General Public License as published by\n"
"the Free Software Foundation, version 3 of the License.\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public License for more details.\n"
"\n"
"You should have received a copy of the GNU General Public License\n"
"along with this program. If not, see <http://www.gnu.org/licenses/>.";
/* TODO:
* - add a "backend" type of plug-ins (asm, hexedit, make, project, UWff...)
* - add a "plug-in" type of plug-ins (time tracker, ...) */
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <libintl.h>
#include <gdk/gdkkeysyms.h>
#include <Desktop.h>
#include "callbacks.h"
#include "coder.h"
#include "../config.h"
#define _(string) gettext(string)
#define N_(string) (string)
#ifndef PREFIX
# define PREFIX "/usr/local"
#endif
#ifndef PROGNAME_HELPER
# define PROGNAME_HELPER "helper"
#endif
/* Coder */
/* private */
/* types */
struct _Coder
{
Config * config;
Project ** projects;
size_t projects_cnt;
Project * cur;
/* widgets */
/* toolbar */
GtkWidget * tb_window;
/* preferences */
GtkWidget * pr_window;
GtkWidget * pr_editor_command;
GtkWidget * pr_editor_terminal;
/* files */
GtkWidget * fi_window;
GtkWidget * fi_combo;
GtkWidget * fi_view;
/* about */
GtkWidget * ab_window;
};
/* constants */
#define ICON_NAME "applications-development"
static char const * _authors[] =
{
"Pierre Pronchery <khorben@defora.org>",
NULL
};
/* menubar */
static const DesktopMenu _coder_menu_file[] =
{
{ N_("_New file..."), G_CALLBACK(on_file_new), GTK_STOCK_NEW,
GDK_CONTROL_MASK, GDK_KEY_N },
{ N_("_Open file..."), G_CALLBACK(on_file_open), GTK_STOCK_OPEN,
GDK_CONTROL_MASK, GDK_KEY_O },
{ "", NULL , NULL, 0, 0 },
{ N_("_Preferences..."), G_CALLBACK(on_file_preferences),
GTK_STOCK_PREFERENCES, GDK_CONTROL_MASK, GDK_KEY_P },
{ "", NULL, NULL, 0, 0 },
{ N_("_Exit"), G_CALLBACK(on_file_exit), GTK_STOCK_QUIT,
GDK_CONTROL_MASK, GDK_KEY_Q },
{ NULL, NULL, NULL, 0, 0 }
};
/* FIXME will certainly be dynamic */
static const DesktopMenu _coder_menu_project[] =
{
{ N_("_New project..."), G_CALLBACK(on_project_new), GTK_STOCK_NEW, 0,
0 },
{ N_("_Open project..."), G_CALLBACK(on_project_open), GTK_STOCK_OPEN,
0, 0 },
{ N_("_Save project"), G_CALLBACK(on_project_save), GTK_STOCK_SAVE,
GDK_CONTROL_MASK, GDK_KEY_S },
{ N_("Save project _As..."), G_CALLBACK(on_project_save_as),
GTK_STOCK_SAVE_AS, 0, 0 },
{ "", NULL, NULL, 0, 0 },
{ N_("_Properties..."), G_CALLBACK(on_project_properties),
GTK_STOCK_PROPERTIES, GDK_MOD1_MASK, GDK_KEY_Return },
{ NULL, NULL, NULL, 0, 0 }
};
static const DesktopMenu _coder_menu_view[] =
{
{ N_("_Files"), G_CALLBACK(on_view_files), NULL, 0, 0 },
{ NULL, NULL, NULL, 0, 0 }
};
static const DesktopMenu _coder_menu_tools[] =
{
{ N_("_Debugger"), G_CALLBACK(on_tools_debugger), NULL, 0, 0 },
{ N_("_PHP console"), G_CALLBACK(on_tools_php_console), NULL, 0, 0 },
{ N_("_Simulator"), G_CALLBACK(on_tools_simulator), "stock_cell-phone",
0, 0 },
{ N_("S_QL console"), G_CALLBACK(on_tools_sql_console),
"stock_insert-table", 0, 0 },
{ NULL, NULL, NULL, 0, 0 }
};
static const DesktopMenu _coder_menu_help[] =
{
{ N_("API _Reference"), G_CALLBACK(on_help_api_reference),
"help-contents", 0, 0 },
{ N_("_Contents"), G_CALLBACK(on_help_contents), "help-contents", 0,
GDK_KEY_F1 },
{ N_("_About"), G_CALLBACK(on_help_about), GTK_STOCK_ABOUT, 0, 0 },
{ NULL, NULL, NULL, 0, 0 }
};
static const DesktopMenubar _coder_menubar[] =
{
{ N_("_File"), _coder_menu_file },
{ N_("_Project"), _coder_menu_project },
{ N_("_View"), _coder_menu_view },
{ N_("_Tools"), _coder_menu_tools },
{ N_("_Help"), _coder_menu_help },
{ NULL, NULL }
};
/* variables */
/* toolbar */
static DesktopToolbar _coder_toolbar[] =
{
{ N_("Exit"), G_CALLBACK(on_file_exit), GTK_STOCK_QUIT, 0, 0, NULL },
{ NULL, NULL, NULL, 0, 0, NULL }
};
/* prototypes */
static Project * _coder_get_current_project(Coder * coder);
/* public */
/* functions */
/* coder_new */
static void _new_config(Coder * g);
Coder * coder_new(void)
{
Coder * coder;
GtkAccelGroup * group;
GtkWidget * vbox;
GtkWidget * hbox;
GtkWidget * widget;
if((coder = malloc(sizeof(*coder))) == NULL)
return NULL;
coder->projects = NULL;
coder->projects_cnt = 0;
coder->cur = NULL;
_new_config(coder);
/* main window */
group = gtk_accel_group_new();
coder->tb_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_add_accel_group(GTK_WINDOW(coder->tb_window), group);
#if GTK_CHECK_VERSION(2, 6, 0)
gtk_window_set_icon_name(GTK_WINDOW(coder->tb_window), ICON_NAME);
#endif
gtk_window_set_title(GTK_WINDOW(coder->tb_window), _("Coder"));
gtk_window_set_resizable(GTK_WINDOW(coder->tb_window), FALSE);
g_signal_connect_swapped(coder->tb_window, "delete-event", G_CALLBACK(
on_closex), coder);
#if GTK_CHECK_VERSION(3, 0, 0)
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
#else
vbox = gtk_vbox_new(FALSE, 0);
#endif
/* menubar */
widget = desktop_menubar_create(_coder_menubar, coder, group);
gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
/* toolbar */
widget = desktop_toolbar_create(_coder_toolbar, coder, group);
g_object_unref(group);
gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(coder->tb_window), vbox);
/* files */
coder->fi_window = NULL;
coder_show_files(coder, TRUE);
/* about */
coder->ab_window = NULL;
gtk_widget_show_all(coder->tb_window);
return coder;
}
static char * _config_file(void);
static void _new_config(Coder * coder)
{
char * filename;
if((coder->config = config_new()) == NULL)
{
coder_error(coder, strerror(errno), 0);
return;
}
config_load(coder->config, PREFIX "/etc/" PACKAGE ".conf");
if((filename = _config_file()) == NULL)
return;
config_load(coder->config, filename);
free(filename);
}
static char * _config_file(void)
{
char const conffile[] = ".coder";
char const * homedir;
size_t len;
char * filename;
if((homedir = getenv("HOME")) == NULL)
return NULL;
len = strlen(homedir) + 1 + strlen(conffile) + 1;
if((filename = malloc(len)) == NULL)
return NULL;
snprintf(filename, len, "%s/%s", homedir, conffile);
return filename;
}
/* coder_delete */
void coder_delete(Coder * coder)
{
char * filename;
size_t i;
if((filename = _config_file()) != NULL)
{
config_save(coder->config, filename);
free(filename);
}
config_delete(coder->config);
for(i = 0; i < coder->projects_cnt; i++)
project_delete(coder->projects[i]);
free(coder->projects);
free(coder);
}
/* useful */
/* coder_about */
static gboolean _about_on_closex(gpointer data);
void coder_about(Coder * coder)
{
if(coder->ab_window != NULL)
{
gtk_window_present(GTK_WINDOW(coder->ab_window));
return;
}
coder->ab_window = desktop_about_dialog_new();
gtk_window_set_transient_for(GTK_WINDOW(coder->ab_window), GTK_WINDOW(
coder->tb_window));
desktop_about_dialog_set_authors(coder->ab_window, _authors);
desktop_about_dialog_set_comments(coder->ab_window,
_("Integrated Development Environment for the DeforaOS"
" desktop"));
desktop_about_dialog_set_copyright(coder->ab_window, _copyright);
desktop_about_dialog_set_logo_icon_name(coder->ab_window, ICON_NAME);
desktop_about_dialog_set_license(coder->ab_window, _license);
desktop_about_dialog_set_name(coder->ab_window, PACKAGE);
desktop_about_dialog_set_version(coder->ab_window, VERSION);
desktop_about_dialog_set_website(coder->ab_window,
"https://www.defora.org/");
g_signal_connect_swapped(coder->ab_window, "delete-event", G_CALLBACK(
_about_on_closex), coder);
gtk_widget_show(coder->ab_window);
}
static gboolean _about_on_closex(gpointer data)
{
Coder * coder = data;
gtk_widget_hide(coder->ab_window);
return TRUE;
}
/* coder_api_reference */
int coder_api_reference(Coder * coder)
{
char const * argv[] = { PROGNAME_HELPER, NULL };
const unsigned int flags = G_SPAWN_SEARCH_PATH;
GError * error = NULL;
if(g_spawn_async(NULL, argv, NULL, flags, NULL, NULL, NULL, &error)
== FALSE)
{
coder_error(coder, error->message, 1);
g_error_free(error);
return -1;
}
return 0;
}
/* coder_error */
int coder_error(Coder * coder, char const * message, int ret)
{
GtkWidget * dialog;
dialog = gtk_message_dialog_new(GTK_WINDOW(coder->tb_window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
#if GTK_CHECK_VERSION(2, 6, 0)
"%s", _("Error"));
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
#endif
"%s", message);
gtk_window_set_title(GTK_WINDOW(dialog), _("Error"));
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
return ret;
}
/* coder_file_open */
void coder_file_open(Coder * coder, char const * filename)
{
/* FIXME really use the MIME sub-system */
char * argv[] = { NULL, NULL, NULL };
char const * p;
GError * error = NULL;
if((p = config_get(coder->config, "editor", "command")) == NULL)
p = "editor"; /* XXX gather defaults in a common place */
if((argv[0] = strdup(p)) == NULL)
return; /* XXX report error */
if(filename != NULL)
argv[1] = strdup(filename); /* XXX check and report error */
if(g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
NULL, &error) != TRUE)
{
coder_error(coder, error->message, 1);
g_error_free(error);
}
free(argv[1]);
free(argv[0]);
}
/* coder_project_open */
int coder_project_open(Coder * coder, char const * filename)
{
Project * project;
if((project = project_new()) == NULL)
return -coder_error(coder, error_get(NULL), 1);
if(project_load(project, filename) != 0
|| coder_project_open_project(coder, project) != 0)
{
project_delete(project);
return -coder_error(coder, error_get(NULL), 1);
}
coder->cur = project;
#if GTK_CHECK_VERSION(2, 24, 0)
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(coder->fi_combo),
project_get_package(project));
#else
gtk_combo_box_append_text(GTK_COMBO_BOX(coder->fi_combo),
project_get_package(project));
#endif
/* FIXME doesn't always select the last project opened */
gtk_combo_box_set_active(GTK_COMBO_BOX(coder->fi_combo),
gtk_combo_box_get_active(GTK_COMBO_BOX(coder->fi_combo))
+ 1);
return 0;
}
/* coder_project_open_dialog */
void coder_project_open_dialog(Coder * coder)
{
GtkWidget * dialog;
GtkFileFilter * filter;
gchar * filename = NULL;
dialog = gtk_file_chooser_dialog_new(_("Open project..."),
GTK_WINDOW(coder->tb_window),
GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL,
GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN,
GTK_RESPONSE_ACCEPT, NULL);
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, _("Project files"));
gtk_file_filter_add_pattern(filter, "project.conf");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, _("All files"));
gtk_file_filter_add_pattern(filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(
dialog));
gtk_widget_destroy(dialog);
if(filename == NULL)
return;
coder_project_open(coder, filename);
g_free(filename);
}
/* coder_project_open_project */
int coder_project_open_project(Coder * coder, Project * project)
{
Project ** p;
if(project == NULL)
return -error_set_code(1, "%s", strerror(EINVAL));;
if((p = realloc(coder->projects, sizeof(*p) * (coder->projects_cnt + 1)))
== NULL)
return -error_set_code(1, "%s", strerror(errno));
coder->projects = p;
coder->projects[coder->projects_cnt++] = project;
return 0;
}
/* coder_project_properties */
void coder_project_properties(Coder * coder)
{
Project * project;
if((project = _coder_get_current_project(coder)) == NULL)
return;
project_properties(project);
}
/* coder_project_save */
int coder_project_save(Coder * coder)
{
Project * project;
if((project = _coder_get_current_project(coder)) == NULL)
return -1;
if(project_get_pathname(project) == NULL)
return coder_project_save_dialog(coder);
return project_save(project);
}
/* coder_project_save_as */
int coder_project_save_as(Coder * coder, char const * filename)
{
Project * project;
if((project = _coder_get_current_project(coder)) == NULL)
return -1;
if(project_set_pathname(project, filename) != 0)
return -1;
return project_save(project);
}
/* coder_project_save_dialog */
int coder_project_save_dialog(Coder * coder)
{
int ret = -1;
Project * project;
GtkWidget * dialog;
gchar * filename = NULL;
if((project = _coder_get_current_project(coder)) == NULL)
return -1;
dialog = gtk_file_chooser_dialog_new(_("Save project as..."), NULL,
GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL,
GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN,
GTK_RESPONSE_ACCEPT, NULL);
/* FIXME add options? (recursive save) */
if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(
dialog));
gtk_widget_destroy(dialog);
if(filename != NULL)
ret = coder_project_save_as(coder, filename);
g_free(filename);
return ret;
}
/* coder_show_files */
static void _show_files_window(Coder * coder);
/* callbacks */
static gboolean _files_on_closex(gpointer data);
void coder_show_files(Coder * coder, gboolean show)
{
if(coder->fi_window == NULL)
_show_files_window(coder);
if(show)
gtk_window_present(GTK_WINDOW(coder->fi_window));
else
gtk_widget_hide(coder->fi_window);
}
static void _show_files_window(Coder * coder)
{
GtkWidget * vbox;
GtkWidget * hbox;
coder->fi_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_default_size(GTK_WINDOW(coder->fi_window), 150, 200);
gtk_window_set_title(GTK_WINDOW(coder->fi_window), _("Files"));
g_signal_connect_swapped(coder->fi_window, "delete-event", G_CALLBACK(
_files_on_closex), coder);
#if GTK_CHECK_VERSION(3, 0, 0)
hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
#else
hbox = gtk_hbox_new(FALSE, 0);
vbox = gtk_vbox_new(FALSE, 0);
#endif
/* FIXME use gtk_container_set_border_width() instead */
gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 2);
#if GTK_CHECK_VERSION(2, 24, 0)
coder->fi_combo = gtk_combo_box_text_new();
#else
coder->fi_combo = gtk_combo_box_new_text();
#endif
gtk_box_pack_start(GTK_BOX(vbox), coder->fi_combo, FALSE, TRUE, 2);
coder->fi_view = gtk_tree_view_new();
gtk_box_pack_start(GTK_BOX(vbox), coder->fi_view, TRUE, TRUE, 2);
gtk_container_add(GTK_CONTAINER(coder->fi_window), hbox);
gtk_widget_show_all(hbox);
}
/* callbacks */
static gboolean _files_on_closex(gpointer data)
{
Coder * coder = data;
gtk_widget_hide(coder->fi_window);
return TRUE;
}
/* coder_show_preferences */
static void _show_preferences_window(Coder * coder);
static void _preferences_set(Coder * coder);
/* callbacks */
static gboolean _on_preferences_closex(gpointer data);
static void _on_preferences_apply(gpointer data);
static void _on_preferences_cancel(gpointer data);
static void _on_preferences_ok(gpointer data);
void coder_show_preferences(Coder * coder, gboolean show)
{
if(coder->pr_window == NULL)
_show_preferences_window(coder);
if(show)
gtk_window_present(GTK_WINDOW(coder->pr_window));
else
gtk_widget_hide(coder->pr_window);
}
static void _show_preferences_window(Coder * coder)
{
GtkWidget * vbox;
GtkWidget * nb;
GtkWidget * nb_vbox;
GtkWidget * hbox;
GtkWidget * b_ok;
GtkWidget * b_apply;
GtkWidget * b_cancel;
coder->pr_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* XXX dialog */
gtk_container_set_border_width(GTK_CONTAINER(coder->pr_window), 4);
gtk_window_set_title(GTK_WINDOW(coder->pr_window), _("Preferences"));
g_signal_connect_swapped(coder->pr_window, "delete-event", G_CALLBACK(
_on_preferences_closex), coder);
vbox = gtk_vbox_new(FALSE, 4);
nb = gtk_notebook_new();
/* notebook page editor */
nb_vbox = gtk_vbox_new(FALSE, 4);
gtk_container_set_border_width(GTK_CONTAINER(nb_vbox), 4);
#if GTK_CHECK_VERSION(3, 0, 0)
hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
#else
hbox = gtk_hbox_new(FALSE, 4);
#endif
gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Editor:")), FALSE,
TRUE, 0);
coder->pr_editor_command = gtk_entry_new();
gtk_box_pack_start(GTK_BOX(hbox), coder->pr_editor_command, TRUE, TRUE,
0);
gtk_box_pack_start(GTK_BOX(nb_vbox), hbox, FALSE, TRUE, 0);
coder->pr_editor_terminal = gtk_check_button_new_with_mnemonic(
_("Run in a _terminal"));
gtk_box_pack_start(GTK_BOX(nb_vbox), coder->pr_editor_terminal, FALSE,
TRUE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(nb), nb_vbox, gtk_label_new(
_("Editor")));
/* notebook page plug-ins */
nb_vbox = gtk_vbox_new(FALSE, 0);
gtk_container_set_border_width(GTK_CONTAINER(nb_vbox), 4);
gtk_notebook_append_page(GTK_NOTEBOOK(nb), nb_vbox, gtk_label_new(
_("Plug-ins")));
gtk_box_pack_start(GTK_BOX(vbox), nb, TRUE, TRUE, 0);
/* buttons */
#if GTK_CHECK_VERSION(3, 0, 0)
hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
#else
hbox = gtk_hbox_new(TRUE, 4);
#endif
b_ok = gtk_button_new_from_stock(GTK_STOCK_OK);
g_signal_connect_swapped(b_ok, "clicked", G_CALLBACK(
_on_preferences_ok), coder);
gtk_box_pack_end(GTK_BOX(hbox), b_ok, FALSE, TRUE, 0);
b_apply = gtk_button_new_from_stock(GTK_STOCK_APPLY);
g_signal_connect_swapped(b_apply, "clicked", G_CALLBACK(
_on_preferences_apply), coder);
gtk_box_pack_end(GTK_BOX(hbox), b_apply, FALSE, TRUE, 0);
b_cancel = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
g_signal_connect_swapped(b_cancel, "clicked", G_CALLBACK(
_on_preferences_cancel), coder);
gtk_box_pack_end(GTK_BOX(hbox), b_cancel, FALSE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(coder->pr_window), vbox);
_preferences_set(coder);
gtk_widget_show_all(vbox);
}
static void _preferences_set(Coder * coder)
{
char const * p;
if((p = config_get(coder->config, "editor", "command")) == NULL)
p = "editor";
gtk_entry_set_text(GTK_ENTRY(coder->pr_editor_command), p);
/* FIXME implement the rest */
}
static void _on_preferences_apply(gpointer data)
{
Coder * coder = data;
config_set(coder->config, "editor", "command", gtk_entry_get_text(
GTK_ENTRY(coder->pr_editor_command)));
/* FIXME implement the rest */
}
static void _on_preferences_cancel(gpointer data)
{
Coder * coder = data;
_preferences_set(coder);
gtk_widget_hide(coder->pr_window);
}
static void _on_preferences_ok(gpointer data)
{
Coder * coder = data;
_on_preferences_apply(coder);
gtk_widget_hide(coder->pr_window);
/* FIXME actually save preferences */
}
/* callbacks */
static gboolean _on_preferences_closex(gpointer data)
{
Coder * coder = data;
_on_preferences_cancel(coder);
return TRUE;
}
/* private */
/* coder_get_current_project */
static Project * _coder_get_current_project(Coder * coder)
{
if(coder->cur == NULL)
{
/* FIXME should not happen (disable callback action) */
coder_error(coder, _("No project opened"), 1);
return NULL;
}
return coder->cur;
}