/*
 #
 #  File        : gmic_gimp.cpp
 #                ( C++ source file )
 #
 #  Description : G'MIC for GIMP - A plug-in to allow the use
 #                of G'MIC commands in GIMP.
 #                This file is a part of the CImg Library project.
 #                ( http://cimg.sourceforge.net )
 #
 #  Copyright   : David Tschumperle
 #                ( http://www.greyc.ensicaen.fr/~dtschump/ )
 #
 #  License     : CeCILL v2.0
 #                ( http://www.cecill.info/licences/Licence_CeCILL_V2-en.html )
 #
 #  This software is governed by the CeCILL  license under French law and
 #  abiding by the rules of distribution of free software.  You can  use,
 #  modify and/ or redistribute the software under the terms of the CeCILL
 #  license as circulated by CEA, CNRS and INRIA at the following URL
 #  "http://www.cecill.info".
 #
 #  As a counterpart to the access to the source code and  rights to copy,
 #  modify and redistribute granted by the license, users are provided only
 #  with a limited warranty  and the software's author,  the holder of the
 #  economic rights,  and the successive licensors  have only  limited
 #  liability.
 #
 #  In this respect, the user's attention is drawn to the risks associated
 #  with loading,  using,  modifying and/or developing or reproducing the
 #  software by the user in light of its specific status of free software,
 #  that may mean  that it is complicated to manipulate,  and  that  also
 #  therefore means  that it is reserved for developers  and  experienced
 #  professionals having in-depth computer knowledge. Users are therefore
 #  encouraged to load and test the software's suitability as regards their
 #  requirements in conditions enabling the security of their systems and/or
 #  data to be ensured and, more generally, to use and operate it in the
 #  same conditions as regards security.
 #
 #  The fact that you are presently reading this means that you have had
 #  knowledge of the CeCILL license and that you accept its terms.
 #
*/

// Include necessary header files.
//--------------------------------
#define cimg_display_type 0
#include "gmic.h"
#undef _gmic_path
#if cimg_OS==2
#define _gmic_path "_gmic\\"
#else
#define _gmic_path ""
#endif
#if !defined(__MACOSX__)  && !defined(__APPLE__)
#include <pthread.h>
#endif
#include <locale>
#include <gtk/gtk.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
#undef min
#undef max
extern char data_gmic_def[];
extern unsigned int size_data_gmic_def;
extern unsigned char data_gmic_logo[];
extern unsigned int size_data_gmic_logo;
using namespace cimg_library;

// Define plug-in global variables.
//---------------------------------
CImgList<char> gmic_entries;                  // The list of recognized G'MIC menu entries.
CImgList<char> gmic_1stlevel_entries;         // The treepath positions of 1st-level G'MIC menu entries.
CImgList<char> gmic_commands;                 // The list of corresponding G'MIC commands to process the image.
CImgList<char> gmic_preview_commands;         // The list of corresponding G'MIC commands to preview the image.
CImgList<char> gmic_arguments;                // The list of corresponding needed filter arguments.
CImgList<char> gmic_faves;                    // The list of favorites filters and their default parameters.
CImgList<double> gmic_preview_factors;        // The list of default preview factors for each filter.
CImg<float> computed_preview;                 // The last computed preview image.
CImg<char> gmic_additional_commands;          // The buffer of additional G'MIC command definitions and implementations.
bool _create_dialog_gui;                      // Return value of the 'create_gui_dialog()' function (set by events handlers).
bool is_block_preview = false;                // Flag to block preview computation, when double-clicking on the filter tree.
void **event_infos;                           // Infos that are passed to the GUI callback functions.
int image_id = 0;                             // The image concerned by the plug-in execution.
unsigned int indice_faves = 0;                // The starting index of favorite filters.
std::FILE *logfile = 0;                       // The log file if any.
GimpRunMode run_mode;                         // Run-mode used to call the plug-in.
GtkTreeStore *tree_view_store = 0;            // The list of the filters as a GtkTreeView model.
GimpDrawable *drawable_preview = 0;           // The drawable used by the preview window.
GtkWidget *dialog_window = 0;                 // The plug-in dialog window.
GtkWidget *left_pane = 0;                     // The left pane, containing the preview window.
GtkWidget *gui_preview = 0;                   // The preview window.
GtkWidget *relabel_hbox = 0;                  // The entire widget to relabel filter.
GtkWidget *relabel_entry = 0;                 // The text entry to relabel filter.
GtkWidget *tree_view = 0;                     // The filter treeview.
GtkWidget *tree_mode_stockbutton = 0;         // A temporary stock button for the expand/collapse button.
GtkWidget *tree_mode_button = 0;              // Expand/Collapse button for the treeview.
GtkWidget *fave_stockbutton = 0;              // A temporary stock button for the fave button.
GtkWidget *fave_button = 0;                   // Fave button.
GtkWidget *right_frame = 0;                   // The right frame containing the filter parameters.
GtkWidget *right_scrolledwindow = 0;          // The right scrolled window, containing the right frame.
GimpPDBStatusType status = GIMP_PDB_SUCCESS;  // The plug-in return status.

#define gmic_xstr(x) gmic_str(x)
#define gmic_str(x) #x
#define gmic_update_server "http://gmic.sourceforge.net/"     // The filters update server address.
#define gmic_update_file "gmic_def." gmic_xstr(gmic_version)  // The filters update filename.

// Set/get plug-in persistent variables, using GIMP {get,set}_data() features.
//-----------------------------------------------------------------------------

// Set/get the indice of the currently selected filter.
void set_current_filter(const unsigned int current_filter) {
  const unsigned int ncurrent_filter = current_filter>=gmic_entries.size()?0:current_filter;
  gimp_set_data("gmic_current_filter",&ncurrent_filter,sizeof(unsigned int));
}

unsigned int get_current_filter() {
  unsigned int current_filter = 0;
  gimp_get_data("gmic_current_filter",&current_filter);
  if (current_filter>=gmic_entries.size()) current_filter = 0;
  return current_filter;
}

// Set/get the number of parameters of the specified filter.
void set_filter_nbparams(const unsigned int filter, const unsigned int nbparams) {
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_filter%u_nbparams",filter);
  gimp_set_data(s_tmp,&nbparams,sizeof(unsigned int));
}

unsigned int get_filter_nbparams(const unsigned int filter) {
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_filter%u_nbparams",filter);
  unsigned int nbparams = 0;
  gimp_get_data(s_tmp,&nbparams);
  return nbparams;
}

// Set/get one particular parameter of a filter.
void set_filter_parameter(const unsigned int filter, const unsigned int n, const char *const param) {
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_filter%u_parameter%u",filter,n);
  gimp_set_data(s_tmp,param,std::strlen(param)+1);
}

char *get_filter_parameter(const unsigned int filter, const unsigned int n) {
  static char s_param[4096];
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_filter%u_parameter%u",filter,n);
  *s_param = 0;
  gimp_get_data(s_tmp,s_param);
  return s_param;
}

// Set/get one particular default parameter of a fave filter.
void set_fave_parameter(const unsigned int filter, const unsigned int n, const char *const param) {
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_fave%u_parameter%u",filter,n);
  gimp_set_data(s_tmp,param,std::strlen(param)+1);
}

char *get_fave_parameter(const unsigned int filter, const unsigned int n) {
  static char s_param[4096];
  char s_tmp[64];
  cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_fave%u_parameter%u",filter,n);
  *s_param = 0;
  gimp_get_data(s_tmp,s_param);
  return s_param;
}

// Reset all parameters of all filters.
void reset_filters_parameters() {
  const char *const empty = "";
  for (unsigned int i = 1; i<gmic_entries.size(); ++i)
    for (unsigned int j = 0; ; ++j) {
      const char *const val = get_filter_parameter(i,j);
      if (*val) set_filter_parameter(i,j,empty); else break;
    }
}

// Set/get the filter input, output and preview and verbosity modes.
void set_input_mode(const unsigned int input_mode) {
  gimp_set_data("gmic_input_mode",&input_mode,sizeof(unsigned int));
}

unsigned int get_input_mode(const bool normalized=true) {
  unsigned int input_mode = 0;
  gimp_get_data("gmic_input_mode",&input_mode);
  return normalized?(input_mode<2?1:(input_mode-2)):input_mode;
}

void set_output_mode(const unsigned int output_mode) {
  gimp_set_data("gmic_output_mode",&output_mode,sizeof(unsigned int));
}

unsigned int get_output_mode(const bool normalized=true) {
  unsigned int output_mode = 0;
  gimp_get_data("gmic_output_mode",&output_mode);
  return normalized?(output_mode<2?0:(output_mode-2)):output_mode;
}

void set_preview_mode(const unsigned int preview_mode) {
  gimp_set_data("gmic_preview_mode",&preview_mode,sizeof(unsigned int));
}

unsigned int get_preview_mode(const bool normalized=true) {
  unsigned int preview_mode = 0;
  gimp_get_data("gmic_preview_mode",&preview_mode);
  return normalized?(preview_mode<2?0:(preview_mode-2)):preview_mode;
}

void set_verbosity_mode(const unsigned int verbosity) {
  gimp_set_data("gmic_verbosity_mode",&verbosity,sizeof(unsigned int));
}

unsigned int get_verbosity_mode(const bool normalized=true) {
  unsigned int verbosity_mode = 0;
  gimp_get_data("gmic_verbosity_mode",&verbosity_mode);
  return normalized?(verbosity_mode<2?0:(verbosity_mode-2)):verbosity_mode;
}

void set_logfile() {
  const unsigned int verbosity = get_verbosity_mode();
  if (verbosity==2 || verbosity==4 || verbosity==6) {
    if (!logfile) {
      char filename[2048];
      cimg_snprintf(filename,sizeof(filename),"%s%cgmic_log",cimg::temporary_path(),cimg_file_separator);
      logfile = std::fopen(filename,"a");
    }
    cimg::output(logfile?logfile:stdout);
  } else {
    if (logfile) std::fclose(logfile);
    logfile = 0;
    cimg::output(stdout);
  }
}

// Set/get the tree collapse/expand mode.
void set_tree_mode(const bool expand) {
  gimp_set_data("gmic_tree_mode",&expand,sizeof(bool));
}

bool get_tree_mode() {
  bool tree_mode = false;
  gimp_get_data("gmic_tree_mode",&tree_mode);
  return tree_mode;
}

// Set/get the net update activation state.
void set_net_update(const bool net_update) {
  gimp_set_data("gmic_net_update",&net_update,sizeof(bool));
}

bool get_net_update() {
  bool net_update = true;
  gimp_get_data("gmic_net_update",&net_update);
  return net_update;
}

// Set/get the current locale.
void set_locale() {
  char locale[16] = { 0 };
  std::sscanf(std::setlocale(LC_ALL,0),"%c%c",&(locale[0]),&(locale[1]));
  cimg::uncase(locale);
  gimp_set_data("gmic_locale",locale,std::strlen(locale)+1);
}

const char *get_locale() {
  static char locale[16];
  *locale = 0;
  gimp_get_data("gmic_locale",locale);
  return locale;
}

// Test if a drawable is valid.
bool gmic_drawable_is_valid(const GimpDrawable *const drawable) {
#if GIMP_MINOR_VERSION<=6
  return gimp_drawable_is_valid(drawable->drawable_id);
#else
  return gimp_item_is_valid(drawable->drawable_id);
#endif
}

