Štěpán Roh

Alive But Sleepy

← Perl 5.6.0 Burrows-Wheelerova transformace →
Saturday, November 29, 2003

Detaily a příklady použití widgetu GtkTextView

by Štěpán Roh

Terminologická poznámka: ačkoliv GTK+ není napsané v objektovém jazyce, dále se kvůli jeho jasně objektovému návrhu používají termíny "třída", "metoda" a popř. "atribut" (neplést s property alias vlastností).

Koncept editace a zobrazení textu v GTK+

Editace a zobrazení textu v GTK+ verze 2 je založeno na dvou základních třídách: GtkTextBuffer a GtkTextView. Jejich vzájemný vztah je vidět na následujícím diagramu:

Diagram tříd

GtkTextBuffer je textový buffer, který je možno připojit k jednomu či více GtkTextView. GtkTextView se stará o zobrazování (a případnou změnu) informací v bufferu obsažených. Buffer nese jak informaci o obsahu, tak i speciální značky (nazývané tagy; třída GtkTextTag) sloužící (nejenom) ke kontrole prezentace textu.

K určování pozice v textu se používají dvě třídy: GtkTextIter a GtkTextMark. Rozdíl mezi nimi je v odolnosti proti změně bufferu. GtkTextIter se stane invalidním, pakliže se buffer změní, zatímco GtkTextMark je stále validní a po textu tzv. "cestuje".

GtkTextView

Jelikož veškerý text je vnitřně uložen v UTF-8 (který kóduje jeden znak jako jeden či více bytů), je třeba rozlišovat mezi počtem znaků a počtem bytů. Počet znaků se v terminologii GTK+ označuje offset, počet bytů pak index.

Tento text zdaleka nepokrývá veškeré možnosti editace a zobrazení víceřádkového textu v GTK+ verze 2, např. využití signálů pro předefinování chování jednotlivých tříd je opominuto. Taktéž pro plný popis API a přesného chování jednotlivých tříd a metod je třeba zkonzultovat referenční dokumentaci.

Vícejazyčná podpora v GTK+

Vytvoření bufferu a práce s ním

Vytvoření bufferu aneb lehčí příklad na úvod (chybové stavy se zde zanedbávají):

    GtkTextBuffer *buf;

    buf = gtk_text_buffer_new(NULL);
    gtk_text_buffer_set_text(buf, "TEXT", 4);
  

Je také možno získat buffer z nějaké instance GtkTextView:

    GtkTextView *view;
    GtkTextBuffer *buf;

    view = gtk_text_view_new();
    buf = gtk_text_view_get_buffer();
  

Pomocí funkcí gtk_text_buffer_get_line_count a gtk_text_buffer_get_char_count lze zjistit počet řádek a počet znaků textu v bufferu.

S textem v bufferu je možné pracovat pomocí metod gtk_text_buffer_set/get_text, a rodiny metod gtk_text_buffer_insert* a gtk_text_buffer_delete*. Kromě běžného textu lze vkládat i obrázky a widgety, což bude předvedeno později.

Iterátory

Veškerá editace se opírá o mechanismus iterátorů (GtkTextIter). Iterátor označuje pozici v textu, přičemž buffer se nesmí změnit mezi vytvořením iterátoru a jeho použitím. Struktura GtkTextIter je dost malá na to, aby se mohla alokovat přímo na zásobníku, tudíž je její použití levné. Ukažme si na větším příkladu:

    FILE *f;
    /* lze si dovolit alokaci přímo na zásobníku */
    GtkTextIter p;
    char fbuf[2000];
    size_t l;

    /* otevření souboru - nic zajímavého */
    if(!(f = fopen(fname, "r"))) {
      g_printerr("%s: %s\n", fname, g_strerror(errno));
      return;
    }
    /* načtení obsahu bufferu - end_iter ukazuje za poslední znak v bufferu */
    gtk_text_buffer_get_end_iter(buf, &p);
    while((l = fread(fbuf, 1, sizeof(fbuf), f)) > 0) {
      GError *err = NULL;
      gsize br, bw;
      gchar *text;
      /* konverze vstupu z aktuálního locale do UTF8 */
      if(!(text = g_locale_to_utf8(fbuf, l, &br, &bw, &err))) {
        g_printerr("Failed locale to UTF-8 conversion: %s\n", err->message);
        g_clear_error(&err);
        break;
      }
      /* vložení textu do bufferu - automaticky je vyslán signál "insert_text", který je standardně obsloužen tak, že předložený iterátor
         poté ukazuje za vložený text */
      gtk_text_buffer_insert(buf, &p, text, bw); /* bw je počet bytů či -1 pro vstup NULL-terminated textu */
      g_free(text);
    }
    /* atribut modified se při každé změně bufferu nastaví na TRUE */
    gtk_text_buffer_set_modified(buf, FALSE);
    /* nastavení pozice kurzoru (bude vysvětleno níže) na začátek textu */
    gtk_text_buffer_get_start_iter(buf, &p);
    gtk_text_buffer_place_cursor(buf, &p);
  

