//$Id: XFileSel.cpp,v 1.28 2006/06/05 16:46:02 markus Rel $

//PROJECT     : XDirComp
//SUBSYSTEM   : XFileSel
//REFERENCES  :
//TODO        :
//BUGS        :
//REVISION    : $Revision: 1.28 $
//AUTHOR      : Markus Schwab
//CREATED     : 14.9.1999
//COPYRIGHT   : Copyright (C) 1999 - 2006

// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


#include "dircomp-cfg.h"

#include <string>

#include <gtkmm/box.h>
#include <gtkmm/table.h>
#include <gtkmm/entry.h>
#include <gtkmm/label.h>
#include <gtkmm/button.h>
#include <gtkmm/treeview.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/scrolledwindow.h>

#include "YGP/Trace.h"
#include "YGP/Check.h"
#include "YGP/PathSrch.h"
#include "YGP/FileRExp.h"

#include "XFileSel.h"


static const char _INCLUDE ('+');
static const char _EXCLUDE ('-');


//-----------------------------------------------------------------------------
/// Constructor; adds all controls to the dialog
/// \param files: String containing the files to in/exclude
/// \param title: Title of dialog
//-----------------------------------------------------------------------------
XFileSelection::XFileSelection (std::string& files, const Glib::ustring& title)
   : XDialog (title, OKCANCEL)
     , actions (new Gtk::VBox) , listActions (new Gtk::VBox)
     , lblFiles (new Gtk::Label (Glib::locale_to_utf8 (_("File:"))))
     , file (new Gtk::Entry ()) , tblFile (new Gtk::Table ())
     , include (new Gtk::Button (Glib::locale_to_utf8 (_("_Include")), true))
     , exclude (new Gtk::Button (Glib::locale_to_utf8 (_("E_xclude")), true))
     , delEntry (new Gtk::Button (Glib::locale_to_utf8 (_("D_elete")), true))
     , upEntry (new Gtk::Button (Glib::locale_to_utf8 (_("_Up")), true))
     , downEntry (new Gtk::Button (Glib::locale_to_utf8 (_("_Down")), true))
     , target (files), vFiles (Gtk::TreeStore::create (cols))
     , treeFiles (new Gtk::TreeView (vFiles))
     , scroll (new Gtk::ScrolledWindow ()) {
   TRACE9 ("XFileSelection::XFileSelection (std::string&, const std::string&)");
   Check3 (lblFiles); Check3 (file);
   Check3 (treeFiles); Check3 (tblFile); Check3 (include); Check3 (exclude);
   Check3 (actions); Check (listActions); Check3 (delEntry); Check3 (upEntry);
   Check3 (downEntry);

   // Action-buttons
   struct {
      Gtk::Button* pButton;
      commands    id; }
   buttons[] = { { include, INCLUDE }, { exclude, EXCLUDE }, { upEntry, UP },
		 {downEntry, DOWN }, { delEntry, DELETE } }, *pAct = buttons;

   while (pAct < buttons + (sizeof (buttons) / sizeof (buttons[0]))) {
      TRACE5 ("XFileSelection::XFileSelection (std::string&, const std::string&) - Create button"
              << pAct->id);
      Check3 (pAct->pButton);

      pAct->pButton->set_sensitive (false);
      pAct->pButton->set_flags (Gtk::CAN_DEFAULT);
      pAct->pButton->set_size_request (90, 35);

      pAct->pButton->signal_clicked ().connect
          (bind (mem_fun (*this, &XFileSelection::command), pAct->id));

      pAct++->pButton->show ();
   } // end-while

   // Create other controls
   actions->pack_start (*include, false, false, 0);
   actions->pack_start (*exclude, false, false, 0);
   actions->show ();

   listActions->pack_start (*upEntry, false, false, 0);
   listActions->pack_start (*downEntry, false, false, 0);
   listActions->pack_start (*delEntry, false, false, 0);
   listActions->show ();

   treeFiles->append_column (Glib::locale_to_utf8 (_("Selected files")), cols.files);
   treeFiles->set_size_request (200, 250);
   treeFiles->get_column (0)->set_min_width (100);
   treeFiles->show ();

   scroll->add (*treeFiles);
   scroll->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
   scroll->show ();

   Glib::RefPtr <Gtk::TreeSelection> sel (treeFiles->get_selection ());
   sel->signal_changed ().connect (mem_fun (*this, &XFileSelection::rowSelected));
   sel->set_select_function (mem_fun (*this, &XFileSelection::canSelect));

   lblFiles->set_alignment (0.0, 0.5);
   lblFiles->show ();
   file->show ();

   file->signal_changed ().connect (mem_fun (*this, &XFileSelection::entryChanged));

   tblFile->attach (*scroll, 0, 2, 0, 1, Gtk::EXPAND | Gtk::FILL,
		    Gtk::EXPAND | Gtk::FILL, 5, 5);
   tblFile->attach (*listActions, 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 5, 5);
   tblFile->attach (*lblFiles, 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 5, 5);
   tblFile->attach (*file, 1, 2, 1, 2, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 5, 5);
   tblFile->attach (*actions, 2, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 5, 5);

   tblFile->show ();

   get_vbox ()->pack_start (*tblFile, true, true, 5);

   file->grab_focus ();

   YGP::PathSearch ps (files);
   std::string node;

   // Insert all existing file-entries into the list
   char type = '?';
   Gtk::TreeRow parent;
   while (!(node = ps.getNextNode ()).empty ()) {
      TRACE8 ("XFileSelection::XFileSelection (std::string&, std::string&) - "
              "Checking node " << node);

      if ((node[0] != _INCLUDE) && (node[0] != _EXCLUDE)) {
         std::string err (Glib::locale_to_utf8 (_("Ignoring invalid input `%1'")));
         err.replace (err.find ("%1"), 2, node);
         Gtk::MessageDialog dlg (err, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK);
         dlg.run ();
         continue;
      }

      if (type != node[0]) {
         parent = *makeParentEntry (type = node[0], vFiles->children ().end ());
         parent[cols.type] = type;
      }

      Gtk::TreeRow child (*vFiles->append (parent.children ()));
      child[cols.files] = node.substr (1);
   } // end-while nodes available

   treeFiles->expand_all ();
   show ();
}