// Translate string into the current locale.
//------------------------------------------
#define _t(source,dest) if (!std::strcmp(source,s)) { static const char *const ns = dest; return ns; }
const char *t(const char *const s) {

  // French translation
  if (!std::strcmp(get_locale(),"fr")) {
    if (!s) {
      static const char *const ns = "<b>Mise &#224; jour depuis Internet impossible !</b>\n\n"
        "Merci de v&#233;rifier l'&#233;tat de votre connexion. Vous pouvez\n"
        "manuellement mettre &#224; jour vos filtres en t&#233;l&#233;chargeant :\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "et en le copiant comme le fichier <i>.%s</i>\n"
        "dans votre r&#233;pertoire <i>Home</i> ou <i>Application Data</i>.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC pour GIMP");
    _t("<i>Select a filter...</i>","<i>Choisissez un filtre...</i>");
    _t("<i>No parameters to set...</i>","<i>Pas de param&#232;tres...</i>");
    _t("<b> Input / Output : </b>","<b> Entr&#233;es / Sorties : </b>");
    _t("Input layers...","Calques d'entr\303\251e...");
    _t("None","Aucun");
    _t("Active (default)","Actif (d\303\251faut)");
    _t("All","Tous");
    _t("Active & below","Actif & en dessous");
    _t("Active & above","Actif & au dessus");
    _t("All visibles","Tous les visibles");
    _t("All invisibles","Tous les invisibles");
    _t("All visibles (decr.)","Tous les visibles (d\303\251cr.)");
    _t("All invisibles (decr.)","Tous les invisibles (d\303\251cr.)");
    _t("All (decr.)","Tous (d\303\251cr.)");
    _t("Output mode...","Mode de sortie...");
    _t("In place (default)","Sur place (d\303\251faut)");
    _t("New layer(s)","Nouveau(x) calque(s)");
    _t("New active layer(s)","Nouveau(x) calque(s) actifs");
    _t("New image","Nouvelle image");
    _t("Output preview...","Mode d'aper\303\247u...");
    _t("1st output (default)","1\303\250re image (d\303\251faut)");
    _t("2nd output","2\303\250me image");
    _t("3rd output","3\303\250me image");
    _t("4th output","4\303\250me image");
    _t("1st -> 2nd","1\303\250re -> 2\303\250me");
    _t("1st -> 3rd","1\303\250re -> 3\303\250me");
    _t("1st -> 4th","1\303\250re -> 4\303\250me");
    _t("All outputs","Toutes les images");
    _t("Output messages...","Messages de sortie...");
    _t("Quiet (default)","Aucun message (d\303\251faut)");
    _t("Verbose (console)","Mode verbeux (console)");
    _t("Verbose (logfile)","Mode verbeux (fichier log)");
    _t("Very verbose (console)","Mode tr\303\250s verbeux (console)");
    _t("Very verbose (logfile)","Mode tr\303\250s verbeux (fichier log)");
    _t("Debug mode (console)","Mode d\303\251bogage (console)");
    _t("Debug mode (logfile)","Mode d\303\251bogage (fichier log)");
    _t(" Available filters (%u) :"," Filtres disponibles (%u) :");
    _t("_Maximize","_Maximiser");
    _t("_Restore","_R\303\251duire");
    _t("Update","Actualiser");
    _t("Rename","Renommer");
  }

  // Catalan translation
  if (!std::strcmp(get_locale(),"ca")) {
    if (!s) {
      static const char *const ns =
        "<b>No ha estat possible establir una connexi&#243; a Internet !</b>\n\n"
        "Verifiqueu l'estat de la vostra connexi&#243;. Podeu\n"
        "actualitzar els vostres filtres descarregant-vos:\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "i copiant-lo com a <i>.%s</i>\n"
        "a la vostra carpeta d'<i>inici</i> o la carpeta de <i>Dades de Programa</i>.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC per al GIMP");
    _t("<i>Select a filter...</i>","<i>Selecciona un filtre...</i>");
    _t("<i>No parameters to set...</i>","<i>Sense par\303\240metres...</i>");
    _t("<b> Input / Output : </b>","<b> Entrades / Sortides : </b>");
    _t("Input layers...","Capes d'entrada...");
    _t("None","Cap");
    _t("Active (default)","Actiu (predet.)");
    _t("All","Tots");
    _t("Active & below","L'activa i les de sota");
    _t("Active & above","L'activa i les de sobre");
    _t("All visibles","Totes les visibles");
    _t("All invisibles","Totes les invisibles");
    _t("All visibles (decr.)","Totes les visibles (decr.)");
    _t("All invisibles (decr.)","Totes les invisibles (decr.)");
    _t("All (decr.)","Totes (decr.)");
    _t("Output mode...","Mode de sortida...");
    _t("In place (default)","A la capa actual (predet.)");
    _t("New layer(s)","Nova/es capa/es");
    _t("New active layer(s)","Nova/es capa/es actius");
    _t("New image","Nova imatge");
    _t("Output preview...","Previsualitzaci\303\263 de sortida...");
    _t("1st output (default)","1era imatge (predet.)");
    _t("2nd output","2ona imatge");
    _t("3rd output","3era imatge");
    _t("4th output","4rta imatge");
    _t("1st -> 2nd","1era -> 2ona");
    _t("1st -> 3rd","1era -> 3era");
    _t("1st -> 4th","1era -> 4rta");
    _t("All outputs","Totes les imatges");
    _t("Output messages...","Missatges de sortida...");
    _t("Quiet (default)","Sense missatges (predet.)");
    _t("Verbose (console)","Verb\303\263s (consola)");
    _t("Verbose (logfile)","Verb\303\263s (arxiu)");
    _t("Very verbose (console)","Molt verb\303\263s (consola)");
    _t("Very verbose (logfile)","Molt verb\303\263s (arxiu)");
    _t("Debug mode (console)","Depuraci\303\263 (consola)");
    _t("Debug mode (logfile)","Depuraci\303\263 (arxiu)");
    _t(" Available filters (%u) :"," Filtres disponibles (%u) :");
    _t("_Maximize","_Maximitzar");
    _t("_Restore","_Restaurar");
    _t("Update","Actualitzaci\303\263");
    _t("Rename","Canviar");
  }

  // Italian translation
  if (!std::strcmp(get_locale(),"it")) {
    if (!s) {
      static const char *const ns = "<b>Impossibile aggiornare da Internet !</b>\n\n"
        "Controllate lo stato della vostra connessione. Potete anche\n"
        "aggiornare manualmente i filtri scaricando :\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "e copiandoli come il file <i>.%s</i>\n"
        "nella directory <i>Home</i> o <i>Application Data</i>.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC per GIMP");
    _t("<i>Select a filter...</i>","<i>Sciegliete un Filtro...</i>");
    _t("<i>No parameters to set...</i>","<i>Filtro senza Parametri...</i>");
    _t("<b> Input / Output : </b>","<b> Input / Output : </b>");
    _t("Input layers...","Input da Layers...");
    _t("None","Nessuno");
    _t("Active (default)","Layer Attivo (default)");
    _t("All","Tutti");
    _t("Active & below","Attivo & superiori");
    _t("Active & above","Attivo & inferiori");
    _t("All visibles","Tutti i Visibili");
    _t("All invisibles","Tutti gli invisibili");
    _t("All visibles (decr.)","Tutti i visibili (dal fondo)");
    _t("All invisibles (decr.)","Tutti gli invisibili (dal fondo)");
    _t("All (decr.)","Tutti");
    _t("Output mode...","Tipo di output...");
    _t("In place (default)","Applica al Layer attivo (default) ");
    _t("New layer(s)","Nuovo(i) Layer(s)");
    _t("New active layer(s)","Attiva Nuovo(i) Layer(s)");
    _t("New image","Nuova Immagine");
    _t("Output preview...","Anteprima...");
    _t("1st output (default)","Primo Output (default)");
    _t("2nd output","Secondo Output");
    _t("3rd output","Terzo Output");
    _t("4th output","Quarto Output");
    _t("1st -> 2nd","1 -> 2");
    _t("1st -> 3rd","1 -> 3");
    _t("1st -> 4th","1 -> 4");
    _t("All outputs","Tutti i layers");
    _t("Output messages...","Messaggi di Output...");
    _t("Quiet (default)","Nessun Messaggio (default)");
    _t("Verbose (console)","Messagi (console)");
    _t("Verbose (logfile)","Messagi (logfile)");
    _t("Very verbose (console)","Messaggi Dettagliati (console)");
    _t("Very verbose (logfile)","Messaggi Dettagliati (logfile)");
    _t("Debug mode (console)","Debug Mode (console)");
    _t("Debug mode (logfile)","Debug Mode (logfile)");
    _t(" Available filters (%u) :"," Filtri disponibili (%u) :");
    _t("Update","Aggiornare");
  }

  // Portuguese translation
  if (!std::strcmp(get_locale(),"pt")) {
    if (!s) {
      static const char *const ns = "<b>A atualiza\303\247\303\243o pela internet falhou !</b>\n\n"
        "Por favor verifique o status da sua conex\303\243o. Voc\303\252 tamb\303\251m pode\n"
        "atualizar sua lista de filtros manualmente se desejar :\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "E copie o mesmo como o arquivo <i>.%s</i>\n"
        "para dentro do seu diret\303\263rio <i>Home</i> ou <i>Aplicativos de dados</i> pasta.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC para o GIMP");
    _t("<i>Select a filter...</i>","<i>Escolha um filtro</i>");
    _t("<i>No parameters to set...</i>","<i>Sem par\303\242metros para configurar...</i>");
    _t("<b> Input / Output : </b>","<b> Entrada / Saida : </b>");
    _t("Input layers...","Camadas de Entrada...");
    _t("None","Nenhuma");
    _t("Active (default)","Ativo (Padr\303\243o)");
    _t("All","Todos");
    _t("Active & below","Ativo & abaixo");
    _t("Active & above","Ativo & acima");
    _t("All visibles","Todos vis\303\255veis");
    _t("All invisibles","Todos invis\303\255veis");
    _t("All visibles (decr.)","Todos vis\303\255veis (decr.)");
    _t("All invisibles (decr.)","Todos invis\303\255veis (decr.)");
    _t("All (decr.)","Todos (decr.)");
    _t("Output mode...","Modo de saida...");
    _t("In place (default)","No lugar (Padr\303\243o)");
    _t("New layer(s)","Nova(s) camada(s)");
    _t("New active layer(s)","Nova(s) camadas(s) ativa");
    _t("New image","Nova imagem");
    _t("Output preview...","Pr\303\251 Visualiza\303\247\303\243o");
    _t("1st output (default)","Primeira pr\303\251via (Padr\303\243o)");
    _t("2nd output","2 pr\303\251via imagem");
    _t("3rd output","3 pr\303\251via imagem");
    _t("4th output","4 pr\303\251via imagem");
    _t("1st -> 2nd","1st -> 2nd");
    _t("1st -> 3rd","1st -> 3rd");
    _t("1st -> 4th","1st -> 4th");
    _t("All outputs","Todas as imagens");
    _t("Output messages...","Mensagens de saida...");
    _t("Quiet (default)","Quieto (Padr\303\243o)");
    _t("Verbose (console)","Mode verbose (console)");
    _t("Verbose (logfile)","Mode verbose (arquivo)");
    _t("Very verbose (console)","Modo verbose ampliada (console)");
    _t("Very verbose (logfile)","Modo verbose ampliada (arquivo)");
    _t("Debug mode (console)","Modo Debug (console)");
    _t("Debug mode (logfile)","Modo Debug (arquivo)");
    _t(" Available filters (%u) :"," Filtros dispon\303\255veis (%u) :");
    _t("_Maximize","_Maximizar");
    _t("_Restore","Restaurar");
    _t("Update","Atualizar");
    _t("Rename","Renomear");
  }

  // German translation
  if (!std::strcmp(get_locale(),"de")) {
    if (!s) {
      static const char *const ns = "<b>Kein Internet-Update m\303\266glich !</b>\n\n"
        "Bitte \303\274berpr\303\274fen Sie Ihre Verbindung. Sie k\303\266nnen\n"
        "ein manuelles Update vornehmen durch Herunterladen der Datei :\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "Danach kopieren Sie die Datei <i>.%s</i>\n"
        "in Ihren Anwendungsdaten-Order.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC f\303\274r GIMP");
    _t("<i>Select a filter...</i>","<i>W\303\244hlen Sie einen Filter...</i>");
    _t("<i>No parameters to set...</i>","<i>Keine w\303\244hlbaren Parameter...</i>");
    _t("<b> Input / Output : </b>","<b> Eingabe / Ausgabe : </b>");
    _t("Input layers...","Eingabeebenen...");
    _t("None","Keine");
    _t("Active (default)","Aktive (Standard)");
    _t("All","Alle");
    _t("Active & below","Aktive & darunterliegende");
    _t("Active & above","Aktive & dar\303\274berliegende");
    _t("All visibles","Alle sichtbaren");
    _t("All invisibles","Alle nicht sichtbaren");
    _t("All visibles (decr.)","Alle sichtbaren (absteigend)");
    _t("All invisibles (decr.)","Alle nicht sichtbaren (absteigend)");
    _t("All (decr.)","Alle (absteigend)");
    _t("Output mode...","Ausgabemodus...");
    _t("In place (default)","Bestehende ersetzen (standard)");
    _t("New layer(s)","Neue Ebene(n)");
    _t("New active layer(s)","Neue aktive Ebene(n)");
    _t("New image","Neues Bild");
    _t("Output preview...","Ausgabevorschau...");
    _t("1st output (default)","1. Ausgabe (Standard)");
    _t("2nd output","2. Ausgabe");
    _t("3rd output","3. Ausgabe");
    _t("4th output","4. Ausgabe");
    _t("1st -> 2nd","1. -> 2.");
    _t("1st -> 3rd","1. -> 3.");
    _t("1st -> 4th","1. -> 4.");
    _t("All outputs","Alle Ausgaben");
    _t("Output messages...","Ausgabemeldungen...");
    _t("Quiet (default)","Keine Meldung (Standard)");
    _t("Verbose (console)","Ausf\303\274hrlich (Konsole)");
    _t("Verbose (logfile)","Ausf\303\274hrlich (Logfile)");
    _t("Very verbose (console)","Sehr ausf\303\274hrlich (Konsole)");
    _t("Very verbose (logfile)","Sehr ausf\303\274hrlich (Logfile)");
    _t("Debug mode (console)","Debug-Modus (Konsole)");
    _t("Debug mode (logfile)","Debug-Modus (Logfile)");
    _t(" Available filters (%u) :"," Verf\303\274gbare Filter (%u) :");
    _t("Rename","Umbenennen");
  }

  // Dutch translation
  if (!std::strcmp(get_locale(),"nl")) {
    if (!s) {
      static const char *const ns = "<b>Geen internet-update mogelijk !</b>\n\n"
        "Controleer aub je verbinding. Je kunt handmatig bijwerken door het downloaden van het "
        "volgende bestand :\n\n"
        "<u><small>%s%s</small></u>\n\n"
        "Vervolgens kopieer je het bestand naar folder met gebruikersinformatie.";
      return ns;
    }
    _t("G'MIC for GIMP","G'MIC voor GIMP");
    _t("<i>Select a filter...</i>","<i>Kies een filter...</i>");
    _t("<i>No parameters to set...</i>","<i>Geen parameters nodig...</i>");
    _t("<b> Input / Output : </b>","<b> Input / Output : </b>");
    _t("Input layers...","Input lagen...");
    _t("None","Geen");
    _t("Active (default)","Actieve laag (standaard)");
    _t("All","Alle");
    _t("Active & below","Actieve & onderliggende");
    _t("Active & above","Actieve & bovenliggende");
    _t("All visibles","Alle zichtbare");
    _t("All invisibles","Alle niet zichtbare");
    _t("All visibles (decr.)","Alle zichtbare (afnemend)");
    _t("All invisibles (decr.)","Alle niet zichtbare (afnemend)");
    _t("All (decr.)","Alle (afnemend)");
    _t("Output mode...","Output mode...");
    _t("In place (default)","Vervang bestaande (standaard)");
    _t("New layer(s)","Nieuwe laag/lagen");
    _t("New active layer(s)","Nieuwe actieve laag/lagen");
    _t("New image","Nieuwe afbeelding");
    _t("Output preview...","Output voorbeeld...");
    _t("1st output (default)","1e Resultaat (standaard)");
    _t("2nd output","2e Resultaat");
    _t("3rd output","3e Resultaat");
    _t("4th output","4e Resultaat");
    _t("1st -> 2nd","1e -> 2e");
    _t("1st -> 3rd","1e -> 3e");
    _t("1st -> 4th","1e -> 4e");
    _t("All outputs","Alle resultaten");
    _t("Output messages...","Output berichten...");
    _t("Quiet (default)","Geen melding (standaard)");
    _t("Verbose (console)","Uitgebreid (console)");
    _t("Verbose (logfile)","Uitgebreid (logfile)");
    _t("Very verbose (console)","Heel uitgebreid (console)");
    _t("Very verbose (logfile)","Heel uitgebreid (logfile)");
    _t("Debug mode (console)","Debug mode (console)");
    _t("Debug mode (logfile)","Debug mode (logfile)");
    _t(" Available filters (%u) :"," Beschikbare filters (%u) :");
    _t("_Maximize","_Maximaliseren");
    _t("_Restore","_Vermindering");
    _t("Rename","Hernoemen");
  }

  // English translation (default)
  if (!s) {
    static const char *const ns = "<b>Filters update from Internet failed !</b>\n\n"
      "Please check your Internet connection. You can\n"
      "manually update your filters by downloading :\n\n"
      "<u><small>%s%s</small></u>\n\n"
      "and copy it as the file <i>.%s</i>\n"
      "in your <i>Home</i> or <i>Application Data</i> folder.";
    return ns;
  }
  return s;
}

// Flush filter tree view
//------------------------
void flush_tree_view(GtkWidget *const tree_view) {
  const unsigned int filter = get_current_filter();
  bool tree_mode = get_tree_mode();
  unsigned int current_dir = 0;
  char current_path[64] = { 0 };
  gimp_get_data("gmic_current_treepath",&current_path);
  if (tree_mode) { // Expand
    cimglist_for(gmic_1stlevel_entries,l) {
      GtkTreePath *path = gtk_tree_path_new_from_string(gmic_1stlevel_entries[l].data());
      gtk_tree_view_expand_row(GTK_TREE_VIEW(tree_view),path,false);
      gtk_tree_path_free(path);
    }
  } else { // Collapse
    if (filter && *current_path && std::sscanf(current_path,"%u",&current_dir)==1) {
      cimglist_for(gmic_1stlevel_entries,l) {
        const char *const s_path = gmic_1stlevel_entries[l].data();
        unsigned int dir = 0;
        if (std::sscanf(s_path,"%u",&dir)!=1 || dir!=current_dir) {
          GtkTreePath *path = gtk_tree_path_new_from_string(gmic_1stlevel_entries[l].data());
          gtk_tree_view_collapse_row(GTK_TREE_VIEW(tree_view),path);
          gtk_tree_path_free(path);
        }
      }
    } else gtk_tree_view_collapse_all(GTK_TREE_VIEW(tree_view));
  }
  if (filter && *current_path) {
    GtkTreePath *path = gtk_tree_path_new_from_string(current_path);
    gtk_tree_view_expand_to_path(GTK_TREE_VIEW(tree_view),path);
    gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree_view),path,NULL,FALSE,0,0);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
    gtk_tree_selection_select_path(selection,path);
    gtk_tree_path_free(path);
  }
  if (indice_faves<gmic_entries.size()) { // Always shows 'Faves' folder when available.
    GtkTreePath *path = gtk_tree_path_new_from_string(gmic_1stlevel_entries.back().data());
    gtk_tree_view_expand_row(GTK_TREE_VIEW(tree_view),path,false);
    gtk_tree_path_free(path);
  }
  if (tree_mode_stockbutton) gtk_widget_destroy(tree_mode_stockbutton);
  tree_mode_stockbutton = gtk_button_new_from_stock(tree_mode?GTK_STOCK_ZOOM_OUT:GTK_STOCK_ZOOM_IN);
  GtkWidget *tree_image = gtk_button_get_image(GTK_BUTTON(tree_mode_stockbutton));
  gtk_button_set_image(GTK_BUTTON(tree_mode_button),tree_image);
  gtk_widget_show(tree_mode_button);

  gtk_tree_view_remove_column(GTK_TREE_VIEW(tree_view),gtk_tree_view_get_column(GTK_TREE_VIEW(tree_view),0));
  GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
  char tree_view_title[64] = { 0 };
  cimg_snprintf(tree_view_title,sizeof(tree_view_title),t(" Available filters (%u) :"),gmic_entries.size()-1);
  GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(tree_view_title,renderer,"markup",1,NULL);

  gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view),column);
}