Práce s iterátory v GtkTextBuffer je vcelku nezajímavá, snad kromě schopnosti získat iterátor dle offsetu i dle indexu v rámci celého textu i v rámci určené řádky (veškeré počty začínají od nuly) (viz metody gtk_text_buffer_get_iter_at_offset, gtk_text_buffer_get_iter_at_line_offset a další). Zajímavými iterátory jsou gtk_text_buffer_get_start_iter a gtk_text_buffer_get_end_iter označující začátek a konec textu (end je nastaven za poslední znak).

Zajímavější je velké množství funkcí přístupných přímo v GtkTextIter. Jednak lze zjišťovat aktuální pozici iterátoru a nastavovat ji, ale zejména se lze dotazovat na velké množství informací týkajících se obsahu bufferu na daném místě. Z velkého množství funkcí lze zmínit např. gtk_text_iter_get_char (vrátí aktuální znak), gtk_text_iter_starts_word (zjistí, zda na daném místě začíná slovo - co je slovo je určeno dle jazyka pomocí knihovny Pango), gtk_text_iter_forward_char (posune iterátor o znak vpřed), gtk_text_iter_compare (porovnání dvou značek) či gtk_text_iter_order (seřazení dvou značek dle pozice v textu). Zájemce o detailní popis nechť se zaměří na referenční příručku.

Značky

Značky (GtkTextMark) implementují označení pozice v textu, které je odolné proti změně bufferu. Je-li na pozici značky něco vloženo, tak se značka přesune na začátek či konec vkládaného textu (možno nastavit parametrem left_gravity (= začátek) při tvorbě značky pomocí metody gtk_text_buffer_create_mark). Značka se dá převést na iterátor voláním metody gtk_text_buffer_get_iter_at_mark. Pro práci se značkami je určena rodina metod gtk_text_buffer_*mark* a gtk_text_mark_*.

Kurzor zmíněný dříve je ve skutečnosti souhrnný název pro dvě speciální značky: "insert" a "selection_bound". První z nich určuje (nejenom) místo, kam se má vkládat text při použití metody gtk_text_buffer_insert_at_cursor, obě dohromady pak určují označený text (tzv. selection).

Selection a clipboard

Jak již bylo zmíněno, tak dvojice značek "insert" a "selection_bound" určují mimo jiné označený text (selection). S tímto označeným textem lze dělat věci známé i odjinud, tj. smazat ho (gtk_text_buffer_delete_selection), zkopírovat do clipboardu (gtk_text_buffer_copy_clipboard), zkopírovat do clipboardu a smazat (gtk_text_buffer_cut_clipboard) a nahradit textem z clipboardu (gtk_text_buffer_paste_clipboard). Nahrazení textem z clipboardu samozřejmě funguje i bez označeného textu, kdy se vkládá na aktuální pozici značky "insert". Je třeba ještě zmínit, že vkládání textu je asynchronní a proběhne až při nejbližším vyřizování událostí.

