From 99614d583412c9a18d9c46ba3b278c29978a857b Mon Sep 17 00:00:00 2001 From: Pascal Niklaus Date: Sat, 30 Mar 2024 09:09:52 +0100 Subject: [PATCH] Propose graphics object store --- .adr-dir | 1 + architecture/gfx_store.md | 289 ------------ .../0001-record-architecture-decisions.md | 19 + .../0002-implement-graphics-object-store.md | 418 ++++++++++++++++++ src/callbacks.c | 54 +++ src/coordlist_ops.c | 104 +++++ src/drawing.c | 104 ----- src/main.c | 6 + 8 files changed, 602 insertions(+), 393 deletions(-) create mode 100644 .adr-dir delete mode 100644 architecture/gfx_store.md create mode 100644 doc/architecture/decisions/0001-record-architecture-decisions.md create mode 100644 doc/architecture/decisions/0002-implement-graphics-object-store.md diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000..0d38988 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +doc/architecture/decisions diff --git a/architecture/gfx_store.md b/architecture/gfx_store.md deleted file mode 100644 index 4443c20..0000000 --- a/architecture/gfx_store.md +++ /dev/null @@ -1,289 +0,0 @@ -# Background and Rationale - -`gromit` currently follows a "draw-and-forget" strategy, i.e. each -drawing operation, once completed, is drawn on a `cairo_surface_t` -(`GromitData.back_buffer`). - -While this is very straightforward, it prevents the implementation of - -- dynamic content, e.g. items that fade after a certain time and - disappear, or otherwise change through time - -- editing of past content (e.g. select and move or delete an old item) - -Currently, a work-around exists in the form of an `aux_backbuffer`, -which stores the state prior to the drawing operation in -progress. This allows dynamic content during the ongoing drawing -operation, and is used in e.g. `LINE`, `SMOOTH`, `RECT`. -However, the drawing is split up over various callbacks (`on_press`, -`on_motion`, and `on_release`, which complicates maintenance). - -Here, I propose to implement a "graphics object store" that contains -the elements that have been produced so far. By re-executing these -drawing operations (function `redraw`), the concent of `backbuffer` -could be re-created at any time. - -# Implementation - -A graphics object store is added to `GromitData`. This object store -simple is a `GList` of `GfxObject`. Each `GfxObject` has the -following fields: - - typedef struct { - guint id; // unique id - GfxType type; // the type of object, e.g. Stroke, or Text - gboolean dynamic; // TRUE = content is dynamically updated - gboolean selected; // TRUE = object is currently selected - guint32 capabilities; // bit mask with capabilities (see below) - BoundingBox extent; // the objects rectangular extent - bool (*do)(GfxObject *, action, void *); // object-specific methods - // ------------- object-specific fields ----------- - ... - } GfxObject; - -The object specific fields allow for an inheritance-like structure, -i.e. each specialized object can be downcast to the above `GfxObject`. - -For a normal stroked path, the specific field could be: - - typedef struct { - .... common fields .... - // ------------- object-specific fields ----------- - GList *coordinates; - GdkRGBA color; - gfloat arrowsize; - ArrowType arrowtype; - } GfxObjectPath; - -For a text object, one could have: - - typedef struct { - .... common fields .... - // ------------- object-specific fields ----------- - GdkRGBA color; - gchar *text; - gfloat fontsize; - gfloat x, y; - } GfxObjectPath; - -A path that fades away after a certain time could look like so: - - typedef struct { - .... common fields .... - // ------------- object-specific fields ----------- - GList *coordinates; - GdkRGBA color; - gfloat arrowsize; - ArrowTyype arrowtype; - guint64 start_fading; // timestamps in - guint64 end_fading; // microseconds - } GfxObjectFadingPath; - -## Capabilities - -Each `gfx_object` has a bit mask indicating its "capabilities". These -capabilities imply that certain actions are implemented in the `do` -function. - - - - - - - - - - - - - - - - - - - - - - - - - - -
Capability Description
draw object can be drawn
draw_transformed object can be drawn in transformed state;
- this allows for dynamic representation during
- operations such as dragging
move object can be moved
isotropic_scale object can be scaled isotropically
anisotropic_scale object can be scaled anisotropically,
- i.e. differently in X and Y direction
- isotropic_scale also must be set
rotate object can be rotated
edit object is editable (e.g. TEXT or parts of a path)
- this probably is hardest to implement, and
- left for the future
- - -### Use of capabilities with selections and during transformations - -When multiple `GfxObject`s are selected, the capability bitmasks could -be `and`ed to obtain the greatest common denominator. For example, -objects may be movable, but not rotateable, and then the rotate handle -is omitted in the GUI. Similarly, isotropic-only scaling would leave -only the corner handles, whereas anisotropic scaling would also -display handles in the middle of the sides (see -[here](#selection-wireframe) for selection wireframe). - -During transform operations, `draw_transformed` is called for each -object, with a 2D affine transformation matrix as argument. - -At the end of the operation, `transform` is called with the final -transformation matrix, which then modifies the object in the object -store. - -The 2D affine transformation functions are already implemented in -`coordlist_ops.c`. - -## Actions - -When calling `do`, the `action` argument selects the task to be performed. - - - - - - - - - - - - - - - - - - - - -
draw draw object in "normal" state
draw_selected draw object in "selected" state
draw_transformed draw object in intermediate "transformed" state;
- this allows to display the object dynamically,
- e.g. while it is being dragged
is_clicked detect whether a click at coordinate (x,y)
- "hits" the object, for example to select it
transform transforms the object, modifying the context
- in the gfx_store
destroy destroys the object, deallocating memory
- - -## Drawing of objects store - -Whenever objects need to be re-drawn, the `redraw` function simply iterates -through the object store and calls `object->do(ptr, draw_action)` for -all objects. - -To improve update speed, a cached `cairo_surface_t` (similar to -`backbuffer`) is maintained. This cache contains all drawing -operations up to but not including the first that is flagged as -`dynamic`. - -In practice, most of the time only the last object would need to be -redrawn atop the cached image. This is no different from the current -code which often draws over `aux_backbuffer`. - -# Migration to new drawing system - -## First step - -* **Object store** - - The object store is added to `GromitData`, together with some - housekeeping functions. - -* **Callbacks** - - Then, the callbacks are adapted to add data to the object store - instead of `GromitData->coordlist`. Direct drawing is replaced by a - subsequent call of `redraw`. - - - `on_press` adds a new item of the respective type to the object - store, and "remembers" its `id` (for example, in the hash_table). - - - `on_motion` appends coordinates to the respective `GfxObject`, and - calls `redraw`. - - - `on_release` finalizes the operation, for example by smoothing a - path (SMOOTH tool), and then calling `redraw`. - - - for each tool, the specific drawing operation is implemented. The - only drawing operation that is required at this first stage is the - drawing of a simple stroke path. - -* **Undo/redo** - - - "Undo" is implemented by simply moving the last `GfxObjects` - from the object store to the head of a separate "redo" list, - followed by a call to `redraw`. - - - "Redo" moves the first element from the "redo" list to the last - position in the object store, followed by a call of `redraw`. - -These are relatively minor changes. With these, the new architecture -already is functional. - -## Second step - -In a next step, a "select" tool is implemented. This requires that the -`GfxObject` implement `is_clicked` and `draw_selected`. - -The select tool draws a wireframe around the selection, showing the -respective handles (see "capabilities"). - -Then, a "delete" function is implemented. It basically calls the -"destroy" method and removes the selected items from the object store, -and then calls `redraw`. - -## Third step - -Depending on the capabilities of the selected objects, suitable -transformations are offered. The simplest one is a "move" handle. - -The items that implement "draw_transformed" are dynamically redrawn -during the "drag" operation. Upon releasing the "move" handle, the -tool calls the "transform" method, which modifies the coordinates of -the selecetd items in the object store. - -Similarly, "scaling" and "rotation" operations are implemented by -adding extra handles to the selection wireframe. Everything else -remains the same. - -# Selection wireframe - -The selection wireframe could look like this: - - R R - R R - R +---+ +---+ +---+ R - | S |------------------| A |------------------| S | - +---+ +---+ +---+ - | | - | | - | | - | | - | | - | ^ | - +---+ | +---+ - | A | <---M---> | A | - +---+ | +---+ - | V | - | | - | BIN | - | ICON | - | | - | | - +---+ +---+ +---+ - | S |------------------| A |------------------| S | - R +---+ +---+ +---+ R - R R - R R - -| Handle | Function | -|--------|---------------------------------------------------------------------------| -| BIN | trash icon, deletes the selected items | -| M | move handle to drag the selection. SHIFT stays H or V | -| S | isotropic scaling handle, opposite corner stays where it is | -| A | anisotropic scaling handle, opposite side stays where it is | -| R | rotate handle, opposite corner is center of rotation, SHIFT for 45° steps | diff --git a/doc/architecture/decisions/0001-record-architecture-decisions.md b/doc/architecture/decisions/0001-record-architecture-decisions.md new file mode 100644 index 0000000..0c2d747 --- /dev/null +++ b/doc/architecture/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2024-03-28 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/doc/architecture/decisions/0002-implement-graphics-object-store.md b/doc/architecture/decisions/0002-implement-graphics-object-store.md new file mode 100644 index 0000000..67db606 --- /dev/null +++ b/doc/architecture/decisions/0002-implement-graphics-object-store.md @@ -0,0 +1,418 @@ +# 2. implement graphics object store + +Date: 2024-03-28 + +## Status + +Proposed + +## Context + +`gromit` currently follows a "draw-and-forget" strategy, i.e. each +drawing operation, once completed, is drawn on a `cairo_surface_t` +(`GromitData.backbuffer`). + +While this is very straightforward, it prevents the implementation of + +- **dynamic content**, e.g. items that fade after a certain time and + disappear, or otherwise change through time + +- **editing** of past content (e.g. select and move or delete an old item) + +Currently, a work-around exists in the form of an `aux_backbuffer`, +which stores the state prior to the drawing operation in +progress. This allows dynamic content during the ongoing drawing +operation, and is used in e.g. `LINE`, `SMOOTH`, `RECT`. +However, the drawing is split up over various callbacks (`on_press`, +`on_motion`, and `on_release`, which complicates code and maintenance). + +Here, I propose to implement a **graphics object store** that contains +the elements that have been produced so far. By re-executing these +drawing operations, the widget is (re)painted. + +## Implementation + +A graphics object store (`objects`) is added to `GromitData`. This +object store simply is a `GList` of `GfxObject`. Each `GfxObject` has +the following fields: + + typedef struct { + guint id; // unique id + GfxType type; // type of object, e.g. Stroke, or Text + gboolean dynamic; // content is dynamically updated + GromitDeviceData selected; // NULL, or which user selected the item + guint32 capabilities; // bit mask with capabilities (see below) + BoundingBox extent; // the objects rectangular extent + void (*exec)(GfxObject *, action, void *); // object-specific methods + } GfxObjectBase; + +This basic set of fields is extended by object-specific fields, which +allows for an inheritance-like pattern, i.e. each specialized object +can be downcast to the above `GfxObjectBase`. + +For a normal stroked path, the specific field could be: + + typedef struct { + GfxObjectBase base; + GList *coordinates; + GdkRGBA color; + gfloat arrowsize; + ArrowType arrowtype; + } GfxObjectPath; + +For a text object, one could have: + + typedef struct { + GfxObjectBase base; + GdkRGBA color; + gchar *text; + gfloat fontsize; + gfloat x, y; + } GfxObjectPath; + +A path that fades away after a certain time could look like so: + + typedef struct { + GfxObjectBase base; + GList *coordinates; + GdkRGBA color; + gfloat arrowsize; + ArrowTyype arrowtype; + guint64 start_fading; // timestamps in + guint64 end_fading; // microseconds + } GfxObjectFadingPath; + +### Capabilities + +Each `gfx_object` contains a bit mask indicating its +"capabilities". These capabilities imply that certain actions are +implemented in the `do_action` function. + + + + + + + + + + + + + + + + + + + + + + + + + + +
Capability Description
draw object can be drawn
+ this would allow to add non-drawable "information" objects
draw_transformed object can be drawn in transformed state;
+ this allows for dynamic representation during operations such as dragging
move object can be moved
isotropic_scale object can be scaled isotropically
anisotropic_scale object can be scaled anisotropically,
+ i.e. differently in X and Y direction isotropic_scale also must be set
rotate object can be rotated
edit object is editable (e.g. TEXT or parts of a path)
+ this probably is hardest to implement, and left for the future
+ + +#### Use of capabilities with selections and during transformations + +When multiple `GfxObject`s are selected, the capability bitmasks could +be `and`ed to obtain the greatest common denominator. For example, +objects may be movable, but not rotateable, and then the rotate handle +is omitted in the GUI. Similarly, isotropic-only scaling would leave +only the corner handles, whereas anisotropic scaling would also +display handles in the middle of the sides (see +[here](#selection-wireframe) for selection wireframe). + +During transform operations, `draw_transformed` is called for each +object, with a 2D affine transformation matrix as argument. + +If an object cannot be drawn dynamically in transformed state, for +example because this is computationally to costly, then the +transformation is just shown by selection wireframe until the +operation completed. + +At the end of the operation, `transform` is called with the final +transformation matrix, which then modifies the object in the object +store. + +The 2D affine transformation functions are already implemented in +`coordlist_ops.c`. + +### Actions + +When calling `exec`, the `action` argument selects the task to be performed. + + + + + + + + + + + + + + + + + + + + + + + +
draw draw object in "normal" state
draw_selected draw object in "selected" state
draw_transformed draw object in intermediate "transformed" state;
+ this allows to display the object dynamically, e.g. while it is being dragged
get_is_clicked detect whether a click at coordinate (x,y) "hits" the object, for example to select it
transform transforms the object, modifying the context in the gfx_store
destroy destroys the object, deallocating memory
clone deep-copies the object
+ + +### Drawing of object store + +Whenever objects need to be re-drawn, the `draw` function simply +iterates through the object store and calls `object->do_action(ptr, +draw_action)` for all objects. Objects with a bounding box not +intersecting the dirty region (`gdk_cairo_get_clip_rectangle()`) are +skipped. + +### Undo/redo system + +Each object that is added or transformed receives a new unique +`id`. This `id` is delivered by a "factory" function and is a simple +`guint32` that is incremented at each request. + +Maintaining a per-user (i.e. per device) undo system is complicated by +the fact that an object can be "taken over" by another user by +manipulating it. This is necessary to enable a truly collaborative +workflow. At the same time, a user should only undo his/her own +operations. + +To solve this conflict, I propose the following system: + +- each device (i.e. user) contains an `GList` with `undo` and `redo` records. + +- `GromitData` is extended with an undo object store (`undo_objects`). + +- the steps necessary to revert a user's operation are recorded in the + respective device's `undo` records. + +- an undo operation executes the last undo operation according to the + `undo` record of the device, and moves it to the `redo` record. + +- a redo operation does the reverse, and moves the record back to the + end of the `undo` records. + +- any non-undo/redo operation clears the `redo` records. + +The `undo` records contain the `id` of the object to move from +`objects` to `undo_objects`, and the `id` of the object to move from +`undo_objects` to `objects`. An `id` of zero indicates that there is +no such object. + +- when a `GfxObject` is added, the `undo` record contains the + instruction to move the `GfxObject` with the particular `id` from + `object`to `undo_objects`. + +- when a `GfxObject` is deleted, this object is moved from `objects` + to `undo_objects`. The `undo` record consists of the instruction to + add the `GfxObject` with the particular `id` back to `objects`. + +- when a `GfxObject` is transformed, the object is deep-copied (action + `clone`) is created and placed in `undo_objects`. The transformed + object remains in `objects`but receives a new `id`. The `undo` + record for the respective device contains the instruction to swap + the old and new object (which are in the `undo_objects` and the + `object_store`, respectively). + +Handling of conflicts: + +- Conflicts emerge when a user has "taken over" an object from another + user. In this case, the objects referenced in the `undo` records no + longer exist. In this case, this `undo` record is discarded and the + next `undo` record processed. + + Example (here I use letters for the `id`): + + - user 1 adds objects A, B, and C: + +``` + objects -> A -> B -> C -> nil + + undo_objects -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> [C->undo] + dev1.redo -> nil +``` + + - user 2 manipulates B, which becomes D: + +``` + objects -> A -> D -> C -> nil + + undo_objects -> B -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> [C->undo] -> nil + dev1.redo -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + + - user 1 undos the last step: + + +``` + objects -> A -> D -> nil + + undo_objects -> B -> C -> nil + + dev1.undo -> [A->undo] -> [B->undo] -> nil + dev1.redo -> [C->store] -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + + - user 1 undos one more step, which fails because B is no longer + there; this step is discarded and the next step executed. + +``` + objects -> D -> nil + + undo_objects -> B -> C -> A -> nil + + dev1.undo -> nil + dev1.redo -> [C->store] -> [A->store] -> nil + + dev2.undo -> [B->store, D->undo] -> nil + dev2.redo -> nil +``` + +Notes: + +- When adding back previously deleted objects from the `undo_objects`, + they are placed at the very end of `objects`. This may change the + Z-order of the objects. + + **Question**: Should we add two more handles to the selection + wireframe? One to move the selection forward, and one to move the + selection backwards in Z-order? + +- Orphans could occur in the undo store. One possibility would be to + reference-count these objects and delete them when the last + reference disappears from the undo/redo buffers, but that would + require pointers or some related mechanism. The other, simpler, + possibility which I prefer is to garbage-collect these once the + `undo_objects` exceeds a maximum size. + +- When `undo_objects` exceeds a maximum size, the oldest items could + be dropped and the `undo`/`redo` records referencing these be deleted. + +## Migration to new drawing system + +### First step + +* **Object store** + + The `objects` store is added to `GromitData`, together with some + housekeeping functions. + +* **Callbacks** + + Then, the callbacks are adapted to add data to the object store + instead of `coordlist`. Direct drawing is replaced by a subsequent + call of `draw`. + + - `on_press` adds a new item of the respective type to the object + store, and "remembers" its `id` (for example, in the hash_table). + + - `on_motion` appends coordinates to the respective `GfxObject`, and + calls `draw`. + + - `on_release` finalizes the operation, for example by smoothing a + path (SMOOTH tool), and then calling `draw`. + + - for each tool, the specific drawing operation is implemented. The + only drawing operation that is required at this first stage is the + drawing of a simple stroke path. + +These are relatively minor changes. With these, the new architecture +already is functional. + +### Second step + +The "undo/redo" functions are implemented. + +### Thirs step + +A "select" tool is implemented. This requires that the `GfxObject` +implement `get_is_clicked` and `draw_selected`. + +The select tool draws a wireframe around the selection, showing the +respective handles (see "capabilities"). + +Then, a "delete" function is implemented. It basically calls the +"destroy" method and removes the selected items from the object store, +and then calls `draw`. + +### Fourth step + +Depending on the capabilities of the selected objects, suitable +transformations are offered. The simplest one is a "move" handle. + +The items that implement "draw_transformed" are dynamically redrawn +during the "drag" operation. Upon releasing the "move" handle, the +tool calls the "transform" method, which modifies the coordinates of +the selecetd items in the object store. + +Similarly, "scaling" and "rotation" operations are implemented by +adding extra handles to the selection wireframe. Everything else +remains the same. + +## Selection wireframe + +The selection wireframe could look like this: + + R R + R R + R +---+ +---+ +---+ R + | S |------------------| A |------------------| S | + +---+ +---+ +---+ + | | + | ^ | | + | Z Z | + | | V | + | | + | ^ | + +---+ | +---+ + | A | <---M---> | A | + +---+ | +---+ + | V | + | | + | BIN | + | ICON | + | | + | | + +---+ +---+ +---+ + | S |------------------| A |------------------| S | + R +---+ +---+ +---+ R + R R + R R + +| Handle | Function | +|--------|---------------------------------------------------------------------------| +| BIN | trash icon, deletes the selected items | +| M | move handle to drag the selection. SHIFT stays H or V | +| S | isotropic scaling handle, opposite corner stays where it is | +| A | anisotropic scaling handle, opposite side stays where it is | +| R | rotate handle, opposite corner is center of rotation, SHIFT for 45° steps | +| Z | move up/down selection in Z-order | + diff --git a/src/callbacks.c b/src/callbacks.c index 1bb0230..239b590 100644 --- a/src/callbacks.c +++ b/src/callbacks.c @@ -930,6 +930,60 @@ void on_intro(GtkMenuItem *menuitem, gtk_widget_show_all (assistant); } +void on_edit_config(GtkMenuItem *menuitem, + gpointer user_data) +{ + /* + Check if user config does not exist or is empty. + If so, copy system config to user config. + */ + gchar *user_config_path = g_strjoin (G_DIR_SEPARATOR_S, g_get_user_config_dir(), "gromit-mpx.cfg", NULL); + GFile *user_config_file = g_file_new_for_path(user_config_path); + + guint64 user_config_size = 0; + GFileInfo *user_config_info = g_file_query_info(user_config_file, G_FILE_ATTRIBUTE_STANDARD_SIZE, 0, NULL, NULL); + if (user_config_info != NULL) { + user_config_size = g_file_info_get_size(user_config_info); + g_object_unref(user_config_info); + } + + if (!g_file_query_exists(user_config_file, NULL) || user_config_size == 0) { + g_print("User config does not exist or is empty, copying system config\n"); + + gchar *system_config_path = g_strjoin (G_DIR_SEPARATOR_S, SYSCONFDIR, "gromit-mpx", "gromit-mpx.cfg", NULL); + GFile *system_config_file = g_file_new_for_path(system_config_path); + + GError *error = NULL; + gboolean result = g_file_copy(system_config_file, user_config_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error); + if (!result) { + g_printerr("Error copying system config to user config: %s\n", error->message); + g_error_free(error); + } + + g_object_unref(system_config_file); + g_free(system_config_path); + } + + + /* + Open user config for editing. + */ + gchar *user_config_uri = g_strjoin (G_DIR_SEPARATOR_S, "file://", user_config_path, NULL); + + gtk_show_uri_on_window (NULL, + user_config_uri, + GDK_CURRENT_TIME, + NULL); + + /* + Clean up + */ + g_object_unref(user_config_file); + g_free(user_config_path); + g_free(user_config_uri); +} + + void on_issues(GtkMenuItem *menuitem, gpointer user_data) { diff --git a/src/coordlist_ops.c b/src/coordlist_ops.c index 012432e..95e9c3b 100644 --- a/src/coordlist_ops.c +++ b/src/coordlist_ops.c @@ -459,6 +459,110 @@ static GList *build_section_list(GList *const coords, return section_list; } +void coord_list_prepend (GromitData *data, + GdkDevice* dev, + gint x, + gint y, + gint width) +{ + /* get the data for this device */ + GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); + + GromitStrokeCoordinate *point; + + point = g_malloc (sizeof (GromitStrokeCoordinate)); + point->x = x; + point->y = y; + point->width = width; + + devdata->coordlist = g_list_prepend (devdata->coordlist, point); +} + + +void coord_list_free (GromitData *data, + GdkDevice* dev) +{ + /* get the data for this device */ + GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); + + GList *ptr; + ptr = devdata->coordlist; + + while (ptr) + { + g_free (ptr->data); + ptr = ptr->next; + } + + g_list_free (devdata->coordlist); + + devdata->coordlist = NULL; +} + +/* + * for double-ended arrows, two separate calls are required + */ + +gboolean coord_list_get_arrow_param (GromitData *data, + GdkDevice *dev, + gint search_radius, + GromitArrowType arrow_end, + gint *x0, + gint *y0, + gint *ret_width, + gfloat *ret_direction) +{ + gint r2, dist; + gboolean success = FALSE; + GromitStrokeCoordinate *cur_point, *valid_point; + /* get the data for this device */ + GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); + GList *ptr = devdata->coordlist; + gfloat width; + + valid_point = NULL; + + if (ptr) + { + if (arrow_end == GROMIT_ARROW_START) + ptr = g_list_last(ptr); + cur_point = ptr->data; + *x0 = cur_point->x; + *y0 = cur_point->y; + r2 = search_radius * search_radius; + dist = 0; + + while (ptr && dist < r2) + { + if (arrow_end == GROMIT_ARROW_END) + ptr = ptr->next; + else + ptr = ptr->prev; + if (ptr) + { + cur_point = ptr->data; + dist = (cur_point->x - *x0) * (cur_point->x - *x0) + + (cur_point->y - *y0) * (cur_point->y - *y0); + width = cur_point->width * devdata->cur_context->arrowsize; + if (width * 2 <= dist && + (!valid_point || valid_point->width < cur_point->width)) + valid_point = cur_point; + } + } + + if (valid_point) + { + *ret_width = MAX (valid_point->width * devdata->cur_context->arrowsize, + 2); + *ret_direction = atan2 (*y0 - valid_point->y, *x0 - valid_point->x); + success = TRUE; + } + } + + return success; +} + + // ----------------------- orthogonalize path ------------------------ void orthogonalize(GList *const coords, diff --git a/src/drawing.c b/src/drawing.c index 5eefb00..1307322 100644 --- a/src/drawing.c +++ b/src/drawing.c @@ -105,107 +105,3 @@ void draw_arrow (GromitData *data, data->painted = 1; } - -void coord_list_prepend (GromitData *data, - GdkDevice* dev, - gint x, - gint y, - gint width) -{ - /* get the data for this device */ - GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); - - GromitStrokeCoordinate *point; - - point = g_malloc (sizeof (GromitStrokeCoordinate)); - point->x = x; - point->y = y; - point->width = width; - - devdata->coordlist = g_list_prepend (devdata->coordlist, point); -} - - -void coord_list_free (GromitData *data, - GdkDevice* dev) -{ - /* get the data for this device */ - GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); - - GList *ptr; - ptr = devdata->coordlist; - - while (ptr) - { - g_free (ptr->data); - ptr = ptr->next; - } - - g_list_free (devdata->coordlist); - - devdata->coordlist = NULL; -} - -/* - * for double-ended arrows, two separate calls are required - */ - -gboolean coord_list_get_arrow_param (GromitData *data, - GdkDevice *dev, - gint search_radius, - GromitArrowType arrow_end, - gint *x0, - gint *y0, - gint *ret_width, - gfloat *ret_direction) -{ - gint r2, dist; - gboolean success = FALSE; - GromitStrokeCoordinate *cur_point, *valid_point; - /* get the data for this device */ - GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, dev); - GList *ptr = devdata->coordlist; - gfloat width; - - valid_point = NULL; - - if (ptr) - { - if (arrow_end == GROMIT_ARROW_START) - ptr = g_list_last(ptr); - cur_point = ptr->data; - *x0 = cur_point->x; - *y0 = cur_point->y; - r2 = search_radius * search_radius; - dist = 0; - - while (ptr && dist < r2) - { - if (arrow_end == GROMIT_ARROW_END) - ptr = ptr->next; - else - ptr = ptr->prev; - if (ptr) - { - cur_point = ptr->data; - dist = (cur_point->x - *x0) * (cur_point->x - *x0) + - (cur_point->y - *y0) * (cur_point->y - *y0); - width = cur_point->width * devdata->cur_context->arrowsize; - if (width * 2 <= dist && - (!valid_point || valid_point->width < cur_point->width)) - valid_point = cur_point; - } - } - - if (valid_point) - { - *ret_width = MAX (valid_point->width * devdata->cur_context->arrowsize, - 2); - *ret_direction = atan2 (*y0 - valid_point->y, *x0 - valid_point->x); - success = TRUE; - } - } - - return success; -} - diff --git a/src/main.c b/src/main.c index f30cdbe..de8c696 100644 --- a/src/main.c +++ b/src/main.c @@ -897,6 +897,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) GtkWidget* sep1_item = gtk_separator_menu_item_new(); GtkWidget* intro_item = gtk_menu_item_new_with_mnemonic(_("_Introduction")); + GtkWidget* edit_config_item = gtk_menu_item_new_with_mnemonic(_("_Edit Config")); GtkWidget* issues_item = gtk_menu_item_new_with_mnemonic(_("_Report Bug / Request Feature")); GtkWidget* support_item = gtk_menu_item_new_with_mnemonic(_("_Support Gromit-MPX")); GtkWidget* about_item = gtk_menu_item_new_with_mnemonic(_("_About")); @@ -919,6 +920,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gtk_menu_shell_append (GTK_MENU_SHELL (menu), sep1_item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), intro_item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), edit_config_item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), issues_item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), support_item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), about_item); @@ -981,6 +983,9 @@ void setup_main_app (GromitData *data, int argc, char ** argv) g_signal_connect(G_OBJECT (intro_item), "activate", G_CALLBACK (on_intro), data); + g_signal_connect(G_OBJECT (edit_config_item), "activate", + G_CALLBACK (on_edit_config), + data); g_signal_connect(G_OBJECT (issues_item), "activate", G_CALLBACK (on_issues), data); @@ -1005,6 +1010,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gtk_widget_show (sep1_item); gtk_widget_show (intro_item); + gtk_widget_show (edit_config_item); gtk_widget_show (issues_item); gtk_widget_show (support_item); gtk_widget_show (about_item);