// Retrieve files and update filter tree structure.
//-------------------------------------------------
bool update_filters(const bool try_net_update) {

  // Free existing definitions.
  if (tree_view_store) g_object_unref(tree_view_store);
  gmic_additional_commands.assign();
  gmic_1stlevel_entries.assign();
  gmic_faves.assign();
  gmic_entries.assign(1);
  gmic_commands.assign(1);
  gmic_preview_commands.assign(1);
  gmic_preview_factors.assign(1);
  gmic_arguments.assign(1);

  // Get filter definitions from the web server, and copy them in local file 'gmic_def.xxxx'.
  const char *const path_home = getenv(cimg_OS!=2?"HOME":"APPDATA");
  bool is_net_update_successful = false;
  if (try_net_update) {
    char update_command[512] = { 0 }, src_filename[1024] = { 0 }, dest_filename[1024] = { 0 };
    const char *const path_tmp = cimg::temporary_path();
    cimg_snprintf(src_filename,sizeof(src_filename),"%s/%s",path_tmp,gmic_update_file);
    cimg_snprintf(dest_filename,sizeof(dest_filename),"%s/.%s",path_home,gmic_update_file);
    std::remove(src_filename);

    if (get_verbosity_mode()) { // Try with 'curl' first.
      cimg_snprintf(update_command,sizeof(update_command),_gmic_path "curl --compressed %s%s -o %s",
                    gmic_update_server,gmic_update_file,src_filename);
      std::fprintf(cimg::output(),
                   "\n[gmic_gimp]./update/ %s\n",
                   update_command);
      std::fflush(cimg::output());
    } else
      cimg_snprintf(update_command,sizeof(update_command),_gmic_path "curl --silent --compressed %s%s -o %s",
                    gmic_update_server,gmic_update_file,src_filename);
    int _status = cimg::system(update_command);
    std::FILE *file_s = std::fopen(src_filename,"rb");

    if (!file_s) { // Try with 'wget' if 'curl' failed.
      if (get_verbosity_mode()) {
        cimg_snprintf(update_command,sizeof(update_command),_gmic_path "wget %s%s -O %s",
                      gmic_update_server,gmic_update_file,src_filename);
        std::fprintf(cimg::output(),
                     "\n[gmic_gimp]./update/ %s\n",
                     update_command);
        std::fflush(cimg::output());
      } else
        cimg_snprintf(update_command,sizeof(update_command),_gmic_path "wget --quiet %s%s -O %s",
                      gmic_update_server,gmic_update_file,src_filename);
      _status = cimg::system(update_command);
      file_s = std::fopen(src_filename,"rb");
    }

    if (file_s) { // Check for '#@gmic' header.
      char sep = 0;
      if (std::fscanf(file_s,"#@gmi%c",&sep)!=1 || sep!='c') { // Perhaps compressed, so try 'gunzip' on it.
        std::fclose(file_s);
        cimg_snprintf(update_command,sizeof(update_command),"%s.gz",
                      src_filename);
        std::rename(src_filename,update_command);
        if (get_verbosity_mode()) {
          cimg_snprintf(update_command,sizeof(update_command),_gmic_path "gunzip %s.gz",
                        src_filename);
          std::fprintf(cimg::output(),
                       "\n[gmic_gimp]./update/ %s\n",
                       update_command);
          std::fflush(cimg::output());
        } else
          cimg_snprintf(update_command,sizeof(update_command),_gmic_path "gunzip --quiet %s.gz",
                        src_filename);
        _status = cimg::system(update_command);
        file_s = std::fopen(src_filename,"rb");
        if (!file_s) {
          cimg_snprintf(update_command,sizeof(update_command),"%s.gz",
                        src_filename);
          std::remove(update_command);
        }
      } else std::rewind(file_s);
    }

    if (file_s) {
      unsigned int size_s = 0;
      std::fseek(file_s,0,SEEK_END);
      size_s = (unsigned int)std::ftell(file_s);
      std::rewind(file_s);
      if (size_s) {
        std::FILE *file_d = std::fopen(dest_filename,"wb");
        CImg<char> buffer(size_s);
        char sep = 0;
        if (file_d &&
            std::fread(buffer,sizeof(char),size_s,file_s)==size_s &&
            std::sscanf(buffer,"#@gmi%c",&sep)==1 && sep=='c' &&
            std::fwrite(buffer,sizeof(char),size_s,file_d)==size_s) {
          is_net_update_successful = true;
          std::fclose(file_d);
        }
      }
      std::fclose(file_s);
    }
  }

  // Get filter definitions from local files '.gmic' and '.gmic_def.xxxx'.
  char filename_gmic_def[1024] = { 0 }, filename_gmic[1024] = { 0 };
  unsigned size_gmic_def = 0, size_gmic = 0;
  cimg_snprintf(filename_gmic_def,sizeof(filename_gmic_def),"%s/.gmic_def.%d",
                path_home,gmic_version);
  cimg_snprintf(filename_gmic,sizeof(filename_gmic),"%s/.gmic",
                path_home);
  std::FILE
    *file_gmic_def = std::fopen(filename_gmic_def,"rb"),
    *file_gmic = std::fopen(filename_gmic,"rb");
  if (file_gmic_def) {
    std::fseek(file_gmic_def,0,SEEK_END);
    size_gmic_def = (unsigned int)std::ftell(file_gmic_def);
    std::rewind(file_gmic_def);
  }
  if (file_gmic) {
    std::fseek(file_gmic,0,SEEK_END);
    size_gmic = (unsigned int)std::ftell(file_gmic);
    std::rewind(file_gmic);
  }
  const unsigned int size_final = size_gmic_def + size_gmic + size_data_gmic_def + 3;
  gmic_additional_commands.assign(size_final);
  char *ptrd = gmic_additional_commands;
  if (size_gmic) { ptrd+=std::fread(ptrd,1,size_gmic,file_gmic); std::fclose(file_gmic); *(ptrd++) = '\n'; }
  if (size_gmic_def) { ptrd+=std::fread(ptrd,1,size_gmic_def,file_gmic_def); std::fclose(file_gmic_def); *(ptrd++) = '\n'; }
  if (size_data_gmic_def) { std::memcpy(ptrd,data_gmic_def,size_data_gmic_def-1); ptrd+=size_data_gmic_def-1; }
  *ptrd = 0;

  // Parse filters descriptions for GIMP, and create corresponding TreeView store.
  GtkTreeIter iter, parent[16];
  tree_view_store = gtk_tree_store_new(2,G_TYPE_UINT,G_TYPE_STRING);
  char
    line[256*1024] = { 0 }, preview_command[256] = { 0 }, arguments[4096] = { 0 }, entry[256] = { 0 },
    command[256] = { 0 }, locale[16] = { 0 };
  std::strcpy(locale,get_locale());
  int level = 0, err = 0;
  cimg_snprintf(line,sizeof(line),"#@gimp_%s ",locale);

  // Use English for default language if no translated filters found.
  if (!std::strstr(gmic_additional_commands,line)) { locale[0] = 'e'; locale[1] = 'n'; locale[2] = 0; }
  for (const char *data = gmic_additional_commands; *data; ) {
    char *_line = line;
    // Read new line.
    while (*data!='\n' && *data && _line<line+sizeof(line)) *(_line++) = *(data++); *_line = 0;
    while (*data=='\n') ++data; // Skip next '\n'.
    for (_line = line; *_line; ++_line) if (*_line<' ') *_line = ' '; // Replace non-usual characters by spaces.
    if (line[0]!='#' || line[1]!='@' || line[2]!='g' || // Check for a '#@gimp' line.
        line[3]!='i' || line[4]!='m' || line[5]!='p') continue;
    if (line[6]=='_') { // Check for a localized filter.
      // Weither the entry match current locale or not.
      if (line[7]==locale[0] && line[8]==locale[1] && line[9]==' ') _line = line + 10;
      else continue;
    } else if (line[6]==' ') _line = line + 7; else continue; // Check for a non-localized filter.

    if (*_line!=':') { // Check for a description of a possible filter or menu folder.
      *entry = *command = *preview_command = *arguments = 0;
      err = std::sscanf(_line," %4095[^:]: %4095[^,]%*c %4095[^,]%*c %4095[^\n]",
                        entry,command,preview_command,arguments);
      if (err==1) { // If entry defines a menu folder.
        cimg::strpare(entry,' ',false,true);
        char *nentry = entry; while (*nentry=='_') { ++nentry; --level; }
        if (level<0) level = 0; else if (level>15) level = 15;
        cimg::strpare(nentry,' ',false,true); cimg::strpare(nentry,'\"',true);
        if (*nentry) {
          gtk_tree_store_append(tree_view_store,&parent[level],level?&parent[level-1]:0);
          gtk_tree_store_set(tree_view_store,&parent[level],0,0,1,nentry,-1);
          if (!level) {
            const char *treepath = gtk_tree_model_get_string_from_iter(GTK_TREE_MODEL(tree_view_store),
                                                                       &parent[level]);
            CImg<char>::string(treepath).move_to(gmic_1stlevel_entries);
          }
          ++level;
        }
      } else if (err>=2) { // If entry defines a regular filter.
        cimg::strpare(entry,' ',false,true);
        char *nentry = entry; while (*nentry=='_') { ++nentry; --level; }
        if (level<0) level = 0; else if (level>15) level = 15;
        cimg::strpare(nentry,' ',false,true); cimg::strpare(nentry,'\"',true);
        cimg::strpare(command,' ',false,true);
        cimg::strpare(arguments,' ',false,true);
        if (*nentry) {
          CImg<char>::string(nentry).move_to(gmic_entries);
          CImg<char>::string(command).move_to(gmic_commands);
          CImg<char>::string(arguments).move_to(gmic_arguments);
          if (err>=3) { // Filter has a specified preview command.
            cimg::strpare(preview_command,' ',false,true);
            char *const preview_mode = std::strchr(preview_command,'(');
            double factor = 1;
            char sep = 0;
            if (preview_mode && std::sscanf(preview_mode+1,"%lf%c",&factor,&sep)==2 && factor>=0 && sep==')')
              *preview_mode = 0;
            else factor = -1;
            CImg<char>::string(preview_command).move_to(gmic_preview_commands);
            CImg<double>::vector(factor).move_to(gmic_preview_factors);
          } else { gmic_preview_commands.insert(1); gmic_preview_factors.insert(1); }
          gtk_tree_store_append(tree_view_store,&iter,level?&parent[level-1]:0);
          gtk_tree_store_set(tree_view_store,&iter,0,gmic_entries.size()-1,1,nentry,-1);
        }
      }
    } else { // Line is the continuation of an entry.
      if (gmic_arguments) {
        if (gmic_arguments.back()) gmic_arguments.back().back() = ' ';
        cimg::strpare(++_line,' ',false,true);
        gmic_arguments.back().append(CImg<char>(_line,std::strlen(_line)+1,1,1,1,true),'x');
      }
    }
  }

  // Load faves file.
  char filename_gmic_faves[1024] = { 0 }, label[256] = { 0 };
  cimg_snprintf(filename_gmic_faves,sizeof(filename_gmic_faves),"%s/.gmic_faves",
                path_home);
  std::FILE *file_gmic_faves = std::fopen(filename_gmic_faves,"rb");
  indice_faves = gmic_entries.size();
  if (file_gmic_faves) {
    gtk_tree_store_prepend(tree_view_store,&parent[0],0);
    gtk_tree_store_set(tree_view_store,&parent[0],0,0,1,"<b>Faves</b>",-1);
    const char *treepath = gtk_tree_model_get_string_from_iter(GTK_TREE_MODEL(tree_view_store),&parent[0]);
    CImg<char>::string(treepath).move_to(gmic_1stlevel_entries);
    for (unsigned int line_nb = 1; std::fscanf(file_gmic_faves,"%[^\n]",line)==1; ++line_nb) {
      std::fgetc(file_gmic_faves);
      char sep = 0;
      if (std::sscanf(line,"{%255[^}]}{%255[^}]}{%255[^}]}{%255[^}]%c",
                      label,entry,command,preview_command,&sep)==5 && sep=='}') {
        const char *_line = line + 8 + std::strlen(label) + std::strlen(entry) + std::strlen(command) + std::strlen(preview_command);
        unsigned int filter = 0;
        for (filter = 1; filter<gmic_entries.size() &&
               (std::strcmp(gmic_entries[filter].data(),entry) ||
                std::strcmp(gmic_commands[filter].data(),command) ||
                std::strcmp(gmic_preview_commands[filter].data(),preview_command)); ++filter) {}

        if (filter==gmic_entries.size())
          std::fprintf(cimg::output(),
                       "\n[gmic_gimp]./warning/ Skipping reference to unknown filter '%s : %s, %s' in fave file '%s'.\n",
                       entry,command,preview_command,filename_gmic_faves);
        else {
          CImg<char>::string(line).move_to(gmic_faves);
          for (char *p = std::strchr(label,_rbrace); p; p = std::strchr(p,_rbrace)) *p = '}';  // Get back '}' if necessary.
          for (char *p = std::strchr(entry,_rbrace); p; p = std::strchr(p,_rbrace)) *p = '}';
          CImg<char>::string(label).move_to(gmic_entries);
          gmic_commands.insert(gmic_commands[filter]);
          gmic_preview_commands.insert(gmic_preview_commands[filter]);
          gmic_arguments.insert(gmic_arguments[filter]);
          gmic_preview_factors.insert(gmic_preview_factors[filter]);
          unsigned int nbp = 0;
          for (nbp = 0; std::sscanf(_line,"{%4095[^}]%c",arguments,&sep)==2 && sep=='}'; ++nbp) {
            for (char *p = std::strchr(arguments,_rbrace); p; p = std::strchr(p,_rbrace)) *p = '}';  // Get back '}' if necessary.
            set_fave_parameter(gmic_entries.size()-1,nbp,arguments);
            _line+=2 + std::strlen(arguments);
          }
          set_filter_nbparams(gmic_entries.size()-1,nbp);
          gtk_tree_store_append(tree_view_store,&iter,&parent[0]);
          gtk_tree_store_set(tree_view_store,&iter,0,gmic_entries.size()-1,1,label,-1);
        }
      } else if (get_verbosity_mode())
        std::fprintf(cimg::output(),
                     "\n[gmic_gimp]./error/ Malformed line %u in fave file '%s' : '%s'.\n",
                     line_nb,filename_gmic_faves,line);
    }
    std::fclose(file_gmic_faves);
  }
  return try_net_update?is_net_update_successful:true;
}

// 'Convert' a CImg<float> image to a RGB[A] CImg<unsigned char> image, withing the same buffer.
//----------------------------------------------------------------------------------------------
void convert_image_float2uchar(CImg<float>& img) {
  const unsigned int siz = img.width()*img.height();
  unsigned char *ptrd = (unsigned char*)img.data();
  switch (img.spectrum()) {
  case 1 : {
    const float *ptr0 = img.data(0,0,0,0);
    for (unsigned int i = 0; i<siz; ++i) *(ptrd++) = (unsigned char)*(ptr0++);
  } break;
  case 2 : {
    const float *ptr0 = img.data(0,0,0,0), *ptr1 = img.data(0,0,0,1);
    for (unsigned int i = 0; i<siz; ++i) {
      *(ptrd++) = (unsigned char)*(ptr0++);
      *(ptrd++) = (unsigned char)*(ptr1++);
    }
  } break;
  case 3 : {
    const float *ptr0 = img.data(0,0,0,0), *ptr1 = img.data(0,0,0,1), *ptr2 = img.data(0,0,0,2);
    for (unsigned int i = 0; i<siz; ++i) {
      *(ptrd++) = (unsigned char)*(ptr0++);
      *(ptrd++) = (unsigned char)*(ptr1++);
      *(ptrd++) = (unsigned char)*(ptr2++);
    }
  } break;
  case 4 : {
    const float *ptr0 = img.data(0,0,0,0), *ptr1 = img.data(0,0,0,1),
      *ptr2 = img.data(0,0,0,2), *ptr3 = img.data(0,0,0,3);
    for (unsigned int i = 0; i<siz; ++i) {
      *(ptrd++) = (unsigned char)*(ptr0++);
      *(ptrd++) = (unsigned char)*(ptr1++);
      *(ptrd++) = (unsigned char)*(ptr2++);
      *(ptrd++) = (unsigned char)*(ptr3++);
    }
  } break;
  default: return;
  }
}

