Fwd: [PATCH] Rectangular selection in GtkTextView



Hello fellow gnomers,

with much joy I want to introduce this patch I've been working on for long time and now I think is in good shape to post it and gather your feedback about it. It adds rectangular selection for GtkTextView, it comes also with a small patch for GtkSourceView needed changes.

The patches are based against latest Gtk3, (gtk+ gtk-3-22 branch, gtksourceview gnome-3-24 branch) this is for convenience for people (and me) to test it, as Gtk4 master is so new there is no even a Gtk4 ported Gedit or gtksourceview, I suppose they are waiting the release of Gtk4 to start porting.

Patches are attached to this mail(gzipped as Mailman blocks > 100kb attachments) and also in its bugzilla entry:

So if you prefer, more general comments can use this mail thread and more specific patch review can use the bugzilla. 

I've also made a video to demo some of its features:
https://youtu.be/j06vdv0Geb0

I think rect mode selection would be a nice feature for code editors like eg. gnome-builder.

Now some details about the patch:

* Starting rect mode on GtkTextView

Rect mode selection it's started when pressing 'Alt' key modifier while selecting with mouse or keyboard, if selecting with mouse you can release the key modifier once you've started selecting text.

The 'Alt' key is taken from gtksourceview, which uses it for moving lines, I made that being Alt + CTRL (see small gtksourceview patch attached), but currently this is also used by gnome-shell for switching desktops. So which key modifier to use is *open for debate*, I hope we can agree in a single(not double) key modifier so to make this feature more visible and easy to activate.

* ::prepare-rect-mode signal

Rect mode has *two constraints* to operate correctly, textview has to be configured  with *line wrap disabled* (which is default value for GtkTextView and most editors) and the font used has to be a *monospace* one. So, if you try to initiate rect mode (via key modifier or programatically via API) and the textview is not configured this way then the ::prepare-rect-mode signal will be fired, which the application can use to show the user a dialog saying something like "if you want to do rect selection you must switch to a monospace font" and a button to automatically do so for the user. If the application does not handle this signal, then the default handler for it will just show a Gtkdialog informing the user about the needed changes (this can be seen at the start of the demo video).

* Some implementation notes