//-----------------------------------------------------------------------------
/// Destructor
//-----------------------------------------------------------------------------
XFileSelection::~XFileSelection () {
   TRACE9 ("XFileSelection::~XFileSelection ()");
}

//-----------------------------------------------------------------------------
//Purpose   : Callback after pressing the OK button
//-----------------------------------------------------------------------------
void XFileSelection::okEvent () {
   target = "";
   for (Gtk::TreeModel::iterator p (vFiles->children ().begin ());
        p != vFiles->children ().end (); ++p)
       for (Gtk::TreeModel::iterator c (p->children ().begin ());
            c != p->children ().end (); ++c) {
          TRACE8 ("XFileSelection::okEvent () - Adding: "
                  << char ((*p)[cols.type]) << std::string ((*c)[cols.files]));

          target += (*p)[cols.type];
          target += (*c)[cols.files];
          target += YGP::PathSearch::PATHSEPARATOR;
       } // end-for all children

   XDialog::okEvent ();
}

//-----------------------------------------------------------------------------
/// Performs the action of the selected button
/// \param action: ID of pressed button
//-----------------------------------------------------------------------------
void XFileSelection::command (commands id) {
   TRACE9 ("XFileSelection::command (commands): " << (int)id);

   switch (id) {
   case INCLUDE:
   case EXCLUDE: {
      Check3 (file->get_text_length ());

      YGP::FileRegularExpr regexp (file->get_text ().c_str ());
      try {
         regexp.checkIntegrity ();
      }
      catch (std::invalid_argument& e) {
         std::string err (Glib::locale_to_utf8 (_("Invalid name for directories!\n\nReason: %1")));
         err.replace (err.find ("%1"), 2, e.what ());
         Gtk::MessageDialog dlg (err, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK);
         dlg.run ();
         break;
      }

      char type ((id == INCLUDE)  ? _INCLUDE : _EXCLUDE);

      Gtk::TreeRow parent (*vFiles->children ().begin ());
      if ((parent == vFiles->children ().end ())
          || (parent = vFiles->children ()[vFiles->children ().size () - 1],
              (parent[cols.type] != type)))
         parent = *makeParentEntry (type, vFiles->children ().end ());

      Gtk::TreeRow child (*vFiles->append (parent.children ()));
      child[cols.files] = file->get_text ();

      treeFiles->expand_row (Gtk::TreePath (parent), true);
      treeFiles->get_selection ()->select (child);
      break; }

   case UP:
      moveSelectedRowUp ();
      break;

   case DOWN:
      moveSelectedRowDown ();
      break;

   case DELETE: {
      Gtk::TreeIter selRow (treeFiles->get_selection ()->get_selected ());
      Gtk::TreeIter parent (selRow->parent ());
      vFiles->erase (selRow);
      if (parent->children ().empty ())
         vFiles->erase (parent);
      break; }

   default:
      Check (0);
   } // end-switch
}