// Calibrate any image to fit the number of required channels (GRAY,GRAYA, RGB or RGBA).
//---------------------------------------------------------------------------------------
void calibrate_image(CImg<float>& img, const unsigned int channels, const bool preview) {
  if (!img || !channels) return;
  switch (channels) {

  case 1 : // To GRAY
    switch (img.spectrum()) {
    case 1 : // from GRAY
      break;
    case 2 : // from GRAYA
      if (preview) {
        float *ptr_r = img.data(0,0,0,0), *ptr_a = img.data(0,0,0,1);
        cimg_forXY(img,x,y) {
          const unsigned int a = (unsigned int)*(ptr_a++), i = 96 + (((x^y)&8)<<3);
          *ptr_r = (float)((a*(unsigned int)*ptr_r + (255-a)*i)>>8);
          ++ptr_r;
        }
      }
      img.channel(0);
      break;
    case 3 : // from RGB
      img.RGBtoYCbCr().channel(0);
      break;
    case 4 : // from RGBA
      img.get_shared_channels(0,2).RGBtoYCbCr();
      if (preview) {
        float *ptr_r = img.data(0,0,0,0), *ptr_a = img.data(0,0,0,3);
        cimg_forXY(img,x,y) {
          const unsigned int a = (unsigned int)*(ptr_a++), i = 96 + (((x^y)&8)<<3);
          *ptr_r = (float)((a*(unsigned int)*ptr_r + (255-a)*i)>>8);
          ++ptr_r;
        }
      }
      img.channel(0);
      break;
    default : // from multi-channel (>4)
      img.channel(0);
    } break;

  case 2: // To GRAYA
    switch (img.spectrum()) {
    case 1: // from GRAY
      img.resize(-100,-100,1,2,0).get_shared_channel(1).fill(255);
      break;
    case 2: // from GRAYA
      break;
    case 3: // from RGB
      img.RGBtoYCbCr().channels(0,1).get_shared_channel(1).fill(255);
      break;
    case 4: // from RGBA
      img.get_shared_channels(0,2).RGBtoYCbCr();
      img.get_shared_channel(1) = img.get_shared_channel(3);
      img.channels(0,1);
      break;
    default: // from multi-channel (>4)
      img.channels(0,1).get_shared_channel(1).fill(255);
    } break;

  case 3: // to RGB
    switch (img.spectrum()) {
    case 1: // from GRAY
      img.resize(-100,-100,1,3);
      break;
    case 2: // from GRAYA
      if (preview) {
        float *ptr_r = img.data(0,0,0,0), *ptr_a = img.data(0,0,0,1);
        cimg_forXY(img,x,y) {
          const unsigned int a = (unsigned int)*(ptr_a++), i = 96 + (((x^y)&8)<<3);
          *ptr_r = (float)((a*(unsigned int)*ptr_r + (255-a)*i)>>8);
          ++ptr_r;
        }
      }
      img.channel(0).resize(-100,-100,1,3);
      break;
    case 3: // from RGB
      break;
    case 4: // from RGBA
      if (preview) {
        float *ptr_r = img.data(0,0,0,0), *ptr_g = img.data(0,0,0,1),
          *ptr_b = img.data(0,0,0,2), *ptr_a = img.data(0,0,0,3);
        cimg_forXY(img,x,y) {
          const unsigned int a = (unsigned int)*(ptr_a++), i = 96 + (((x^y)&8)<<3);
          *ptr_r = (float)((a*(unsigned int)*ptr_r + (255-a)*i)>>8);
          *ptr_g = (float)((a*(unsigned int)*ptr_g + (255-a)*i)>>8);
          *ptr_b = (float)((a*(unsigned int)*ptr_b + (255-a)*i)>>8);
          ++ptr_r; ++ptr_g; ++ptr_b;
        }
      }
      img.channels(0,2);
      break;
    default: // from multi-channel (>4)
      img.channels(0,2);
    } break;

  case 4: // to RGBA
    switch (img.spectrum()) {
    case 1: // from GRAY
      img.resize(-100,-100,1,4).get_shared_channel(3).fill(255);
      break;
    case 2: // from GRAYA
      img.resize(-100,-100,1,4,0);
      img.get_shared_channel(3) = img.get_shared_channel(1);
      img.get_shared_channel(1) = img.get_shared_channel(0);
      img.get_shared_channel(2) = img.get_shared_channel(0);
      break;
    case 3: // from RGB
      img.resize(-100,-100,1,4,0).get_shared_channel(3).fill(255);
      break;
    case 4: // from RGBA
      break;
    default: // from multi-channel (>4)
      img.resize(-100,-100,1,4,0);
    } break;
  }
}

// Get the input layers of a GIMP image as a list of CImg<float>.
//---------------------------------------------------------------
template<typename T>
CImg<int> get_input_layers(CImgList<T>& images) {

  // Retrieve the list of desired layers.
  int
    nb_layers = 0,
    *layers = gimp_image_get_layers(image_id,&nb_layers),
    active_layer = gimp_image_get_active_layer(image_id);
  CImg<int> input_layers;
  const unsigned int input_mode = get_input_mode();
  switch (input_mode) {
  case 0 : // Input none
    break;
  case 1 : // Input active layer
    input_layers = CImg<int>::vector(active_layer);
    break;
  case 2 : case 9 : // Input all image layers
    input_layers = CImg<int>(layers,1,nb_layers);
    if (input_mode==9) input_layers.mirror('y');
    break;
  case 3 : { // Input active & below layers
    int i = 0; for (i = 0; i<nb_layers; ++i) if (layers[i]==active_layer) break;
    if (i<nb_layers-1) input_layers = CImg<int>::vector(active_layer,layers[i+1]);
    else input_layers = CImg<int>::vector(active_layer);
  } break;
  case 4 : { // Input active & above layers
    int i = 0; for (i = 0; i<nb_layers; ++i) if (layers[i]==active_layer) break;
    if (i>0) input_layers = CImg<int>::vector(active_layer,layers[i-1]);
    else input_layers = CImg<int>::vector(active_layer);
  } break;
  case 5 : case 7 : { // Input all visible image layers
    CImgList<int> visible_layers;
    for (int i = 0; i<nb_layers; ++i)
      if (gimp_drawable_get_visible(layers[i])) CImg<int>::vector(layers[i]).move_to(visible_layers);
    input_layers = visible_layers>'y';
    if (input_mode==7) input_layers.mirror('y');
  } break;
  default : { // Input all invisible image layers
    CImgList<int> invisible_layers;
    for (int i = 0; i<nb_layers; ++i)
      if (!gimp_drawable_get_visible(layers[i])) CImg<int>::vector(layers[i]).move_to(invisible_layers);
    input_layers = invisible_layers>'y';
    if (input_mode==8) input_layers.mirror('y');
  } break;
  }

  // Read input image data into a CImgList<float>.
  images.assign(input_layers.height());
  GimpPixelRgn region;
  gint x1, y1, x2, y2;
  cimglist_for(images,l) {
    GimpDrawable *drawable = gimp_drawable_get(input_layers[l]);
    if (!gmic_drawable_is_valid(drawable)) continue;
    gimp_drawable_mask_bounds(drawable->drawable_id,&x1,&y1,&x2,&y2);
    const int channels = drawable->bpp;
    gimp_pixel_rgn_init(&region,drawable,x1,y1,x2-x1,y2-y1,false,false);
    guchar *const row = g_new(guchar,(x2-x1)*channels), *ptrs = 0;
    CImg<T> img(x2-x1,y2-y1,1,channels);
    switch (channels) {
    case 1 : {
      T *ptr_r = img.data(0,0,0,0);
      cimg_forY(img,y) {
        gimp_pixel_rgn_get_row(&region,ptrs=row,x1,y1+y,img.width());
        cimg_forX(img,x) *(ptr_r++) = (T)*(ptrs++);
      }
    } break;
    case 2 : {
      T *ptr_r = img.data(0,0,0,0), *ptr_g = img.data(0,0,0,1);
      cimg_forY(img,y) {
        gimp_pixel_rgn_get_row(&region,ptrs=row,x1,y1+y,img.width());
        cimg_forX(img,x) {
          *(ptr_r++) = (T)*(ptrs++);
          *(ptr_g++) = (T)*(ptrs++);
        }
      }
    } break;
    case 3 : {
      T *ptr_r = img.data(0,0,0,0), *ptr_g = img.data(0,0,0,1), *ptr_b = img.data(0,0,0,2);
      cimg_forY(img,y) {
        gimp_pixel_rgn_get_row(&region,ptrs=row,x1,y1+y,img.width());
        cimg_forX(img,x) {
          *(ptr_r++) = (T)*(ptrs++);
          *(ptr_g++) = (T)*(ptrs++);
          *(ptr_b++) = (T)*(ptrs++);
        }
      }
    } break;
    case 4 : {
      T *ptr_r = img.data(0,0,0,0), *ptr_g = img.data(0,0,0,1),
        *ptr_b = img.data(0,0,0,2), *ptr_a = img.data(0,0,0,3);
      cimg_forY(img,y) {
        gimp_pixel_rgn_get_row(&region,ptrs=row,x1,y1+y,img.width());
        cimg_forX(img,x) {
          *(ptr_r++) = (T)*(ptrs++);
          *(ptr_g++) = (T)*(ptrs++);
          *(ptr_b++) = (T)*(ptrs++);
          *(ptr_a++) = (T)*(ptrs++);
        }
      }
    } break;
    }
    g_free(row);
    gimp_drawable_detach(drawable);
    img.move_to(images[l]);
  }
  return input_layers;
}

// Return the G'MIC command line needed to run the selected filter.
//-----------------------------------------------------------------
const char* get_commands_line(const bool preview) {
  const unsigned int
    filter = get_current_filter(),
    nbparams = get_filter_nbparams(filter);
  if (!filter) return 0;
  static CImg<char> res;
  CImgList<char> lres;
  switch (get_verbosity_mode()) {
  case 0: case 1: case 2: CImg<char>("-v -99 -",8).move_to(lres); break;  // Quiet or Verbose.
  case 3: case 4 : CImg<char>("-",1).move_to(lres); break;                // Very verbose.
  default: CImg<char>("-debug -",8).move_to(lres);                        // Debug.
  }
  const CImg<char> &command_item = (preview?gmic_preview_commands[filter]:gmic_commands[filter]);
  if (command_item) {
    lres.insert(command_item);
    if (nbparams) {
      lres[1].back() = ' ';
      for (unsigned int p = 0; p<nbparams; ++p) {
        const char *ss = get_filter_parameter(filter,p);
        char nparam[1024] = { 0 }, *sd = nparam;
        const unsigned int l = std::strlen(ss);
        if (l>=2 && ss[0]=='\"' && ss[l-1]=='\"') { // Replace special characters in a string or a filename.
          ++ss; *(sd++) = '\"';
          for (unsigned int i = 1; i<l-1; ++i, ++ss) { const char c = *ss; *(sd++) = c=='\"'?_dquote:c; }
          *(sd++) = '\"'; *(sd++) = 0;
          CImg<char>(nparam,l+1).move_to(lres);
        } else CImg<char>(ss,l+1).move_to(lres);
        lres.back().back() = ',';
      }
    }
    (res=lres>'x').back() = 0;
  }
  return res.data();
}

// Set defaut zoom factor for preview of the current filter.
//----------------------------------------------------------
void set_preview_factor() {
  const unsigned int filter = get_current_filter();
  if (filter && gmic_preview_factors[filter] && GIMP_IS_PREVIEW(gui_preview)) {
    double factor = gmic_preview_factors(filter,0);
    if (factor>=0) {
      if (!factor) { // Compute factor so that 1:1 preview of the image is displayed.
        int _pw = 0, _ph = 0; gimp_preview_get_size(GIMP_PREVIEW(gui_preview),&_pw,&_ph);
        const float pw = (float)_pw, ph = (float)_ph,
          dw = (float)drawable_preview->width, dh = (float)drawable_preview->height;
        factor = std::sqrt((dw*dw+dh*dh)/(pw*pw+ph*ph));
      }
      gimp_zoom_model_zoom(gimp_zoom_preview_get_model(GIMP_ZOOM_PREVIEW(gui_preview)),GIMP_ZOOM_TO,factor);
    }
  }
}

// Handle GUI event functions.
//----------------------------
void create_parameters_gui(const bool);
void process_image(const char *const);
void process_preview();

// Secure function for invalidate preview.
void _gimp_preview_invalidate() {
  computed_preview.assign();
  if (GIMP_IS_PREVIEW(gui_preview) && gmic_drawable_is_valid(drawable_preview))
    gimp_preview_invalidate(GIMP_PREVIEW(gui_preview));
  else {
    if (GTK_IS_WIDGET(gui_preview)) gtk_widget_destroy(gui_preview);
    drawable_preview = gimp_drawable_get(gimp_image_get_active_drawable(image_id));
    gui_preview = gimp_zoom_preview_new(drawable_preview);
    gtk_widget_show(gui_preview);
    gtk_box_pack_end(GTK_BOX(left_pane),gui_preview,true,true,0);
    g_signal_connect(gui_preview,"invalidated",G_CALLBACK(process_preview),0);
  }
}

// Handle preview resize event.
void on_dialog_resized() {
  static int opw = 0, oph = 0;
  int pw = 0, ph = 0;
  if (GIMP_IS_PREVIEW(gui_preview)) {
    gimp_preview_get_size(GIMP_PREVIEW(gui_preview),&pw,&ph);
    if (!opw || !oph) { opw = pw; oph = ph; computed_preview.assign(); } else {
      if (pw!=opw || ph!=oph) { set_preview_factor(); opw = pw; oph = ph; computed_preview.assign(); }
    }
  }
}

// Handle widgets events related to parameter changes.
void on_float_parameter_changed(GtkAdjustment *const adjustment, const void *const event_infos) {
  double value = 0;
  gimp_double_adjustment_update(adjustment,&value);
  char s_value[32] = { 0 };
  cimg_snprintf(s_value,sizeof(s_value),"%g",value);
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_value);
  _create_dialog_gui = true;
}

void on_int_parameter_changed(GtkAdjustment *const adjustment, const void *const event_infos) {
  int value = 0;
  gimp_int_adjustment_update(adjustment,&value);
  char s_value[32] = { 0 };
  cimg_snprintf(s_value,sizeof(s_value),"%d",value);
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_value);
  _create_dialog_gui = true;
}

void on_bool_parameter_changed(GtkCheckButton *const checkbutton, const void *const event_infos) {
  int value = 0;
  g_object_get(checkbutton,"active",&value,NULL);
  char s_value[4] = { 0 };
  cimg_snprintf(s_value,sizeof(s_value),"%d",value?1:0);
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_value);
  _create_dialog_gui = true;
}

void on_list_parameter_changed(GtkComboBox *const combobox, const void *const event_infos) {
  int value = 0;
  g_object_get(combobox,"active",&value,NULL);
  char s_value[32] = { 0 };
  cimg_snprintf(s_value,sizeof(s_value),"%d",value);
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_value);
  _create_dialog_gui = true;
}

void on_text_parameter_changed(const void *const event_infos) {
  GtkWidget *entry = *((GtkWidget**)event_infos+1);
  const char *const s_value = gtk_entry_get_text(GTK_ENTRY(entry));
  char s_param[1024] = { 0 };
  if (s_value && *s_value) cimg_snprintf(s_param,sizeof(s_param),"\"%s\"",s_value);
  else std::strcpy(s_param,"\"\"");
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_param);
  _create_dialog_gui = true;
}

void on_file_parameter_changed(GtkFileChooser *const file_chooser, const void *const event_infos) {
  const char *const s_value = gtk_file_chooser_get_filename(file_chooser);
  char s_param[1024] = { 0 };
  if (s_value && *s_value) cimg_snprintf(s_param,sizeof(s_param),"\"%s\"",s_value);
  else std::strcpy(s_param,"\"\"");
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_param);
  _create_dialog_gui = true;
}

void on_color_parameter_changed(GtkColorButton *const color_button, const void *const event_infos) {
  GdkColor color;
  gtk_color_button_get_color(color_button,&color);
  char s_value[256] = { 0 };
  if (gtk_color_button_get_use_alpha(color_button))
    cimg_snprintf(s_value,sizeof(s_value),"%d,%d,%d,%d",
                  color.red/257,color.green/257,color.blue/257,gtk_color_button_get_alpha(color_button)/257);
  else cimg_snprintf(s_value,sizeof(s_value),"%d,%d,%d",
                     color.red/257,color.green/257,color.blue/257);
  set_filter_parameter(get_current_filter(),*(int*)event_infos,s_value);
  _create_dialog_gui = true;
}

// Handle responses to the dialog window buttons.
void on_dialog_input_mode_changed(GtkComboBox *const combobox) {
  int value = 0;
  g_object_get(combobox,"active",&value,NULL);
  if (value<2) gtk_combo_box_set_active(combobox,value=3);
  set_input_mode((unsigned int)value);
  _gimp_preview_invalidate();
}

void on_dialog_output_mode_changed(GtkComboBox *const combobox) {
  int value = 0;
  g_object_get(combobox,"active",&value,NULL);
  if (value<2) gtk_combo_box_set_active(combobox,value=2);
  set_output_mode((unsigned int)value);
}

void on_dialog_verbosity_mode_changed(GtkComboBox *const combobox) {
  int value = 0;
  g_object_get(combobox,"active",&value,NULL);
  if (value<2) gtk_combo_box_set_active(combobox,value=2);
  set_verbosity_mode((unsigned int)value);
  set_logfile();
  _gimp_preview_invalidate();
}

void on_dialog_preview_mode_changed(GtkComboBox *const combobox) {
  int value = 0;
  g_object_get(combobox,"active",&value,NULL);
  if (value<2) gtk_combo_box_set_active(combobox,value=2);
  set_preview_mode((unsigned int)value);
  _gimp_preview_invalidate();
}