Do problematiky práce s clipboardem zde nebudeme zabíhat, pouze některé drobnosti znázorníme na příkladu:

    /* iniciální selection */
    GdkAtom sel_atom = GDK_SELECTION_PRIMARY;
    GtkTextBuffer *buf;
    GtkTextIter p;

    case MENU_CUT:
      /* gtk_clipboard_get vrací GtkClipboard odpovidající danému selection
         - třetí parametr default_editable označuje, zda je buffer editovatelný či nikoliv (lze lokálně změnit pomocí tagů)
       */
      gtk_text_buffer_cut_clipboard(buf, gtk_clipboard_get(sel_atom), TRUE);
      break;
    case MENU_COPY:
      gtk_text_buffer_copy_clipboard(buf, gtk_clipboard_get(sel_atom));
      break;
    case MENU_PASTE:
      gtk_text_buffer_paste_clipboard(buf, gtk_clipboard_get(sel_atom), NULL, TRUE);
      break;
    case MENU_USE_CLIPBOARD:
      /* na základě uživatelem vybrané volby se použije clipboard či primární selection */
      sel_atom = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(gtk_item_factory_get_item(main_menu, "<TextView>/Edit/Use Clipboard"))) ?
        GDK_SELECTION_CLIPBOARD : GDK_SELECTION_PRIMARY;
      break;
    case MENU_SELECT_ALL:
      /* označení veškerého textu nastavením kurzoru na začátek a nastavením "selection_bound" na konec */
      gtk_text_buffer_get_start_iter(buf, &p);
      gtk_text_buffer_place_cursor(buf, &p);
      gtk_text_buffer_get_end_iter(buf, &p);
      gtk_text_buffer_move_mark_by_name(buf, "selection_bound", &p);
      break;
  

Označený text lze automaticky propagovat do X selection, k čemuž slouží metody gtk_text_buffer_add_selection_clipboard a gtk_text_buffer_remove_selection_clipboard. Je-li GtkTextBuffer zobrazen v nějakém GtkTextView, pak se automaticky propaguje do primárního X selection (GDK_SELECTION_PRIMARY).

Tagy

Koncept tagů umožňuje změnit properties pouze určitému bloku textu. Tohoto mechanismu se využívá pro změnu stylu písma, pro změnu stylu odstavce či pro povolení a zakázání editace daného bloku.

Tag je reprezentován třídou GtkTextTag. Každý tag má asociován seznam properties, které změní pro daný text. Nejčastěji se používají pojmenované tagy, které lze jednoduše aplikovat. Pokročilejšími funkcemi je možnost nastavování priorit jednotlivých tagů a také sdílení tagů mezi více buffery (viz třída GtkTextTagTable). Zájemci nechť nahlédnou do dokumentace.

