| /* |
| * Copyright 2019 University of Toronto |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| * Authors: Mario Badr, Sameh Attia and Tanner Young-Schultz |
| */ |
| |
| #include "ezgl/application.hpp" |
| |
| namespace ezgl { |
| // A flag to disable event loop (default is false) |
| bool disable_event_loop = false; |
| |
| void application::startup(GtkApplication *, gpointer user_data) |
| { |
| auto ezgl_app = static_cast<application *>(user_data); |
| g_return_if_fail(ezgl_app != nullptr); |
| |
| char const *main_ui_resource = ezgl_app->m_main_ui.c_str(); |
| |
| // Build the main user interface from the XML resource. |
| GError *error = nullptr; |
| if(gtk_builder_add_from_resource(ezgl_app->m_builder, main_ui_resource, &error) == 0) { |
| g_error("%s.", error->message); |
| } |
| |
| for(auto &c_pair : ezgl_app->m_canvases) { |
| GObject *drawing_area = ezgl_app->get_object(c_pair.second->id()); |
| c_pair.second->initialize(GTK_WIDGET(drawing_area)); |
| } |
| |
| g_info("application::startup successful."); |
| } |
| |
| void application::activate(GtkApplication *, gpointer user_data) |
| { |
| auto ezgl_app = static_cast<application *>(user_data); |
| g_return_if_fail(ezgl_app != nullptr); |
| |
| // The main parent window needs to be explicitly added to our GTK application. |
| GObject *window = ezgl_app->get_object(ezgl_app->m_window_id.c_str()); |
| gtk_application_add_window(ezgl_app->m_application, GTK_WINDOW(window)); |
| |
| // Setup the default callbacks for the mouse and key events |
| register_default_events_callbacks(ezgl_app); |
| |
| if(ezgl_app->m_register_callbacks != nullptr) { |
| ezgl_app->m_register_callbacks(ezgl_app); |
| } else { |
| // Setup the default callbacks for the prebuilt buttons |
| register_default_buttons_callbacks(ezgl_app); |
| } |
| |
| if(ezgl_app->initial_setup_callback != nullptr) |
| ezgl_app->initial_setup_callback(ezgl_app); |
| |
| g_info("application::activate successful."); |
| } |
| |
| application::application(application::settings s) |
| : m_main_ui(s.main_ui_resource) |
| , m_window_id(s.window_identifier) |
| , m_canvas_id(s.canvas_identifier) |
| , m_application(gtk_application_new("ezgl.app", G_APPLICATION_FLAGS_NONE)) |
| , m_builder(gtk_builder_new()) |
| , m_register_callbacks(s.setup_callbacks) |
| { |
| #ifdef EZGL_USE_X11 |
| // Prefer x11 first, then other backends. |
| gdk_set_allowed_backends("x11,*"); |
| #endif |
| |
| // Connect our static functions application::{startup, activate} to their callbacks. We pass 'this' as the userdata |
| // so that we can use it in our static functions. |
| g_signal_connect(m_application, "startup", G_CALLBACK(startup), this); |
| g_signal_connect(m_application, "activate", G_CALLBACK(activate), this); |
| |
| first_run = true; |
| } |
| |
| application::~application() |
| { |
| // GTK uses reference counting to track object lifetime. Since we called *_new() for our application and builder, we |
| // need to unreference them. This should set their reference count to 0, letting GTK know that they should be cleaned |
| // up in memory. |
| g_object_unref(m_builder); |
| g_object_unref(m_application); |
| } |
| |
| canvas *application::get_canvas(const std::string &canvas_id) const |
| { |
| auto it = m_canvases.find(canvas_id); |
| if(it != m_canvases.end()) { |
| return it->second.get(); |
| } |
| |
| g_warning("Could not find canvas with name %s.", canvas_id.c_str()); |
| return nullptr; |
| } |
| |
| canvas *application::add_canvas(std::string const &canvas_id, |
| draw_canvas_fn draw_callback, |
| rectangle coordinate_system, |
| color background_color) |
| { |
| if(draw_callback == nullptr) { |
| // A NULL draw callback means the canvas will never render anything to the screen. |
| g_warning("Canvas %s's draw callback is NULL.", canvas_id.c_str()); |
| } |
| |
| // Can't use make_unique with protected constructor without fancy code that will confuse students, so we use new |
| // instead. |
| std::unique_ptr<canvas> canvas_ptr(new canvas(canvas_id, draw_callback, coordinate_system, background_color)); |
| auto it = m_canvases.emplace(canvas_id, std::move(canvas_ptr)); |
| |
| if(!it.second) { |
| // std::map's emplace does not insert the value when the key is already present. |
| g_warning("Duplicate key (%s) ignored in application::add_canvas.", canvas_id.c_str()); |
| } else { |
| g_info("The %s canvas has been added to the application.", canvas_id.c_str()); |
| } |
| |
| return it.first->second.get(); |
| } |
| |
| GObject *application::get_object(gchar const *name) const |
| { |
| // Getting an object from the GTK builder does not increase its reference count. |
| GObject *object = gtk_builder_get_object(m_builder, name); |
| g_return_val_if_fail(object != nullptr, nullptr); |
| |
| return object; |
| } |
| |
| int application::run(setup_callback_fn initial_setup_user_callback, |
| mouse_callback_fn mouse_press_user_callback, |
| mouse_callback_fn mouse_move_user_callback, |
| key_callback_fn key_press_user_callback) |
| { |
| if(disable_event_loop) |
| return 0; |
| |
| initial_setup_callback = initial_setup_user_callback; |
| mouse_press_callback = mouse_press_user_callback; |
| mouse_move_callback = mouse_move_user_callback; |
| key_press_callback = key_press_user_callback; |
| |
| // The result of calling g_application_run() again after it returns is unspecified. |
| // So we have to destruct and reconstruct the GTKApplication |
| if(!first_run) { |
| // Destroy the GTK application |
| g_object_unref(m_application); |
| g_object_unref(m_builder); |
| |
| // Reconstruct the GTK application |
| m_application = (gtk_application_new("edu.toronto.eecg.ezgl.app", G_APPLICATION_FLAGS_NONE)); |
| m_builder = (gtk_builder_new()); |
| g_signal_connect(m_application, "startup", G_CALLBACK(startup), this); |
| g_signal_connect(m_application, "activate", G_CALLBACK(activate), this); |
| } |
| |
| // set the first_run flag to false |
| first_run = false; |
| |
| g_info("The event loop is now starting."); |
| |
| // see: https://developer.gnome.org/gio/unstable/GApplication.html#g-application-run |
| return g_application_run(G_APPLICATION(m_application), 0, 0); |
| } |
| |
| void application::quit() |
| { |
| // Close the current window |
| GObject *window = get_object(m_window_id.c_str()); |
| gtk_window_close(GTK_WINDOW(window)); |
| |
| // Quit the GTK application |
| g_application_quit(G_APPLICATION(m_application)); |
| } |
| |
| void application::register_default_events_callbacks(ezgl::application *application) |
| { |
| // Get a pointer to the main window GUI object by using its name. |
| std::string main_window_id = application->get_main_window_id(); |
| GObject *window = application->get_object(main_window_id.c_str()); |
| |
| // Get a pointer to the main canvas GUI object by using its name. |
| std::string main_canvas_id = application->get_main_canvas_id(); |
| GObject *main_canvas = application->get_object(main_canvas_id.c_str()); |
| |
| // Connect press_key function to keyboard presses in the MainWindow. |
| g_signal_connect(window, "key_press_event", G_CALLBACK(press_key), application); |
| |
| // Connect press_mouse function to mouse presses and releases in the MainWindow. |
| g_signal_connect(main_canvas, "button_press_event", G_CALLBACK(press_mouse), application); |
| |
| // Connect release_mouse function to mouse presses and releases in the MainWindow. |
| g_signal_connect(main_canvas, "button_release_event", G_CALLBACK(release_mouse), application); |
| |
| // Connect release_mouse function to mouse presses and releases in the MainWindow. |
| g_signal_connect(main_canvas, "motion_notify_event", G_CALLBACK(move_mouse), application); |
| |
| // Connect scroll_mouse function to the mouse scroll event (up, down, left and right) |
| g_signal_connect(main_canvas, "scroll_event", G_CALLBACK(scroll_mouse), application); |
| } |
| |
| void application::register_default_buttons_callbacks(ezgl::application *application) |
| { |
| // Connect press_zoom_fit function to the Zoom-fit button |
| GObject *zoom_fit_button = application->get_object("ZoomFitButton"); |
| g_signal_connect(zoom_fit_button, "clicked", G_CALLBACK(press_zoom_fit), application); |
| |
| // Connect press_zoom_in function to the Zoom-in button |
| GObject *zoom_in_button = application->get_object("ZoomInButton"); |
| g_signal_connect(zoom_in_button, "clicked", G_CALLBACK(press_zoom_in), application); |
| |
| // Connect press_zoom_out function to the Zoom-out button |
| GObject *zoom_out_button = application->get_object("ZoomOutButton"); |
| g_signal_connect(zoom_out_button, "clicked", G_CALLBACK(press_zoom_out), application); |
| |
| // Connect press_up function to the Up button |
| GObject *shift_up_button = application->get_object("UpButton"); |
| g_signal_connect(shift_up_button, "clicked", G_CALLBACK(press_up), application); |
| |
| // Connect press_down function to the Down button |
| GObject *shift_down_button = application->get_object("DownButton"); |
| g_signal_connect(shift_down_button, "clicked", G_CALLBACK(press_down), application); |
| |
| // Connect press_left function to the Left button |
| GObject *shift_left_button = application->get_object("LeftButton"); |
| g_signal_connect(shift_left_button, "clicked", G_CALLBACK(press_left), application); |
| |
| // Connect press_right function to the Right button |
| GObject *shift_right_button = application->get_object("RightButton"); |
| g_signal_connect(shift_right_button, "clicked", G_CALLBACK(press_right), application); |
| |
| // Connect press_proceed function to the Proceed button |
| GObject *proceed_button = application->get_object("ProceedButton"); |
| g_signal_connect(proceed_button, "clicked", G_CALLBACK(press_proceed), application); |
| } |
| |
| void application::update_message(std::string const &message) |
| { |
| // Get the StatusBar Object |
| GtkStatusbar *status_bar = (GtkStatusbar *)get_object("StatusBar"); |
| |
| // Remove all previous messages from the message stack |
| gtk_statusbar_remove_all(status_bar, 0); |
| |
| // Push user message to the message stack |
| gtk_statusbar_push(status_bar, 0, message.c_str()); |
| } |
| |
| void application::create_button(const char *button_text, |
| int left, |
| int top, |
| int width, |
| int height, |
| button_callback_fn button_func) |
| { |
| // get the internal Gtk grid |
| GtkGrid *in_grid = (GtkGrid *)get_object("InnerGrid"); |
| |
| // create the new button with the given label |
| GtkWidget *new_button = gtk_button_new_with_label(button_text); |
| |
| // connect the buttons clicked event to the callback |
| if(button_func != NULL) { |
| g_signal_connect(G_OBJECT(new_button), "clicked", G_CALLBACK(button_func), this); |
| } |
| |
| // add the new button |
| gtk_grid_attach(in_grid, new_button, left, top, width, height); |
| |
| // show the button |
| gtk_widget_show(new_button); |
| } |
| |
| void application::create_button(const char *button_text, |
| int insert_row, |
| button_callback_fn button_func) |
| { |
| // get the internal Gtk grid |
| GtkGrid *in_grid = (GtkGrid *)get_object("InnerGrid"); |
| |
| // add a row where we want to insert |
| gtk_grid_insert_row(in_grid, insert_row); |
| |
| // create the button |
| create_button(button_text, 0, insert_row, 3, 1, button_func); |
| } |
| |
| bool application::destroy_button(const char *button_text_to_destroy) |
| { |
| // get the inner grid |
| GtkGrid *in_grid = (GtkGrid *)get_object("InnerGrid"); |
| |
| // the text to delete, in c++ string form |
| std::string text_to_del = std::string(button_text_to_destroy); |
| |
| // iterate over all of the children in the grid and find the button by it's text |
| GList *children, *iter; |
| children = gtk_container_get_children(GTK_CONTAINER(in_grid)); |
| for(iter = children; iter != NULL; iter = g_list_next(iter)) { |
| // iterator to widget |
| GtkWidget *widget = GTK_WIDGET(iter->data); |
| |
| // check if widget is a button |
| if(GTK_IS_BUTTON(widget)) { |
| // convert to button |
| GtkButton *button = GTK_BUTTON(widget); |
| |
| // get the button label |
| const char *button_label = gtk_button_get_label(button); |
| if(button_label != nullptr) { |
| std::string button_text = std::string(button_label); |
| |
| // does the label of the button match the one we want to delete? |
| if(button_text == text_to_del) { |
| // destroy the button (widget) and return true |
| gtk_widget_destroy(widget); |
| // free the children list |
| g_list_free (children); |
| return true; |
| } |
| } |
| } |
| } |
| |
| // free the children list |
| g_list_free (children); |
| // couldn't find the button with the label 'button_text_to_destroy' |
| return false; |
| } |
| |
| void application::change_button_text(const char *button_text, const char *new_button_text) |
| { |
| // get the inner grid |
| GtkGrid *in_grid = (GtkGrid *)get_object("InnerGrid"); |
| |
| // the text to change, in c++ string form |
| std::string text_to_change = std::string(button_text); |
| |
| // iterate over all of the children in the grid and find the button by it's text |
| GList *children, *iter; |
| children = gtk_container_get_children(GTK_CONTAINER(in_grid)); |
| for(iter = children; iter != NULL; iter = g_list_next(iter)) { |
| // iterator to widget |
| GtkWidget *widget = GTK_WIDGET(iter->data); |
| |
| // check if widget is a button |
| if(GTK_IS_BUTTON(widget)) { |
| // convert to button |
| GtkButton *button = GTK_BUTTON(widget); |
| |
| // get the button label |
| const char *button_label = gtk_button_get_label(button); |
| if(button_label != nullptr) { |
| std::string button_text_str = std::string(button_label); |
| |
| // does the label of the button match the one we want to change? |
| if(button_text_str == text_to_change) { |
| // change button label |
| gtk_button_set_label(button, new_button_text); |
| } |
| } |
| } |
| } |
| |
| // free the children list |
| g_list_free (children); |
| } |
| |
| void application::change_canvas_world_coordinates(std::string const &canvas_id, |
| rectangle coordinate_system) |
| { |
| // get the canvas |
| canvas *cnv = get_canvas(canvas_id); |
| |
| // reset the camera system with the new coordinates |
| if (cnv != nullptr) { |
| cnv->get_camera().reset_world(coordinate_system); |
| } |
| } |
| |
| void application::refresh_drawing() |
| { |
| // get the main canvas |
| canvas *cnv = get_canvas(m_canvas_id); |
| |
| // force redrawing |
| cnv->redraw(); |
| } |
| |
| void application::flush_drawing() |
| { |
| // get the main drawing area widget |
| GtkWidget *drawing_area = (GtkWidget *)get_object(m_canvas_id.c_str()); |
| |
| // queue a redraw of the GtkWidget |
| gtk_widget_queue_draw(drawing_area); |
| } |
| |
| renderer application::get_renderer() |
| { |
| // get the main canvas |
| canvas *cnv = get_canvas(m_canvas_id); |
| |
| return cnv->create_temporary_renderer(); |
| } |
| |
| void set_disable_event_loop(bool new_setting) |
| { |
| disable_event_loop = new_setting; |
| } |
| } |