void on_dialog_maximize_button_clicked(GtkButton *const button) {
  static int ow = 0, oh = 0;
  GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(dialog_window));
  int
    width = gdk_screen_get_width(screen),
    height = gdk_screen_get_height(screen);
  if (width>0 && height>0 && !ow && !oh) {
    gtk_window_get_size(GTK_WINDOW(dialog_window),&ow,&oh);
#if cimg_OS==2
    // Subtract the height of the taskbar on windows.
    RECT rect;
    HWND taskBar = FindWindow("Shell_traywnd",NULL);
    if (taskBar && GetWindowRect(taskBar,&rect)) height-=rect.bottom - rect.top;
#endif
    if (height>0) gtk_window_resize(GTK_WINDOW(dialog_window),width,height);
    gtk_window_move(GTK_WINDOW(dialog_window),0,0);
    gtk_button_set_label(button,t("_Restore"));
  } else if (ow>0 && oh>0) {
    gtk_window_resize(GTK_WINDOW(dialog_window),ow,oh);
    ow = oh = 0;
    gtk_button_set_label(button,t("_Maximize"));
  }
  _gimp_preview_invalidate();
}

void on_dialog_reset_clicked() {
  create_parameters_gui(true);
  _create_dialog_gui = true;
  _gimp_preview_invalidate();
}

void on_dialog_cancel_clicked() {
  _create_dialog_gui = false;
  gtk_main_quit();
}

void on_dialog_apply_clicked() {
  process_image(0);
  _create_dialog_gui = false;
  _gimp_preview_invalidate();
}

void on_dialog_net_update_toggled(GtkToggleButton *const toggle_button) {
  set_net_update(gtk_toggle_button_get_active(toggle_button));
}

void on_dialog_tree_mode_clicked(GtkWidget *const tree_view) {
  set_tree_mode(!get_tree_mode());
  flush_tree_view(tree_view);
}

void on_dialog_fave_clicked(GtkWidget *const tree_view) {
  const unsigned int filter = get_current_filter();
  gtk_widget_hide(relabel_hbox);
  if (filter) {
    char filename[1024] = { 0 };
    cimg_snprintf(filename,sizeof(filename),"%s/.gmic_faves",
                  getenv(cimg_OS!=2?"HOME":"APPDATA"));
    std::FILE *file = std::fopen(filename,"wb");
    if (file) {
      if (filter<indice_faves) { // Add fave filter.
        char label[256] = { 0 };
        unsigned int ind = 0;
        std::strcpy(label,gmic_entries[filter].data());
        cimglist_for(gmic_faves,l) {
          std::fprintf(file,"%s\n",gmic_faves[l].data());
          if (!std::strcmp(label,gmic_entries[indice_faves+l].data()))
            std::sprintf(label,"%s (%u)",gmic_entries[filter].data(),++ind);
        }
        CImg<char> entry = gmic_entries[filter];
        for (char *p = std::strchr(label,'}'); p; p = std::strchr(p,'}')) *p = _rbrace;  // Convert '}' if necessary.
        for (char *p = std::strchr(entry,'}'); p; p = std::strchr(p,'}')) *p = _rbrace;
        std::fprintf(file,"{%s}{%s}{%s}{%s}",
                     label,
                     entry.data(),
                     gmic_commands[filter].data(),
                     gmic_preview_commands[filter].data());
        const unsigned int nbp = get_filter_nbparams(filter);
        for (unsigned int n = 0; n<nbp; ++n) {
          char *const param = get_filter_parameter(filter,n);
          set_filter_parameter(gmic_entries.size(),n,param);
          for (char *p = std::strchr(param,'}'); p; p = std::strchr(p,'}')) *p = _rbrace;
          std::fprintf(file,"{%s}",param);
        }
        std::fputc('\n',file);
        std::fclose(file);
      } else { // Remove fave filter.
        const unsigned int _filter = filter - indice_faves;
        if (gmic_faves.size()==1) { std::fclose(file); std::remove(filename); }
        else {
          cimglist_for(gmic_faves,l) if (l!=(int)_filter) std::fprintf(file,"%s\n",gmic_faves[l].data());
          std::fclose(file);
          for (unsigned int i = filter; i<gmic_entries.size()-1; ++i) { // Shift current parameters.
            const unsigned int nbp = get_filter_nbparams(i+1);
            for (unsigned int n = 0; n<nbp; ++n)
              set_filter_parameter(i,n,get_filter_parameter(i+1,n));
          }
        }
      }
      update_filters(false);
    } else if (get_verbosity_mode())
      std::fprintf(cimg::output(),
                   "\n[gmic_gimp]./error/ Unable to write fave file '%s'.\n",
                   filename);
  }
  gtk_tree_view_set_model(GTK_TREE_VIEW(tree_view),GTK_TREE_MODEL(tree_view_store));
  gimp_set_data("gmic_current_treepath","0",2);
  set_current_filter(0);
  flush_tree_view(tree_view);
  GtkTreePath *path = gtk_tree_path_new_from_string("0");
  gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree_view),path,NULL,FALSE,0,0);
  GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
  gtk_tree_selection_select_path(selection,path);
  gtk_tree_path_free(path);
  create_parameters_gui(false);
  _gimp_preview_invalidate();
}

void on_dialog_refresh_clicked(GtkWidget *const tree_view) {
  gtk_widget_hide(relabel_hbox);
  if (!update_filters(get_net_update())) {
    GtkWidget *const
      message = gtk_message_dialog_new_with_markup(0,GTK_DIALOG_MODAL,GTK_MESSAGE_ERROR,GTK_BUTTONS_OK,
                                                   t(0),gmic_update_server,gmic_update_file,gmic_update_file);
    gtk_widget_show(message);
    gtk_dialog_run(GTK_DIALOG(message));
    gtk_widget_destroy(message);
  } else reset_filters_parameters();
  gtk_tree_view_set_model(GTK_TREE_VIEW(tree_view),GTK_TREE_MODEL(tree_view_store));
  flush_tree_view(tree_view);
  create_parameters_gui(true);
  _gimp_preview_invalidate();
}

void on_filter_selected(GtkWidget *const tree_view) {
  GtkTreeSelection *const selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
  GtkTreeIter iter;
  GtkTreeModel *model;
  unsigned int filter = 0;
  if (gtk_tree_selection_get_selected(selection,&model,&iter)) {
    gtk_tree_model_get(model,&iter,0,&filter,-1);
    const char *const treepath = gtk_tree_model_get_string_from_iter(GTK_TREE_MODEL(tree_view_store),&iter);
    gimp_set_data("gmic_current_treepath",treepath,std::strlen(treepath)+1);
  }
  if (filter!=get_current_filter()) {
    gtk_widget_hide(relabel_hbox);
    set_current_filter(filter);
    create_parameters_gui(false);
    _create_dialog_gui = true;
    _gimp_preview_invalidate();
  }
}

void _on_filter_doubleclicked(GtkWidget *const entry) {
  const unsigned int filter = get_current_filter();
  cimg::unused(entry);
  gtk_widget_hide(relabel_hbox);
  if (filter>=indice_faves) {
    const char *const __label = gtk_entry_get_text(GTK_ENTRY(relabel_entry));
    char *const label = g_markup_escape_text(__label,std::strlen(__label));
    char filename[1024] = { 0 };
    cimg_snprintf(filename,sizeof(filename),"%s/.gmic_faves",
                  getenv(cimg_OS!=2?"HOME":"APPDATA"));
    std::FILE *file = std::fopen(filename,"wb");
    if (file) {
      const unsigned int _filter = filter - indice_faves;
      cimglist_for(gmic_faves,l) if (l!=(int)_filter) std::fprintf(file,"%s\n",gmic_faves[l].data());
      else {
        CImg<char> _label = CImg<char>::string(label);
        for (char *p = std::strchr(_label,'}'); p; p = std::strchr(p,'}')) *p = _rbrace; // Convert '}' if necessary.
        std::fprintf(file,"{%s%s\n",_label.data(),std::strchr(gmic_faves[l].data(),'}'));
      }
      std::fclose(file);
      update_filters(false);
    } else if (get_verbosity_mode())
      std::fprintf(cimg::output(),
                   "\n[gmic_gimp]./error/ Unable to write fave file '%s'.\n",
                   filename);
    gtk_tree_view_set_model(GTK_TREE_VIEW(tree_view),GTK_TREE_MODEL(tree_view_store));
    gimp_set_data("gmic_current_treepath","0",2);
    set_current_filter(0);
    flush_tree_view(tree_view);
    GtkTreePath *path = gtk_tree_path_new_from_string("0");
    gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree_view),path,NULL,FALSE,0,0);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
    gtk_tree_selection_select_path(selection,path);
    gtk_tree_path_free(path);
    create_parameters_gui(false);
    g_free(label);
  }
}

void on_filter_doubleclicked(GtkWidget *const tree_view) {
  const unsigned int filter = get_current_filter();
  if (!filter) { // Expand/collapse folder.
    char current_path[64] = { 0 };
    gimp_get_data("gmic_current_treepath",&current_path);
    if (*current_path) {
      GtkTreePath *path = gtk_tree_path_new_from_string(current_path);
      const bool is_expanded = gtk_tree_view_row_expanded(GTK_TREE_VIEW(tree_view),path);
      if (is_expanded) gtk_tree_view_collapse_row(GTK_TREE_VIEW(tree_view),path);
      else gtk_tree_view_expand_row(GTK_TREE_VIEW(tree_view),path,false);
      gtk_tree_path_free(path);
    }
  } else if (filter>=indice_faves) { // Rename fave filter.
    is_block_preview = true;
    GtkWidget *const markup2ascii = gtk_label_new(0);
    gtk_label_set_markup(GTK_LABEL(markup2ascii),gmic_entries[filter].data());
    gtk_entry_set_text(GTK_ENTRY(relabel_entry),gtk_label_get_text(GTK_LABEL(markup2ascii)));
    gtk_widget_destroy(markup2ascii);
    gtk_widget_show(relabel_hbox);
    gtk_widget_grab_focus(relabel_entry);
  } else on_dialog_fave_clicked(tree_view); // Add fave filter.
}

// Process image data with the G'MIC interpreter.
//-----------------------------------------------

// This structure stores the arguments required by the processing thread.
struct st_process_thread {
  CImgList<float> images;
  CImg<char> error_message;
  bool is_thread, is_preview;
  unsigned int verbosity_mode;
  const char *commands_line;
  float progress;
#if !defined(__MACOSX__)  && !defined(__APPLE__)
  pthread_mutex_t is_running;
  pthread_mutex_t wait_lock;
  pthread_cond_t wait_cond;
  pthread_t thread;
#endif
};

#if !defined(__MACOSX__)  && !defined(__APPLE__)
#endif

// Thread that runs the G'MIC interpreter.
void *process_thread(void *arg) {
  st_process_thread &spt = *(st_process_thread*)arg;
#if !defined(__MACOSX__)  && !defined(__APPLE__)
  if (spt.is_thread) {
    pthread_mutex_lock(&spt.is_running);
    pthread_mutex_lock(&spt.wait_lock);
    pthread_cond_signal(&spt.wait_cond);
    pthread_mutex_unlock(&spt.wait_lock);
  }
#endif
  try {
    if (spt.verbosity_mode) {
      CImg<char> cl = CImg<char>::string(spt.commands_line);
      gmic_strreplace(cl);
      std::fprintf(cimg::output(),
                   "\n[gmic_gimp]./%s/ %s\n",
                   spt.is_preview?"preview":"apply",
                   cl.data());
      std::fflush(cimg::output());
    }
    gmic(spt.commands_line,spt.images,gmic_additional_commands,true,&spt.progress);
  } catch (gmic_exception &e) {
    spt.images.assign();
    CImg<char>::string(e.what()).move_to(spt.error_message);
    if (spt.verbosity_mode) {
      std::fprintf(cimg::output(),
                   "\n[gmic_gimp]./error/ %s\n",
                   spt.error_message.data());
      std::fflush(cimg::output());
    }
  }
#if !defined(__MACOSX__)  && !defined(__APPLE__)
  if (spt.is_thread) {
    pthread_mutex_unlock(&spt.is_running);
    pthread_exit(0);
  }
#endif
  return 0;
}