//-----------------------------------------------------------------------------
/// Entryfield for files changed -> adapt button-status
//-----------------------------------------------------------------------------
void XFileSelection::entryChanged () {
   Check3 (file);

   bool status (file->get_text_length ());
   include->set_sensitive (status);
   exclude->set_sensitive (status);
}

//-----------------------------------------------------------------------------
/// Handling of row in file-list selected
//-----------------------------------------------------------------------------
void XFileSelection::rowSelected () {
   TRACE5 ("XFileSelection::rowSelected ()");
   Check3 (upEntry); Check3 (downEntry); Check3 (delEntry);

   Gtk::TreeIter selRow (treeFiles->get_selection ()->get_selected ());
   if (selRow) {
      Gtk::TreeIter parent (selRow->parent ());
      Check3 (parent);

      upEntry->set_sensitive ((parent != vFiles->children ().begin ())
                              || (selRow != parent->children ().begin ()));
      downEntry->set_sensitive (++selRow || ++parent);
      delEntry->set_sensitive (true);
   }
   else {
      delEntry->set_sensitive (false);
      upEntry->set_sensitive (false);
      downEntry->set_sensitive (false);
   }
}

//-----------------------------------------------------------------------------
/// Creates a parent entry for the list of the specified type
/// \param type: Type of parent node (In/Exclude)
/// \returns Gtk::TreeIter to created entry
/// \pre type must be either _INCLUDE or _EXCLUDE
//-----------------------------------------------------------------------------
Gtk::TreeIter XFileSelection::makeParentEntry (char type,
                                               Gtk::TreeIter pos) {
   Check1 ((type == _INCLUDE) || (type == _EXCLUDE));

   Gtk::TreeIter parent (vFiles->insert (pos));
   (*parent)[cols.files] = Glib::locale_to_utf8 (_((type == _INCLUDE)
                                                   ? N_("Include") : N_("Exclude")));
   (*parent)[cols.type] = type;
   return *parent;
}

//-----------------------------------------------------------------------------
/// Searches for the previous to the act entry in a list
/// \param list: List to search in
/// \param act: Actual position (must be a member of the list)
/// \returns Gtk::TreeIter: Previous element to act
/// \pre - act element of list
///      - act not the first element
//-----------------------------------------------------------------------------
Gtk::TreeIter XFileSelection::getPrevious (const Gtk::TreeNodeChildren& list,
                                           const Gtk::TreeIter act) const {
   Check1 (list.begin () != act);

   Gtk::TreeIter prevRow (list.begin ());
   Gtk::TreeIter work (prevRow);
   while (++work != act) {
       Check1 (work != list.end ());
       prevRow = work;
   }
   return prevRow;
}

//-----------------------------------------------------------------------------
/// Moves the selected row one line up; which might mean into an other
/// group of a different type
//-----------------------------------------------------------------------------
void XFileSelection::moveSelectedRowUp () {
   Gtk::TreeIter selRow (treeFiles->get_selection ()->get_selected ());
   Gtk::TreeIter parent (selRow->parent ());

   if (selRow != parent->children ().begin ()) {            // Not first child?
      vFiles->iter_swap (selRow, getPrevious (parent->children (), selRow));
      rowSelected ();
   }
   else {
      TRACE3 ("XFileSelection::moveSelectedRowUp () - Crossing nodes");

      // Get previous (parent) entry
      Check3 (parent != vFiles->children ().begin ());
      Gtk::TreeIter prevNode (getPrevious (vFiles->children (), parent));
      Check3 ((*prevNode)[cols.type] != (*parent)[cols.type]);
      Check3 (!prevNode->children ().empty ());

      Gtk::TreeIter newEntry;
      Gtk::TreeIter newParent;

      // First find/make new node for last child of prevNode (which will be
      // deleted afterwards - along with the parent if it only had 1 kid)
      TRACE6 ("XFileSelection::moveSelectedRowUp () - Needs new parent?");
      newParent = parent;
      if (!++newParent
          || (parent->children ().size () > 1))
         newParent = makeParentEntry ((*prevNode)[cols.type], parent);
      newEntry = (vFiles->append (newParent->children ()));
      treeFiles->expand_row (Gtk::TreePath (newParent), true);

      TRACE6 ("XFileSelection::moveSelectedRowUp () - Copy last entry of prev");
      Gtk::TreeRow t(prevNode->children ()[prevNode->children ().size () - 1]);
      (*newEntry)[cols.files] = std::string (t[cols.files]);

      // Check if previous node has more than one child or is the first entry
      if ((prevNode->children ().size () > 1)
          || (prevNode == vFiles->children ().begin ()))
         newParent = (*makeParentEntry ((*parent)[cols.type], newParent));
      else {
         // If not: Add selected row to pre-previous parent
         TRACE9 ("XFileSelection::moveSelectedRowUp () - Prev has only 1 kid");

         newParent = getPrevious (vFiles->children (), prevNode);
         Check3 ((*newParent)[cols.type] == (*parent)[cols.type]);
         Check3 (!newParent->children ().empty ());
      }

      vFiles->erase (t);
      if (prevNode->children ().empty ())
          vFiles->erase (prevNode);

      // After the final preparation: Append the moved line to the new parent
      Check3 (newParent);
      newEntry = (vFiles->append (newParent->children ()));
      (*newEntry)[cols.files] = std::string ((*selRow)[cols.files]);
      treeFiles->expand_row (Gtk::TreePath (newParent), true);

      vFiles->erase (selRow);
      if (parent->children ().empty ())
         vFiles->erase (parent);

      treeFiles->get_selection ()->select (newEntry);
   }
}