Použití tagů na příkladu:

    GtkTextBuffer buf;

    /* vytvoření dvou tagů pro změnu stylu písma */
    /* textu se změní váha na tučné */
    gtk_text_buffer_create_tag(buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
    /* textu se změní styl na italiku */
    gtk_text_buffer_create_tag(buf, "italics", "style", PANGO_STYLE_ITALIC, NULL);

    /* a o něco dále */
    GtkTextIter p, q;

    case MENU_FONT_NORMAL:
        /* označený text */
        gtk_text_buffer_get_iter_at_mark(buf, &p, gtk_text_buffer_get_mark(buf, "insert"));
        gtk_text_buffer_get_iter_at_mark(buf, &q, gtk_text_buffer_get_mark(buf, "selection_bound"));
        /* označený text nebude ani tučný ani italikou */
        gtk_text_buffer_remove_tag_by_name(buf, "bold", &p, &q);
        gtk_text_buffer_remove_tag_by_name(buf, "italics", &p, &q);
        break;
    case MENU_FONT_BOLD:
        /* označený text */
        gtk_text_buffer_get_iter_at_mark(buf, &p, gtk_text_buffer_get_mark(buf, "insert"));
        gtk_text_buffer_get_iter_at_mark(buf, &q, gtk_text_buffer_get_mark(buf, "selection_bound"));
        /* označený text bude tučný */
        gtk_text_buffer_apply_tag_by_name(buf, "bold", &p, &q);
        break;
    case MENU_FONT_ITALICS:
        /* označený text */
        gtk_text_buffer_get_iter_at_mark(buf, &p, gtk_text_buffer_get_mark(buf, "insert"));
        gtk_text_buffer_get_iter_at_mark(buf, &q, gtk_text_buffer_get_mark(buf, "selection_bound"));
        /* označený text bude italikou */
        gtk_text_buffer_apply_tag_by_name(buf, "italics", &p, &q);
        break;
  

Jak vidno na příkladu, tak tag se neodstraňuje z celého rozsahu, na který byl aplikován, ale pouze z určeného textu, tj. tag je vlastnost znaku a ne bloku znaků.

Některé zajímavé properties (další properties budou zmíněny později):

background-gdk
barva pozadí (konkrétně GdkColor, lze i jiné formy pomocí jiných properties)
foreground-gdk
barva textu (konkrétně GdkColor, lze i jiné formy pomocí jiných properties)
invisible
viditelnost textu
style
styl textu
underline
podtržení
weight
váha fontu
size
velikost fontu (absolutní v bodech)
scale
zvětšení fontu (relativní v násobcích)

Properties týkající se fontů a jazyka apod. jsou implementovány knihovnou Pango.

Použití tagů pro změnu stylu a fontu

Interaktivní metody

Velké množství metod třídy GtkTextBuffer má svoji *_interactive verzi (za všechny např. gtk_text_buffer_insert_interactive). Takovéto metody slouží hlavně třídě GtkTextView k implementaci uživatelských akcí. Tyto metody totiž berou v úvahu zda dotčená část bufferu je editovatelná či nikoliv. Editovatelnost je ovlivněna dodatečným parametrem default_editable, což je implicitní hodnota editovatelnosti pro části bufferu u nichž není explicitně určena pomocí příslušného tagu (konkrétně pomocí tagu nastavujícího property "editable").

Zobrazení textu

O zobrazení textu se stará widget GtkTextView. Tato třída zobrazuje jí přiřazený buffer, přičemž bere v úvahu properties přiřazené textu pomocí tagů. K properties uvedeným dříve přibylo pár dalších, specifických pro GtkTextView. Mezi zajímavé properties patří "cursor-visible" (viditelnost kurzoru, nebo-li značky "insert"), "editable" (editovatelnost), "justification" (zarovnání odstavce - na praporek vlevo, vpravo, na střed, do bloku - viz výčet GtkJustification) a "wrap-mode" (mód dělení řádek - nedělený, na hranici písmen, na hranici slov - viz výčet GtkWrapMode). Pro účely zobrazení se jako odstavec bere úsek textu ukončený znakem nového řádku. Veškeré properties lze nastavovat i globálně pomocí set metod třídy GtkTextView.

Různé módy zobrazení textu v GtkTextView

Obrázky a widgety

Do bufferu lze vkládat (a následně i zobrazovat) libovolné obrázky. K tomu slouží metoda gtk_text_buffer_insert_pixbuf, která vkládá GdkPixbuf. Jeden obrázek se počítá jakožto jeden znak. Při kopírování textu skrz clipboard se kopírují i obrázky (i když pouze v rámci jedné aplikace).

Obrázky v GtkTextView

Přímo do textu lze vkládat také libovolné widgety, a to pomocí speciálních objektů, tzv. kotev (GtkTextChildAnchor). Kotva je umístěna v bufferu a zabírá jeden znak. Více viz metody gtk_text_child_anchor_* a gtk_text_buffer_*_child_anchor. Widgety skrz clipboard kopírovat nelze.

Widgety v GtkTextView

S problematikou obrázků a widgetů ještě souvisí rozdíl mezi metodami gtk_text_buffer_get_text a gtk_text_buffer_get_slice. Prvně jmenovaná totiž obrázky a widgety ignoruje, zatímco druhá na jejich pozicích vrací Unicode znak 0xFFFC.

Zdroje

  1. Multiline Text Editor (GTK+ Reference Manual)
    http://developer.gnome.org/doc/API/2.0/gtk/TextWidgetObjects.html
  2. Ilustrační příklady pochází z demonstrační mini-aplikace od Martina Berana
    http://www.ms.mff.cuni.cz/~beran/vyuka/X/prog/gtk/text_view.c
  3. Ilustrační obrázky pochází z demonstrační aplikace gtk-demo
← Perl 5.6.0 ↑Back to top Burrows-Wheelerova transformace →