// Process the selected image/layers.
//------------------------------------
void process_image(const char *const commands_line) {
  if (!gimp_image_is_valid(image_id)) return;
  const unsigned int filter = get_current_filter();
  if (!commands_line && !filter) return;
  const char *const _commands_line = commands_line?commands_line:get_commands_line(false);
  if (!_commands_line || std::strstr(_commands_line,"-_none_")) return;

  char new_label[256] = { 0 };
  if (run_mode!=GIMP_RUN_NONINTERACTIVE) {
    GtkWidget *const markup2ascii = gtk_label_new(0);
    gtk_label_set_markup(GTK_LABEL(markup2ascii),gmic_entries[filter].data());
    gimp_progress_init_printf(" G'MIC : %s...",gtk_label_get_text(GTK_LABEL(markup2ascii)));
    const char *const cl = _commands_line + (!std::strncmp(_commands_line,"-v -99 ",7) || !std::strncmp(_commands_line,"-debug ",7)?7:0);
    cimg_snprintf(new_label,sizeof(new_label),"[G'MIC] %s : %s",gtk_label_get_text(GTK_LABEL(markup2ascii)),cl);
    gtk_widget_destroy(markup2ascii);
  } else cimg_snprintf(new_label,sizeof(new_label),"[G'MIC] : %s",_commands_line);

  // Get input layers for the chosen filter.
  st_process_thread spt;
  spt.is_preview = false;
  spt.commands_line = _commands_line;
  spt.verbosity_mode = get_verbosity_mode();
  spt.progress = -1;

  const CImg<int> layers = get_input_layers(spt.images);
  CImg<int> dimensions(spt.images.size(),4);
  cimglist_for(spt.images,l) {
    const CImg<float>& img = spt.images[l];
    dimensions(l,0) = img.width(); dimensions(l,1) = img.height();
    dimensions(l,2) = img.depth(); dimensions(l,3) = img.spectrum();
  }

  // Create processing thread and wait for its completion.
  if (run_mode!=GIMP_RUN_NONINTERACTIVE) {
#if !defined(__MACOSX__)  && !defined(__APPLE__)
    spt.is_thread = true;
    pthread_mutex_init(&spt.is_running,0);
    pthread_mutex_init(&spt.wait_lock,0);
    pthread_cond_init(&spt.wait_cond,0);
    pthread_mutex_lock(&spt.wait_lock);
    pthread_create(&(spt.thread),0,process_thread,(void*)&spt);
    pthread_cond_wait(&spt.wait_cond,&spt.wait_lock);  // Wait for the thread to lock the mutex.
    pthread_mutex_unlock(&spt.wait_lock);
    pthread_mutex_destroy(&spt.wait_lock);

    while (pthread_mutex_trylock(&spt.is_running)) {
      if (spt.progress>=0) gimp_progress_update(cimg::min(1.0,spt.progress/100.0));
      else gimp_progress_pulse();
      cimg::wait(350);
    }

    gimp_progress_update(1.0);
    pthread_join(spt.thread,0);
    pthread_mutex_unlock(&spt.is_running);
    pthread_mutex_destroy(&spt.is_running);
#else
  gimp_progress_update(0.5);
  process_thread(&spt);
  gimp_progress_update(1.0);
#endif
  } else {
    spt.is_thread = false;
    process_thread(&spt);
  }

  // Check that everything went fine, else display an error dialog.
  if (spt.error_message) {
    gimp_message(spt.error_message.data());
    status = GIMP_PDB_CALLING_ERROR;
  } else {

    // Get output layers dimensions and check if input/output layers have compatible dimensions.
    unsigned int max_width = 0, max_height = 0, max_channels = 0;
    cimglist_for(spt.images,l) {
      if (spt.images[l]._width>max_width) max_width = spt.images[l]._width;
      if (spt.images[l]._height>max_height) max_height = spt.images[l]._height;
      if (spt.images[l]._spectrum>max_channels) max_channels = spt.images[l]._spectrum;
    }
    bool is_compatible_dimensions = (spt.images.size()==layers._height);
    for (unsigned int p = 0; p<spt.images.size() && is_compatible_dimensions; ++p) {
      const CImg<float>& img = spt.images[p];
      if (img.width()!=dimensions(p,0) ||
          img.height()!=dimensions(p,1) ||
          img.spectrum()>dimensions(p,3)) is_compatible_dimensions = false;
    }

    // Transfer the output layers back into GIMP.
    GimpPixelRgn region;
    gint x1, y1, x2, y2;
    const unsigned int output_mode = get_output_mode();
    switch (output_mode) {
    case 0 : { // Output in 'Replace' mode.
      gimp_image_undo_group_start(image_id);
      if (is_compatible_dimensions) cimglist_for(spt.images,l) { // Direct replacement of the layer data.
          CImg<float> &img = spt.images[l];
          calibrate_image(img,dimensions(l,3),false);
          GimpDrawable *drawable = gimp_drawable_get(layers[l]);
          gimp_drawable_mask_bounds(drawable->drawable_id,&x1,&y1,&x2,&y2);
          gimp_pixel_rgn_init(&region,drawable,x1,y1,x2-x1,y2-y1,true,true);
          convert_image_float2uchar(img);
          gimp_pixel_rgn_set_rect(&region,(guchar*)img.data(),x1,y1,x2-x1,y2-y1);
          img.assign();
          gimp_drawable_flush(drawable);
          gimp_drawable_merge_shadow(drawable->drawable_id,true);
          gimp_drawable_update(drawable->drawable_id,x1,y1,x2-x1,y2-y1);
          gimp_drawable_detach(drawable);
        } else { // Indirect replacement : create new layers.
        gimp_selection_none(image_id);
#if GIMP_MINOR_VERSION<=6
        const int layer_pos = gimp_image_get_layer_position(image_id,layers[0]);
#else
        const int layer_pos = gimp_image_get_item_position(image_id,layers[0]);
#endif
        for (unsigned int i = 0; i<layers._height; ++i) gimp_image_remove_layer(image_id,layers[i]);
        cimglist_for(spt.images,p) {
          CImg<float> &img = spt.images[p];
          if (gimp_image_base_type(image_id)==GIMP_GRAY) calibrate_image(img,(img.spectrum()==1 ||
                                                                              img.spectrum()==3)?1:2,false);
          else calibrate_image(img,(img.spectrum()==1 || img.spectrum()==3)?3:4,false);
          gint layer_id = gimp_layer_new(image_id,new_label,img.width(),img.height(),
                                         img.spectrum()==1?GIMP_GRAY_IMAGE:
                                         img.spectrum()==2?GIMP_GRAYA_IMAGE:
                                         img.spectrum()==3?GIMP_RGB_IMAGE:GIMP_RGBA_IMAGE,
                                         100.0,GIMP_NORMAL_MODE);
#if GIMP_MINOR_VERSION<=6
          gimp_image_add_layer(image_id,layer_id,layer_pos+p);
#else
          gimp_image_insert_layer(image_id,layer_id,-1,layer_pos+p);
#endif
          GimpDrawable *drawable = gimp_drawable_get(layer_id);
          gimp_pixel_rgn_init(&region,drawable,0,0,drawable->width,drawable->height,true,true);
          convert_image_float2uchar(img);
          gimp_pixel_rgn_set_rect(&region,(guchar*)img.data(),0,0,img.width(),img.height());
          img.assign();
          gimp_drawable_flush(drawable);
          gimp_drawable_merge_shadow(drawable->drawable_id,true);
          gimp_drawable_update(drawable->drawable_id,0,0,drawable->width,drawable->height);
          gimp_drawable_detach(drawable);
        }
        gimp_image_resize_to_layers(image_id);
      }
      gimp_image_undo_group_end(image_id);
    } break;
    case 1 : case 2 : { // Output in 'New layer(s)' mode.
      gimp_image_undo_group_start(image_id);
      gimp_selection_none(image_id);
      const gint active_layer_id = gimp_image_get_active_layer(image_id);
      gint layer_id = 0;
      cimglist_for(spt.images,p) {
        CImg<float> &img = spt.images[p];
        if (gimp_image_base_type(image_id)==GIMP_GRAY)
          calibrate_image(img,(img.spectrum()==1 || img.spectrum()==3)?1:2,false);
        else
          calibrate_image(img,(img.spectrum()==1 || img.spectrum()==3)?3:4,false);
        layer_id = gimp_layer_new(image_id,new_label,img.width(),img.height(),
                                  img.spectrum()==1?GIMP_GRAY_IMAGE:
                                  img.spectrum()==2?GIMP_GRAYA_IMAGE:
                                  img.spectrum()==3?GIMP_RGB_IMAGE:GIMP_RGBA_IMAGE,
                                  100.0,GIMP_NORMAL_MODE);
#if GIMP_MINOR_VERSION<=6
        gimp_image_add_layer(image_id,layer_id,p);
#else
        gimp_image_insert_layer(image_id,layer_id,-1,p);
#endif
        GimpDrawable *drawable = gimp_drawable_get(layer_id);
        gimp_pixel_rgn_init(&region,drawable,0,0,drawable->width,drawable->height,true,true);
        convert_image_float2uchar(img);
        gimp_pixel_rgn_set_rect(&region,(guchar*)img.data(),0,0,img.width(),img.height());
        img.assign();
        gimp_drawable_flush(drawable);
        gimp_drawable_merge_shadow(drawable->drawable_id,true);
        gimp_drawable_update(drawable->drawable_id,0,0,drawable->width,drawable->height);
        gimp_drawable_detach(drawable);
      }
      gimp_image_resize_to_layers(image_id);
      if (output_mode==1) gimp_image_set_active_layer(image_id,active_layer_id);
      else gtk_widget_destroy(gui_preview);  // Will force the preview to refresh on the new active layer.
      gimp_image_undo_group_end(image_id);
    } break;
    default : { // Output in 'New image' mode.
      if (spt.images.size()) {
        const int nimage_id = gimp_image_new(max_width,max_height,max_channels<=2?GIMP_GRAY:GIMP_RGB);
        gimp_image_undo_group_start(nimage_id);
        cimglist_for(spt.images,p) {
          CImg<float> &img = spt.images[p];
          if (gimp_image_base_type(nimage_id)!=GIMP_GRAY)
            calibrate_image(img,(img.spectrum()==1 || img.spectrum()==3)?3:4,false);
          gint layer_id = gimp_layer_new(nimage_id,new_label,img.width(),img.height(),
                                         img.spectrum()==1?GIMP_GRAY_IMAGE:
                                         img.spectrum()==2?GIMP_GRAYA_IMAGE:
                                         img.spectrum()==3?GIMP_RGB_IMAGE:GIMP_RGBA_IMAGE,
                                         100.0,GIMP_NORMAL_MODE);
#if GIMP_MINOR_VERSION<=6
          gimp_image_add_layer(nimage_id,layer_id,p);
#else
          gimp_image_insert_layer(nimage_id,layer_id,-1,p);
#endif
          GimpDrawable *drawable = gimp_drawable_get(layer_id);
          GimpPixelRgn dest_region;
          gimp_pixel_rgn_init(&dest_region,drawable,0,0,drawable->width,drawable->height,true,true);
          convert_image_float2uchar(img);
          gimp_pixel_rgn_set_rect(&dest_region,(guchar*)img.data(),0,0,img.width(),img.height());
          img.assign();
          gimp_drawable_flush(drawable);
          gimp_drawable_merge_shadow(drawable->drawable_id,true);
          gimp_drawable_update(drawable->drawable_id,0,0,drawable->width,drawable->height);
          gimp_drawable_detach(drawable);
        }
        gimp_display_new(nimage_id);
        gimp_image_undo_group_end(nimage_id);
      }
    }
    }
  }
  if (run_mode!=GIMP_RUN_NONINTERACTIVE) {
    gimp_progress_end();
    gimp_displays_flush();
  }
}

// Process the preview image.
//---------------------------
void process_preview() {
  if (is_block_preview) { is_block_preview = false; return; }
  if (!gimp_image_is_valid(image_id)) return;
  const unsigned int filter = get_current_filter();
  if (!filter) return;
  const char *const commands_line = get_commands_line(true);
  if (!commands_line || std::strstr(commands_line,"-_none_")) return;
  int w, h, channels;
  guchar *const ptr0 = gimp_zoom_preview_get_source(GIMP_ZOOM_PREVIEW(gui_preview),&w,&h,&channels);
  static int _xp = -1, _yp = -1;
  int xp, yp; gimp_preview_get_position(GIMP_PREVIEW(gui_preview),&xp,&yp);
  if (xp!=_xp || _yp!=yp) { _xp = xp; _yp = yp; computed_preview.assign(); }
  if (!computed_preview) {

    // Get input layers for the chosen filter and convert then to the preview size if necessary.
    st_process_thread spt;
    spt.is_preview = true;
    spt.is_thread = false;
    spt.commands_line = commands_line;
    spt.verbosity_mode = get_verbosity_mode();
    spt.progress = -1;

    const unsigned int input_mode = get_input_mode();
    int nb_layers = 0, *layers = gimp_image_get_layers(image_id,&nb_layers);
    guchar *ptrs = ptr0;
    if (nb_layers && input_mode) {
      if (input_mode==1 ||
          (input_mode==2 && nb_layers==1) ||
          (input_mode==3 && nb_layers==1 && gimp_drawable_get_visible(layers[0])) ||
          (input_mode==4 && nb_layers==1 && !gimp_drawable_get_visible(layers[0])) ||
          (input_mode==5 && nb_layers==1)) {
        // If only one input layer, use the default thumbnail provided by GIMP.
        spt.images.assign(1,w,h,1,channels);
        const int wh = w*h;
        switch (channels) {
        case 1 : {
          float *ptr_r = spt.images[0].data(0,0,0,0);
          for (int xy = 0; xy<wh; ++xy) *(ptr_r++) = (float)*(ptrs++);
        } break;
        case 2 : {
          float
            *ptr_r = spt.images[0].data(0,0,0,0),
            *ptr_g = spt.images[0].data(0,0,0,1);
          for (int xy = 0; xy<wh; ++xy) {
            *(ptr_r++) = (float)*(ptrs++);
            *(ptr_g++) = (float)*(ptrs++);
          }
        } break;
        case 3 : {
          float
            *ptr_r = spt.images[0].data(0,0,0,0),
            *ptr_g = spt.images[0].data(0,0,0,1),
            *ptr_b = spt.images[0].data(0,0,0,2);
          for (int xy = 0; xy<wh; ++xy) {
            *(ptr_r++) = (float)*(ptrs++);
            *(ptr_g++) = (float)*(ptrs++);
            *(ptr_b++) = (float)*(ptrs++);
          }
        } break;
        case 4 : {
          float
            *ptr_r = spt.images[0].data(0,0,0,0),
            *ptr_g = spt.images[0].data(0,0,0,1),
            *ptr_b = spt.images[0].data(0,0,0,2),
            *ptr_a = spt.images[0].data(0,0,0,3);
          for (int xy = 0; xy<wh; ++xy) {
            *(ptr_r++) = (float)*(ptrs++);
            *(ptr_g++) = (float)*(ptrs++);
            *(ptr_b++) = (float)*(ptrs++);
            *(ptr_a++) = (float)*(ptrs++);
          }
        } break;
        }
      } else { // Else, compute a 'hand-made' set of thumbnails.
        CImgList<unsigned char> images_uchar;
        get_input_layers(images_uchar);
        const double factor = gimp_zoom_preview_get_factor(GIMP_ZOOM_PREVIEW(gui_preview));
        int xp, yp; gimp_preview_get_position(GIMP_PREVIEW(gui_preview),&xp,&yp);
        spt.images.assign(images_uchar.size());
        cimglist_for(images_uchar,l) {
          const int
            cw = images_uchar[l].width(),
            ch = images_uchar[l].height(),
            x0 = (int)(xp/factor)*cw/w,
            y0 = (int)(yp/factor)*ch/h,
            x1 = (int)((xp+w)/factor)*cw/w - 1,
            y1 = (int)((yp+h)/factor)*ch/h - 1;
          images_uchar[l].get_crop(x0,y0,x1,y1).resize(w,h,1,-100).move_to(spt.images[l]);
          images_uchar[l].assign();
        }
      }
    }

    // Run G'MIC.
    process_thread(&spt);
    if (spt.error_message) {
      const float white[] = { 255,255,255 };
      spt.images.assign(1).back().fill(0).draw_text(0,0," Preview \n  error ",white,0,1,57).
        resize(-100,-100,1,4).get_shared_channel(3).dilate(5);
      spt.images[0].resize(w,h,1,4,0,0,0.5,0.5);
    }

    // Transfer the output layers back into GIMP preview.
    computed_preview.assign();
    switch (get_preview_mode()) {
    case 0 : // Preview 1st layer
      if (spt.images && spt.images.size()>0) spt.images[0].move_to(computed_preview);
      calibrate_image(computed_preview,channels,true);
      break;
    case 1 : // Preview 2nd layer
      if (spt.images && spt.images.size()>1) spt.images[1].move_to(computed_preview);
      calibrate_image(computed_preview,channels,true);
      break;
    case 2 : // Preview 3rd layer
      if (spt.images && spt.images.size()>2) spt.images[2].move_to(computed_preview);
      calibrate_image(computed_preview,channels,true);
      break;
    case 3 : // Preview 4th layer
      if (spt.images && spt.images.size()>3) spt.images[3].move_to(computed_preview);
      calibrate_image(computed_preview,channels,true);
      break;
    case 4 : { // Preview 1st->2nd layers
      if (spt.images.size()>2) spt.images.remove(2,spt.images.size()-1);
      cimglist_for(spt.images,l) calibrate_image(spt.images[l],channels,true);
      (spt.images>'x').move_to(computed_preview);
    } break;
    case 5 : { // Preview 1st->3nd layers
      if (spt.images.size()>3) spt.images.remove(3,spt.images.size()-1);
      cimglist_for(spt.images,l) calibrate_image(spt.images[l],channels,true);
      (spt.images>'x').move_to(computed_preview);
    } break;
    case 6 : { // Preview 1st->4nd layers
      if (spt.images.size()>4) spt.images.remove(4,spt.images.size()-1);
      cimglist_for(spt.images,l) calibrate_image(spt.images[l],channels,true);
      (spt.images>'x').move_to(computed_preview);
    } break;
    default : // Preview all layers
      cimglist_for(spt.images,l) calibrate_image(spt.images[l],channels,true);
      (spt.images>'x').move_to(computed_preview);
    }
    spt.images.assign();
    if (!computed_preview) {
      computed_preview.assign(w,h,1,4,0);
      calibrate_image(computed_preview,channels,true);
    }
    if (computed_preview.width()>computed_preview.height()) {
      const unsigned int _nh = computed_preview._height*w/computed_preview._width, nh = _nh?_nh:1;
      computed_preview.resize(w,nh,1,-100,2);
    } else {
      const unsigned int _nw = computed_preview._width*h/computed_preview._height, nw = _nw?_nw:1;
      computed_preview.resize(nw,h,1,-100,2);
    }
    if (computed_preview.width()!=w || computed_preview.height()!=h)
      computed_preview.resize(w,h,1,-100,0,0,0.5,0.5);
    convert_image_float2uchar(computed_preview);
    computed_preview.channel(0);
  }

  std::memcpy(ptr0,computed_preview.data(),w*h*channels*sizeof(unsigned char));
  gimp_preview_draw_buffer(GIMP_PREVIEW(gui_preview),ptr0,w*channels);
  g_free(ptr0);
}

