From 550b632907f47d3e54315994e2e255fccb503437 Mon Sep 17 00:00:00 2001 From: ksterker Date: Sun, 25 Jul 2010 23:52:28 +0200 Subject: [PATCH] ADDED logic to object action GUI --- src/mapedit/Makefile.am | 2 + src/mapedit/entity-properties.glade | 21 +- src/mapedit/gui_entity_dialog.cc | 424 +++++---------------- src/mapedit/gui_entity_dialog.h | 24 +- src/mapedit/gui_script_selector.cc | 551 ++++++++++++++++++++++++++++ src/mapedit/gui_script_selector.h | 168 +++++++++ 6 files changed, 840 insertions(+), 350 deletions(-) create mode 100644 src/mapedit/gui_script_selector.cc create mode 100644 src/mapedit/gui_script_selector.h diff --git a/src/mapedit/Makefile.am b/src/mapedit/Makefile.am index 9f89901..bb90397 100644 --- a/src/mapedit/Makefile.am +++ b/src/mapedit/Makefile.am @@ -18,6 +18,7 @@ noinst_HEADERS = \ gui_mapedit_events.h \ gui_mapview.h \ gui_mapview_events.h \ + gui_script_selector.h \ gui_renderheight.h \ gui_zone.h \ gui_zone_dialog.h \ @@ -38,6 +39,7 @@ mapedit_SOURCES = \ gui_mapedit_events.cc \ gui_mapview.cc \ gui_mapview_events.cc \ + gui_script_selector.cc \ gui_renderheight.cc \ gui_zone.cc \ gui_zone_dialog.cc \ diff --git a/src/mapedit/entity-properties.glade b/src/mapedit/entity-properties.glade index 1fd3d14..c8f844a 100644 --- a/src/mapedit/entity-properties.glade +++ b/src/mapedit/entity-properties.glade @@ -356,9 +356,9 @@ - + True - 2 + vertical True @@ -385,7 +385,7 @@ - + True 1 Arguments @@ -394,11 +394,10 @@ 2 3 GTK_FILL - GTK_FILL - + True True @@ -408,7 +407,6 @@ 2 2 3 - GTK_FILL @@ -425,8 +423,6 @@ 1 2 - GTK_FILL - GTK_FILL @@ -438,16 +434,14 @@ 1 2 - GTK_FILL - GTK_FILL - + True script_methods - + 0 @@ -458,8 +452,6 @@ 2 1 2 - GTK_FILL - GTK_FILL @@ -475,6 +467,7 @@ + False 0 diff --git a/src/mapedit/gui_entity_dialog.cc b/src/mapedit/gui_entity_dialog.cc index aecdd74..97c8eff 100644 --- a/src/mapedit/gui_entity_dialog.cc +++ b/src/mapedit/gui_entity_dialog.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2009 Kai Sterker + Copyright (C) 2009/2010 Kai Sterker Part of the Adonthell Project http://adonthell.linuxgames.com Mapedit is free software; you can redistribute it and/or modify @@ -24,18 +24,14 @@ * @brief View and edit entity properties. */ -#include -#include -#include - #include -#include #include #include "gui_mapedit.h" #include "gui_entity_dialog.h" #include "gui_entity_list.h" +#include "gui_script_selector.h" // Ui definition static char edit_entity_ui[] = @@ -90,142 +86,6 @@ static void on_state_changed (GtkComboBox *cbox, gpointer user_data) } } -// destroy schedule arguments -static void on_clear_args (gpointer data) -{ - PyObject *args = (PyObject*) data; - Py_DECREF(args); -} - -// create UI for editing schedule arguments -static void update_arg_table (GtkTable *arg_table, std::string *arg_list, const long & len) -{ - // remove previous widgets - GList *children = gtk_container_get_children (GTK_CONTAINER(arg_table)); - while (children != NULL) - { - GtkWidget *widget = GTK_WIDGET(children->data); - const gchar* name = gtk_widget_get_name (widget); - if (strncmp ("entry_args", name, 10) == 0 || - strncmp ("lbl_args", name, 8) == 0) - { - gtk_container_remove (GTK_CONTAINER(arg_table), widget); - } - children = g_list_next (children); - } - - // resize table - gtk_table_resize (arg_table, len == 0 ? 2: len + 1, 2); - - // add new widgets - if (len == 0) - { - GtkWidget* label = gtk_label_new ("No arguments required"); - gtk_widget_set_name (label, "lbl_args"); - gtk_misc_set_alignment (GTK_MISC(label), 0.5f, 0.5f); - gtk_widget_set_sensitive (label, false); - gtk_table_attach_defaults (arg_table, label, 0, 2, 1, 2); - } - else - { - char tmp[64]; - GtkAttachOptions fill = GTK_FILL; - GtkAttachOptions fill_expand = (GtkAttachOptions) (GTK_FILL | GTK_EXPAND); - - for (int i = 0; i < len; i++) - { - // add argument name - GtkWidget* label = gtk_label_new (arg_list[i].c_str()); - sprintf (tmp, "lbl_args_%i", i); - gtk_widget_set_name (label, tmp); - gtk_misc_set_alignment (GTK_MISC(label), 1.0f, 0.5f); - gtk_table_attach (arg_table, label, 0, 1, i+1, i+2, fill, fill_expand, 0, 0); - - // add argument value entry - GtkWidget *entry = gtk_entry_new (); - sprintf (tmp, "entry_args_%i", i); - gtk_widget_set_name (entry, tmp); - gtk_table_attach (arg_table, entry, 1, 2, i+1, i+2, fill_expand, fill_expand, 0, 0); - } - } - - // store argument tuple - PyObject *args = PyTuple_New (len); - g_object_set_data_full (G_OBJECT(arg_table), "num-args", (gpointer) args, on_clear_args); - - // make everything visible - gtk_widget_show_all (GTK_WIDGET(arg_table)); -} - -// user selects a different schedule -static void on_schedule_changed (GtkComboBox *cbox, gpointer user_data) -{ - GtkTreeIter iter; - if (gtk_combo_box_get_active_iter (cbox, &iter)) - { - gchar *schedule_name; - GtkTreeModel *model = gtk_combo_box_get_model (cbox); - gtk_tree_model_get (model, &iter, 0, &schedule_name, -1); - - // When a user selects the script, we need to figure out - // if there are additional arguments required for the - // constructor and create the neccessary input fields on the fly. - - // import module - PyObject *module = PyImport_ImportModule ((char*)(SCHEDULE_DIR + std::string (schedule_name)).c_str()); - if (!module) return; - - // try to get schedule class - PyObject *classobj = PyObject_GetAttrString (module, schedule_name); - Py_DECREF (module); - if (!classobj) return; - - PyObject *globals = PyDict_New (); - if (globals != NULL) - { - // need access to the inspect module ... - PyObject *inspect = PyImport_ImportModule ("inspect"); - PyDict_SetItemString (globals, "inspect", inspect); - // ... and the object whose constructor args we want to retrieve - PyDict_SetItemString (globals, "x", classobj); - - // evaluating "x.__init__.im_func.func_code.co_varnames" raises - // a "RuntimeError: restricted attribute", but using the builtin - // inspect module will work. - PyObject *result = python::run_string ("args = inspect.getargspec(x.__init__)[0];", Py_file_input, globals); - if (result != NULL) - { - // get result from globals - PyObject *args = PyDict_GetItemString (globals, "args"); - if (PyList_Check(args)) - { - // iterate over the list of constructor arguments - // we can skip the first (self) and second (schedule) - long len = PyList_GET_SIZE(args); - std::string arg_list[len-2]; - for (long i = 2; i < len; i++) - { - PyObject *arg = PyList_GET_ITEM(args, i); - const char* arg_name = PyString_AsString (arg); - arg_list[i-2] = std::string(arg_name); - } - - GtkTable *arg_table = GTK_TABLE(user_data); - update_arg_table (arg_table, arg_list, len-2); - } - - Py_DECREF(result); - Py_XDECREF(args); - } - - Py_DECREF(globals); - Py_XDECREF(inspect); - } - - Py_DECREF(classobj); - } -} - // the entity object type has changed static void on_type_changed (GtkToggleButton * button, gpointer user_data) { @@ -260,6 +120,24 @@ static void on_type_changed (GtkToggleButton * button, gpointer user_data) } } +// callback for location changed +static void selected_event (GtkTreeSelection *selection, gpointer user_data) +{ + GtkTreeModel *model; + GtkTreeIter iter; + + // anything selected at all? + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + // get location at selected row + world::chunk_info *loc; + gtk_tree_model_get (model, &iter, 1, &loc, -1); + + GuiEntityDialog *dlg = (GuiEntityDialog *) user_data; + dlg->setLocation (loc); + } +} + // ctor GuiEntityDialog::GuiEntityDialog (MapEntity *entity, const GuiEntityDialog::Mode & mode) : GuiModalDialog (GTK_WINDOW(GuiMapedit::window->getWindow())) @@ -275,7 +153,8 @@ GuiEntityDialog::GuiEntityDialog (MapEntity *entity, const GuiEntityDialog::Mode EntityType = 'A'; EntityState = ""; ObjType = world::OBJECT; - + Selector = NULL; + if (!gtk_builder_add_from_string(Ui, edit_entity_ui, -1, &err)) { g_message ("building entity dialog failed: %s", err->message); @@ -328,10 +207,11 @@ GuiEntityDialog::GuiEntityDialog (MapEntity *entity, const GuiEntityDialog::Mode widget = gtk_builder_get_object (Ui, "cb_state"); g_signal_connect (widget, "changed", G_CALLBACK (on_state_changed), this); - widget = gtk_builder_get_object (Ui, "cb_schedule"); - g_signal_connect (widget, "changed", G_CALLBACK (on_schedule_changed), - gtk_builder_get_object (Ui, "tbl_schedule")); - + + widget = gtk_builder_get_object (Ui, "location_list"); + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(widget)); + g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK(selected_event), this); + // disable goto button (until we actually have a use for it) widget = gtk_builder_get_object (Ui, "btn_goto"); gtk_widget_set_sensitive (GTK_WIDGET (widget), FALSE); @@ -388,44 +268,33 @@ GuiEntityDialog::GuiEntityDialog (MapEntity *entity, const GuiEntityDialog::Mode gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE(state_list), 0, sort_strings, NULL, NULL); gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(state_list), 0, GTK_SORT_ASCENDING); - - // set valid manager schedules - scanMgrSchedules(); - + // set placeable type world::placeable_type obj_type = entity->get_object_type(); + set_object_type (obj_type); switch (obj_type) { case world::OBJECT: { - widget = gtk_builder_get_object (Ui, "type_scenery"); - set_page_active (SCENERY_PAGE, Entity->getLocation() != NULL); - set_page_active (CHARACTER_PAGE, false); + world::chunk_info *loc = Entity->getLocation(); + if (loc != NULL) init_from_scenery (loc); break; } case world::CHARACTER: { - widget = gtk_builder_get_object (Ui, "type_character"); world::character *chr = dynamic_cast(entity->object()); if (chr != NULL) init_from_character (chr); - set_page_active (SCENERY_PAGE, false); break; } case world::ITEM: { - widget = gtk_builder_get_object (Ui, "type_item"); break; } default: { - widget = NULL; break; } } - if (widget != NULL) - { - gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE); - } // in editing mode, change of object type is no longer allowed if (mode == UPDATE_PROPERTIES) @@ -538,6 +407,10 @@ void GuiEntityDialog::applyChanges() // set entity state objToUpdate->object()->set_state (EntityState); + if (ObjType == world::OBJECT) + { + set_scenery_data (Entity->getLocation()); + } // set character schedule, if neccessary if (ObjType == world::CHARACTER) { @@ -562,12 +435,22 @@ void GuiEntityDialog::set_object_type (const world::placeable_type & type) GObject *cb_anonymous = gtk_builder_get_object (Ui, "entity_anonymous"); GObject *cb_shared = gtk_builder_get_object (Ui, "entity_shared"); GObject *cb_unique = gtk_builder_get_object (Ui, "entity_unique"); - + + delete Selector; + // depending on the placeable type, only a number of entity types make sense switch (type) { case world::OBJECT: { + Selector = new GuiScriptSelector ( + GTK_COMBO_BOX(gtk_builder_get_object (Ui, "cb_scripts")), + GTK_COMBO_BOX(gtk_builder_get_object (Ui, "cb_methods")), + GTK_CONTAINER(gtk_builder_get_object (Ui, "tbl_scripts"))); + Selector->set_argument_offset (2); + Selector->set_script_filter (""); + Selector->set_script_package (ACTION_DIR); + gtk_widget_set_sensitive (GTK_WIDGET (cb_anonymous), TRUE); gtk_widget_set_sensitive (GTK_WIDGET (cb_shared), TRUE); gtk_widget_set_sensitive (GTK_WIDGET (cb_unique), TRUE); @@ -579,6 +462,13 @@ void GuiEntityDialog::set_object_type (const world::placeable_type & type) } case world::CHARACTER: { + Selector = new GuiScriptSelector ( + GTK_COMBO_BOX(gtk_builder_get_object (Ui, "cb_schedule")), NULL, + GTK_CONTAINER(gtk_builder_get_object (Ui, "tbl_schedule"))); + Selector->set_argument_offset (1); + Selector->set_script_filter ("run"); + Selector->set_script_package (SCHEDULE_DIR); + gtk_widget_set_sensitive (GTK_WIDGET (cb_anonymous), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (cb_shared), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (cb_unique), TRUE); @@ -591,6 +481,8 @@ void GuiEntityDialog::set_object_type (const world::placeable_type & type) } case world::ITEM: { + Selector = NULL; + gtk_widget_set_sensitive (GTK_WIDGET (cb_anonymous), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (cb_shared), TRUE); gtk_widget_set_sensitive (GTK_WIDGET (cb_unique), TRUE); @@ -606,6 +498,7 @@ void GuiEntityDialog::set_object_type (const world::placeable_type & type) } default: { + Selector = NULL; break; } } @@ -656,68 +549,14 @@ void GuiEntityDialog::set_entity_state (const std::string & state) Entity->object()->set_state (prevState); } -// find all valid manager schedule scripts -void GuiEntityDialog::scanMgrSchedules () +// init values on scenery page +void GuiEntityDialog::init_from_scenery (world::chunk_info *location) { - std::string scheduledir = base::Paths.user_data_dir () + "/schedules/char"; - - DIR *dir; - GtkTreeIter iter; - struct dirent *dirent; - struct stat statbuf; - - GObject *widget = gtk_builder_get_object (Ui, "cb_schedule"); - GtkListStore *schedule_list = GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX(widget))); - gtk_list_store_clear (schedule_list); - - // open directory - if ((dir = opendir (scheduledir.c_str ())) != NULL) + if (location->has_action()) { - // read directory contents - while ((dirent = readdir (dir)) != NULL) - { - // skip anything starting with . - if (dirent->d_name[0] == '.') continue; - - string filepath = scheduledir + "/"; - filepath += dirent->d_name; - - if (stat (filepath.c_str (), &statbuf) != -1) - { - // schedules are .py files - if (S_ISREG (statbuf.st_mode) && filepath.compare (filepath.length() - 3, 3, ".py") == 0) - { - // check if this is a valid manager schedule - std::string name (dirent->d_name, strlen(dirent->d_name) - 3); - - // import module - PyObject *module = PyImport_ImportModule ((char *) (SCHEDULE_DIR + name).c_str ()); - if (!module) continue; - - // try to get schedule class - PyObject *classobj = PyObject_GetAttrString (module, (char *) name.c_str ()); - Py_DECREF (module); - if (!classobj) continue; - - // check class object for run method - bool hasRunMet = PyObject_HasAttrString (classobj, "run"); - Py_DECREF (classobj); - if (!hasRunMet) continue; - - // passed, so add it to list - gchar *mgr_schedule = g_strdup (name.c_str()); - - // get new row - gtk_list_store_append (schedule_list, &iter); - - // set our data - gtk_list_store_set (schedule_list, &iter, 0, mgr_schedule, -1); - } - } - } - - closedir (dir); - } + world::action *act = location->get_action(); + Selector->init (act->get_method(), act->get_args()); + } } // init values on character page @@ -725,122 +564,40 @@ void GuiEntityDialog::init_from_character (world::character *chr) { world::schedule *sdl = chr->get_schedule(); const python::script *scr = sdl->get_manager(); - if (scr != NULL) - { - GtkTreeIter iter; - GtkComboBox *widget = GTK_COMBO_BOX(gtk_builder_get_object (Ui, "cb_schedule")); - GtkTreeModel *model = gtk_combo_box_get_model (widget); - - // select script in drop down list - std::string name = scr->class_name(); - if (gtk_tree_model_get_iter_first (model, &iter)) - { - gchar *val; - do - { - gtk_tree_model_get (model, &iter, 0, &val, -1); - if (strcmp (name.c_str(), val) == 0) - { - gtk_combo_box_set_active_iter (widget, &iter); - break; - } - } - while (gtk_tree_model_iter_next (model, &iter)); - } + Selector->init (scr); +} - // not found? - if (gtk_combo_box_get_active (widget) < 0) - { - // smells fishy, but lets keep existing data - gchar *val = g_strdup (name.c_str()); - gtk_list_store_append (GTK_LIST_STORE (model), &iter); - gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, val, -1); - gtk_combo_box_set_active_iter (widget, &iter); - } - - // set argument value(s) - PyObject *args = scr->get_args(); - if (args != NULL) - { - GtkContainer *arg_table = GTK_CONTAINER (gtk_builder_get_object (Ui, "tbl_schedule")); - GList *children = gtk_container_get_children (arg_table); - while (children != NULL) - { - GtkWidget *widget = GTK_WIDGET(children->data); - const gchar* name = gtk_widget_get_name (widget); - if (strncmp ("entry_args", name, 10) == 0) - { - int index; - if (sscanf(name, "entry_args_%d", &index) == 1) - { - PyObject *arg = PyTuple_GetItem (args, index+1); // first arg is the schedule itself - if (arg != NULL) - { - PyObject *val = PyObject_Str (arg); - gtk_entry_set_text (GTK_ENTRY(widget), g_strdup(PyString_AsString(val))); - Py_DECREF(val); - } - } - } - - children = g_list_next (children); - } - } - } +// set scenery specific data +void GuiEntityDialog::set_scenery_data (world::chunk_info *location) +{ + if (location == NULL) return; + + // get script name (if any) + std::string script = Selector->get_script_name(); + if ("" == script) return; + + // get method name (if any) + std::string method = Selector->get_method_name(); + if ("" == method) return; + + // get method arguments + PyObject *args = Selector->get_arguments(); + + // set selected action + world::action *act = location->set_action(); + act->init (script, method, args); + Py_DECREF(args); } // set character specific data void GuiEntityDialog::set_character_data (world::character *chr) { // get schedule name (if any) - GtkComboBox *widget = GTK_COMBO_BOX(gtk_builder_get_object (Ui, "cb_schedule")); - const gchar *value = gtk_combo_box_get_active_text (GTK_COMBO_BOX (widget)); - if (value == NULL) return; + std::string name = Selector->get_script_name(); + if ("" == name) return; - std::string name (value); - // get schedule arguments - GtkContainer *arg_table = GTK_CONTAINER (gtk_builder_get_object (Ui, "tbl_schedule")); - PyObject *args = (PyObject*) g_object_get_data (G_OBJECT(arg_table), "num-args"); - if (args != NULL) - { - Py_INCREF (args); - } - else - { - args = PyTuple_New (0); - } - - long num_args = PyTuple_GET_SIZE(args); - if (num_args > 0) - { - GList *children = gtk_container_get_children (arg_table); - while (children != NULL) - { - GtkWidget *widget = GTK_WIDGET(children->data); - const gchar* name = gtk_widget_get_name (widget); - if (strncmp ("entry_args", name, 10) == 0) - { - int index; - if (sscanf(name, "entry_args_%d", &index) == 1) - { - char *pend = NULL; - PyObject *py_val = NULL; - const gchar *val = gtk_entry_get_text (GTK_ENTRY(widget)); - // try cast to int first - py_val = PyInt_FromString((char*) val, &pend, 0); - if (py_val == NULL || *pend != NULL) - { - // fallback to string on error - py_val = PyString_FromString(val); - } - PyTuple_SetItem (args, index, py_val); - } - } - - children = g_list_next (children); - } - } + PyObject *args = Selector->get_arguments(); // set selected schedule world::schedule *sdl = chr->get_schedule(); @@ -888,3 +645,10 @@ void GuiEntityDialog::setLocations () gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE(location_list), 0, GTK_SORT_ASCENDING); } } + +// update location of the entity being edited +void GuiEntityDialog::setLocation (world::chunk_info *location) +{ + Entity->setLocation (location); + init_from_scenery (location); +} diff --git a/src/mapedit/gui_entity_dialog.h b/src/mapedit/gui_entity_dialog.h index 4a18ad7..73aa640 100644 --- a/src/mapedit/gui_entity_dialog.h +++ b/src/mapedit/gui_entity_dialog.h @@ -35,6 +35,8 @@ namespace world class character; } +class GuiScriptSelector; + /** * A dialog to display and edit map entity properties. */ @@ -89,6 +91,8 @@ class GuiEntityDialog : public GuiModalDialog * @param state name of the state to set/display. */ void set_entity_state (const std::string & state); + + void setLocation (world::chunk_info *location); //@} protected: @@ -99,23 +103,29 @@ class GuiEntityDialog : public GuiModalDialog */ void set_page_active (const int & page, const bool & active); - /** - * Scan character schedule directory for valid manager schedule - * scripts and populate the drop down menu accordingly. - */ - void scanMgrSchedules (); - /** * Populate the list of locations at which the entity exists. */ void setLocations (); + /** + * Initialize scenery-specific page(s). + * @param location the object's location. + */ + void init_from_scenery (world::chunk_info *location); + /** * Initialize character-specific page(s). * @param chr the character instance to pick values from. */ void init_from_character (world::character *chr); + /** + * Store data from scenery-specific page(s). + * @param location the scenery instance to update. + */ + void set_scenery_data (world::chunk_info *location); + /** * Store data from character-specific page(s). * @param chr the character instance to update. @@ -134,6 +144,8 @@ class GuiEntityDialog : public GuiModalDialog char EntityType; /// the user interface GtkBuilder *Ui; + /// Python script helper + GuiScriptSelector *Selector; }; #endif diff --git a/src/mapedit/gui_script_selector.cc b/src/mapedit/gui_script_selector.cc new file mode 100644 index 0000000..a2b151d --- /dev/null +++ b/src/mapedit/gui_script_selector.cc @@ -0,0 +1,551 @@ +/* + Copyright (C) 2010 Kai Sterker + Part of the Adonthell Project http://adonthell.linuxgames.com + + Mapedit 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. + + Mapedit 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 Mapedit; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +/** + * @file mapedit/gui_script_selector.cc + * + * @author Kai Sterker + * @brief assign python scripts and arguments. + */ + +#include +#include +#include + +#include +#include +#include + +#include "gui_script_selector.h" + +// destroy schedule arguments +static void on_clear_args (gpointer data) +{ + PyObject *args = (PyObject*) data; + Py_DECREF(args); +} + +// user selects a different schedule +static void on_script_changed (GtkComboBox *cbox, gpointer user_data) +{ + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (cbox, &iter)) + { + gchar *scrpt_name; + GtkTreeModel *model = gtk_combo_box_get_model (cbox); + gtk_tree_model_get (model, &iter, 0, &scrpt_name, -1); + + GuiScriptSelector *selector = (GuiScriptSelector*) user_data; + selector->script_selected (scrpt_name); + } +} + +// user selects a different method +static void on_method_changed (GtkComboBox *cbox, gpointer user_data) +{ + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (cbox, &iter)) + { + gchar *met_name; + GtkTreeModel *model = gtk_combo_box_get_model (cbox); + gtk_tree_model_get (model, &iter, 0, &met_name, -1); + + GuiScriptSelector *selector = (GuiScriptSelector*) user_data; + selector->method_selected (met_name); + } +} + +// ctor +GuiScriptSelector::GuiScriptSelector (GtkComboBox *scrpt, GtkComboBox *met, GtkContainer *args) +{ + Script = scrpt; + Method = met; + Arguments = args; + FilterMethod = ""; + CurScript = ""; + CurMethod = ""; + ArgOffset = 0; + + g_signal_connect (G_OBJECT(scrpt), "changed", G_CALLBACK (on_script_changed), this); + if (met != NULL) + { + g_signal_connect (G_OBJECT(met), "changed", G_CALLBACK (on_method_changed), this); + } +} + +// set place containing python scripts +void GuiScriptSelector::set_script_package (const std::string & dir) +{ + ScriptPkg = dir; + + std::string scandir = dir; + for (unsigned long i = 0; i < scandir.length(); i++) + { + if (scandir[i] == '.') scandir[i] = '/'; + } + + GtkListStore *script_list = GTK_LIST_STORE (gtk_combo_box_get_model (Script)); + gtk_list_store_clear (script_list); + + scan_script_dir (base::Paths.game_data_dir () + scandir); + scan_script_dir (base::Paths.user_data_dir () + scandir); +} + +// find all valid manager schedule scripts +void GuiScriptSelector::scan_script_dir (const std::string & script_dir) +{ + DIR *dir; + GtkTreeIter iter; + struct dirent *dirent; + struct stat statbuf; + + // open directory + if ((dir = opendir (script_dir.c_str ())) != NULL) + { + GtkListStore *script_list = GTK_LIST_STORE (gtk_combo_box_get_model (Script)); + printf ("*** info: scanning '%s' for python scripts.\n", script_dir.c_str()); + + // read directory contents + while ((dirent = readdir (dir)) != NULL) + { + // skip anything starting with . + if (dirent->d_name[0] == '.') continue; + + string filepath = script_dir + dirent->d_name; + if (stat (filepath.c_str (), &statbuf) != -1) + { + // scripts are .py files + if (S_ISREG (statbuf.st_mode) && filepath.compare (filepath.length() - 3, 3, ".py") == 0) + { + // check if this is a valid script + std::string name (dirent->d_name, strlen(dirent->d_name) - 3); + if ("__init__" == name) continue; + + printf (" - trying '%s' ... ", name.c_str()); + + // import module + PyObject *module = PyImport_ImportModule ((char *) (ScriptPkg + name).c_str ()); + if (!module) + { + printf ("import failed\n"); + continue; + } + + // try to get class + PyObject *classobj = PyObject_GetAttrString (module, (char *) name.c_str ()); + Py_DECREF (module); + if (!classobj) + { + printf ("class '%s' not found in script\n", name.c_str()); + continue; + } + + // check class object for required method + bool hasMet = true; + if (FilterMethod.length() > 0) + { + hasMet = PyObject_HasAttrString (classobj, (char *) FilterMethod.c_str()); + } + + Py_DECREF (classobj); + if (!hasMet) + { + printf ("method '%s' not found in class\n", FilterMethod.c_str()); + continue; + } + + // passed, so add it to list + gchar *script_name = g_strdup (name.c_str()); + + // FIXME: check if script is already contained + + // get new row + gtk_list_store_append (script_list, &iter); + + // set our data + gtk_list_store_set (script_list, &iter, 0, script_name, -1); + + printf ("okay\n"); + } + } + } + + closedir (dir); + } +} + +// init the selector from a python script object +void GuiScriptSelector::init (const python::script *scrpt) +{ + if (scrpt != NULL) + { + // select script in drop down list + select_entry (Script, scrpt->class_name()); + + // set argument value(s) + set_arguments (scrpt->get_args()); + } +} + +// init from method and arguments +void GuiScriptSelector::init (const python::method *met, PyObject *args) +{ + if (met != NULL) + { + // select script in drop down list + select_entry (Script, met->script ()); + + // select method in drop down list + select_entry (Method, met->name ()); + + // set argument value(s) + set_arguments (args); + } +} + +// get argument list +PyObject *GuiScriptSelector::get_arguments () const +{ + PyObject *args = (PyObject*) g_object_get_data (G_OBJECT(Arguments), "num-args"); + if (args != NULL) + { + Py_INCREF (args); + } + else + { + args = PyTuple_New (0); + } + + long num_args = PyTuple_GET_SIZE(args); + if (num_args > 0) + { + printf ("*** info: collecting %i argument(s)\n", num_args); + + GList *children = gtk_container_get_children (Arguments); + while (children != NULL) + { + GtkWidget *widget = GTK_WIDGET(children->data); + const gchar* name = gtk_widget_get_name (widget); + if (strncmp ("entry_args", name, 10) == 0) + { + int index; + if (sscanf(name, "entry_args_%d", &index) == 1) + { + char *pend = NULL; + const gchar *val = gtk_entry_get_text (GTK_ENTRY(widget)); + printf (" - arg %i = %s\n", index, val); + + // try cast to int first + PyObject *py_val = PyInt_FromString((char*) val, &pend, 0); + if (py_val == NULL || *pend != NULL) + { + // fallback to string on error + py_val = PyString_FromString(val); + } + + PyTuple_SetItem (args, index, py_val); + } + } + + children = g_list_next (children); + } + } + + return args; +} + +// new script selected in list +void GuiScriptSelector::script_selected (const std::string & name) +{ + CurScript = name; + + if (Method == NULL) + { + // we have a class and need to gather the arguments required for the constructor + method_selected ("__init__"); + } + else + { + // import module + PyObject *module = PyImport_ImportModule ((char*)(ScriptPkg + CurScript).c_str()); + if (!module) return; + + // try to get class + PyObject *classobj = PyObject_GetAttrString (module, (char *) CurScript.c_str()); + Py_DECREF (module); + if (!classobj) return; + + // extract all methods of this class + get_class_methods (classobj); + + // reset arguments + update_arg_table (NULL, 0); + + // cleanup + Py_DECREF (classobj); + } +} + +// new method selected in list +void GuiScriptSelector::method_selected (const std::string & name) +{ + CurMethod = name; + + // When a user selects the method, we need to figure out + // if there are additional arguments required to call it + // and create the neccessary input fields on the fly. + + // import module + PyObject *module = PyImport_ImportModule ((char*)(ScriptPkg + CurScript).c_str()); + if (!module) return; + + // try to get class object of same name + PyObject *classobj = PyObject_GetAttrString (module, (char *) CurScript.c_str()); + Py_DECREF (module); + if (!classobj) return; + + // read method arguments + get_method_arguments (classobj, name); + + // cleanup + Py_DECREF (classobj); +} + +// get the methods of a python class +void GuiScriptSelector::get_class_methods (PyObject *classobj) +{ + PyObject *globals = PyDict_New (); + if (globals != NULL) + { + // need access to the inspect module ... + PyObject *inspect = PyImport_ImportModule ("inspect"); + PyDict_SetItemString (globals, "inspect", inspect); + // ... and the object whose constructor args we want to retrieve + PyDict_SetItemString (globals, "x", classobj); + + std::string cmd = "meths = [a[0] for a in inspect.getmembers(x, inspect.ismethod)];"; + PyObject *result = python::run_string (cmd, Py_file_input, globals); + if (result != NULL) + { + // get result from globals + PyObject *meths = PyDict_GetItemString (globals, "meths"); + if (PyList_Check(meths)) + { + GtkTreeIter iter; + GtkListStore *method_list = GTK_LIST_STORE (gtk_combo_box_get_model (Method)); + gtk_list_store_clear (method_list); + + long len = PyList_GET_SIZE(meths); + for (long i = 0; i < len; i++) + { + // get method name + PyObject *met = PyList_GET_ITEM(meths, i); + gchar* met_name = PyString_AsString (met); + + // append the new row + gtk_list_store_append (method_list, &iter); + gtk_list_store_set (method_list, &iter, 0, met_name, -1); + } + } + + Py_DECREF(result); + Py_XDECREF(meths); + } + + Py_DECREF(globals); + Py_XDECREF(inspect); + } +} + +// get the arguments of a python method +void GuiScriptSelector::get_method_arguments (PyObject *classobj, const std::string & method) +{ + PyObject *globals = PyDict_New (); + if (globals != NULL) + { + // need access to the inspect module ... + PyObject *inspect = PyImport_ImportModule ("inspect"); + PyDict_SetItemString (globals, "inspect", inspect); + // ... and the object whose constructor args we want to retrieve + PyDict_SetItemString (globals, "x", classobj); + + // evaluating "x..im_func.func_code.co_varnames" raises + // a "RuntimeError: restricted attribute", but using the builtin + // inspect module will work. + std::string cmd = "args = inspect.getargspec(x." + method + ")[0];"; + PyObject *result = python::run_string (cmd, Py_file_input, globals); + if (result != NULL) + { + // get result from globals + PyObject *args = PyDict_GetItemString (globals, "args"); + if (PyList_Check(args)) + { + // iterate over the list of constructor arguments + // we can skip the first (self) and subsequent fixed arguments + int offset = ArgOffset + 1; + long len = PyList_GET_SIZE(args); + std::string arg_list[len-offset]; + for (long i = offset; i < len; i++) + { + PyObject *arg = PyList_GET_ITEM(args, i); + const char* arg_name = PyString_AsString (arg); + arg_list[i-offset] = std::string(arg_name); + } + + update_arg_table (arg_list, len-offset); + } + + Py_DECREF(result); + Py_XDECREF(args); + } + + Py_DECREF(globals); + Py_XDECREF(inspect); + } +} + +// create UI for editing arguments +void GuiScriptSelector::update_arg_table (std::string *arg_list, const long & len) +{ + int rows; + // count remaining table rows --> we have to keep those + g_object_get(G_OBJECT(Arguments), "n-rows", &rows, NULL); + + // remove previous widgets + GList *children = gtk_container_get_children (GTK_CONTAINER(Arguments)); + while (children != NULL) + { + GtkWidget *widget = GTK_WIDGET(children->data); + const gchar* name = gtk_widget_get_name (widget); + if (strncmp ("entry_args", name, 10) == 0) + { + gtk_container_remove (GTK_CONTAINER(Arguments), widget); + } + else if (strncmp ("lbl_args", name, 8) == 0) + { + gtk_container_remove (GTK_CONTAINER(Arguments), widget); + rows--; + } + children = g_list_next (children); + } + + // resize table + gtk_table_resize (GTK_TABLE(Arguments), len == 0 ? rows + 1 : rows + len, 2); + + // add new widgets + if (len == 0) + { + GtkWidget* label = gtk_label_new ("No arguments required"); + gtk_widget_set_name (label, "lbl_args"); + gtk_misc_set_alignment (GTK_MISC(label), 0.5f, 0.5f); + gtk_widget_set_sensitive (label, false); + gtk_table_attach_defaults (GTK_TABLE(Arguments), label, 0, 2, rows, rows + 1); + } + else + { + char tmp[64]; + GtkAttachOptions fill = GTK_FILL; + GtkAttachOptions fill_expand = (GtkAttachOptions) (GTK_FILL | GTK_EXPAND); + + for (int i = 0; i < len; i++) + { + // add argument name + GtkWidget* label = gtk_label_new (arg_list[i].c_str()); + sprintf (tmp, "lbl_args_%i", i); + gtk_widget_set_name (label, tmp); + gtk_misc_set_alignment (GTK_MISC(label), 1.0f, 0.5f); + gtk_table_attach (GTK_TABLE(Arguments), label, 0, 1, i+rows, i+rows+1, fill, fill_expand, 0, 0); + + // add argument value entry + GtkWidget *entry = gtk_entry_new (); + sprintf (tmp, "entry_args_%i", i); + gtk_widget_set_name (entry, tmp); + gtk_table_attach (GTK_TABLE(Arguments), entry, 1, 2, i+rows, i+rows+1, fill_expand, fill_expand, 0, 0); + } + } + + // store argument tuple + PyObject *args = PyTuple_New (len); + g_object_set_data_full (G_OBJECT(Arguments), "num-args", (gpointer) args, on_clear_args); + + // make everything visible + gtk_widget_show_all (GTK_WIDGET(Arguments)); +} + +// set given arguments in UI +void GuiScriptSelector::set_arguments (PyObject *args) +{ + if (args != NULL) + { + GList *children = gtk_container_get_children (Arguments); + while (children != NULL) + { + GtkWidget *widget = GTK_WIDGET(children->data); + const gchar* name = gtk_widget_get_name (widget); + if (strncmp ("entry_args", name, 10) == 0) + { + int index; + if (sscanf(name, "entry_args_%d", &index) == 1) + { + PyObject *arg = PyTuple_GetItem (args, index + ArgOffset); + if (arg != NULL) + { + PyObject *val = PyObject_Str (arg); + gtk_entry_set_text (GTK_ENTRY(widget), g_strdup(PyString_AsString(val))); + Py_DECREF(val); + } + } + } + + children = g_list_next (children); + } + } +} + +// set combobox selection +void GuiScriptSelector::select_entry (GtkComboBox *cb, const std::string & entry) +{ + GtkTreeIter iter; + GtkTreeModel *model = gtk_combo_box_get_model (cb); + + if (gtk_tree_model_get_iter_first (model, &iter)) + { + gchar *val; + do + { + gtk_tree_model_get (model, &iter, 0, &val, -1); + if (strcmp (entry.c_str(), val) == 0) + { + gtk_combo_box_set_active_iter (cb, &iter); + break; + } + } + while (gtk_tree_model_iter_next (model, &iter)); + } + + // not found? + if (gtk_combo_box_get_active (cb) < 0) + { + // smells fishy, but lets keep existing data + gchar *val = g_strdup (entry.c_str()); + gtk_list_store_append (GTK_LIST_STORE (model), &iter); + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, val, -1); + gtk_combo_box_set_active_iter (cb, &iter); + } +} diff --git a/src/mapedit/gui_script_selector.h b/src/mapedit/gui_script_selector.h new file mode 100644 index 0000000..daef902 --- /dev/null +++ b/src/mapedit/gui_script_selector.h @@ -0,0 +1,168 @@ +/* + Copyright (C) 2010 Kai Sterker + Part of the Adonthell Project http://adonthell.linuxgames.com + + Mapedit 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. + + Mapedit 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 Mapedit; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/** + * @file mapedit/gui_script_selector.h + * + * @author Kai Sterker + * @brief assign python scripts and arguments. + */ + +#ifndef GUI_SCRIPT_SELECTOR_H +#define GUI_SCRIPT_SELECTOR_H + +#include +#include + +/** + * Helper class to select from a list of python scripts, + * an (optional) list of methods from the selected scripts + * and finally fill in the the arguments required either + * for the scripts constructor or the selected method. + */ +class GuiScriptSelector +{ +public: + /** + * Create a script selector instance. + * @param scrpt a combobox used to present the python scripts/classes. + * @param met a combobox used to present the methods of the selected scripts (or NULL) + * @param args a table to edit the arguments for method or constructor call. + */ + GuiScriptSelector (GtkComboBox *scrpt, GtkComboBox *met, GtkContainer *args); + + /** + * @name Customize selector behaviour. + */ + //@{ + /** + * Set the Python package that contains the available scripts. + * Should be relative to the respective game data directory. + * @param dir a python package. + */ + void set_script_package (const std::string & dir); + + /** + * Set the number of fixed arguments. Most scripts used by the + * Adonthell engine have some arguments that are supplied by the + * engine itself, followed by an optional number of custom arguments. + * This method allows to exclude former, so only the custom arguments + * are presented to and editable by the user. + * @param number of arguments to exclude from editing. + */ + void set_argument_offset (const int & offset) { ArgOffset = offset; } + + /** + * Some scripts require a certain method to be present. Setting this + * method name as filter assures that only those scripts show up in + * the list that implement this method. Set to "" to disable the filter. + * @param filter a method name to filter scripts by. + */ + void set_script_filter (const std::string & filter) { FilterMethod = filter; } + //@} + + /** + * @name Setting initial state. + */ + //@{ + /** + * Initialize selector that only displays a script selection. + * Arguments are set from the scripts constructor. + * @param scrpt the script being preselected. + */ + void init (const python::script *scrpt); + + /** + * Initialize selector that displays both script and method + * selection. Arguments are those used to call the given method. + * @param met the script and method to preselect. + * @param args the method arguments. + */ + void init (const python::method *met, PyObject *args); + //@} + + /** + * @name Result extraction + */ + //@{ + /** + * Get name of the selected script. + * @return Script the user has selected. + */ + std::string get_script_name () const { return CurScript; } + + /** + * Get name of the selected script. + * @return Script the user has selected. + */ + std::string get_method_name () const { return CurMethod; } + + /** + * Get tuple containing the optional arguments. + * @return Arguments the user has specified. + */ + PyObject *get_arguments () const; + //@} + + /** + * @name Callbacks. + */ + //@{ + /** + * Called when the user selected a new item in + * the list of scripts. + * @param name name of the selected script. + */ + void script_selected (const std::string & name); + + /** + * Called when the user selected a new item in + * the list of methods. + * @param name name of the selected method. + */ + void method_selected (const std::string & name); + //@} + +private: + void scan_script_dir (const std::string & script_dir); + void update_arg_table (std::string *arg_list, const long & len); + void set_arguments (PyObject *args); + void get_class_methods (PyObject *classobj); + void get_method_arguments (PyObject *classobj, const std::string & method); + void select_entry (GtkComboBox *cb, const std::string & entry); + + /// a list of scripts contained in the package directory + GtkComboBox *Script; + /// a list of methods contained in the script + GtkComboBox *Method; + /// a GtkTable to edit the method arguments + GtkContainer *Arguments; + /// scripts to include must implement this method + std::string FilterMethod; + /// the package from which to load scripts + std::string ScriptPkg; + /// the currently selected script + std::string CurScript; + /// the currently selected method + std::string CurMethod; + /// number of fixed arguments past to the method + int ArgOffset; +}; + +#endif