[Firstly a brief mention that I've made my effort to document well all functions, signals, properties in the patch, so it can be easy to read/understand]

Rectangular selection is tracked via "insert" and "sel_bound" iters same way as in normal selection, plus a new "rect_voffset" value, which is the horizontal end limit of the rectangular selection in visual column offset. It's a value that stipulates at which horizontal visual column does the rectangular selection ends (in forward selections) or starts (in backwards selections). So it's a value that has to sit in the View, as follows:

guint
gtk_text_view_get_rect_voffset (GtkTextView *text_view)
{
  return gtk_text_layout_get_rect_voffset (text_view->priv->layout);
}

Whenever the Buffer (GtkTextBuffer) wants to perform an operation on the rect selection text (copy, paste, delete,etc)  will trigger the private ::get-rect-ranges signal to which the View will respond storing the current TextIter ranges of the selection in the Buffer, so he can operate on it.

In case there's two or more Views connected to the same Buffer there will be no problem, as this signals are connected/disconnected every time you enter rect_mode (connected for the one View that is entering rect mode, and disconnected for the others) and this works fine also because of the fact that gtk+ only allows one selection be active at the same time in several Views.


* Future of GtkTextView

I am aware of the Splitview feature (a much needed redesign imo):
https://wiki.gnome.org/Projects/GTK+/GtkTextView/Splitview

Indeed that would have made rect selection easier to implement, but unfortunately this remains a planned feature waiting for people to work on it, in that regard and after having worked on this patch, I'm now more interested in contributing toward Splitview. If Splitview gets to be implemented in the future (gtk5 hopefully) I'll happily port rectangular selection over it (in case is needed, cause no port should be needed if backward compat is kept).

* Public API for this patch

[Note: The GDK_AVAILABLE_IN_3_22 macros in the patch are temporary, dependant of what version this gets landed finally.]

Besides prepare-rec-mode signal, more API is provided to be able to manage rect selection programatically, specifically I'm proposing following 8 functions which deal with getting and setting the rect selection:

gboolean gtk_text_buffer_get_rect_mode (GtkTextBuffer *buffer);

gchar*   gtk_text_buffer_get_rect_sel_text (GtkTextBuffer *buffer);

gboolean gtk_text_buffer_copy_rect_sel (GtkTextBuffer *buffer,
                                        GtkTextIter   *dest_iter);

gboolean gtk_text_view_set_rect_selection (GtkTextView       *text_view,
                                           const GtkTextIter *start,
                                           const GtkTextIter *end,
                                           guint              rect_voffset);

guint gtk_text_view_get_rect_voffset (GtkTextView *text_view);

guint gtk_text_view_get_visual_column (GtkTextView       *text_view,
                                       const GtkTextIter *iter);

Following two are not so crucial, but they can be in handy too:

gboolean gtk_text_buffer_insert_range_rect (GtkTextBuffer       *buffer,
                                            const GtkTextIter   *at_iter,
                                            const GtkTextIter   *start,
                                            const GtkTextIter   *end);

gboolean gtk_text_view_set_visual_column (GtkTextView  *textview,
                                          GtkTextIter    *iter,
                                          guint          voffset,
                                          gboolean       after_tab,
                                          guint          *voffset_placed);



Thank you for reading till here!!, I'll now finish this mail by pasting prior functions with their gtk-doc comments, so you don't have to look them up in the patch:


/**
 * gtk_text_buffer_get_rect_mode:
 * @buffer: a #GtkTextBuffer
 *
 * Indicates whether @buffer has rectangular selection mode activated.
 *
 * The setter counterpart of this function is not public, in case you
 * were looking for it to set off rect mode.
 *
 * To set off rect mode and remove any current rectangular selection,
 * just call gtk_text_buffer_place_cursor() or gtk_text_view_unselect().
 * Interactively, an Escape keystroke will also terminate rect mode and
 * deselect any current selection.
 *
 * Returns: %TRUE if rectangular selection mode is activated, %FALSE otherwise.
 *
 **/
gboolean
gtk_text_buffer_get_rect_mode (GtkTextBuffer *buffer);


/**
 * gtk_text_buffer_get_rect_sel_text:
 * @buffer: a #GtkTextBuffer
 *
 * Returns a string with the visible text of current rectangular
 * selection, or %NULL if rectangular mode is off.
 * See gtk_text_buffer_get_rect_mode()
 *
 * Returns: (nullable) (transfer full): string containing visible
 * text of current rectangular selection, or %NULL if rectangular
 * mode is off.
 **/
gchar*
gtk_text_buffer_get_rect_sel_text (GtkTextBuffer *buffer);


/**
 * gtk_text_buffer_copy_rect_sel:
 * @buffer: a #GtkTextBuffer where rect selection will be copied from
 * @dest_iter: a #GtkTextIter from a #GtkTextBuffer different than
 * @buffer, marking the position to insert the copy.
 *
 * Copies current rectangular selection of @buffer to @dest_iter.
 * It uses gtk_text_buffer_insert_range() to copy the individual
 * ranges, so text, tags and images should be preserved.
 * @dest_iter must be in a different buffer than @buffer, and the
 * two buffers must share the same tag table.
 *
 * See gtk_text_buffer_insert_range().
 *
 * Returns: %TRUE if copy was successful, %FALSE otherwise.
 **/
gboolean
gtk_text_buffer_copy_rect_sel (GtkTextBuffer *buffer,
                               GtkTextIter   *dest_iter);


/**
 * gtk_text_view_set_rect_selection:
 * @text_view: a #GtkTextView.
 * @start: Position in @text_view that marks the start of the selection,
 * i.e. where "selection bound" mark will be set.
 * @end: Position in @text_view marking the end of the selection, i.e.
 * where "selection insert" mark will be set.
 * @rect_voffset: the visual column marking the rectangular end limit of the
 * selection. A zero value will be special cased to just use the visual column
 * of @end instead. Note: A visual column value is like the 'column' number
 * commonly showed in text editor's statusbar, but zero based (first column
 * starts at zero, not one).
 *
 * Public function to programatically perform a rectangular selection
 * on @text_view. Please note:
 *
 *  - After a succesful return from this function, rect mode will be activated,
 * i.e. gtk_text_buffer_get_rect_mode() will return %TRUE. If you want to set off
 * rect mode and remove any current rectangular selection, just call
 * gtk_text_buffer_place_cursor() or gtk_text_view_unselect(). Interactively, an
 * Escape keystroke will also terminate rect mode and deselect any current selection.
 *
 *  - If @rect_voffset is less than @end's visual column, then the later
 * will be used.
 *
 *  - If @rect_voffset is greater than the end-of-line visual column of the
 * longest line between @start and @end, then the later will be used.
 *
 * Other related functions:
 * - gtk_text_view_get_visual_column()
 * - gtk_text_view_get_rect_voffset()
 * - gtk_text_buffer_get_rect_mode()
 *
 * Returns: %TRUE on success, %FALSE otherwise.
 */
gboolean
gtk_text_view_set_rect_selection (GtkTextView       *text_view,
                                  const GtkTextIter *start,
                                  const GtkTextIter *end,
                                  guint              rect_voffset);


/**
 * gtk_text_view_get_rect_voffset:
 * @text_view: a #GtkTextView.
 *
 * Returns the current value for the horizontal end limit of the
 * rectangular selection in visual column offset (also referred to
 * as 'rect_voffset' for brevity). It's a value that stipulates at
 * which horizontal visual column does the rectangular selection
 * ends (in forward selections) or starts (in backwards selections).
 *
 * Note: A visual column value is like the 'column' number
 * commonly showed in text editor's statusbar, but zero based
 * (first column starts at zero not one).
 * Note: Forward selection means a selection where the visual column
 * of 'insert' mark is to the right of the visual column of the
 * 'selection bound' mark. In backward selections the 'insert' mark
 * would be to the left instead.
 *
 * See also: gtk_text_view_set_rect_selection() .
 *
 * Returns: the visual column (zero-based) value which represents
 * the current horizontal end limit for the rectangular selection.
 */
guint
gtk_text_view_get_rect_voffset (GtkTextView *text_view);


/**
 * gtk_text_view_get_visual_column:
 * @text_view: a #GtkTextView.
 * @iter: a position in @text_view.
 *
 * Determines the visual column at @iter, taking into consideration
 * any tabs on the line and the current tab-width of @text_view.
 *
 * Note: A visual column value is like the 'column' number
 * commonly showed in text editor's statusbar, but zero based
 * (first column starts at zero, not one).
 *
 * Returns: the visual column at @iter.
 */
guint
gtk_text_view_get_visual_column (GtkTextView       *text_view,
                                 const GtkTextIter *iter);

  /**
   * GtkTextView::prepare-rect-mode:
   * @text_view: The text view on which the signal is emitted
   * @has_monospace: whether text view is using a monospace font
   * @wrap_enabled:  whether text view has line wraping enabled
   *
   * The ::prepare-rect-mode signal gets emitted when the user
   * tries to initiate a rectangular selection but the @text_view
   * does not met the two main constraints needed for rectangular selection
   * mode which are:
   * - Monospace (i.e. fixed width) font, @text_view has to be using
   *   a monospace font for rectangular selection mode to work properly.
   * - Line wrap disabled, for rectangular selection mode to work properly
   *   @text_view line wraping has to be off, i.e. #GtkTextView:wrap-mode
   *   set to GTK_WRAP_NONE.
   *
   * This signal has a default handler that will just show a #GtkDialog
   * informing the user about the needed changes, so to offer a better
   * experience for the user, applications are encouraged to connect to
   * this signal and provide their own dialog that could automatically
   * change font and wrap mode at the user's click. If you do that,
   * remember to return %GDK_EVENT_STOP from your connected handler so
   * the information dialog from the default handler is not shown.
   *
   * Returns: %GDK_EVENT_STOP to stop other handlers from being invoked for
   * the event. %GDK_EVENT_PROPAGATE to propagate the event further.
   */
  signals[PREPARE_RECT_MODE] =
    g_signal_new (I_("prepare-rect-mode"),
                  G_OBJECT_CLASS_TYPE (gobject_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkTextViewClass, prepare_rect_mode),
                  g_signal_accumulator_true_handled, NULL,
                  _gtk_marshal_BOOLEAN__BOOLEAN_BOOLEAN,
                  G_TYPE_BOOLEAN, 2,
                  G_TYPE_BOOLEAN,
                  G_TYPE_BOOLEAN);

/**
 * gtk_text_buffer_insert_range_rect:
 * @buffer: a #GtkTextBuffer where insertion will occur
 * @at_iter: (nullable): a #GtkTextIter on @buffer to be starting point
 * of insertion or %NULL to insert at current rectangular selection.
 * @start: a #GtkTextIter corresponding to start of insertion text
 * @end: a #GtkTextIter corresponding to end of insertion text
 *
 * Inserts the text enclosed between @start and @end in the current
 * rectangular selection of @buffer when @at_iter is %NULL; otherwise
 * the text will be inserted at @at_iter if it's non %NULL.
 * @start and @end must belong to a #GtkTextBuffer different than @buffer
 * but both must share the same tag table.
 *
 * Implemented by means of a private signal handled by the view layer
 * (GtkTextLayout) which will insert the text, when @at_iter is %NULL,
 * into current visual rectangular selection; or will insert it at
 * @at_iter when it is non %NULL.
 *
 * Returns: %TRUE if insertion was successful, %FALSE otherwise.
 **/
gboolean
gtk_text_buffer_insert_range_rect (GtkTextBuffer       *buffer,
                                   const GtkTextIter   *at_iter,
                                   const GtkTextIter   *start,
                                   const GtkTextIter   *end);

/**
 * gtk_text_view_set_visual_column:
 * @text_view: a #GtkTextView.
 * @iter: a position in @text_view.
 * @voffset: visual column to move @iter to.
 * @after_tab: when @voffset falls inside a tab, whether to move @iter after or
 * before that tab. %TRUE for after tab, %FALSE for before tab.
 * @voffset_placed: (out) (allow-none): return location for the voffset where
 * @iter was finally moved to (as @voffset_placed can differ from @voffset when
 * the later falls inside a tab), you can pass %NULL if you are not interested
 * on that value.
 *
 * Moves @iter to @voffset visual column in the line taking into consideration
 * the current tab-width of @text_view. If @voffset falls in the middle of a tab
 * then @iter is positioned just after or before that tab according to @after_tab
 * parameter. If @voffset is beyond last visual column of the line, then @iter
 * will be moved to last position in line and %FALSE would be returned, otherwise
 * @iter was moved successfully and %TRUE is returned.
 *
 * Returns: %TRUE if @iter was moved successfully to desired position, %FALSE
 * otherwise.
 */
gboolean
gtk_text_view_set_visual_column (GtkTextView  *textview,
                                 GtkTextIter    *iter,
                                 guint          voffset,
                                 gboolean       after_tab,
                                 guint          *voffset_placed); 


 

Attachment: 0001-gtksourceview.c-support-new-GtkTextView-rectangular-.patch
Description: Binary data

Attachment: 0001-Implement-rectangular-text-selection-in-GtkTextView.tar.gz
Description: GNU Zip compressed data



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]