// Create the parameters GUI for the chosen filter.
//--------------------------------------------------
void create_parameters_gui(const bool reset_params) {
  const unsigned int filter = get_current_filter();

  // Remove existing table in the parameters frame if necessary.
  GtkWidget *const child = GTK_WIDGET(gtk_bin_get_child(GTK_BIN(right_frame)));
  if (child) gtk_container_remove(GTK_CONTAINER(right_frame),child);

  // Create new table for the parameters frame.
  GtkWidget *table = 0;
  if (!filter) {  // No filter selected -> 1x1 table with default message.
    table = gtk_table_new(1,1,false);
    gtk_widget_show(table);
    GtkWidget *const label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label),t("<i>Select a filter...</i>"));
    gtk_widget_show(label);
    gtk_table_attach(GTK_TABLE(table),label,0,1,0,1,
                     (GtkAttachOptions)(GTK_EXPAND),(GtkAttachOptions)(GTK_EXPAND),0,0);
    gtk_misc_set_alignment (GTK_MISC(label),0,0.5);
    gtk_frame_set_label(GTK_FRAME(right_frame),NULL);
  } else { // Filter selected -> Build the table for setting the parameters.
    char s_label[256] = { 0 };
    cimg_snprintf(s_label,sizeof(s_label),"<b>  %s : </b>",gmic_entries[filter].data());
    GtkWidget *const frame_title = gtk_label_new(NULL);
    gtk_widget_show(frame_title);
    gtk_label_set_markup(GTK_LABEL(frame_title),s_label);
    gtk_frame_set_label_widget(GTK_FRAME(right_frame),frame_title);

    // Count number of filter arguments.
    char argument_name[256] = { 0 }, _argument_type[32] = { 0 }, argument_arg[4096] = { 0 };
    unsigned int nb_arguments = 0;
    for (const char *argument = gmic_arguments[filter].data(); *argument; ) {
      int err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-z](%4095[^)]",
                            argument_name,_argument_type,&(argument_arg[0]=0));
      if (err!=3) err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-z]{%4095[^}]",
                                    argument_name,_argument_type,argument_arg);
      if (err!=3) err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-z][%4095[^]]",
                                    argument_name,_argument_type,argument_arg);
      if (err>=2) {
        argument += std::strlen(argument_name) + std::strlen(_argument_type) + std::strlen(argument_arg) + 3;
        if (*argument) ++argument;
        ++nb_arguments;
      } else break;
    }

    if (!nb_arguments) { // Filter requires no parameters -> 1x1 table with default message.
      table = gtk_table_new(1,1,false);
      gtk_widget_show(table);
      GtkWidget *label = gtk_label_new(NULL);
      gtk_label_set_markup(GTK_LABEL(label),t("<i>No parameters to set...</i>"));
      gtk_widget_show(label);
      gtk_table_attach(GTK_TABLE(table),label,0,1,0,1,
                       (GtkAttachOptions)(GTK_EXPAND),(GtkAttachOptions)(GTK_EXPAND),0,0);
      gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
      if (event_infos) delete[] event_infos; event_infos = 0;
      set_filter_nbparams(filter,0);

    } else { // Filter requires parameters -> Create parameters table.

      // Create new table for putting parameters inside.
      table = gtk_table_new(nb_arguments,3,false);
      gtk_widget_show(table);
      gtk_table_set_row_spacings(GTK_TABLE(table),6);
      gtk_table_set_col_spacings(GTK_TABLE(table),6);
      gtk_container_set_border_width(GTK_CONTAINER(table),8);

      // Parse arguments list and add recognized one to the table.
      if (event_infos) delete[] event_infos; event_infos = new void*[2*nb_arguments];
      int current_argument = 0, current_table_line = 0;
      const bool is_fave = filter>=indice_faves;
      for (const char *argument = gmic_arguments[filter].data(); *argument; ) {
        int err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-Z_](%4095[^)]",
                              argument_name,_argument_type,&(argument_arg[0]=0));
        if (err!=3) err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-Z_][%4095[^]]",
                                      argument_name,_argument_type,argument_arg);
        if (err!=3) err = std::sscanf(argument,"%4095[^=]=%4095[ a-zA-Z_]{%4095[^}]",
                                      argument_name,_argument_type,argument_arg);

        if (err>=2) {
          argument += std::strlen(argument_name) + std::strlen(_argument_type) + std::strlen(argument_arg) + 3;
          if (*argument) ++argument;
          cimg::strpare(argument_name,' ',false,true);
          cimg::strpare(argument_name,'\"',true);
          cimg::strescape(argument_name);
          cimg::strpare(_argument_type,' ',false,true);
          const bool is_silent_argument = (*_argument_type=='_');
          char
            *const argument_type = _argument_type + (is_silent_argument?1:0),
            *const argument_value = get_filter_parameter(filter,current_argument),
            *const argument_fave = get_fave_parameter(filter,current_argument);

          // Check for a float-valued argument.
          bool found_valid_argument = false;
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"float")) {
            float value = 0, min_value = 0, max_value = 100;
            std::setlocale(LC_NUMERIC,"C");
            std::sscanf(argument_arg,"%f%*c%f%*c%f",&value,&min_value,&max_value);
            if (is_fave) std::sscanf(argument_fave,"%f",&value);
            if (!reset_params) std::sscanf(argument_value,"%f",&value);
            GtkObject *const
              scale = gimp_scale_entry_new(GTK_TABLE(table),0,current_table_line,argument_name,50,6,
                                           (double)value,(double)min_value,(double)max_value,
                                           (double)(max_value-min_value)/100,
                                           (double)(max_value-min_value)/20,
                                           2,true,0,0,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_float_parameter_changed(GTK_ADJUSTMENT(scale),event_infos+2*current_argument);
            g_signal_connect(scale,"value_changed",G_CALLBACK(on_float_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(scale,"value_changed",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for an int-valued argument.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"int")) {
            float value = 0, min_value = 0, max_value = 100;
            std::setlocale(LC_NUMERIC,"C");
            std::sscanf(argument_arg,"%f%*c%f%*c%f",&value,&min_value,&max_value);
            if (is_fave) std::sscanf(argument_fave,"%f",&value);
            if (!reset_params) std::sscanf(argument_value,"%f",&value);
            GtkObject *const
              scale = gimp_scale_entry_new(GTK_TABLE(table),0,current_table_line,argument_name,50,6,
                                           (double)(int)cimg::round(value,1.0f),
                                           (double)(int)cimg::round(min_value,1.0f),
                                           (double)(int)cimg::round(max_value,1.0f),
                                           (double)1,
                                           (double)cimg::max(1.0,cimg::round((max_value-min_value)/20,1,1)),
                                           0,true,0,0,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_int_parameter_changed(GTK_ADJUSTMENT(scale),event_infos+2*current_argument);
            g_signal_connect(scale,"value_changed",G_CALLBACK(on_int_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(scale,"value_changed",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for a bool-valued argument.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"bool")) {
            cimg::strpare(argument_arg,' ',false,true); cimg::strpare(argument_arg,'\"',true);
            bool
              value = !(!*argument_arg || !cimg::strcasecmp(argument_arg,"false") ||
                        (argument_arg[0]=='0' && argument_arg[1]==0));
            if (is_fave && *argument_fave)
              value = !(!cimg::strcasecmp(argument_fave,"false") ||
                        (argument_fave[0]=='0' && argument_fave[1]==0));
            if (!reset_params && *argument_value)
              value = !(!cimg::strcasecmp(argument_value,"false") ||
                        (argument_value[0]=='0' && argument_value[1]==0));
            GtkWidget *const checkbutton = gtk_check_button_new_with_label(argument_name);
            gtk_widget_show(checkbutton);
            gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbutton),value);
            gtk_table_attach(GTK_TABLE(table),checkbutton,0,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),GTK_SHRINK,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_bool_parameter_changed(GTK_CHECK_BUTTON(checkbutton),event_infos+2*current_argument);
            g_signal_connect(checkbutton,"toggled",G_CALLBACK(on_bool_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(checkbutton,"toggled",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for a choice-valued argument.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"choice")) {
            GtkWidget *const label = gtk_label_new(argument_name);
            gtk_widget_show(label);
            gtk_table_attach(GTK_TABLE(table),label,0,1,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
            GtkWidget *const combobox = gtk_combo_box_new_text();
            gtk_widget_show(combobox);
            char s_entry[256] = { 0 }, end = 0; int err = 0;
            unsigned int value = 0;
            const char *entries = argument_arg;
            if (std::sscanf(entries,"%u",&value)==1)
              entries+=cimg_snprintf(s_entry,sizeof(s_entry),"%u",value) + 1;
            while (*entries) {
              if ((err = std::sscanf(entries,"%4095[^,]%c",s_entry,&end))>0) {
                entries += std::strlen(s_entry) + (err==2?1:0);
                cimg::strpare(s_entry,' ',false,true); cimg::strpare(s_entry,'\"',true);
                gtk_combo_box_append_text(GTK_COMBO_BOX(combobox),s_entry);
              } else break;
            }
            if (is_fave) std::sscanf(argument_fave,"%u",&value);
            if (!reset_params) std::sscanf(argument_value,"%u",&value);
            gtk_combo_box_set_active(GTK_COMBO_BOX(combobox),value);
            gtk_table_attach(GTK_TABLE(table),combobox,1,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),GTK_FILL,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_list_parameter_changed(GTK_COMBO_BOX(combobox),event_infos+2*current_argument);
            g_signal_connect(combobox,"changed",G_CALLBACK(on_list_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(combobox,"changed",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for a text-valued argument.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"text")) {
            GtkWidget *const label = gtk_label_new(argument_name);
            gtk_widget_show(label);
            gtk_table_attach(GTK_TABLE(table),label,0,1,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
            GtkWidget *const entry = gtk_entry_new_with_max_length(1023);
            gtk_widget_show(entry);
            char *value = argument_arg;
            if (is_fave) value = argument_fave;
            if (!reset_params && *argument_value) value = argument_value;
            cimg::strpare(value,' ',false,true); cimg::strpare(value,'\"',true);
            gtk_entry_set_text(GTK_ENTRY(entry),value);
            gtk_table_attach(GTK_TABLE(table),entry,1,2,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),(GtkAttachOptions)0,0,0);
            GtkWidget *const button = gtk_button_new_with_label(t("Update"));
            gtk_widget_show(button);
            gtk_table_attach(GTK_TABLE(table),button,2,3,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)entry;
            on_text_parameter_changed(event_infos+2*current_argument);
            g_signal_connect_swapped(button,"clicked",G_CALLBACK(on_text_parameter_changed),
                                     event_infos+2*current_argument);
            g_signal_connect_swapped(entry,"changed",G_CALLBACK(on_text_parameter_changed),
                                     event_infos+2*current_argument);
            if (!is_silent_argument) {
              g_signal_connect(button,"clicked",G_CALLBACK(_gimp_preview_invalidate),0);
              g_signal_connect(entry,"activate",G_CALLBACK(_gimp_preview_invalidate),0);
            }
            found_valid_argument = true; ++current_argument;
          }

          // Check for a filename or folder name argument.
          if (!found_valid_argument && (!cimg::strcasecmp(argument_type,"file") ||
                                        !cimg::strcasecmp(argument_type,"folder"))) {
            GtkWidget *const label = gtk_label_new(argument_name);
            gtk_widget_show(label);
            gtk_table_attach(GTK_TABLE(table),label,0,1,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
            GtkWidget *const
              file_chooser = gtk_file_chooser_button_new(argument_name,
                                                         cimg::uncase(argument_type[1])=='i'?
                                                         GTK_FILE_CHOOSER_ACTION_OPEN:
                                                         GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
            gtk_widget_show(file_chooser);
            char *value = argument_arg;
            if (is_fave) value = argument_fave;
            if (!reset_params && *argument_value) value = argument_value;
            cimg::strpare(value,' ',false,true); cimg::strpare(value,'\"',true);
            gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(file_chooser),value);
            gtk_table_attach(GTK_TABLE(table),file_chooser,1,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),(GtkAttachOptions)0,0,0);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_file_parameter_changed(GTK_FILE_CHOOSER(file_chooser),event_infos+2*current_argument);
            g_signal_connect(file_chooser,"selection-changed",G_CALLBACK(on_file_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(file_chooser,"selection-changed",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for a color argument.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"color")) {
            GtkWidget *const label = gtk_label_new(argument_name);
            gtk_widget_show(label);
            gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
            gtk_table_attach(GTK_TABLE(table),label,0,1,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            GtkWidget *const hbox = gtk_hbox_new(false,6);
            gtk_widget_show(hbox);
            gtk_table_attach(GTK_TABLE(table),hbox,1,2,current_table_line,current_table_line+1,
                             GTK_FILL,GTK_SHRINK,0,0);
            GtkWidget *const color_chooser = gtk_color_button_new();
            gtk_widget_show(color_chooser);
            gtk_color_button_set_title(GTK_COLOR_BUTTON(color_chooser),argument_name);
            gtk_button_set_alignment(GTK_BUTTON(color_chooser),0,0.5);
            gtk_box_pack_start(GTK_BOX(hbox),color_chooser,false,false,0);

            float red = 0, green = 0, blue = 0, alpha = 255;
            const int err = std::sscanf(argument_arg,"%f%*c%f%*c%f%*c%f",&red,&green,&blue,&alpha);
            if (is_fave) std::sscanf(argument_fave,"%f%*c%f%*c%f%*c%f",&red,&green,&blue,&alpha);
            if (!reset_params) std::sscanf(argument_value,"%f%*c%f%*c%f%*c%f",&red,&green,&blue,&alpha);
            red = red<0?0:red>255?255:red;
            green = green<0?0:green>255?255:green;
            blue = blue<0?0:blue>255?255:blue;
            GdkColor color;
            color.pixel = 0;
            color.red = (unsigned int)(red*257);
            color.green = (unsigned int)(green*257);
            color.blue = (unsigned int)(blue*257);
            gtk_color_button_set_color(GTK_COLOR_BUTTON(color_chooser),&color);
            if (err==4) {
              gtk_color_button_set_use_alpha(GTK_COLOR_BUTTON(color_chooser),true);
              gtk_color_button_set_alpha(GTK_COLOR_BUTTON(color_chooser),(unsigned int)(alpha*257));
            } else gtk_color_button_set_use_alpha(GTK_COLOR_BUTTON(color_chooser),false);
            event_infos[2*current_argument] = (void*)current_argument;
            event_infos[2*current_argument+1] = (void*)0;
            on_color_parameter_changed(GTK_COLOR_BUTTON(color_chooser),event_infos+2*current_argument);
            g_signal_connect(color_chooser,"color-set",G_CALLBACK(on_color_parameter_changed),
                             event_infos+2*current_argument);
            if (!is_silent_argument)
              g_signal_connect(color_chooser,"color-set",G_CALLBACK(_gimp_preview_invalidate),0);
            found_valid_argument = true; ++current_argument;
          }

          // Check for a note.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"note")) {
            cimg::strpare(argument_arg,' ',false,true);
            cimg::strpare(argument_arg,'\"',true);
            cimg::strescape(argument_arg);
            GtkWidget *const label = gtk_label_new(NULL);
            gtk_label_set_markup(GTK_LABEL(label),argument_arg);
            gtk_label_set_line_wrap(GTK_LABEL(label),true);
            gtk_widget_show(label);
            gtk_table_attach(GTK_TABLE(table),label,0,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),GTK_SHRINK,0,0);
            gtk_misc_set_alignment(GTK_MISC(label),0,0.5);
            found_valid_argument = true;
          }

          // Check for a link.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"link")) {
            char label[1024] = { 0 }, url[1024] = { 0 };
            float alignment = 0.5f;
            switch (std::sscanf(argument_arg,"%f,%1023[^,],%1023s",&alignment,label,url)) {
            case 2 : std::strcpy(url,label); break;
            case 1 : cimg_snprintf(url,sizeof(url),"%g",alignment); break;
            case 0 : if (std::sscanf(argument_arg,"%1023[^,],%1023s",label,url)==1) std::strcpy(url,label); break;
            }
            cimg::strpare(label,' ',false,true);
            cimg::strpare(label,'\"',true);
            cimg::strescape(label);
            cimg::strpare(url,' ',false,true);
            cimg::strpare(url,'\"',true);
            GtkWidget *const link = gtk_link_button_new_with_label(url,label);
            gtk_widget_show(link);
            gtk_button_set_alignment(GTK_BUTTON(link),alignment,0.5);
            gtk_table_attach(GTK_TABLE(table),link,0,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),GTK_SHRINK,0,0);
            found_valid_argument = true;
          }

          // Check for an horizontal separator.
          if (!found_valid_argument && !cimg::strcasecmp(argument_type,"separator")) {
            GtkWidget *const separator = gtk_hseparator_new();
            gtk_widget_show(separator);
            gtk_table_attach(GTK_TABLE(table),separator,0,3,current_table_line,current_table_line+1,
                             (GtkAttachOptions)(GTK_EXPAND | GTK_FILL),GTK_SHRINK,0,0);
            found_valid_argument = true;
          }

          if (!found_valid_argument) {
            if (get_verbosity_mode()) {
              std::fprintf(cimg::output(),
                           "\n[gmic_gimp]./error/ Found invalid parameter type '%s' for argument '%s'.\n",
                           argument_type,argument_name);
              std::fflush(cimg::output());
            }
          } else ++current_table_line;
        } else break;
      }
      set_filter_nbparams(filter,current_argument);
    }
  }

  gtk_container_add(GTK_CONTAINER(right_frame),table);

  // Take care of the size of the parameter table.
  GtkRequisition requisition; gtk_widget_size_request(table,&requisition);
  gtk_widget_set_size_request(right_scrolledwindow,cimg::max(490,requisition.width),-1);
  gtk_widget_show(dialog_window);
  set_preview_factor();

  // Set correct icon for fave button.
  if (fave_stockbutton) gtk_widget_destroy(fave_stockbutton);
  fave_stockbutton = gtk_button_new_from_stock(!filter?GTK_STOCK_ABOUT:
                                               filter>=indice_faves?GTK_STOCK_DELETE:GTK_STOCK_ADD);
  GtkWidget *fave_image = gtk_button_get_image(GTK_BUTTON(fave_stockbutton));
  gtk_button_set_image(GTK_BUTTON(fave_button),fave_image);
  gtk_widget_show(fave_button);
}

// Create main plug-in dialog window and wait for events.
//-------------------------------------------------------
bool create_dialog_gui() {

  // Init GUI_specific variables
  _create_dialog_gui = true;
  gimp_ui_init("gmic",true);
  event_infos = 0;

  // Create main dialog window with buttons.
  char dialog_title[64] = { 0 };
  cimg_snprintf(dialog_title,sizeof(dialog_title),"%s - %d.%d.%d.%d",
                t("G'MIC for GIMP"),gmic_version/1000,(gmic_version/100)%10,(gmic_version/10)%10,gmic_version%10);

  dialog_window = gimp_dialog_new(dialog_title,"gmic",0,(GtkDialogFlags)0,0,0,NULL);
  gimp_window_set_transient(GTK_WINDOW(dialog_window));

  g_signal_connect(dialog_window,"close",G_CALLBACK(on_dialog_cancel_clicked),0);
  g_signal_connect(dialog_window,"delete-event",G_CALLBACK(on_dialog_cancel_clicked),0);

  GtkWidget *const cancel_button = gtk_dialog_add_button(GTK_DIALOG(dialog_window),
                                                         GTK_STOCK_CANCEL,GTK_RESPONSE_CANCEL);
  g_signal_connect(cancel_button,"clicked",G_CALLBACK(on_dialog_cancel_clicked),0);

  GtkWidget *const reset_button = gtk_dialog_add_button(GTK_DIALOG(dialog_window),GIMP_STOCK_RESET,1);
  g_signal_connect(reset_button,"clicked",G_CALLBACK(on_dialog_reset_clicked),0);

  GtkWidget *const maximize_button = gtk_dialog_add_button(GTK_DIALOG(dialog_window),t("_Maximize"),1);
  g_signal_connect(maximize_button,"clicked",G_CALLBACK(on_dialog_maximize_button_clicked),0);

  GtkWidget *const apply_button = gtk_dialog_add_button(GTK_DIALOG(dialog_window),
                                                        GTK_STOCK_APPLY,GTK_RESPONSE_APPLY);
  g_signal_connect(apply_button,"clicked",G_CALLBACK(on_dialog_apply_clicked),0);

  GtkWidget *const ok_button = gtk_dialog_add_button(GTK_DIALOG(dialog_window),
                                                     GTK_STOCK_OK,GTK_RESPONSE_OK);
  g_signal_connect(ok_button,"clicked",G_CALLBACK(gtk_main_quit),0);

  GtkWidget *const dialog_hbox = gtk_hbox_new(false,0);
  gtk_widget_show(dialog_hbox);
  gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog_window)->vbox),dialog_hbox);

  // Create the left pane.
  left_pane = gtk_vbox_new(false,4);
  gtk_widget_show(left_pane);
  gtk_box_pack_start(GTK_BOX(dialog_hbox),left_pane,true,true,0);

  GtkWidget *const image_align = gtk_alignment_new(0.1,0,0,0);
  gtk_widget_show(image_align);
  gtk_box_pack_end(GTK_BOX(left_pane),image_align,false,false,0);
  const unsigned int logo_width = 102, logo_height = 22;
  GdkPixbuf *const pixbuf = gdk_pixbuf_new_from_data(data_gmic_logo,GDK_COLORSPACE_RGB,
                                                     false,8,logo_width,logo_height,3*logo_width,0,0);
  GtkWidget *const image = gtk_image_new_from_pixbuf(pixbuf);
  gtk_widget_show(image);
  gtk_container_add(GTK_CONTAINER(image_align),image);

  GtkWidget *const left_align = gtk_alignment_new(0,0,0,0);
  gtk_widget_show(left_align);
  gtk_box_pack_end(GTK_BOX(left_pane),left_align,false,false,0);

  GtkWidget *const left_frame = gtk_frame_new(NULL);
  gtk_widget_show(left_frame);
  gtk_container_set_border_width(GTK_CONTAINER(left_frame),4);
  gtk_container_add(GTK_CONTAINER(left_align),left_frame);

  GtkWidget *const frame_title = gtk_label_new(NULL);
  gtk_widget_show(frame_title);
  gtk_label_set_markup(GTK_LABEL(frame_title),t("<b> Input / Output : </b>"));
  gtk_frame_set_label_widget(GTK_FRAME(left_frame),frame_title);

  GtkWidget *const left_table = gtk_table_new(4,1,false);
  gtk_widget_show(left_table);
  gtk_table_set_row_spacings(GTK_TABLE(left_table),6);
  gtk_table_set_col_spacings(GTK_TABLE(left_table),6);
  gtk_container_set_border_width(GTK_CONTAINER(left_table),8);
  gtk_container_add(GTK_CONTAINER(left_frame),left_table);

  GtkWidget *const input_combobox = gtk_combo_box_new_text();
  gtk_widget_show(input_combobox);
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("Input layers..."));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),"-");
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("None"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("Active (default)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("Active & below"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("Active & above"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All visibles"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All invisibles"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All visibles (decr.)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All invisibles (decr.)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(input_combobox),t("All (decr.)"));
  gtk_combo_box_set_active(GTK_COMBO_BOX(input_combobox),get_input_mode(false));

  gtk_table_attach_defaults(GTK_TABLE(left_table),input_combobox,0,1,0,1);
  g_signal_connect(input_combobox,"changed",G_CALLBACK(on_dialog_input_mode_changed),0);

  GtkWidget *const output_combobox = gtk_combo_box_new_text();
  gtk_widget_show(output_combobox);
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),t("Output mode..."));
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),"-");
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),t("In place (default)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),t("New layer(s)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),t("New active layer(s)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(output_combobox),t("New image"));
  gtk_combo_box_set_active(GTK_COMBO_BOX(output_combobox),get_output_mode(false));
  gtk_table_attach_defaults(GTK_TABLE(left_table),output_combobox,0,1,1,2);
  g_signal_connect(output_combobox,"changed",G_CALLBACK(on_dialog_output_mode_changed),0);

 GtkWidget *const verbosity_combobox = gtk_combo_box_new_text();
  gtk_widget_show(verbosity_combobox);
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Output messages..."));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),"-");
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Quiet (default)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Verbose (console)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Verbose (logfile)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Very verbose (console)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Very verbose (logfile)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Debug mode (console)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(verbosity_combobox),t("Debug mode (logfile)"));
  gtk_combo_box_set_active(GTK_COMBO_BOX(verbosity_combobox),get_verbosity_mode(false));
  gtk_table_attach_defaults(GTK_TABLE(left_table),verbosity_combobox,0,1,2,3);
  g_signal_connect(verbosity_combobox,"changed",G_CALLBACK(on_dialog_verbosity_mode_changed),0);

  GtkWidget *const preview_combobox = gtk_combo_box_new_text();
  gtk_widget_show(preview_combobox);
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("Output preview..."));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),"-");
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("1st output (default)"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("2nd output"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("3rd output"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("4th output"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("1st -> 2nd"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("1st -> 3rd"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("1st -> 4th"));
  gtk_combo_box_append_text(GTK_COMBO_BOX(preview_combobox),t("All outputs"));
  gtk_combo_box_set_active(GTK_COMBO_BOX(preview_combobox),get_preview_mode(false));
  gtk_table_attach_defaults(GTK_TABLE(left_table),preview_combobox,0,1,3,4);
  g_signal_connect(preview_combobox,"changed",G_CALLBACK(on_dialog_preview_mode_changed),0);

  drawable_preview = gimp_drawable_get(gimp_image_get_active_drawable(image_id));
  gui_preview = gimp_zoom_preview_new(drawable_preview);
  gtk_widget_show(gui_preview);
  gtk_box_pack_end(GTK_BOX(left_pane),gui_preview,true,true,0);
  g_signal_connect(gui_preview,"invalidated",G_CALLBACK(process_preview),0);
  g_signal_connect(dialog_window,"size-request",G_CALLBACK(on_dialog_resized),0);

  // Create the middle pane.
  GtkWidget *const middle_frame = gtk_frame_new(NULL);
  gtk_widget_show(middle_frame);
  gtk_container_set_border_width(GTK_CONTAINER(middle_frame),4);
  gtk_box_pack_start(GTK_BOX(dialog_hbox),middle_frame,false,false,0);

  GtkWidget *const middle_pane = gtk_vbox_new(false,4);
  gtk_widget_show(middle_pane);
  gtk_container_add(GTK_CONTAINER(middle_frame),middle_pane);

  relabel_hbox = gtk_hbox_new(false,3);
  gtk_box_pack_start(GTK_BOX(middle_pane),relabel_hbox,false,false,0);
  relabel_entry = gtk_entry_new_with_max_length(255);
  gtk_widget_show(relabel_entry);
  gtk_box_pack_start(GTK_BOX(relabel_hbox),relabel_entry,false,true,0);
  GtkWidget *const relabel_button = gtk_button_new_with_label(t("Rename"));
  gtk_widget_show(relabel_button);
  gtk_box_pack_start(GTK_BOX(relabel_hbox),relabel_button,false,true,0);
  g_signal_connect(relabel_button,"clicked",G_CALLBACK(_on_filter_doubleclicked),0);
  g_signal_connect(relabel_entry,"activate",G_CALLBACK(_on_filter_doubleclicked),0);

  GtkWidget *const scrolled_window = gtk_scrolled_window_new(NULL,NULL);
  gtk_widget_show(scrolled_window);
  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
  gtk_box_pack_start(GTK_BOX(middle_pane),scrolled_window,true,true,0);

  tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(tree_view_store));
  gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view),true);
#if GTK_CHECK_VERSION(2,18,0)
  gtk_widget_set_can_focus(tree_view,false);
#else
  GValue val = {0, {{0}, {0}}};
  g_value_init(&val,G_TYPE_BOOLEAN);
  g_value_set_boolean(&val,FALSE);
  g_object_set_property(G_OBJECT(tree_view),"can-focus",&val);
#endif
  gtk_widget_show(tree_view);
  gtk_container_add(GTK_CONTAINER(scrolled_window),tree_view);

  GtkWidget *const tree_hbox = gtk_hbox_new(false,6);
  gtk_widget_show(tree_hbox);
  gtk_box_pack_start(GTK_BOX(middle_pane),tree_hbox,false,false,0);

  fave_button = gtk_button_new();
  gtk_box_pack_start(GTK_BOX(tree_hbox),fave_button,false,false,0);
  g_signal_connect_swapped(fave_button,"clicked",G_CALLBACK(on_dialog_fave_clicked),tree_view);

  GtkWidget *const update_button = gtk_button_new_from_stock(GTK_STOCK_REFRESH);
  gtk_widget_show(update_button);
  gtk_box_pack_start(GTK_BOX(tree_hbox),update_button,false,false,0);
  g_signal_connect_swapped(update_button,"clicked",G_CALLBACK(on_dialog_refresh_clicked),tree_view);

  GtkWidget *const internet_checkbutton = gtk_check_button_new_with_mnemonic(t("Internet"));
  gtk_widget_show(internet_checkbutton);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(internet_checkbutton),get_net_update());
  gtk_box_pack_start(GTK_BOX(tree_hbox),internet_checkbutton,false,false,0);
  g_signal_connect(internet_checkbutton,"toggled",G_CALLBACK(on_dialog_net_update_toggled),0);

  tree_mode_button = gtk_button_new();
  gtk_box_pack_start(GTK_BOX(tree_hbox),tree_mode_button,false,false,0);
  g_signal_connect_swapped(tree_mode_button,"clicked",G_CALLBACK(on_dialog_tree_mode_clicked),tree_view);

  GtkTreeViewColumn *const column = gtk_tree_view_column_new();
  gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view),column);
  flush_tree_view(tree_view);

  g_signal_connect(tree_view,"cursor-changed",G_CALLBACK(on_filter_selected),0);
  g_signal_connect(tree_view,"row-activated",G_CALLBACK(on_filter_doubleclicked),0);

  // Create the right pane.
  GtkWidget *const right_pane = gtk_vbox_new(false,0);
  gtk_widget_show(right_pane);
  gtk_box_pack_start(GTK_BOX(dialog_hbox),right_pane,false,false,0);

  right_scrolledwindow = gtk_scrolled_window_new(NULL,NULL);
  gtk_widget_show(right_scrolledwindow);
  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(right_scrolledwindow),
                                 GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
  gtk_box_pack_start(GTK_BOX(right_pane),right_scrolledwindow,true,true,0);

  right_frame = gtk_frame_new(NULL);
  gtk_widget_show(right_frame);
  gtk_container_set_border_width(GTK_CONTAINER(right_frame),4);
  gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(right_scrolledwindow),right_frame);

  // Show dialog window and wait for user response.
  create_parameters_gui(false);
  gtk_main();

  // Destroy dialog box widget and free resources.
  gtk_widget_destroy(dialog_window);
  if (tree_mode_stockbutton) gtk_widget_destroy(tree_mode_stockbutton);
  if (fave_stockbutton) gtk_widget_destroy(fave_stockbutton);
  if (event_infos) delete[] event_infos;
  return _create_dialog_gui;
}