//-----------------------------------------------------------------------------
/// Moves the selected row one line down; which might mean into an
/// other group of a different type
//-----------------------------------------------------------------------------
void XFileSelection::moveSelectedRowDown () {
   Gtk::TreeIter selRow (treeFiles->get_selection ()->get_selected ());
   Gtk::TreeIter parent (selRow->parent ());
   Gtk::TreeIter nextRow (selRow);

   if (++nextRow)                            {               // Not last child?
      vFiles->iter_swap (selRow, nextRow);
      rowSelected ();
   }
   else {
      TRACE3 ("XFileSelection::moveSelectedRowDown () - Crossing nodes");

      // Get next (parent) entry (where to move selected line into
      Gtk::TreeIter nextParent (parent);
      ++nextParent;
      Check3 (nextParent);
      Check3 ((*nextParent)[cols.type] != (*parent)[cols.type]);
      Check3 (!nextParent->children ().empty ());

      Gtk::TreeIter newEntry;
      Gtk::TreeIter newParent (parent);

      // First find/make new node for first child of nextParent (which will be
      // deleted afterwards - along with the parent if it only had 1 kid)
      TRACE6 ("XFileSelection::moveSelectedRowDown () - Needs new parent?");
      if ((parent == vFiles->children ().begin ())
          || (parent->children ().size () > 1))
         newParent = makeParentEntry ((*nextParent)[cols.type], nextParent);
      else
         newParent = getPrevious (vFiles->children (), parent);
      newEntry = (vFiles->append (newParent->children ()));
      treeFiles->expand_row (Gtk::TreePath (newParent), true);

      TRACE6 ("XFileSelection::moveSelectedRowDown () - Copy last entry of prev");
      Gtk::TreeRow t (*nextParent->children ().begin ());
      (*newEntry)[cols.files] = std::string (t[cols.files]);

      // Check if next node has more than one child or is the last entry
      newParent = nextParent;
      ++newParent;
      if ((nextParent->children ().size () > 1) || !newParent)
          newParent = (*makeParentEntry ((*parent)[cols.type], nextParent));

      vFiles->erase (t);
      if (nextParent->children ().empty ())
          vFiles->erase (nextParent);

      // After the final preparation: Prepend the moved line to the new parent
      Check3 (newParent);
      newEntry = (vFiles->prepend (newParent->children ()));
      (*newEntry)[cols.files] = std::string ((*selRow)[cols.files]);
      treeFiles->expand_row (Gtk::TreePath (newParent), true);

      vFiles->erase (selRow);
      if (parent->children ().empty ())
         vFiles->erase (parent);

      treeFiles->get_selection ()->select (newEntry);
   }
}

//-----------------------------------------------------------------------------
/// Tests, if the passed line can be selected by the user. This is
/// true only for leaf nodes (containing the files to in/exclude).
/// \param model: List-model
/// \param path: Path to entry in model which should be checked
/// \param bool: Flag, if row is actually selected
/// \returns bool: True, if the passed row can be selected
//-----------------------------------------------------------------------------
bool XFileSelection::canSelect (const Glib::RefPtr<Gtk::TreeModel>& model,
                                const Gtk::TreeModel::Path& path, bool) const {
   const Gtk::TreeModel::iterator iter (model->get_iter (path));
   return iter->parent ();
}


syntax highlighted by Code2HTML, v. 0.9.1