view dw/selection.cc @ 2030:382160be8c2f

cookies comments I was going to say something in the comments about simplicity and not implementing every little bit of the rfc when we don't know of cookies that make it necessary, but then I suppose that's all implied with dillo.
author corvid <corvid@lavabit.com>
date Tue, 17 May 2011 22:48:50 +0000
parents e3683ea10681
children 6dc9350d8193
line wrap: on
line source
/*
 * Dillo Widget
 *
 * Copyright 2005-2007 Sebastian Geerken <sgeerken@dillo.org>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */



#include "core.hh"

#include <string.h>

using namespace lout;

/*
 * strndup() is a GNU extension.
 */
extern "C" char *strndup(const char *s, size_t size)
{
   char *r = (char *) malloc (size + 1);

   if (r) {
      strncpy (r, s, size);
      r[size] = 0;
   }

   return r;
}

namespace dw {
namespace core {

SelectionState::SelectionState ()
{
   layout = NULL;

   selectionState = NONE;
   from = NULL;
   to = NULL;

   linkState = LINK_NONE;
   link = NULL;
}

SelectionState::~SelectionState ()
{
   reset ();
}


bool SelectionState::DoubleClickEmitter::emitToReceiver (lout::signal::Receiver
                                                         *receiver,
                                                         int signalNo,
                                                         int argc,
                                                         Object **argv)
{
   ((DoubleClickReceiver*)receiver)->doubleClick ();
   return false;
}

void SelectionState::reset ()
{
   resetSelection ();
   resetLink ();
}

void SelectionState::resetSelection ()
{
   if (from)
      delete from;
   from = NULL;
   if (to)
      delete to;
   to = NULL;
   selectionState = NONE;
}


void SelectionState::resetLink ()
{
   if (link)
      delete link;
   link = NULL;
   linkState = LINK_NONE;
}

bool SelectionState::buttonPress (Iterator *it, int charPos, int linkNo,
                                  EventButton *event, bool withinContent)
{
   Widget *itWidget = it->getWidget ();
   bool ret = false;

   if (event && event->button == 1 &&
       !withinContent && event->numPressed == 2) {
      // When the user double-clicks on empty parts, emit the double click
      // signal instead of normal processing. Used for full screen
      // mode.
      doubleClickEmitter.emitDoubleClick ();
      // Reset everything, so that dw::core::Selection::buttonRelease will
      // ignore the "release" event following soon.
      highlight (false, 0);
      reset ();
      ret = true;
   } else {
      if (linkNo != -1) {
         // link handling
         if (event) {
            (void) layout->emitLinkPress (itWidget, linkNo, -1, -1, -1, event);
            resetLink ();
            linkState = LINK_PRESSED;
            linkButton = event->button;
            DeepIterator *newLink = new DeepIterator (it);
            if (newLink->isEmpty ()) {
               delete newLink;
               resetLink ();
            } else {
               link = newLink;
               // It may be that the user has pressed on something activatable
               // (linkNo != -1), but there is no contents, e.g. with images
               // without ALTernative text.
               if (link) {
                  linkChar = correctCharPos (link, charPos);
                  linkNumber = linkNo;
               }
            }
            // We do not return the value of the signal method,
            // but we do actually process this event.
            ret = true;
         }
      } else {
         // normal selection handling
         if (event && event->button == 1) {
            highlight (false, 0);
            resetSelection ();

            selectionState = SELECTING;
            DeepIterator *newFrom = new DeepIterator (it);
            if (newFrom->isEmpty ()) {
               delete newFrom;
               resetSelection ();
            } else {
               from = newFrom;
               fromChar = correctCharPos (from, charPos);
               to = from->cloneDeepIterator ();
               toChar = correctCharPos (to, charPos);
            }
            ret = true;
         } else {
            if (event && event->button == 3) {
               // menu popup
               layout->emitLinkPress (itWidget, -1, -1, -1, -1, event);
               ret = true;
            }
         }
      }
   }

   return ret;
}

bool SelectionState::buttonRelease (Iterator *it, int charPos, int linkNo,
                                    EventButton *event, bool withinContent)
{
   Widget *itWidget = it->getWidget ();
   bool ret = false;

   if (linkState == LINK_PRESSED && event && event->button == linkButton) {
      // link handling
      ret = true;
      if (linkNo != -1)
         (void) layout->emitLinkRelease (itWidget, linkNo, -1, -1, -1, event);

      // The link where the user clicked the mouse button?
      if (linkNo == linkNumber) {
         resetLink ();
         (void) layout->emitLinkClick (itWidget, linkNo, -1, -1, -1, event);
      } else {
         if (event->button == 1)
            // Reset links and switch to selection mode. The selection
            // state will be set to SELECTING, which is handled some lines
            // below.
            switchLinkToSelection (it, charPos);
      }
   }

   if (selectionState == SELECTING && event && event->button == 1) {
      // normal selection
      ret = true;
      adjustSelection (it, charPos);

      if (from->compareTo (to) == 0 && fromChar == toChar)
         // nothing selected
         resetSelection ();
      else {
         copy ();
         selectionState = SELECTED;
      }
   }

   return ret;
}

bool SelectionState::buttonMotion (Iterator *it, int charPos, int linkNo,
                                   EventMotion *event, bool withinContent)
{
   if (linkState == LINK_PRESSED) {
      //link handling
      if (linkNo != linkNumber)
         // No longer the link where the user clicked the mouse button.
         // Reset links and switch to selection mode.
         switchLinkToSelection (it, charPos);
      // Still in link: do nothing.
   } else if (selectionState == SELECTING) {
      // selection
      adjustSelection (it, charPos);
   }

   return true;
}

/**
 * \brief General form of dw::core::SelectionState::buttonPress,
 *    dw::core::SelectionState::buttonRelease and
 *    dw::core::SelectionState::buttonMotion.
 */
bool SelectionState::handleEvent (EventType eventType, Iterator *it,
                                  int charPos, int linkNo,
                                  MousePositionEvent *event,
                                  bool withinContent)
{
   switch (eventType) {
   case BUTTON_PRESS:
      return buttonPress (it, charPos, linkNo, (EventButton*)event,
                          withinContent);

   case BUTTON_RELEASE:
      return buttonRelease (it, charPos, linkNo, (EventButton*)event,
                            withinContent);

   case BUTTON_MOTION:
      return buttonMotion (it, charPos, linkNo, (EventMotion*)event,
                           withinContent);


   default:
      misc::assertNotReached ();
   }

   return false;
}


/**
 * \brief This method is called when the user decides not to activate a link,
 *    but instead select text.
 */
void SelectionState::switchLinkToSelection (Iterator *it, int charPos)
{
   // It may be that selection->link is NULL, see a_Selection_button_press.
   if (link) {
      // Reset old selection.
      highlight (false, 0);
      resetSelection ();

      // Transfer link state into selection state.
      from = link->cloneDeepIterator ();
      fromChar = linkChar;
      to = from->createVariant (it);
      toChar = correctCharPos (to, charPos);
      selectionState = SELECTING;

      // Reset link status.
      resetLink ();

      highlight (true, 0);

   } else {
      // A link was pressed on, but there is nothing to select. Reset
      // everything.
      resetSelection ();
      resetLink ();
   }
}

/**
 * \brief This method is used by core::dw::SelectionState::buttonMotion and
 *    core::dw::SelectionState::buttonRelease, and changes the second limit of
 *    the already existing selection region.
 */
void SelectionState::adjustSelection (Iterator *it, int charPos)
{
   DeepIterator *newTo;
   int newToChar, cmpOld, cmpNew, cmpDiff, len;
   bool bruteHighlighting = false;

   newTo = to->createVariant (it);
   newToChar = correctCharPos (newTo, charPos);

   cmpOld = to->compareTo (from);
   cmpNew = newTo->compareTo (from);

   if (cmpOld == 0 || cmpNew == 0) {
      // Either before, or now, the limits differ only by the character
      // position.
      bruteHighlighting = true;
   } else if (cmpOld * cmpNew < 0) {
      // The selection order has changed, i.e. the user moved the selection
      // end again beyond the position he started.
      bruteHighlighting = true;
   } else {
      // Here, cmpOld and cmpNew are equivalent and != 0.
      cmpDiff = newTo->compareTo (to);

      if (cmpOld * cmpDiff > 0) {
         // The user has enlarged the selection. Highlight the difference.
         if (cmpDiff < 0) {
            len = correctCharPos (to, END_OF_WORD);
            highlight0 (true, newTo, newToChar, to, len + 1, 1);
         } else {
            highlight0 (true, to, 0, newTo, newToChar, -1);
         }
      } else {
         if (cmpOld * cmpDiff < 0) {
            // The user has reduced the selection. Unighlight the difference.
            highlight0 (false, to, 0, newTo, 0, cmpDiff);
         }

         // Otherwise, the user has changed the position only slightly.
         // In both cases, re-highlight the new position.
         if (cmpOld < 0) {
            len = correctCharPos (newTo, END_OF_WORD);
            newTo->highlight (newToChar, len + 1, HIGHLIGHT_SELECTION);
         } else
            newTo->highlight (0, newToChar, HIGHLIGHT_SELECTION);
      }
   }

   if (bruteHighlighting)
      highlight (false, 0);

   delete to;
   to = newTo;
   toChar = newToChar;

   if (bruteHighlighting)
      highlight (true, 0);
}

/**
 * \brief This method deals especially with the case that a widget passes
 *    dw::core::SelectionState::END_OF_WORD.
 */
int SelectionState::correctCharPos (DeepIterator *it, int charPos)
{
   Iterator *top = it->getTopIterator ();
   int len;

   if (top->getContent()->type == Content::TEXT)
      len = strlen(top->getContent()->text);
   else
      len = 1;

   return misc::min(charPos, len);
}

void  SelectionState::highlight0 (bool fl, DeepIterator *from, int fromChar,
                                  DeepIterator *to, int toChar, int dir)
{
   DeepIterator *a, *b, *i;
   int cmp, aChar, bChar;
   bool start;

   if (from && to) {
      cmp = from->compareTo (to);
      if (cmp == 0) {
         if (fl) {
            if (fromChar < toChar)
               from->highlight (fromChar, toChar, HIGHLIGHT_SELECTION);
            else
               from->highlight (toChar, fromChar, HIGHLIGHT_SELECTION);
         } else
            from->unhighlight (0, HIGHLIGHT_SELECTION);
         return;
      }

      if (cmp < 0) {
         a = from;
         aChar = fromChar;
         b = to;
         bChar = toChar;
      } else {
         a = to;
         aChar = toChar;
         b = from;
         bChar = fromChar;
      }

      for (i = a->cloneDeepIterator (), start = true;
           (cmp = i->compareTo (b)) <= 0;
           i->next (), start = false) {
         if (i->getContent()->type == Content::TEXT) {
            if (fl) {
               if (start) {
                  i->highlight (aChar, strlen (i->getContent()->text) + 1,
                                HIGHLIGHT_SELECTION);
               } else if (cmp == 0) {
                  // the end
                  i->highlight (0, bChar, HIGHLIGHT_SELECTION);
               } else {
                  i->highlight (0, strlen (i->getContent()->text) + 1,
                                HIGHLIGHT_SELECTION);
               }
            } else {
               i->unhighlight (dir, HIGHLIGHT_SELECTION);
            }
         }
      }
      delete i;
   }
}

void SelectionState::copy()
{
   if (from && to) {
      Iterator *si;
      DeepIterator *a, *b, *i;
      int cmp, aChar, bChar;
      bool start;
      char *tmp;
      misc::StringBuffer strbuf;

      cmp = from->compareTo (to);
      if (cmp == 0) {
         if (from->getContent()->type == Content::TEXT) {
            si = from->getTopIterator ();
            if (fromChar < toChar)
               tmp = strndup (si->getContent()->text + fromChar,
                              toChar - fromChar);
            else
               tmp = strndup (si->getContent()->text + toChar,
                              fromChar - toChar);
            strbuf.appendNoCopy (tmp);
         }
      } else {
         if (cmp < 0) {
            a = from;
            aChar = fromChar;
            b = to;
            bChar = toChar;
         } else {
            a = to;
            aChar = toChar;
            b = from;
            bChar = fromChar;
         }

         for (i = a->cloneDeepIterator (), start = true;
              (cmp = i->compareTo (b)) <= 0;
              i->next (), start = false) {
            si = i->getTopIterator ();
            switch (si->getContent()->type) {
            case Content::TEXT:
               if (start) {
                  tmp = strndup (si->getContent()->text + aChar,
                                 strlen (i->getContent()->text) - aChar);
                  strbuf.appendNoCopy (tmp);
               } else if (cmp == 0) {
                  // the end
                  tmp = strndup (si->getContent()->text, bChar);
                  strbuf.appendNoCopy (tmp);
               } else
                  strbuf.append (si->getContent()->text);

               if (si->getContent()->space && cmp != 0)
                  strbuf.append (" ");

               break;

            case Content::BREAK:
               if (si->getContent()->breakSpace > 0)
                  strbuf.append ("\n\n");
               else
                  strbuf.append ("\n");
               break;
            default:
               // Make pedantic compilers happy. Especially
               // DW_CONTENT_WIDGET is never returned by a DwDeepIterator.
               break;
            }
         }
         delete i;
      }

      layout->copySelection(strbuf.getChars());
   }
}

} // namespace dw
} // namespace core