// 'Run' function, required by the GIMP plug-in API.
//--------------------------------------------------
void gmic_run(const gchar *name, gint nparams, const GimpParam *param,
              gint *nreturn_vals, GimpParam **return_vals) {

  // Init plug-in variables.
  static GimpParam return_values[1];
  *return_vals  = return_values;
  *nreturn_vals = 1;
  return_values[0].type = GIMP_PDB_STATUS;
  cimg::unused(name,nparams);
  run_mode = (GimpRunMode)param[0].data.d_int32;
  set_logfile();
  set_locale();
  status = GIMP_PDB_SUCCESS;

  try {

    // Init filters and images.
    update_filters(false);
    image_id = param[1].data.d_drawable;
    gimp_tile_cache_ntiles(2*(gimp_image_width(image_id)/gimp_tile_width()+1));

    // Check for run mode.
    switch (run_mode) {

    case GIMP_RUN_INTERACTIVE : {
      if (create_dialog_gui()) {
        process_image(0);
        const char *const commands_line = get_commands_line(false);
        if (commands_line) { // Remember command line for the next use of the filter.
          char s_tmp[64] = { 0 };
          cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_commands_line%u",get_current_filter());
          gimp_set_data(s_tmp,commands_line,std::strlen(commands_line));
        }
      }
    } break;

    case GIMP_RUN_WITH_LAST_VALS : {
      const unsigned int filter = get_current_filter();
      if (filter) {
        char s_tmp[64] = { 0 };
        cimg_snprintf(s_tmp,sizeof(s_tmp),"gmic_commands_line%u",filter);
        char commands_line[4096] = { 0 };
        gimp_get_data(s_tmp,&commands_line);
        process_image(commands_line);
      }
    } break;

    case GIMP_RUN_NONINTERACTIVE : {
      const unsigned int _input_mode = get_input_mode();
      set_input_mode(param[3].data.d_int32 + 2);
      process_image(param[4].data.d_string);
      set_input_mode(_input_mode + 2);
    } break;
    }

  } catch (CImgException &e) {
    gimp_message(e.what());
    status = GIMP_PDB_CALLING_ERROR;
  }

  if (logfile) std::fclose(logfile);
  return_values[0].data.d_status = status;
}

// 'Query' function, required by the GIMP plug-in API.
//----------------------------------------------------
void gmic_query() {
  static const GimpParamDef args[] = {
    {GIMP_PDB_INT32,    (gchar*)"run_mode", (gchar*)"Interactive, non-interactive"},
    {GIMP_PDB_IMAGE,    (gchar*)"image",    (gchar*)"Input image"},
    {GIMP_PDB_DRAWABLE, (gchar*)"drawable", (gchar*)"Input drawable (unused)"},
    {GIMP_PDB_INT32,    (gchar*)"input",    (gchar*)"Input layers mode, when non-interactive"
     "(0=none, 1=active, 2=all, 3=active & below, 4=active & above, 5=all visibles, 6=all invisibles, "
     "7=all visibles (decr.), 8=all invisibles (decr.), 9=all (decr.))"},
    {GIMP_PDB_STRING,   (gchar*)"command",  (gchar*)"G'MIC command string, when non-interactive"},
  };

  set_locale();
  gimp_install_procedure("plug-in-gmic",             // name
                         "G'MIC",                    // blurb
                         "G'MIC",                    // help
                         "David Tschumperl\303\251", // author
                         "David Tschumperl\303\251", // copyright
                         "2008",                     // date
                         "_G'MIC...",                // menu_path
                         "RGB*, GRAY*",              // image_types
                         GIMP_PLUGIN,                // type
                         G_N_ELEMENTS(args),         // nparams
                         0,                          // nreturn_vals
                         args,                       // params
                         0);                         // return_vals

  gimp_plugin_menu_register("plug-in-gmic", "<Image>/Filters");
}

GimpPlugInInfo PLUG_IN_INFO = { 0, 0, gmic_query, gmic_run };
MAIN()
